ai-database 2.0.1 → 2.1.1

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 (88) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/actions.d.ts +247 -0
  3. package/dist/actions.d.ts.map +1 -0
  4. package/dist/actions.js +260 -0
  5. package/dist/actions.js.map +1 -0
  6. package/dist/ai-promise-db.d.ts +34 -2
  7. package/dist/ai-promise-db.d.ts.map +1 -1
  8. package/dist/ai-promise-db.js +511 -66
  9. package/dist/ai-promise-db.js.map +1 -1
  10. package/dist/constants.d.ts +16 -0
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/constants.js +16 -0
  13. package/dist/constants.js.map +1 -0
  14. package/dist/events.d.ts +153 -0
  15. package/dist/events.d.ts.map +1 -0
  16. package/dist/events.js +154 -0
  17. package/dist/events.js.map +1 -0
  18. package/dist/index.d.ts +8 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +13 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/memory-provider.d.ts +144 -2
  23. package/dist/memory-provider.d.ts.map +1 -1
  24. package/dist/memory-provider.js +569 -13
  25. package/dist/memory-provider.js.map +1 -1
  26. package/dist/schema/cascade.d.ts +96 -0
  27. package/dist/schema/cascade.d.ts.map +1 -0
  28. package/dist/schema/cascade.js +528 -0
  29. package/dist/schema/cascade.js.map +1 -0
  30. package/dist/schema/index.d.ts +197 -0
  31. package/dist/schema/index.d.ts.map +1 -0
  32. package/dist/schema/index.js +1211 -0
  33. package/dist/schema/index.js.map +1 -0
  34. package/dist/schema/parse.d.ts +225 -0
  35. package/dist/schema/parse.d.ts.map +1 -0
  36. package/dist/schema/parse.js +732 -0
  37. package/dist/schema/parse.js.map +1 -0
  38. package/dist/schema/provider.d.ts +176 -0
  39. package/dist/schema/provider.d.ts.map +1 -0
  40. package/dist/schema/provider.js +258 -0
  41. package/dist/schema/provider.js.map +1 -0
  42. package/dist/schema/resolve.d.ts +87 -0
  43. package/dist/schema/resolve.d.ts.map +1 -0
  44. package/dist/schema/resolve.js +474 -0
  45. package/dist/schema/resolve.js.map +1 -0
  46. package/dist/schema/semantic.d.ts +53 -0
  47. package/dist/schema/semantic.d.ts.map +1 -0
  48. package/dist/schema/semantic.js +247 -0
  49. package/dist/schema/semantic.js.map +1 -0
  50. package/dist/schema/types.d.ts +528 -0
  51. package/dist/schema/types.d.ts.map +1 -0
  52. package/dist/schema/types.js +9 -0
  53. package/dist/schema/types.js.map +1 -0
  54. package/dist/schema.d.ts +24 -867
  55. package/dist/schema.d.ts.map +1 -1
  56. package/dist/schema.js +41 -1124
  57. package/dist/schema.js.map +1 -1
  58. package/dist/semantic.d.ts +175 -0
  59. package/dist/semantic.d.ts.map +1 -0
  60. package/dist/semantic.js +338 -0
  61. package/dist/semantic.js.map +1 -0
  62. package/dist/types.d.ts +14 -0
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/types.js.map +1 -1
  65. package/package.json +13 -4
  66. package/.turbo/turbo-build.log +0 -5
  67. package/TESTING.md +0 -410
  68. package/TEST_SUMMARY.md +0 -250
  69. package/TODO.md +0 -128
  70. package/src/ai-promise-db.ts +0 -1243
  71. package/src/authorization.ts +0 -1102
  72. package/src/durable-clickhouse.ts +0 -596
  73. package/src/durable-promise.ts +0 -582
  74. package/src/execution-queue.ts +0 -608
  75. package/src/index.test.ts +0 -868
  76. package/src/index.ts +0 -337
  77. package/src/linguistic.ts +0 -404
  78. package/src/memory-provider.test.ts +0 -1036
  79. package/src/memory-provider.ts +0 -1119
  80. package/src/schema.test.ts +0 -1254
  81. package/src/schema.ts +0 -2296
  82. package/src/tests.ts +0 -725
  83. package/src/types.ts +0 -1177
  84. package/test/README.md +0 -153
  85. package/test/edge-cases.test.ts +0 -646
  86. package/test/provider-resolution.test.ts +0 -402
  87. package/tsconfig.json +0 -9
  88. package/vitest.config.ts +0 -19
