digital-objects 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +25 -0
  3. package/LICENSE +21 -0
  4. package/README.md +476 -0
  5. package/dist/ai-database-adapter.d.ts +49 -0
  6. package/dist/ai-database-adapter.d.ts.map +1 -0
  7. package/dist/ai-database-adapter.js +89 -0
  8. package/dist/ai-database-adapter.js.map +1 -0
  9. package/dist/errors.d.ts +47 -0
  10. package/dist/errors.d.ts.map +1 -0
  11. package/dist/errors.js +72 -0
  12. package/dist/errors.js.map +1 -0
  13. package/dist/http-schemas.d.ts +165 -0
  14. package/dist/http-schemas.d.ts.map +1 -0
  15. package/dist/http-schemas.js +55 -0
  16. package/dist/http-schemas.js.map +1 -0
  17. package/dist/index.d.ts +29 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +32 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/linguistic.d.ts +54 -0
  22. package/dist/linguistic.d.ts.map +1 -0
  23. package/dist/linguistic.js +226 -0
  24. package/dist/linguistic.js.map +1 -0
  25. package/dist/memory-provider.d.ts +46 -0
  26. package/dist/memory-provider.d.ts.map +1 -0
  27. package/dist/memory-provider.js +279 -0
  28. package/dist/memory-provider.js.map +1 -0
  29. package/dist/ns-client.d.ts +88 -0
  30. package/dist/ns-client.d.ts.map +1 -0
  31. package/dist/ns-client.js +253 -0
  32. package/dist/ns-client.js.map +1 -0
  33. package/dist/ns-exports.d.ts +23 -0
  34. package/dist/ns-exports.d.ts.map +1 -0
  35. package/dist/ns-exports.js +21 -0
  36. package/dist/ns-exports.js.map +1 -0
  37. package/dist/ns.d.ts +60 -0
  38. package/dist/ns.d.ts.map +1 -0
  39. package/dist/ns.js +818 -0
  40. package/dist/ns.js.map +1 -0
  41. package/dist/r2-persistence.d.ts +112 -0
  42. package/dist/r2-persistence.d.ts.map +1 -0
  43. package/dist/r2-persistence.js +252 -0
  44. package/dist/r2-persistence.js.map +1 -0
  45. package/dist/schema-validation.d.ts +80 -0
  46. package/dist/schema-validation.d.ts.map +1 -0
  47. package/dist/schema-validation.js +233 -0
  48. package/dist/schema-validation.js.map +1 -0
  49. package/dist/types.d.ts +184 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +26 -0
  52. package/dist/types.js.map +1 -0
  53. package/package.json +55 -0
  54. package/src/ai-database-adapter.test.ts +610 -0
  55. package/src/ai-database-adapter.ts +189 -0
  56. package/src/benchmark.test.ts +109 -0
  57. package/src/errors.ts +91 -0
  58. package/src/http-schemas.ts +67 -0
  59. package/src/index.ts +87 -0
  60. package/src/linguistic.test.ts +1107 -0
  61. package/src/linguistic.ts +253 -0
  62. package/src/memory-provider.ts +470 -0
  63. package/src/ns-client.test.ts +1360 -0
  64. package/src/ns-client.ts +342 -0
  65. package/src/ns-exports.ts +23 -0
  66. package/src/ns.test.ts +1381 -0
  67. package/src/ns.ts +1215 -0
  68. package/src/provider.test.ts +675 -0
  69. package/src/r2-persistence.test.ts +263 -0
  70. package/src/r2-persistence.ts +367 -0
  71. package/src/schema-validation.test.ts +167 -0
  72. package/src/schema-validation.ts +330 -0
  73. package/src/types.ts +252 -0
  74. package/test/action-status.test.ts +42 -0
  75. package/test/batch-limits.test.ts +165 -0
  76. package/test/docs.test.ts +48 -0
  77. package/test/errors.test.ts +148 -0
  78. package/test/http-validation.test.ts +401 -0
  79. package/test/ns-client-errors.test.ts +208 -0
  80. package/test/ns-namespace.test.ts +307 -0
  81. package/test/performance.test.ts +168 -0
  82. package/test/schema-validation-error.test.ts +213 -0
  83. package/test/schema-validation.test.ts +440 -0
  84. package/test/search-escaping.test.ts +359 -0
  85. package/test/security.test.ts +322 -0
  86. package/tsconfig.json +10 -0
  87. package/wrangler.jsonc +16 -0
