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.
- package/CHANGELOG.md +43 -0
- package/dist/actions.d.ts +247 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +260 -0
- package/dist/actions.js.map +1 -0
- package/dist/ai-promise-db.d.ts +34 -2
- package/dist/ai-promise-db.d.ts.map +1 -1
- package/dist/ai-promise-db.js +511 -66
- package/dist/ai-promise-db.js.map +1 -1
- package/dist/constants.d.ts +16 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +16 -0
- package/dist/constants.js.map +1 -0
- package/dist/events.d.ts +153 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +154 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/memory-provider.d.ts +144 -2
- package/dist/memory-provider.d.ts.map +1 -1
- package/dist/memory-provider.js +569 -13
- package/dist/memory-provider.js.map +1 -1
- package/dist/schema/cascade.d.ts +96 -0
- package/dist/schema/cascade.d.ts.map +1 -0
- package/dist/schema/cascade.js +528 -0
- package/dist/schema/cascade.js.map +1 -0
- package/dist/schema/index.d.ts +197 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +1211 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/parse.d.ts +225 -0
- package/dist/schema/parse.d.ts.map +1 -0
- package/dist/schema/parse.js +732 -0
- package/dist/schema/parse.js.map +1 -0
- package/dist/schema/provider.d.ts +176 -0
- package/dist/schema/provider.d.ts.map +1 -0
- package/dist/schema/provider.js +258 -0
- package/dist/schema/provider.js.map +1 -0
- package/dist/schema/resolve.d.ts +87 -0
- package/dist/schema/resolve.d.ts.map +1 -0
- package/dist/schema/resolve.js +474 -0
- package/dist/schema/resolve.js.map +1 -0
- package/dist/schema/semantic.d.ts +53 -0
- package/dist/schema/semantic.d.ts.map +1 -0
- package/dist/schema/semantic.js +247 -0
- package/dist/schema/semantic.js.map +1 -0
- package/dist/schema/types.d.ts +528 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +9 -0
- package/dist/schema/types.js.map +1 -0
- package/dist/schema.d.ts +24 -867
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +41 -1124
- package/dist/schema.js.map +1 -1
- package/dist/semantic.d.ts +175 -0
- package/dist/semantic.d.ts.map +1 -0
- package/dist/semantic.js +338 -0
- package/dist/semantic.js.map +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +13 -4
- package/.turbo/turbo-build.log +0 -5
- package/TESTING.md +0 -410
- package/TEST_SUMMARY.md +0 -250
- package/TODO.md +0 -128
- package/src/ai-promise-db.ts +0 -1243
- package/src/authorization.ts +0 -1102
- package/src/durable-clickhouse.ts +0 -596
- package/src/durable-promise.ts +0 -582
- package/src/execution-queue.ts +0 -608
- package/src/index.test.ts +0 -868
- package/src/index.ts +0 -337
- package/src/linguistic.ts +0 -404
- package/src/memory-provider.test.ts +0 -1036
- package/src/memory-provider.ts +0 -1119
- package/src/schema.test.ts +0 -1254
- package/src/schema.ts +0 -2296
- package/src/tests.ts +0 -725
- package/src/types.ts +0 -1177
- package/test/README.md +0 -153
- package/test/edge-cases.test.ts +0 -646
- package/test/provider-resolution.test.ts +0 -402
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -19
package/src/schema.test.ts
DELETED
|
@@ -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
|
-
})
|