@@ -1,1254 +0,0 @@
1
- /**
2
- * Tests for schema parsing and bi-directional relationships
3
- *
4
- * These are pure unit tests - no database calls needed.
5
- */
6
-
7
- import { describe, it, expect } from 'vitest'
8
- import {
9
- parseSchema,
10
- DB,
11
- defineNoun,
12
- defineVerb,
13
- nounToSchema,
14
- getVerbFields,
15
- Verbs,
16
- conjugate,
17
- pluralize,
18
- singularize,
19
- inferNoun,
20
- Type,
21
- getTypeMeta,
22
- createTypeMeta,
23
- SystemSchema,
24
- ThingSchema,
25
- NounSchema,
26
- VerbSchema,
27
- EdgeSchema,
28
- createNounRecord,
29
- createEdgeRecords,
30
- setNLQueryGenerator,
31
- toExpanded,
32
- toFlat,
33
- } from './schema.js'
34
- import type { DatabaseSchema, ParsedField, Noun, Verb, TypeMeta, NLQueryPlan, ThingFlat, ThingExpanded } from './schema.js'
35
-
36
- describe('Thing types (mdxld)', () => {
37
- describe('ThingFlat', () => {
38
- it('represents entity with $-prefixed metadata', () => {
39
- const post: ThingFlat = {
40
- $id: 'post-123',
41
- $type: 'Post',
42
- $context: 'https://schema.org',
43
- title: 'Hello World',
44
- content: 'This is my post',
45
- }
46
-
47
- expect(post.$id).toBe('post-123')
48
- expect(post.$type).toBe('Post')
49
- expect(post.$context).toBe('https://schema.org')
50
- expect(post.title).toBe('Hello World')
51
- })
52
-
53
- it('allows optional $context', () => {
54
- const post: ThingFlat = {
55
- $id: 'post-123',
56
- $type: 'Post',
57
- title: 'Hello',
58
- }
59
-
60
- expect(post.$context).toBeUndefined()
61
- })
62
- })
63
-
64
- describe('ThingExpanded', () => {
65
- it('represents entity with mdxld structure', () => {
66
- const post: ThingExpanded = {
67
- id: 'post-123',
68
- type: 'Post',
69
- context: 'https://schema.org',
70
- data: { title: 'Hello World', author: 'john' },
71
- content: '# Hello World\n\nThis is my post...',
72
- }
73
-
74
- expect(post.id).toBe('post-123')
75
- expect(post.type).toBe('Post')
76
- expect(post.context).toBe('https://schema.org')
77
- expect(post.data.title).toBe('Hello World')
78
- expect(post.content).toContain('Hello World')
79
- })
80
- })
81
-
82
- describe('toExpanded', () => {
83
- it('converts flat to expanded format', () => {
84
- const flat: ThingFlat = {
85
- $id: 'post-123',
86
- $type: 'Post',
87
- $context: 'https://schema.org',
88
- title: 'Hello World',
89
- author: 'john',
90
- }
91
-
92
- const expanded = toExpanded(flat)
93
-
94
- expect(expanded.id).toBe('post-123')
95
- expect(expanded.type).toBe('Post')
96
- expect(expanded.context).toBe('https://schema.org')
97
- expect(expanded.data.title).toBe('Hello World')
98
- expect(expanded.data.author).toBe('john')
99
- })
100
-
101
- it('handles content field specially', () => {
102
- const flat: ThingFlat = {
103
- $id: 'post-123',
104
- $type: 'Post',
105
- title: 'Hello',
106
- content: '# Markdown content',
107
- }
108
-
109
- const expanded = toExpanded(flat)
110
-
111
- expect(expanded.content).toBe('# Markdown content')
112
- expect(expanded.data.content).toBe('# Markdown content')
113
- })
114
-
115
- it('handles missing context', () => {
116
- const flat: ThingFlat = {
117
- $id: 'post-123',
118
- $type: 'Post',
119
- title: 'Hello',
120
- }
121
-
122
- const expanded = toExpanded(flat)
123
-
124
- expect(expanded.context).toBeUndefined()
125
- })
126
- })
127
-
128
- describe('toFlat', () => {
129
- it('converts expanded to flat format', () => {
130
- const expanded: ThingExpanded = {
131
- id: 'post-123',
132
- type: 'Post',
133
- context: 'https://schema.org',
134
- data: { title: 'Hello World', author: 'john' },
135
- content: '',
136
- }
137
-
138
- const flat = toFlat(expanded)
139
-
140
- expect(flat.$id).toBe('post-123')
141
- expect(flat.$type).toBe('Post')
142
- expect(flat.$context).toBe('https://schema.org')
143
- expect(flat.title).toBe('Hello World')
144
- expect(flat.author).toBe('john')
145
- })
146
-
147
- it('includes content in flat output when present', () => {
148
- const expanded: ThingExpanded = {
149
- id: 'post-123',
150
- type: 'Post',
151
- data: { title: 'Hello' },
152
- content: '# Markdown content',
153
- }
154
-
155
- const flat = toFlat(expanded)
156
-
157
- expect(flat.content).toBe('# Markdown content')
158
- })
159
-
160
- it('omits content when empty', () => {
161
- const expanded: ThingExpanded = {
162
- id: 'post-123',
163
- type: 'Post',
164
- data: { title: 'Hello' },
165
- content: '',
166
- }
167
-
168
- const flat = toFlat(expanded)
169
-
170
- expect(flat.content).toBeUndefined()
171
- })
172
- })
173
-
174
- describe('round-trip conversion', () => {
175
- it('preserves data through flat -> expanded -> flat', () => {
176
- const original: ThingFlat = {
177
- $id: 'post-123',
178
- $type: 'Post',
179
- $context: 'https://schema.org',
180
- title: 'Hello World',
181
- author: 'john',
182
- tags: ['typescript', 'ai'],
183
- }
184
-
185
- const expanded = toExpanded(original)
186
- const roundTripped = toFlat(expanded)
187
-
188
- expect(roundTripped.$id).toBe(original.$id)
189
- expect(roundTripped.$type).toBe(original.$type)
190
- expect(roundTripped.$context).toBe(original.$context)
191
- expect(roundTripped.title).toBe(original.title)
192
- expect(roundTripped.author).toBe(original.author)
193
- expect(roundTripped.tags).toEqual(original.tags)
194
- })
195
- })
196
- })
197
-
198
- describe('parseSchema', () => {
199
- describe('primitive fields', () => {
200
- it('parses basic primitive types', () => {
201
- const schema: DatabaseSchema = {
202
- User: {
203
- name: 'string',
204
- age: 'number',
205
- active: 'boolean',
206
- created: 'date',
207
- },
208
- }
209
-
210
- const parsed = parseSchema(schema)
211
- const user = parsed.entities.get('User')
212
-
213
- expect(user).toBeDefined()
214
- expect(user!.fields.size).toBe(4)
215
-
216
- const name = user!.fields.get('name')
217
- expect(name?.type).toBe('string')
218
- expect(name?.isRelation).toBe(false)
219
- expect(name?.isArray).toBe(false)
220
- expect(name?.isOptional).toBe(false)
221
- })
222
-
223
- it('parses optional fields with ? modifier', () => {
224
- const schema: DatabaseSchema = {
225
- User: {
226
- bio: 'string?',
227
- age: 'number?',
228
- },
229
- }
230
-
231
- const parsed = parseSchema(schema)
232
- const user = parsed.entities.get('User')
233
-
234
- const bio = user!.fields.get('bio')
235
- expect(bio?.isOptional).toBe(true)
236
- expect(bio?.type).toBe('string')
237
- })
238
-
239
- it('parses array fields with [] modifier', () => {
240
- const schema: DatabaseSchema = {
241
- User: {
242
- tags: 'string[]',
243
- scores: 'number[]',
244
- },
245
- }
246
-
247
- const parsed = parseSchema(schema)
248
- const user = parsed.entities.get('User')
249
-
250
- const tags = user!.fields.get('tags')
251
- expect(tags?.isArray).toBe(true)
252
- expect(tags?.type).toBe('string')
253
- expect(tags?.isRelation).toBe(false)
254
- })
255
-
256
- it('parses array fields with literal syntax', () => {
257
- const schema: DatabaseSchema = {
258
- User: {
259
- tags: ['string'],
260
- scores: ['number'],
261
- },
262
- }
263
-
264
- const parsed = parseSchema(schema)
265
- const user = parsed.entities.get('User')
266
-
267
- const tags = user!.fields.get('tags')
268
- expect(tags?.isArray).toBe(true)
269
- expect(tags?.type).toBe('string')
270
- })
271
-
272
- it('parses all primitive types', () => {
273
- const schema: DatabaseSchema = {
274
- Entity: {
275
- str: 'string',
276
- num: 'number',
277
- bool: 'boolean',
278
- dt: 'date',
279
- dtt: 'datetime',
280
- json: 'json',
281
- md: 'markdown',
282
- url: 'url',
283
- },
284
- }
285
-
286
- const parsed = parseSchema(schema)
287
- const entity = parsed.entities.get('Entity')
288
-
289
- expect(entity!.fields.size).toBe(8)
290
- expect(entity!.fields.get('str')?.type).toBe('string')
291
- expect(entity!.fields.get('num')?.type).toBe('number')
292
- expect(entity!.fields.get('bool')?.type).toBe('boolean')
293
- expect(entity!.fields.get('dt')?.type).toBe('date')
294
- expect(entity!.fields.get('dtt')?.type).toBe('datetime')
295
- expect(entity!.fields.get('json')?.type).toBe('json')
296
- expect(entity!.fields.get('md')?.type).toBe('markdown')
297
- expect(entity!.fields.get('url')?.type).toBe('url')
298
- })
299
- })
300
-
301
- describe('simple relations', () => {
302
- it('parses relation without backref', () => {
303
- const schema: DatabaseSchema = {
304
- Post: {
305
- author: 'Author',
306
- },
307
- Author: {
308
- name: 'string',
309
- },
310
- }
311
-
312
- const parsed = parseSchema(schema)
313
- const post = parsed.entities.get('Post')
314
-
315
- const author = post!.fields.get('author')
316
- expect(author?.isRelation).toBe(true)
317
- expect(author?.relatedType).toBe('Author')
318
- expect(author?.backref).toBeUndefined()
319
- })
320
-
321
- it('parses relation with explicit backref', () => {
322
- const schema: DatabaseSchema = {
323
- Post: {
324
- author: 'Author.posts',
325
- },
326
- Author: {
327
- name: 'string',
328
- },
329
- }
330
-
331
- const parsed = parseSchema(schema)
332
- const post = parsed.entities.get('Post')
333
- const author = parsed.entities.get('Author')
334
-
335
- const authorField = post!.fields.get('author')
336
- expect(authorField?.isRelation).toBe(true)
337
- expect(authorField?.relatedType).toBe('Author')
338
- expect(authorField?.backref).toBe('posts')
339
-
340
- // Check backref was auto-created
341
- const postsField = author!.fields.get('posts')
342
- expect(postsField).toBeDefined()
343
- expect(postsField?.isRelation).toBe(true)
344
- expect(postsField?.isArray).toBe(true)
345
- expect(postsField?.relatedType).toBe('Post')
346
- expect(postsField?.backref).toBe('author')
347
- })
348
- })
349
-
350
- describe('bi-directional relationships', () => {
351
- it('creates automatic backref for one-to-many', () => {
352
- const schema: DatabaseSchema = {
353
- Post: {
354
- title: 'string',
355
- author: 'Author.posts',
356
- },
357
- Author: {
358
- name: 'string',
359
- // posts: Post[] should be auto-created
360
- },
361
- }
362
-
363
- const parsed = parseSchema(schema)
364
- const author = parsed.entities.get('Author')
365
- const post = parsed.entities.get('Post')
366
-
367
- // Check Post.author
368
- const authorField = post!.fields.get('author')
369
- expect(authorField?.isRelation).toBe(true)
370
- expect(authorField?.isArray).toBe(false)
371
- expect(authorField?.relatedType).toBe('Author')
372
- expect(authorField?.backref).toBe('posts')
373
-
374
- // Check auto-created Author.posts
375
- const postsField = author!.fields.get('posts')
376
- expect(postsField).toBeDefined()
377
- expect(postsField?.isRelation).toBe(true)
378
- expect(postsField?.isArray).toBe(true)
379
- expect(postsField?.relatedType).toBe('Post')
380
- expect(postsField?.backref).toBe('author')
381
- })
382
-
383
- it('creates automatic backref for many-to-many', () => {
384
- const schema: DatabaseSchema = {
385
- Post: {
386
- tags: ['Tag.posts'],
387
- },
388
- Tag: {
389
- name: 'string',
390
- // posts: Post[] should be auto-created
391
- },
392
- }
393
-
394
- const parsed = parseSchema(schema)
395
- const post = parsed.entities.get('Post')
396
- const tag = parsed.entities.get('Tag')
397
-
398
- // Check Post.tags
399
- const tagsField = post!.fields.get('tags')
400
- expect(tagsField?.isRelation).toBe(true)
401
- expect(tagsField?.isArray).toBe(true)
402
- expect(tagsField?.relatedType).toBe('Tag')
403
- expect(tagsField?.backref).toBe('posts')
404
-
405
- // Check auto-created Tag.posts
406
- const postsField = tag!.fields.get('posts')
407
- expect(postsField).toBeDefined()
408
- expect(postsField?.isRelation).toBe(true)
409
- expect(postsField?.isArray).toBe(true)
410
- expect(postsField?.relatedType).toBe('Post')
411
- expect(postsField?.backref).toBe('tags')
412
- })
413
-
414
- it('does not duplicate existing backref', () => {
415
- const schema: DatabaseSchema = {
416
- Post: {
417
- author: 'Author.posts',
418
- },
419
- Author: {
420
- posts: ['Post.author'],
421
- },
422
- }
423
-
424
- const parsed = parseSchema(schema)
425
- const author = parsed.entities.get('Author')
426
-
427
- // Should only have the explicitly defined posts field
428
- expect(author!.fields.size).toBe(1)
429
- const postsField = author!.fields.get('posts')
430
- expect(postsField?.isArray).toBe(true)
431
- expect(postsField?.relatedType).toBe('Post')
432
- })
433
- })
434
-
435
- describe('complex schemas', () => {
436
- it('parses multi-entity schema with various field types', () => {
437
- const schema: DatabaseSchema = {
438
- Post: {
439
- title: 'string',
440
- content: 'markdown',
441
- published: 'boolean',
442
- author: 'Author.posts',
443
- tags: ['Tag.posts'],
444
- },
445
- Author: {
446
- name: 'string',
447
- email: 'string',
448
- bio: 'string?',
449
- },
450
- Tag: {
451
- name: 'string',
452
- slug: 'string',
453
- },
454
- }
455
-
456
- const parsed = parseSchema(schema)
457
-
458
- expect(parsed.entities.size).toBe(3)
459
- expect(parsed.entities.has('Post')).toBe(true)
460
- expect(parsed.entities.has('Author')).toBe(true)
461
- expect(parsed.entities.has('Tag')).toBe(true)
462
-
463
- // Check Post fields
464
- const post = parsed.entities.get('Post')
465
- expect(post!.fields.size).toBe(5)
466
-
467
- // Check Author backref
468
- const author = parsed.entities.get('Author')
469
- expect(author!.fields.has('posts')).toBe(true)
470
-
471
- // Check Tag backref
472
- const tag = parsed.entities.get('Tag')
473
- expect(tag!.fields.has('posts')).toBe(true)
474
- })
475
-
476
- it('handles optional relations', () => {
477
- const schema: DatabaseSchema = {
478
- User: {
479
- profile: 'Profile.user?',
480
- },
481
- Profile: {
482
- bio: 'string',
483
- },
484
- }
485
-
486
- const parsed = parseSchema(schema)
487
- const user = parsed.entities.get('User')
488
-
489
- const profile = user!.fields.get('profile')
490
- expect(profile?.isOptional).toBe(true)
491
- expect(profile?.isRelation).toBe(true)
492
- })
493
-
494
- it('handles self-referential relations', () => {
495
- const schema: DatabaseSchema = {
496
- User: {
497
- name: 'string',
498
- manager: 'User.reports?',
499
- },
500
- }
501
-
502
- const parsed = parseSchema(schema)
503
- const user = parsed.entities.get('User')
504
-
505
- expect(user!.fields.has('manager')).toBe(true)
506
- expect(user!.fields.has('reports')).toBe(true)
507
-
508
- const manager = user!.fields.get('manager')
509
- expect(manager?.relatedType).toBe('User')
510
- expect(manager?.backref).toBe('reports')
511
-
512
- const reports = user!.fields.get('reports')
513
- expect(reports?.isArray).toBe(true)
514
- expect(reports?.relatedType).toBe('User')
515
- })
516
- })
517
-
518
- describe('edge cases', () => {
519
- it('handles empty schema', () => {
520
- const schema: DatabaseSchema = {}
521
- const parsed = parseSchema(schema)
522
- expect(parsed.entities.size).toBe(0)
523
- })
524
-
525
- it('handles entity with no fields', () => {
526
- const schema: DatabaseSchema = {
527
- Empty: {},
528
- }
529
- const parsed = parseSchema(schema)
530
- const empty = parsed.entities.get('Empty')
531
- expect(empty).toBeDefined()
532
- expect(empty!.fields.size).toBe(0)
533
- })
534
-
535
- it('handles relation to non-existent entity', () => {
536
- const schema: DatabaseSchema = {
537
- Post: {
538
- author: 'Author.posts',
539
- },
540
- }
541
-
542
- const parsed = parseSchema(schema)
543
- const post = parsed.entities.get('Post')
544
- const author = post!.fields.get('author')
545
-
546
- expect(author?.isRelation).toBe(true)
547
- expect(author?.relatedType).toBe('Author')
548
- // Backref won't be created since Author doesn't exist
549
- expect(parsed.entities.has('Author')).toBe(false)
550
- })
551
- })
552
- })
553
-
554
- describe('DB factory', () => {
555
- it('creates a typed database from schema', () => {
556
- const schema: DatabaseSchema = {
557
- User: {
558
- name: 'string',
559
- email: 'string',
560
- },
561
- }
562
-
563
- const { db } = DB(schema)
564
-
565
- expect(db).toBeDefined()
566
- expect(db.$schema).toBeDefined()
567
- expect(db.User).toBeDefined()
568
- expect(typeof db.User.get).toBe('function')
569
- expect(typeof db.User.list).toBe('function')
570
- expect(typeof db.User.create).toBe('function')
571
- expect(typeof db.User.update).toBe('function')
572
- expect(typeof db.User.delete).toBe('function')
573
- })
574
-
575
- it('creates operations for all entity types', () => {
576
- const schema: DatabaseSchema = {
577
- Post: { title: 'string' },
578
- Author: { name: 'string' },
579
- Tag: { name: 'string' },
580
- }
581
-
582
- const { db } = DB(schema)
583
-
584
- expect(db.Post).toBeDefined()
585
- expect(db.Author).toBeDefined()
586
- expect(db.Tag).toBeDefined()
587
- })
588
-
589
- it('includes global search and get methods', () => {
590
- const schema: DatabaseSchema = {
591
- User: { name: 'string' },
592
- }
593
-
594
- const { db } = DB(schema)
595
-
596
- expect(typeof db.get).toBe('function')
597
- expect(typeof db.search).toBe('function')
598
- })
599
-
600
- it('preserves parsed schema in $schema', () => {
601
- const schema: DatabaseSchema = {
602
- User: {
603
- name: 'string',
604
- posts: ['Post.author'],
605
- },
606
- Post: {
607
- title: 'string',
608
- },
609
- }
610
-
611
- const { db } = DB(schema)
612
-
613
- expect(db.$schema.entities.size).toBe(2)
614
- const user = db.$schema.entities.get('User')
615
- expect(user!.fields.size).toBe(2)
616
- })
617
- })
618
-
619
- describe('type inference', () => {
620
- it('infers entity types from schema', () => {
621
- const schema = {
622
- User: {
623
- name: 'string',
624
- age: 'number',
625
- active: 'boolean',
626
- },
627
- } as const
628
-
629
- const { db } = DB(schema)
630
-
631
- // TypeScript should infer these types
632
- // Runtime check that the structure is correct
633
- expect(db.User).toBeDefined()
634
- expect(typeof db.User.get).toBe('function')
635
- })
636
-
637
- it('infers relation types', () => {
638
- const schema = {
639
- Post: {
640
- title: 'string',
641
- author: 'Author.posts',
642
- },
643
- Author: {
644
- name: 'string',
645
- },
646
- } as const
647
-
648
- const { db } = DB(schema)
649
-
650
- expect(db.Post).toBeDefined()
651
- expect(db.Author).toBeDefined()
652
- })
653
- })
654
-
655
- describe('Noun & Verb', () => {
656
- describe('defineVerb', () => {
657
- it('creates a verb with all conjugations', () => {
658
- const publish = defineVerb({
659
- action: 'publish',
660
- actor: 'publisher',
661
- act: 'publishes',
662
- activity: 'publishing',
663
- result: 'publication',
664
- reverse: { at: 'publishedAt', by: 'publishedBy' },
665
- inverse: 'unpublish',
666
- })
667
-
668
- expect(publish.action).toBe('publish')
669
- expect(publish.actor).toBe('publisher')
670
- expect(publish.act).toBe('publishes')
671
- expect(publish.activity).toBe('publishing')
672
- expect(publish.result).toBe('publication')
673
- expect(publish.reverse?.at).toBe('publishedAt')
674
- expect(publish.reverse?.by).toBe('publishedBy')
675
- expect(publish.inverse).toBe('unpublish')
676
- })
677
-
678
- it('provides standard CRUD verbs', () => {
679
- expect(Verbs.create.action).toBe('create')
680
- expect(Verbs.create.actor).toBe('creator')
681
- expect(Verbs.create.activity).toBe('creating')
682
- expect(Verbs.create.reverse?.at).toBe('createdAt')
683
- expect(Verbs.create.reverse?.by).toBe('createdBy')
684
- expect(Verbs.create.inverse).toBe('delete')
685
-
686
- expect(Verbs.update.action).toBe('update')
687
- expect(Verbs.delete.action).toBe('delete')
688
- expect(Verbs.publish.action).toBe('publish')
689
- expect(Verbs.archive.action).toBe('archive')
690
- })
691
- })
692
-
693
- describe('defineNoun', () => {
694
- it('creates a noun with properties and relationships', () => {
695
- const Post = defineNoun({
696
- singular: 'post',
697
- plural: 'posts',
698
- description: 'A blog post or article',
699
- properties: {
700
- title: { type: 'string', description: 'The post title' },
701
- content: { type: 'markdown', description: 'The post body' },
702
- status: { type: 'string', optional: true },
703
- },
704
- relationships: {
705
- author: { type: 'Author', backref: 'posts', description: 'Who wrote this' },
706
- tags: { type: 'Tag[]', backref: 'posts' },
707
- },
708
- actions: ['create', 'update', 'delete', 'publish'],
709
- events: ['created', 'updated', 'deleted', 'published'],
710
- })
711
-
712
- expect(Post.singular).toBe('post')
713
- expect(Post.plural).toBe('posts')
714
- expect(Post.properties?.title.type).toBe('string')
715
- expect(Post.properties?.title.description).toBe('The post title')
716
- expect(Post.properties?.status?.optional).toBe(true)
717
- expect(Post.relationships?.author.type).toBe('Author')
718
- expect(Post.relationships?.author.backref).toBe('posts')
719
- expect(Post.relationships?.tags.type).toBe('Tag[]')
720
- expect(Post.actions).toContain('publish')
721
- expect(Post.events).toContain('published')
722
- })
723
- })
724
-
725
- describe('nounToSchema', () => {
726
- it('converts noun to entity schema', () => {
727
- const Post = defineNoun({
728
- singular: 'post',
729
- plural: 'posts',
730
- properties: {
731
- title: { type: 'string' },
732
- content: { type: 'markdown' },
733
- draft: { type: 'boolean', optional: true },
734
- tags: { type: 'string', array: true },
735
- },
736
- relationships: {
737
- author: { type: 'Author', backref: 'posts' },
738
- },
739
- })
740
-
741
- const schema = nounToSchema(Post)
742
-
743
- expect(schema.title).toBe('string')
744
- expect(schema.content).toBe('markdown')
745
- expect(schema.draft).toBe('boolean?')
746
- expect(schema.tags).toBe('string[]')
747
- expect(schema.author).toBe('Author.posts')
748
- })
749
-
750
- it('handles many-to-many relationships', () => {
751
- const Post = defineNoun({
752
- singular: 'post',
753
- plural: 'posts',
754
- relationships: {
755
- tags: { type: 'Tag[]', backref: 'posts' },
756
- },
757
- })
758
-
759
- const schema = nounToSchema(Post)
760
-
761
- expect(schema.tags).toEqual(['Tag.posts'])
762
- })
763
-
764
- it('handles relationships without backref', () => {
765
- const Post = defineNoun({
766
- singular: 'post',
767
- plural: 'posts',
768
- relationships: {
769
- category: { type: 'Category' },
770
- },
771
- })
772
-
773
- const schema = nounToSchema(Post)
774
-
775
- expect(schema.category).toBe('Category')
776
- })
777
- })
778
-
779
- describe('getVerbFields', () => {
780
- it('returns reverse fields for standard verbs', () => {
781
- const createFields = getVerbFields('create')
782
- expect(createFields.at).toBe('createdAt')
783
- expect(createFields.by).toBe('createdBy')
784
- expect(createFields.in).toBe('createdIn')
785
- expect(createFields.for).toBe('createdFor')
786
-
787
- const updateFields = getVerbFields('update')
788
- expect(updateFields.at).toBe('updatedAt')
789
- expect(updateFields.by).toBe('updatedBy')
790
-
791
- const publishFields = getVerbFields('publish')
792
- expect(publishFields.at).toBe('publishedAt')
793
- expect(publishFields.by).toBe('publishedBy')
794
- })
795
- })
796
-
797
- describe('integration with DB', () => {
798
- it('uses noun-derived schema with DB()', () => {
799
- const Post = defineNoun({
800
- singular: 'post',
801
- plural: 'posts',
802
- properties: {
803
- title: { type: 'string' },
804
- content: { type: 'markdown' },
805
- },
806
- relationships: {
807
- author: { type: 'Author', backref: 'posts' },
808
- },
809
- })
810
-
811
- const Author = defineNoun({
812
- singular: 'author',
813
- plural: 'authors',
814
- properties: {
815
- name: { type: 'string' },
816
- email: { type: 'string' },
817
- },
818
- })
819
-
820
- const { db } = DB({
821
- Post: nounToSchema(Post),
822
- Author: nounToSchema(Author),
823
- })
824
-
825
- expect(db.Post).toBeDefined()
826
- expect(db.Author).toBeDefined()
827
- expect(db.$schema.entities.get('Author')?.fields.has('posts')).toBe(true)
828
- })
829
- })
830
- })
831
-
832
- describe('AI Auto-Generation', () => {
833
- describe('conjugate', () => {
834
- it('returns known verbs from Verbs constant', () => {
835
- const create = conjugate('create')
836
- expect(create.action).toBe('create')
837
- expect(create.actor).toBe('creator')
838
- expect(create.act).toBe('creates')
839
- expect(create.activity).toBe('creating')
840
- expect(create.result).toBe('creation')
841
- expect(create.inverse).toBe('delete')
842
- })
843
-
844
- it('auto-generates conjugations for unknown verbs', () => {
845
- const approve = conjugate('approve')
846
- expect(approve.action).toBe('approve')
847
- expect(approve.actor).toBe('approver')
848
- expect(approve.act).toBe('approves')
849
- expect(approve.activity).toBe('approving')
850
- expect(approve.reverse?.at).toBe('approvedAt')
851
- expect(approve.reverse?.by).toBe('approvedBy')
852
- })
853
-
854
- it('handles verbs ending in consonant', () => {
855
- const submit = conjugate('submit')
856
- expect(submit.actor).toBe('submitter')
857
- expect(submit.activity).toBe('submitting')
858
- expect(submit.reverse?.at).toBe('submittedAt')
859
- })
860
-
861
- it('handles verbs ending in y', () => {
862
- const apply = conjugate('apply')
863
- expect(apply.actor).toBe('applier')
864
- expect(apply.act).toBe('applies')
865
- expect(apply.activity).toBe('applying')
866
- expect(apply.reverse?.at).toBe('appliedAt')
867
- })
868
-
869
- it('handles -ify verbs', () => {
870
- const verify = conjugate('verify')
871
- expect(verify.result).toBe('verification')
872
- })
873
-
874
- it('handles -ize verbs', () => {
875
- const authorize = conjugate('authorize')
876
- expect(authorize.result).toBe('authorization')
877
- })
878
- })
879
-
880
- describe('pluralize', () => {
881
- it('handles regular plurals', () => {
882
- expect(pluralize('post')).toBe('posts')
883
- expect(pluralize('user')).toBe('users')
884
- expect(pluralize('tag')).toBe('tags')
885
- })
886
-
887
- it('handles -y endings', () => {
888
- expect(pluralize('category')).toBe('categories')
889
- expect(pluralize('company')).toBe('companies')
890
- expect(pluralize('story')).toBe('stories')
891
- })
892
-
893
- it('handles -y with vowel before', () => {
894
- expect(pluralize('day')).toBe('days')
895
- expect(pluralize('key')).toBe('keys')
896
- expect(pluralize('toy')).toBe('toys')
897
- })
898
-
899
- it('handles -s, -x, -z, -ch, -sh endings', () => {
900
- expect(pluralize('class')).toBe('classes')
901
- expect(pluralize('box')).toBe('boxes')
902
- expect(pluralize('quiz')).toBe('quizzes')
903
- expect(pluralize('match')).toBe('matches')
904
- expect(pluralize('wish')).toBe('wishes')
905
- })
906
-
907
- it('handles -f and -fe endings', () => {
908
- expect(pluralize('leaf')).toBe('leaves')
909
- expect(pluralize('knife')).toBe('knives')
910
- expect(pluralize('life')).toBe('lives')
911
- })
912
-
913
- it('handles irregular plurals', () => {
914
- expect(pluralize('person')).toBe('people')
915
- expect(pluralize('child')).toBe('children')
916
- expect(pluralize('man')).toBe('men')
917
- expect(pluralize('woman')).toBe('women')
918
- expect(pluralize('mouse')).toBe('mice')
919
- expect(pluralize('datum')).toBe('data')
920
- expect(pluralize('criterion')).toBe('criteria')
921
- })
922
-
923
- it('preserves case', () => {
924
- expect(pluralize('Person')).toBe('People')
925
- expect(pluralize('Category')).toBe('Categories')
926
- })
927
- })
928
-
929
- describe('singularize', () => {
930
- it('handles regular singulars', () => {
931
- expect(singularize('posts')).toBe('post')
932
- expect(singularize('users')).toBe('user')
933
- expect(singularize('tags')).toBe('tag')
934
- })
935
-
936
- it('handles -ies endings', () => {
937
- expect(singularize('categories')).toBe('category')
938
- expect(singularize('companies')).toBe('company')
939
- expect(singularize('stories')).toBe('story')
940
- })
941
-
942
- it('handles -es endings', () => {
943
- expect(singularize('classes')).toBe('class')
944
- expect(singularize('boxes')).toBe('box')
945
- expect(singularize('matches')).toBe('match')
946
- expect(singularize('wishes')).toBe('wish')
947
- })
948
-
949
- it('handles -ves endings', () => {
950
- expect(singularize('leaves')).toBe('leaf')
951
- expect(singularize('knives')).toBe('knife') // via irregular list
952
- expect(singularize('lives')).toBe('life')
953
- expect(singularize('wolves')).toBe('wolf') // via regular rule
954
- })
955
-
956
- it('handles irregular singulars', () => {
957
- expect(singularize('people')).toBe('person')
958
- expect(singularize('children')).toBe('child')
959
- expect(singularize('men')).toBe('man')
960
- expect(singularize('women')).toBe('woman')
961
- expect(singularize('mice')).toBe('mouse')
962
- expect(singularize('data')).toBe('datum')
963
- expect(singularize('criteria')).toBe('criterion')
964
- })
965
- })
966
-
967
- describe('inferNoun', () => {
968
- it('infers noun from PascalCase type name', () => {
969
- const post = inferNoun('Post')
970
- expect(post.singular).toBe('post')
971
- expect(post.plural).toBe('posts')
972
- })
973
-
974
- it('handles multi-word type names', () => {
975
- const blogPost = inferNoun('BlogPost')
976
- expect(blogPost.singular).toBe('blog post')
977
- expect(blogPost.plural).toBe('blog posts')
978
- })
979
-
980
- it('handles complex type names', () => {
981
- const userProfile = inferNoun('UserProfile')
982
- expect(userProfile.singular).toBe('user profile')
983
- expect(userProfile.plural).toBe('user profiles')
984
- })
985
-
986
- it('includes default actions and events', () => {
987
- const post = inferNoun('Post')
988
- expect(post.actions).toContain('create')
989
- expect(post.actions).toContain('update')
990
- expect(post.actions).toContain('delete')
991
- expect(post.events).toContain('created')
992
- expect(post.events).toContain('updated')
993
- expect(post.events).toContain('deleted')
994
- })
995
-
996
- it('handles irregular pluralization', () => {
997
- const person = inferNoun('Person')
998
- expect(person.singular).toBe('person')
999
- expect(person.plural).toBe('people')
1000
- })
1001
- })
1002
-
1003
- describe('Type and TypeMeta', () => {
1004
- it('creates TypeMeta from type name', () => {
1005
- const meta = Type('Post')
1006
-
1007
- expect(meta.name).toBe('Post')
1008
- expect(meta.singular).toBe('post')
1009
- expect(meta.plural).toBe('posts')
1010
- expect(meta.slug).toBe('post')
1011
- expect(meta.slugPlural).toBe('posts')
1012
- })
1013
-
1014
- it('handles multi-word type names', () => {
1015
- const meta = Type('BlogPost')
1016
-
1017
- expect(meta.singular).toBe('blog post')
1018
- expect(meta.plural).toBe('blog posts')
1019
- expect(meta.slug).toBe('blog-post')
1020
- expect(meta.slugPlural).toBe('blog-posts')
1021
- })
1022
-
1023
- it('provides verb-derived fields', () => {
1024
- const meta = Type('Post')
1025
-
1026
- expect(meta.creator).toBe('creator')
1027
- expect(meta.createdAt).toBe('createdAt')
1028
- expect(meta.createdBy).toBe('createdBy')
1029
- expect(meta.updatedAt).toBe('updatedAt')
1030
- expect(meta.updatedBy).toBe('updatedBy')
1031
- })
1032
-
1033
- it('provides event type names', () => {
1034
- const meta = Type('Post')
1035
-
1036
- expect(meta.created).toBe('Post.created')
1037
- expect(meta.updated).toBe('Post.updated')
1038
- expect(meta.deleted).toBe('Post.deleted')
1039
- })
1040
-
1041
- it('caches TypeMeta instances', () => {
1042
- const meta1 = getTypeMeta('Post')
1043
- const meta2 = getTypeMeta('Post')
1044
-
1045
- expect(meta1).toBe(meta2) // Same instance
1046
- })
1047
-
1048
- it('creates different instances for different types', () => {
1049
- const post = Type('Post')
1050
- const author = Type('Author')
1051
-
1052
- expect(post).not.toBe(author)
1053
- expect(post.name).toBe('Post')
1054
- expect(author.name).toBe('Author')
1055
- })
1056
- })
1057
-
1058
- describe('System Schema', () => {
1059
- it('defines ThingSchema with type relationship', () => {
1060
- expect(ThingSchema).toBeDefined()
1061
- expect(ThingSchema.type).toBe('Noun.things')
1062
- })
1063
-
1064
- it('defines NounSchema with all required fields', () => {
1065
- expect(NounSchema).toBeDefined()
1066
- expect(NounSchema.name).toBe('string')
1067
- expect(NounSchema.singular).toBe('string')
1068
- expect(NounSchema.plural).toBe('string')
1069
- expect(NounSchema.slug).toBe('string')
1070
- expect(NounSchema.description).toBe('string?')
1071
- expect(NounSchema.properties).toBe('json?')
1072
- expect(NounSchema.relationships).toBe('json?')
1073
- expect(NounSchema.actions).toBe('json?')
1074
- expect(NounSchema.events).toBe('json?')
1075
- })
1076
-
1077
- it('defines VerbSchema with conjugation fields', () => {
1078
- expect(VerbSchema).toBeDefined()
1079
- expect(VerbSchema.action).toBe('string')
1080
- expect(VerbSchema.actor).toBe('string?')
1081
- expect(VerbSchema.act).toBe('string?')
1082
- expect(VerbSchema.activity).toBe('string?')
1083
- expect(VerbSchema.result).toBe('string?')
1084
- expect(VerbSchema.reverse).toBe('json?')
1085
- expect(VerbSchema.inverse).toBe('string?')
1086
- })
1087
-
1088
- it('defines EdgeSchema for relationship graph', () => {
1089
- expect(EdgeSchema).toBeDefined()
1090
- expect(EdgeSchema.from).toBe('string')
1091
- expect(EdgeSchema.name).toBe('string')
1092
- expect(EdgeSchema.to).toBe('string')
1093
- expect(EdgeSchema.backref).toBe('string?')
1094
- expect(EdgeSchema.cardinality).toBe('string')
1095
- })
1096
-
1097
- it('combines all system types in SystemSchema', () => {
1098
- expect(SystemSchema).toBeDefined()
1099
- expect(SystemSchema.Thing).toBe(ThingSchema)
1100
- expect(SystemSchema.Noun).toBe(NounSchema)
1101
- expect(SystemSchema.Verb).toBe(VerbSchema)
1102
- expect(SystemSchema.Edge).toBe(EdgeSchema)
1103
- })
1104
- })
1105
-
1106
- describe('createNounRecord', () => {
1107
- it('creates noun record from type name', () => {
1108
- const record = createNounRecord('Post')
1109
-
1110
- expect(record.name).toBe('Post')
1111
- expect(record.singular).toBe('post')
1112
- expect(record.plural).toBe('posts')
1113
- expect(record.slug).toBe('post')
1114
- expect(record.slugPlural).toBe('posts')
1115
- expect(record.actions).toContain('create')
1116
- expect(record.events).toContain('created')
1117
- })
1118
-
1119
- it('creates noun record from multi-word type name', () => {
1120
- const record = createNounRecord('BlogPost')
1121
-
1122
- expect(record.name).toBe('BlogPost')
1123
- expect(record.singular).toBe('blog post')
1124
- expect(record.plural).toBe('blog posts')
1125
- expect(record.slug).toBe('blog-post')
1126
- })
1127
-
1128
- it('includes properties from schema', () => {
1129
- const schema = {
1130
- title: 'string',
1131
- content: 'markdown',
1132
- published: 'boolean?',
1133
- }
1134
- const record = createNounRecord('Post', schema)
1135
-
1136
- expect(record.properties).toBeDefined()
1137
- expect((record.properties as Record<string, unknown>).title).toEqual({
1138
- type: 'string',
1139
- optional: false,
1140
- array: false,
1141
- })
1142
- expect((record.properties as Record<string, unknown>).published).toEqual({
1143
- type: 'boolean',
1144
- optional: true,
1145
- array: false,
1146
- })
1147
- })
1148
-
1149
- it('overrides with custom noun definition', () => {
1150
- const nounDef = {
1151
- singular: 'article',
1152
- plural: 'articles',
1153
- description: 'A news article',
1154
- }
1155
- const record = createNounRecord('Post', undefined, nounDef)
1156
-
1157
- expect(record.singular).toBe('article')
1158
- expect(record.plural).toBe('articles')
1159
- expect(record.description).toBe('A news article')
1160
- })
1161
- })
1162
-
1163
- describe('createEdgeRecords', () => {
1164
- it('creates edge records from relationships', () => {
1165
- const schema: DatabaseSchema = {
1166
- Post: {
1167
- title: 'string',
1168
- author: 'Author.posts',
1169
- },
1170
- Author: {
1171
- name: 'string',
1172
- },
1173
- }
1174
-
1175
- const parsed = parseSchema(schema)
1176
- const postEntity = parsed.entities.get('Post')!
1177
- const edges = createEdgeRecords('Post', schema.Post, postEntity)
1178
-
1179
- expect(edges).toHaveLength(1)
1180
- expect(edges[0]).toEqual({
1181
- from: 'Post',
1182
- name: 'author',
1183
- to: 'Author',
1184
- backref: 'posts',
1185
- cardinality: 'many-to-one',
1186
- })
1187
- })
1188
-
1189
- it('creates many-to-many edges for array relationships', () => {
1190
- const schema: DatabaseSchema = {
1191
- Post: {
1192
- tags: ['Tag.posts'],
1193
- },
1194
- Tag: {
1195
- name: 'string',
1196
- },
1197
- }
1198
-
1199
- const parsed = parseSchema(schema)
1200
- const postEntity = parsed.entities.get('Post')!
1201
- const edges = createEdgeRecords('Post', schema.Post, postEntity)
1202
-
1203
- expect(edges).toHaveLength(1)
1204
- expect(edges[0]?.cardinality).toBe('many-to-many')
1205
- })
1206
-
1207
- it('creates edges without backref', () => {
1208
- const schema: DatabaseSchema = {
1209
- Post: {
1210
- category: 'Category',
1211
- },
1212
- Category: {
1213
- name: 'string',
1214
- },
1215
- }
1216
-
1217
- const parsed = parseSchema(schema)
1218
- const postEntity = parsed.entities.get('Post')!
1219
- const edges = createEdgeRecords('Post', schema.Post, postEntity)
1220
-
1221
- expect(edges).toHaveLength(1)
1222
- expect(edges[0]?.backref).toBeUndefined()
1223
- expect(edges[0]?.cardinality).toBe('one-to-one')
1224
- })
1225
-
1226
- it('returns empty array for schemas without relationships', () => {
1227
- const schema: DatabaseSchema = {
1228
- User: {
1229
- name: 'string',
1230
- email: 'string',
1231
- },
1232
- }
1233
-
1234
- const parsed = parseSchema(schema)
1235
- const userEntity = parsed.entities.get('User')!
1236
- const edges = createEdgeRecords('User', schema.User, userEntity)
1237
-
1238
- expect(edges).toHaveLength(0)
1239
- })
1240
- })
1241
-
1242
- describe('NL Query Infrastructure', () => {
1243
- it('allows setting custom NL query generator', () => {
1244
- const mockGenerator = async (prompt: string, context: unknown): Promise<NLQueryPlan> => ({
1245
- types: ['Post'],
1246
- interpretation: `Search for: ${prompt}`,
1247
- confidence: 0.9,
1248
- })
1249
-
1250
- // Should not throw
1251
- expect(() => setNLQueryGenerator(mockGenerator)).not.toThrow()
1252
- })
1253
- })
1254
- })