@@ -0,0 +1,610 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+ import { createDBProviderAdapter, type DBProvider } from './ai-database-adapter.js'
3
+ import { MemoryProvider } from './memory-provider.js'
4
+
5
+ describe('ai-database Adapter', () => {
6
+ let memoryProvider: MemoryProvider
7
+ let adapter: DBProvider
8
+
9
+ beforeEach(() => {
10
+ memoryProvider = new MemoryProvider()
11
+ adapter = createDBProviderAdapter(memoryProvider)
12
+ })
13
+
14
+ describe('get(type, id)', () => {
15
+ it('should retrieve an entity by type and id', async () => {
16
+ // Setup: create a thing directly in the memory provider
17
+ await memoryProvider.defineNoun({ name: 'User' })
18
+ const thing = await memoryProvider.create(
19
+ 'User',
20
+ { name: 'Alice', email: 'alice@example.com' },
21
+ 'user-1'
22
+ )
23
+
24
+ // Test: get via adapter
25
+ const entity = await adapter.get('User', 'user-1')
26
+
27
+ expect(entity).not.toBeNull()
28
+ expect(entity!.$id).toBe('user-1')
29
+ expect(entity!.$type).toBe('User')
30
+ expect(entity!.name).toBe('Alice')
31
+ expect(entity!.email).toBe('alice@example.com')
32
+ })
33
+
34
+ it('should return null for non-existent entity', async () => {
35
+ const entity = await adapter.get('User', 'non-existent')
36
+ expect(entity).toBeNull()
37
+ })
38
+
39
+ it('should return null if entity exists but type does not match', async () => {
40
+ await memoryProvider.defineNoun({ name: 'User' })
41
+ await memoryProvider.create('User', { name: 'Bob' }, 'user-1')
42
+
43
+ // Try to get as a different type
44
+ const entity = await adapter.get('Admin', 'user-1')
45
+ expect(entity).toBeNull()
46
+ })
47
+ })
48
+
49
+ describe('list(type, options)', () => {
50
+ beforeEach(async () => {
51
+ await memoryProvider.defineNoun({ name: 'Product' })
52
+ await memoryProvider.create(
53
+ 'Product',
54
+ { name: 'Apple', price: 1.5, category: 'fruit' },
55
+ 'prod-1'
56
+ )
57
+ await memoryProvider.create(
58
+ 'Product',
59
+ { name: 'Banana', price: 0.75, category: 'fruit' },
60
+ 'prod-2'
61
+ )
62
+ await memoryProvider.create(
63
+ 'Product',
64
+ { name: 'Carrot', price: 0.5, category: 'vegetable' },
65
+ 'prod-3'
66
+ )
67
+ })
68
+
69
+ it('should list all entities of a type', async () => {
70
+ const entities = await adapter.list('Product')
71
+
72
+ expect(entities).toHaveLength(3)
73
+ expect(entities.every((e) => e.$type === 'Product')).toBe(true)
74
+ })
75
+
76
+ it('should filter entities with where clause', async () => {
77
+ const entities = await adapter.list('Product', { where: { category: 'fruit' } })
78
+
79
+ expect(entities).toHaveLength(2)
80
+ expect(entities.every((e) => e.category === 'fruit')).toBe(true)
81
+ })
82
+
83
+ it('should limit results', async () => {
84
+ const entities = await adapter.list('Product', { limit: 2 })
85
+ expect(entities).toHaveLength(2)
86
+ })
87
+
88
+ it('should offset results', async () => {
89
+ const entities = await adapter.list('Product', { offset: 1 })
90
+ expect(entities).toHaveLength(2)
91
+ })
92
+
93
+ it('should order results ascending', async () => {
94
+ const entities = await adapter.list('Product', { orderBy: 'price', order: 'asc' })
95
+
96
+ expect(entities[0].name).toBe('Carrot')
97
+ expect(entities[1].name).toBe('Banana')
98
+ expect(entities[2].name).toBe('Apple')
99
+ })
100
+
101
+ it('should order results descending', async () => {
102
+ const entities = await adapter.list('Product', { orderBy: 'price', order: 'desc' })
103
+
104
+ expect(entities[0].name).toBe('Apple')
105
+ expect(entities[1].name).toBe('Banana')
106
+ expect(entities[2].name).toBe('Carrot')
107
+ })
108
+
109
+ it('should combine limit and offset for pagination', async () => {
110
+ const page1 = await adapter.list('Product', { limit: 2, offset: 0 })
111
+ const page2 = await adapter.list('Product', { limit: 2, offset: 2 })
112
+
113
+ expect(page1).toHaveLength(2)
114
+ expect(page2).toHaveLength(1)
115
+ })
116
+
117
+ it('should return empty array for unknown type', async () => {
118
+ const entities = await adapter.list('Unknown')
119
+ expect(entities).toEqual([])
120
+ })
121
+ })
122
+
123
+ describe('search(type, query, options)', () => {
124
+ beforeEach(async () => {
125
+ await memoryProvider.defineNoun({ name: 'Article' })
126
+ await memoryProvider.defineNoun({ name: 'Comment' })
127
+ await memoryProvider.create(
128
+ 'Article',
129
+ { title: 'TypeScript Guide', content: 'Learn TypeScript basics' },
130
+ 'art-1'
131
+ )
132
+ await memoryProvider.create(
133
+ 'Article',
134
+ { title: 'JavaScript Tips', content: 'Improve your JS skills' },
135
+ 'art-2'
136
+ )
137
+ await memoryProvider.create('Comment', { text: 'Great TypeScript article!' }, 'com-1')
138
+ })
139
+
140
+ it('should search entities by query', async () => {
141
+ const entities = await adapter.search('Article', 'TypeScript')
142
+
143
+ expect(entities).toHaveLength(1)
144
+ expect(entities[0].$id).toBe('art-1')
145
+ expect(entities[0].$type).toBe('Article')
146
+ })
147
+
148
+ it('should filter by type (not return other types)', async () => {
149
+ // Search for 'TypeScript' which exists in both Article and Comment
150
+ const articles = await adapter.search('Article', 'TypeScript')
151
+ const comments = await adapter.search('Comment', 'TypeScript')
152
+
153
+ expect(articles).toHaveLength(1)
154
+ expect(articles[0].$type).toBe('Article')
155
+ expect(comments).toHaveLength(1)
156
+ expect(comments[0].$type).toBe('Comment')
157
+ })
158
+
159
+ it('should be case-insensitive', async () => {
160
+ const entities = await adapter.search('Article', 'typescript')
161
+ expect(entities).toHaveLength(1)
162
+ })
163
+
164
+ it('should return empty array for no matches', async () => {
165
+ const entities = await adapter.search('Article', 'Python')
166
+ expect(entities).toEqual([])
167
+ })
168
+
169
+ it('should respect limit option', async () => {
170
+ await memoryProvider.create(
171
+ 'Article',
172
+ { title: 'More TypeScript', content: 'Advanced TypeScript' },
173
+ 'art-3'
174
+ )
175
+
176
+ // The search matches all content containing the query
177
+ const entities = await adapter.search('Article', 'TypeScript', { limit: 1 })
178
+ expect(entities).toHaveLength(1)
179
+ })
180
+ })
181
+
182
+ describe('create(type, id, data)', () => {
183
+ it('should create an entity with provided id', async () => {
184
+ const entity = await adapter.create('Task', 'task-1', {
185
+ title: 'Complete tests',
186
+ done: false,
187
+ })
188
+
189
+ expect(entity.$id).toBe('task-1')
190
+ expect(entity.$type).toBe('Task')
191
+ expect(entity.title).toBe('Complete tests')
192
+ expect(entity.done).toBe(false)
193
+ })
194
+
195
+ it('should create an entity with auto-generated id', async () => {
196
+ const entity = await adapter.create('Task', undefined, { title: 'Auto ID task' })
197
+
198
+ expect(entity.$id).toBeDefined()
199
+ expect(entity.$id).not.toBe('')
200
+ expect(entity.$type).toBe('Task')
201
+ })
202
+
203
+ it('should auto-define noun if not already defined', async () => {
204
+ // Noun does not exist yet
205
+ let noun = await memoryProvider.getNoun('NewType')
206
+ expect(noun).toBeNull()
207
+
208
+ // Create via adapter
209
+ await adapter.create('NewType', 'nt-1', { value: 42 })
210
+
211
+ // Noun should now exist
212
+ noun = await memoryProvider.getNoun('NewType')
213
+ expect(noun).not.toBeNull()
214
+ expect(noun!.name).toBe('NewType')
215
+ })
216
+
217
+ it('should not re-define noun if already exists', async () => {
218
+ // Pre-define noun with description
219
+ await memoryProvider.defineNoun({ name: 'Widget', description: 'A widget thing' })
220
+
221
+ // Create via adapter
222
+ await adapter.create('Widget', 'w-1', { size: 'large' })
223
+
224
+ // Original noun should still have description
225
+ const noun = await memoryProvider.getNoun('Widget')
226
+ expect(noun!.description).toBe('A widget thing')
227
+ })
228
+
229
+ it('should strip $id and $type from data before storing', async () => {
230
+ const entity = await adapter.create('Item', 'item-1', {
231
+ $id: 'should-be-ignored',
232
+ $type: 'ShouldBeIgnored',
233
+ name: 'Real Name',
234
+ })
235
+
236
+ // Entity should have correct $id and $type
237
+ expect(entity.$id).toBe('item-1')
238
+ expect(entity.$type).toBe('Item')
239
+ expect(entity.name).toBe('Real Name')
240
+
241
+ // Underlying thing should not have $id/$type in data
242
+ const thing = await memoryProvider.get('item-1')
243
+ expect(thing!.data).not.toHaveProperty('$id')
244
+ expect(thing!.data).not.toHaveProperty('$type')
245
+ })
246
+ })
247
+
248
+ describe('update(type, id, data)', () => {
249
+ beforeEach(async () => {
250
+ await memoryProvider.defineNoun({ name: 'Note' })
251
+ await memoryProvider.create(
252
+ 'Note',
253
+ { title: 'Original', content: 'Initial content' },
254
+ 'note-1'
255
+ )
256
+ })
257
+
258
+ it('should update an entity', async () => {
259
+ const updated = await adapter.update('Note', 'note-1', { title: 'Updated Title' })
260
+
261
+ expect(updated.$id).toBe('note-1')
262
+ expect(updated.$type).toBe('Note')
263
+ expect(updated.title).toBe('Updated Title')
264
+ expect(updated.content).toBe('Initial content') // Preserved
265
+ })
266
+
267
+ it('should merge new data with existing data', async () => {
268
+ await adapter.update('Note', 'note-1', { tags: ['important'] })
269
+
270
+ const entity = await adapter.get('Note', 'note-1')
271
+ expect(entity!.title).toBe('Original')
272
+ expect(entity!.content).toBe('Initial content')
273
+ expect(entity!.tags).toEqual(['important'])
274
+ })
275
+
276
+ it('should throw error for non-existent entity', async () => {
277
+ await expect(adapter.update('Note', 'non-existent', { title: 'Test' })).rejects.toThrow(
278
+ 'Thing not found'
279
+ )
280
+ })
281
+
282
+ it('should strip $id and $type from update data', async () => {
283
+ const updated = await adapter.update('Note', 'note-1', {
284
+ $id: 'ignored',
285
+ $type: 'Ignored',
286
+ content: 'New content',
287
+ })
288
+
289
+ expect(updated.$id).toBe('note-1')
290
+ expect(updated.$type).toBe('Note')
291
+ expect(updated.content).toBe('New content')
292
+ })
293
+ })
294
+
295
+ describe('delete(type, id)', () => {
296
+ beforeEach(async () => {
297
+ await memoryProvider.defineNoun({ name: 'Record' })
298
+ await memoryProvider.create('Record', { value: 123 }, 'rec-1')
299
+ })
300
+
301
+ it('should delete an existing entity', async () => {
302
+ const result = await adapter.delete('Record', 'rec-1')
303
+
304
+ expect(result).toBe(true)
305
+ expect(await adapter.get('Record', 'rec-1')).toBeNull()
306
+ })
307
+
308
+ it('should return false for non-existent entity', async () => {
309
+ const result = await adapter.delete('Record', 'non-existent')
310
+ expect(result).toBe(false)
311
+ })
312
+
313
+ it('should delete entity regardless of type parameter', async () => {
314
+ // The adapter's delete doesn't check type, just passes id to provider
315
+ const result = await adapter.delete('WrongType', 'rec-1')
316
+ expect(result).toBe(true)
317
+ expect(await memoryProvider.get('rec-1')).toBeNull()
318
+ })
319
+ })
320
+
321
+ describe('related(type, id, relation)', () => {
322
+ beforeEach(async () => {
323
+ await memoryProvider.defineNoun({ name: 'Author' })
324
+ await memoryProvider.defineNoun({ name: 'Book' })
325
+ await memoryProvider.defineNoun({ name: 'Publisher' })
326
+ await memoryProvider.defineVerb({ name: 'wrote' })
327
+ await memoryProvider.defineVerb({ name: 'publishedBy' })
328
+
329
+ await memoryProvider.create('Author', { name: 'Stephen King' }, 'author-1')
330
+ await memoryProvider.create('Book', { title: 'The Shining' }, 'book-1')
331
+ await memoryProvider.create('Book', { title: 'It' }, 'book-2')
332
+ await memoryProvider.create('Publisher', { name: 'Scribner' }, 'pub-1')
333
+
334
+ // Author wrote books
335
+ await memoryProvider.perform('wrote', 'author-1', 'book-1')
336
+ await memoryProvider.perform('wrote', 'author-1', 'book-2')
337
+ // Book published by publisher
338
+ await memoryProvider.perform('publishedBy', 'book-1', 'pub-1')
339
+ })
340
+
341
+ it('should get related entities of specified type via relation', async () => {
342
+ const books = await adapter.related('Book', 'author-1', 'wrote')
343
+
344
+ expect(books).toHaveLength(2)
345
+ expect(books.every((b) => b.$type === 'Book')).toBe(true)
346
+ expect(books.map((b) => b.title)).toContain('The Shining')
347
+ expect(books.map((b) => b.title)).toContain('It')
348
+ })
349
+
350
+ it('should filter results by type', async () => {
351
+ // Even if author has relations to books, asking for Publisher type should return empty
352
+ const publishers = await adapter.related('Publisher', 'author-1', 'wrote')
353
+ expect(publishers).toEqual([])
354
+ })
355
+
356
+ it('should handle inbound relations (both direction)', async () => {
357
+ // Get authors related to book-1 via 'wrote'
358
+ const authors = await adapter.related('Author', 'book-1', 'wrote')
359
+
360
+ expect(authors).toHaveLength(1)
361
+ expect(authors[0].name).toBe('Stephen King')
362
+ })
363
+
364
+ it('should return empty array for entity with no relations', async () => {
365
+ await memoryProvider.create('Author', { name: 'New Author' }, 'author-2')
366
+
367
+ const books = await adapter.related('Book', 'author-2', 'wrote')
368
+ expect(books).toEqual([])
369
+ })
370
+
371
+ it('should return empty array for unknown relation', async () => {
372
+ const items = await adapter.related('Book', 'author-1', 'unknownRelation')
373
+ expect(items).toEqual([])
374
+ })
375
+ })
376
+
377
+ describe('relate(fromType, fromId, relation, toType, toId)', () => {
378
+ beforeEach(async () => {
379
+ await memoryProvider.defineNoun({ name: 'Person' })
380
+ await memoryProvider.defineNoun({ name: 'Project' })
381
+ await memoryProvider.create('Person', { name: 'Alice' }, 'person-1')
382
+ await memoryProvider.create('Project', { name: 'Secret Project' }, 'project-1')
383
+ })
384
+
385
+ it('should create a relation between entities', async () => {
386
+ await adapter.relate('Person', 'person-1', 'worksOn', 'Project', 'project-1')
387
+
388
+ // Verify via related
389
+ const projects = await adapter.related('Project', 'person-1', 'worksOn')
390
+ expect(projects).toHaveLength(1)
391
+ expect(projects[0].$id).toBe('project-1')
392
+ })
393
+
394
+ it('should auto-define verb if not already defined', async () => {
395
+ // Verb does not exist yet
396
+ let verb = await memoryProvider.getVerb('manages')
397
+ expect(verb).toBeNull()
398
+
399
+ // Create relation
400
+ await adapter.relate('Person', 'person-1', 'manages', 'Project', 'project-1')
401
+
402
+ // Verb should now exist
403
+ verb = await memoryProvider.getVerb('manages')
404
+ expect(verb).not.toBeNull()
405
+ expect(verb!.name).toBe('manages')
406
+ })
407
+
408
+ it('should not re-define verb if already exists', async () => {
409
+ // Pre-define verb with inverse
410
+ await memoryProvider.defineVerb({ name: 'owns', inverse: 'ownedBy' })
411
+
412
+ // Create relation
413
+ await adapter.relate('Person', 'person-1', 'owns', 'Project', 'project-1')
414
+
415
+ // Original verb should still have inverse
416
+ const verb = await memoryProvider.getVerb('owns')
417
+ expect(verb!.inverse).toBe('ownedBy')
418
+ })
419
+
420
+ it('should pass metadata to perform', async () => {
421
+ await memoryProvider.defineVerb({ name: 'likes' })
422
+
423
+ await adapter.relate('Person', 'person-1', 'likes', 'Project', 'project-1', {
424
+ matchMode: 'fuzzy',
425
+ similarity: 0.85,
426
+ matchedType: 'Project',
427
+ })
428
+
429
+ // Verify action was created with metadata
430
+ const actions = await memoryProvider.listActions({ verb: 'likes' })
431
+ expect(actions).toHaveLength(1)
432
+ expect(actions[0].data).toEqual({
433
+ matchMode: 'fuzzy',
434
+ similarity: 0.85,
435
+ matchedType: 'Project',
436
+ })
437
+ })
438
+
439
+ it('should allow multiple relations between same entities', async () => {
440
+ await adapter.relate('Person', 'person-1', 'owns', 'Project', 'project-1')
441
+ await adapter.relate('Person', 'person-1', 'manages', 'Project', 'project-1')
442
+
443
+ const ownedProjects = await adapter.related('Project', 'person-1', 'owns')
444
+ const managedProjects = await adapter.related('Project', 'person-1', 'manages')
445
+
446
+ expect(ownedProjects).toHaveLength(1)
447
+ expect(managedProjects).toHaveLength(1)
448
+ })
449
+ })
450
+
451
+ describe('unrelate()', () => {
452
+ it('should delete the action representing the relation', async () => {
453
+ await memoryProvider.defineNoun({ name: 'Person' })
454
+ await memoryProvider.defineNoun({ name: 'Project' })
455
+ await memoryProvider.defineVerb({ name: 'owns' })
456
+ await memoryProvider.create('Person', { name: 'Test' }, 'person-1')
457
+ await memoryProvider.create('Project', { name: 'Proj' }, 'project-1')
458
+
459
+ // Create a relation
460
+ await adapter.relate('Person', 'person-1', 'owns', 'Project', 'project-1')
461
+
462
+ // Verify relation exists
463
+ const actionsBefore = await memoryProvider.listActions({
464
+ verb: 'owns',
465
+ subject: 'person-1',
466
+ object: 'project-1',
467
+ })
468
+ expect(actionsBefore).toHaveLength(1)
469
+
470
+ // Unrelate
471
+ await adapter.unrelate('Person', 'person-1', 'owns', 'Project', 'project-1')
472
+
473
+ // Verify relation is deleted
474
+ const actionsAfter = await memoryProvider.listActions({
475
+ verb: 'owns',
476
+ subject: 'person-1',
477
+ object: 'project-1',
478
+ })
479
+ expect(actionsAfter).toHaveLength(0)
480
+ })
481
+
482
+ it('should not throw an error when no relation exists', async () => {
483
+ await expect(adapter.unrelate('A', 'a1', 'rel', 'B', 'b1')).resolves.toBeUndefined()
484
+ })
485
+
486
+ it('should delete multiple matching actions (GDPR compliance)', async () => {
487
+ await memoryProvider.defineNoun({ name: 'User' })
488
+ await memoryProvider.defineNoun({ name: 'Document' })
489
+ await memoryProvider.defineVerb({ name: 'viewed' })
490
+ await memoryProvider.create('User', { name: 'Test' }, 'user-1')
491
+ await memoryProvider.create('Document', { name: 'Doc' }, 'doc-1')
492
+
493
+ // Create multiple view actions (e.g., user viewed same document multiple times)
494
+ await adapter.relate('User', 'user-1', 'viewed', 'Document', 'doc-1')
495
+ await adapter.relate('User', 'user-1', 'viewed', 'Document', 'doc-1')
496
+ await adapter.relate('User', 'user-1', 'viewed', 'Document', 'doc-1')
497
+
498
+ // Verify 3 relations exist
499
+ const actionsBefore = await memoryProvider.listActions({
500
+ verb: 'viewed',
501
+ subject: 'user-1',
502
+ object: 'doc-1',
503
+ })
504
+ expect(actionsBefore).toHaveLength(3)
505
+
506
+ // Unrelate should delete all
507
+ await adapter.unrelate('User', 'user-1', 'viewed', 'Document', 'doc-1')
508
+
509
+ // Verify all relations are deleted
510
+ const actionsAfter = await memoryProvider.listActions({
511
+ verb: 'viewed',
512
+ subject: 'user-1',
513
+ object: 'doc-1',
514
+ })
515
+ expect(actionsAfter).toHaveLength(0)
516
+ })
517
+ })
518
+
519
+ describe('Entity format conversion', () => {
520
+ it('should convert Thing to entity format with $id and $type', async () => {
521
+ await memoryProvider.defineNoun({ name: 'Entity' })
522
+ await memoryProvider.create('Entity', { foo: 'bar', count: 42 }, 'e-1')
523
+
524
+ const entity = await adapter.get('Entity', 'e-1')
525
+
526
+ expect(entity).toEqual({
527
+ $id: 'e-1',
528
+ $type: 'Entity',
529
+ foo: 'bar',
530
+ count: 42,
531
+ })
532
+ })
533
+
534
+ it('should preserve all data fields when converting', async () => {
535
+ await memoryProvider.defineNoun({ name: 'Complex' })
536
+ await memoryProvider.create(
537
+ 'Complex',
538
+ {
539
+ string: 'text',
540
+ number: 123,
541
+ boolean: true,
542
+ array: [1, 2, 3],
543
+ nested: { a: 1, b: 2 },
544
+ null: null,
545
+ },
546
+ 'c-1'
547
+ )
548
+
549
+ const entity = await adapter.get('Complex', 'c-1')
550
+
551
+ expect(entity!.string).toBe('text')
552
+ expect(entity!.number).toBe(123)
553
+ expect(entity!.boolean).toBe(true)
554
+ expect(entity!.array).toEqual([1, 2, 3])
555
+ expect(entity!.nested).toEqual({ a: 1, b: 2 })
556
+ expect(entity!.null).toBeNull()
557
+ })
558
+ })
559
+
560
+ describe('Integration scenarios', () => {
561
+ it('should handle a complete CRUD workflow', async () => {
562
+ // Create
563
+ const created = await adapter.create('Todo', 'todo-1', {
564
+ title: 'Write tests',
565
+ completed: false,
566
+ })
567
+ expect(created.$id).toBe('todo-1')
568
+
569
+ // Read
570
+ const read = await adapter.get('Todo', 'todo-1')
571
+ expect(read!.title).toBe('Write tests')
572
+
573
+ // Update
574
+ const updated = await adapter.update('Todo', 'todo-1', { completed: true })
575
+ expect(updated.completed).toBe(true)
576
+
577
+ // List
578
+ const list = await adapter.list('Todo')
579
+ expect(list).toHaveLength(1)
580
+
581
+ // Delete
582
+ const deleted = await adapter.delete('Todo', 'todo-1')
583
+ expect(deleted).toBe(true)
584
+
585
+ // Verify deletion
586
+ const afterDelete = await adapter.get('Todo', 'todo-1')
587
+ expect(afterDelete).toBeNull()
588
+ })
589
+
590
+ it('should handle a graph workflow with relations', async () => {
591
+ // Create entities
592
+ await adapter.create('Team', 'team-1', { name: 'Engineering' })
593
+ await adapter.create('Member', 'member-1', { name: 'Alice' })
594
+ await adapter.create('Member', 'member-2', { name: 'Bob' })
595
+
596
+ // Create relations
597
+ await adapter.relate('Member', 'member-1', 'belongsTo', 'Team', 'team-1')
598
+ await adapter.relate('Member', 'member-2', 'belongsTo', 'Team', 'team-1')
599
+ await adapter.relate('Member', 'member-1', 'manages', 'Member', 'member-2')
600
+
601
+ // Query relations
602
+ const teamMembers = await adapter.related('Member', 'team-1', 'belongsTo')
603
+ expect(teamMembers).toHaveLength(2)
604
+
605
+ const managed = await adapter.related('Member', 'member-1', 'manages')
606
+ expect(managed).toHaveLength(1)
607
+ expect(managed[0].name).toBe('Bob')
608
+ })
609
+ })
610
+ })