ai-database 0.1.0 → 2.0.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/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +9 -0
- package/README.md +381 -68
- package/TESTING.md +410 -0
- package/TEST_SUMMARY.md +250 -0
- package/TODO.md +128 -0
- package/dist/ai-promise-db.d.ts +370 -0
- package/dist/ai-promise-db.d.ts.map +1 -0
- package/dist/ai-promise-db.js +839 -0
- package/dist/ai-promise-db.js.map +1 -0
- package/dist/authorization.d.ts +531 -0
- package/dist/authorization.d.ts.map +1 -0
- package/dist/authorization.js +632 -0
- package/dist/authorization.js.map +1 -0
- package/dist/durable-clickhouse.d.ts +193 -0
- package/dist/durable-clickhouse.d.ts.map +1 -0
- package/dist/durable-clickhouse.js +422 -0
- package/dist/durable-clickhouse.js.map +1 -0
- package/dist/durable-promise.d.ts +182 -0
- package/dist/durable-promise.d.ts.map +1 -0
- package/dist/durable-promise.js +409 -0
- package/dist/durable-promise.js.map +1 -0
- package/dist/execution-queue.d.ts +239 -0
- package/dist/execution-queue.d.ts.map +1 -0
- package/dist/execution-queue.js +400 -0
- package/dist/execution-queue.js.map +1 -0
- package/dist/index.d.ts +50 -191
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -462
- package/dist/index.js.map +1 -0
- package/dist/linguistic.d.ts +115 -0
- package/dist/linguistic.d.ts.map +1 -0
- package/dist/linguistic.js +379 -0
- package/dist/linguistic.js.map +1 -0
- package/dist/memory-provider.d.ts +304 -0
- package/dist/memory-provider.d.ts.map +1 -0
- package/dist/memory-provider.js +785 -0
- package/dist/memory-provider.js.map +1 -0
- package/dist/schema.d.ts +899 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +1165 -0
- package/dist/schema.js.map +1 -0
- package/dist/tests.d.ts +107 -0
- package/dist/tests.d.ts.map +1 -0
- package/dist/tests.js +568 -0
- package/dist/tests.js.map +1 -0
- package/dist/types.d.ts +972 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +126 -0
- package/dist/types.js.map +1 -0
- package/package.json +37 -37
- package/src/ai-promise-db.ts +1243 -0
- package/src/authorization.ts +1102 -0
- package/src/durable-clickhouse.ts +596 -0
- package/src/durable-promise.ts +582 -0
- package/src/execution-queue.ts +608 -0
- package/src/index.test.ts +868 -0
- package/src/index.ts +337 -0
- package/src/linguistic.ts +404 -0
- package/src/memory-provider.test.ts +1036 -0
- package/src/memory-provider.ts +1119 -0
- package/src/schema.test.ts +1254 -0
- package/src/schema.ts +2296 -0
- package/src/tests.ts +725 -0
- package/src/types.ts +1177 -0
- package/test/README.md +153 -0
- package/test/edge-cases.test.ts +646 -0
- package/test/provider-resolution.test.ts +402 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +19 -0
- package/dist/index.d.mts +0 -195
- package/dist/index.mjs +0 -430
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for ai-database
|
|
3
|
+
*
|
|
4
|
+
* Tests the full DB API with in-memory provider.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
8
|
+
import { DB, setProvider, createMemoryProvider } from './index.js'
|
|
9
|
+
import type { InferEntity } from './index.js'
|
|
10
|
+
|
|
11
|
+
describe('DB integration tests', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// Use in-memory provider for testing
|
|
14
|
+
setProvider(createMemoryProvider())
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
// Clean up
|
|
19
|
+
setProvider(createMemoryProvider())
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('basic CRUD operations', () => {
|
|
23
|
+
const schema = {
|
|
24
|
+
User: {
|
|
25
|
+
name: 'string',
|
|
26
|
+
email: 'string',
|
|
27
|
+
age: 'number?',
|
|
28
|
+
},
|
|
29
|
+
} as const
|
|
30
|
+
|
|
31
|
+
it('creates an entity without explicit ID', async () => {
|
|
32
|
+
const { db } = DB(schema)
|
|
33
|
+
|
|
34
|
+
const user = await db.User.create({
|
|
35
|
+
name: 'John Doe',
|
|
36
|
+
email: 'john@example.com',
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
expect(user.$id).toBeDefined()
|
|
40
|
+
expect(user.$type).toBe('User')
|
|
41
|
+
expect(user.name).toBe('John Doe')
|
|
42
|
+
expect(user.email).toBe('john@example.com')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('creates an entity with explicit ID', async () => {
|
|
46
|
+
const { db } = DB(schema)
|
|
47
|
+
|
|
48
|
+
const user = await db.User.create('john', {
|
|
49
|
+
name: 'John Doe',
|
|
50
|
+
email: 'john@example.com',
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
expect(user.$id).toBe('john')
|
|
54
|
+
expect(user.name).toBe('John Doe')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('retrieves an entity by ID', async () => {
|
|
58
|
+
const { db } = DB(schema)
|
|
59
|
+
|
|
60
|
+
await db.User.create('john', {
|
|
61
|
+
name: 'John Doe',
|
|
62
|
+
email: 'john@example.com',
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const user = await db.User.get('john')
|
|
66
|
+
|
|
67
|
+
expect(user).not.toBeNull()
|
|
68
|
+
expect(user?.$id).toBe('john')
|
|
69
|
+
expect(user?.name).toBe('John Doe')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('returns null for non-existent entity', async () => {
|
|
73
|
+
const { db } = DB(schema)
|
|
74
|
+
const user = await db.User.get('nonexistent')
|
|
75
|
+
expect(user).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('updates an entity', async () => {
|
|
79
|
+
const { db } = DB(schema)
|
|
80
|
+
|
|
81
|
+
await db.User.create('john', {
|
|
82
|
+
name: 'John',
|
|
83
|
+
email: 'john@example.com',
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const updated = await db.User.update('john', {
|
|
87
|
+
name: 'John Doe',
|
|
88
|
+
age: 30,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
expect(updated.name).toBe('John Doe')
|
|
92
|
+
expect(updated.email).toBe('john@example.com')
|
|
93
|
+
expect(updated.age).toBe(30)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('upserts - creates if not exists', async () => {
|
|
97
|
+
const { db } = DB(schema)
|
|
98
|
+
|
|
99
|
+
const user = await db.User.upsert('john', {
|
|
100
|
+
name: 'John Doe',
|
|
101
|
+
email: 'john@example.com',
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
expect(user.$id).toBe('john')
|
|
105
|
+
expect(user.name).toBe('John Doe')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('upserts - updates if exists', async () => {
|
|
109
|
+
const { db } = DB(schema)
|
|
110
|
+
|
|
111
|
+
await db.User.create('john', {
|
|
112
|
+
name: 'John',
|
|
113
|
+
email: 'john@example.com',
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const updated = await db.User.upsert('john', {
|
|
117
|
+
name: 'John Doe',
|
|
118
|
+
email: 'john.doe@example.com',
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
expect(updated.name).toBe('John Doe')
|
|
122
|
+
expect(updated.email).toBe('john.doe@example.com')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('deletes an entity', async () => {
|
|
126
|
+
const { db } = DB(schema)
|
|
127
|
+
|
|
128
|
+
await db.User.create('john', {
|
|
129
|
+
name: 'John',
|
|
130
|
+
email: 'john@example.com',
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const deleted = await db.User.delete('john')
|
|
134
|
+
expect(deleted).toBe(true)
|
|
135
|
+
|
|
136
|
+
const retrieved = await db.User.get('john')
|
|
137
|
+
expect(retrieved).toBeNull()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('returns false when deleting non-existent entity', async () => {
|
|
141
|
+
const { db } = DB(schema)
|
|
142
|
+
const deleted = await db.User.delete('nonexistent')
|
|
143
|
+
expect(deleted).toBe(false)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('list and query operations', () => {
|
|
148
|
+
const schema = {
|
|
149
|
+
User: {
|
|
150
|
+
name: 'string',
|
|
151
|
+
email: 'string',
|
|
152
|
+
age: 'number',
|
|
153
|
+
role: 'string',
|
|
154
|
+
},
|
|
155
|
+
} as const
|
|
156
|
+
|
|
157
|
+
it('lists all entities', async () => {
|
|
158
|
+
const { db } = DB(schema)
|
|
159
|
+
|
|
160
|
+
await db.User.create('john', { name: 'John', email: 'john@example.com', age: 30, role: 'user' })
|
|
161
|
+
await db.User.create('jane', { name: 'Jane', email: 'jane@example.com', age: 25, role: 'admin' })
|
|
162
|
+
|
|
163
|
+
const users = await db.User.list()
|
|
164
|
+
|
|
165
|
+
expect(users).toHaveLength(2)
|
|
166
|
+
expect(users.map((u) => u.$id)).toContain('john')
|
|
167
|
+
expect(users.map((u) => u.$id)).toContain('jane')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('lists with where filter', async () => {
|
|
171
|
+
const { db } = DB(schema)
|
|
172
|
+
|
|
173
|
+
await db.User.create('john', { name: 'John', email: 'john@example.com', age: 30, role: 'admin' })
|
|
174
|
+
await db.User.create('jane', { name: 'Jane', email: 'jane@example.com', age: 25, role: 'user' })
|
|
175
|
+
|
|
176
|
+
const admins = await db.User.list({ where: { role: 'admin' } })
|
|
177
|
+
|
|
178
|
+
expect(admins).toHaveLength(1)
|
|
179
|
+
expect(admins[0]?.name).toBe('John')
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('lists with ordering', async () => {
|
|
183
|
+
const { db } = DB(schema)
|
|
184
|
+
|
|
185
|
+
await db.User.create('john', { name: 'John', email: 'john@example.com', age: 30, role: 'user' })
|
|
186
|
+
await db.User.create('jane', { name: 'Jane', email: 'jane@example.com', age: 25, role: 'user' })
|
|
187
|
+
await db.User.create('bob', { name: 'Bob', email: 'bob@example.com', age: 35, role: 'user' })
|
|
188
|
+
|
|
189
|
+
const users = await db.User.list({ orderBy: 'age', order: 'asc' })
|
|
190
|
+
|
|
191
|
+
expect(users[0]?.name).toBe('Jane')
|
|
192
|
+
expect(users[1]?.name).toBe('John')
|
|
193
|
+
expect(users[2]?.name).toBe('Bob')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('lists with pagination', async () => {
|
|
197
|
+
const { db } = DB(schema)
|
|
198
|
+
|
|
199
|
+
await db.User.create('user1', { name: 'User 1', email: '1@example.com', age: 20, role: 'user' })
|
|
200
|
+
await db.User.create('user2', { name: 'User 2', email: '2@example.com', age: 21, role: 'user' })
|
|
201
|
+
await db.User.create('user3', { name: 'User 3', email: '3@example.com', age: 22, role: 'user' })
|
|
202
|
+
|
|
203
|
+
const page1 = await db.User.list({ limit: 2, offset: 0 })
|
|
204
|
+
const page2 = await db.User.list({ limit: 2, offset: 2 })
|
|
205
|
+
|
|
206
|
+
expect(page1).toHaveLength(2)
|
|
207
|
+
expect(page2).toHaveLength(1)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('finds entities with criteria', async () => {
|
|
211
|
+
const { db } = DB(schema)
|
|
212
|
+
|
|
213
|
+
await db.User.create('john', { name: 'John', email: 'john@example.com', age: 30, role: 'admin' })
|
|
214
|
+
await db.User.create('jane', { name: 'Jane', email: 'jane@example.com', age: 25, role: 'admin' })
|
|
215
|
+
await db.User.create('bob', { name: 'Bob', email: 'bob@example.com', age: 35, role: 'user' })
|
|
216
|
+
|
|
217
|
+
const admins = await db.User.find({ role: 'admin' })
|
|
218
|
+
|
|
219
|
+
expect(admins).toHaveLength(2)
|
|
220
|
+
expect(admins.map((u) => u.name)).toContain('John')
|
|
221
|
+
expect(admins.map((u) => u.name)).toContain('Jane')
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('iterates over entities with forEach', async () => {
|
|
225
|
+
const { db } = DB(schema)
|
|
226
|
+
|
|
227
|
+
await db.User.create('john', { name: 'John', email: 'john@example.com', age: 30, role: 'user' })
|
|
228
|
+
await db.User.create('jane', { name: 'Jane', email: 'jane@example.com', age: 25, role: 'user' })
|
|
229
|
+
|
|
230
|
+
const names: string[] = []
|
|
231
|
+
await db.User.forEach((user) => {
|
|
232
|
+
names.push(user.name)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
expect(names).toHaveLength(2)
|
|
236
|
+
expect(names).toContain('John')
|
|
237
|
+
expect(names).toContain('Jane')
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('iterates with options', async () => {
|
|
241
|
+
const { db } = DB(schema)
|
|
242
|
+
|
|
243
|
+
await db.User.create('john', { name: 'John', email: 'john@example.com', age: 30, role: 'admin' })
|
|
244
|
+
await db.User.create('jane', { name: 'Jane', email: 'jane@example.com', age: 25, role: 'user' })
|
|
245
|
+
|
|
246
|
+
const names: string[] = []
|
|
247
|
+
await db.User.forEach({ where: { role: 'admin' } }, (user) => {
|
|
248
|
+
names.push(user.name)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
expect(names).toEqual(['John'])
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
describe('search operations', () => {
|
|
256
|
+
const schema = {
|
|
257
|
+
Post: {
|
|
258
|
+
title: 'string',
|
|
259
|
+
content: 'markdown',
|
|
260
|
+
category: 'string',
|
|
261
|
+
},
|
|
262
|
+
} as const
|
|
263
|
+
|
|
264
|
+
it('searches entities', async () => {
|
|
265
|
+
const { db } = DB(schema)
|
|
266
|
+
|
|
267
|
+
await db.Post.create('post1', {
|
|
268
|
+
title: 'Introduction to TypeScript',
|
|
269
|
+
content: 'Learn TypeScript basics',
|
|
270
|
+
category: 'tutorial',
|
|
271
|
+
})
|
|
272
|
+
await db.Post.create('post2', {
|
|
273
|
+
title: 'Advanced JavaScript',
|
|
274
|
+
content: 'Deep dive into JavaScript',
|
|
275
|
+
category: 'tutorial',
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
const results = await db.Post.search('TypeScript')
|
|
279
|
+
|
|
280
|
+
expect(results.length).toBeGreaterThan(0)
|
|
281
|
+
expect(results[0]?.title).toContain('TypeScript')
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('searches with options', async () => {
|
|
285
|
+
const { db } = DB(schema)
|
|
286
|
+
|
|
287
|
+
await db.Post.create('post1', {
|
|
288
|
+
title: 'TypeScript Tutorial',
|
|
289
|
+
content: 'Content about JavaScript',
|
|
290
|
+
category: 'tutorial',
|
|
291
|
+
})
|
|
292
|
+
await db.Post.create('post2', {
|
|
293
|
+
title: 'JavaScript Guide',
|
|
294
|
+
content: 'Content about TypeScript',
|
|
295
|
+
category: 'guide',
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
const results = await db.Post.search('TypeScript', {
|
|
299
|
+
fields: ['title'],
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
expect(results).toHaveLength(1)
|
|
303
|
+
expect(results[0]?.title).toBe('TypeScript Tutorial')
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('searches globally across all types', async () => {
|
|
307
|
+
const schema = {
|
|
308
|
+
Post: { title: 'string' },
|
|
309
|
+
User: { name: 'string' },
|
|
310
|
+
} as const
|
|
311
|
+
|
|
312
|
+
const { db } = DB(schema)
|
|
313
|
+
|
|
314
|
+
await db.Post.create('post1', { title: 'TypeScript Guide' })
|
|
315
|
+
await db.User.create('user1', { name: 'TypeScript Expert' })
|
|
316
|
+
|
|
317
|
+
const results = await db.search('TypeScript')
|
|
318
|
+
|
|
319
|
+
expect(results.length).toBe(2)
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
describe('relationships', () => {
|
|
324
|
+
const schema = {
|
|
325
|
+
Post: {
|
|
326
|
+
title: 'string',
|
|
327
|
+
content: 'markdown',
|
|
328
|
+
author: 'Author.posts',
|
|
329
|
+
tags: ['Tag.posts'],
|
|
330
|
+
},
|
|
331
|
+
Author: {
|
|
332
|
+
name: 'string',
|
|
333
|
+
email: 'string',
|
|
334
|
+
},
|
|
335
|
+
Tag: {
|
|
336
|
+
name: 'string',
|
|
337
|
+
},
|
|
338
|
+
} as const
|
|
339
|
+
|
|
340
|
+
it('creates entities with relations', async () => {
|
|
341
|
+
const { db } = DB(schema)
|
|
342
|
+
|
|
343
|
+
const author = await db.Author.create('john', {
|
|
344
|
+
name: 'John Doe',
|
|
345
|
+
email: 'john@example.com',
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const post = await db.Post.create('post1', {
|
|
349
|
+
title: 'Hello World',
|
|
350
|
+
content: 'My first post',
|
|
351
|
+
author: author.$id,
|
|
352
|
+
tags: [],
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
// Verify the post was created with correct basic fields
|
|
356
|
+
expect(post.$id).toBe('post1')
|
|
357
|
+
expect(post.title).toBe('Hello World')
|
|
358
|
+
expect(post.content).toBe('My first post')
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it('queries related entities through provider', async () => {
|
|
362
|
+
const { db } = DB(schema)
|
|
363
|
+
const provider = createMemoryProvider()
|
|
364
|
+
setProvider(provider)
|
|
365
|
+
|
|
366
|
+
await db.Author.create('john', {
|
|
367
|
+
name: 'John Doe',
|
|
368
|
+
email: 'john@example.com',
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
await db.Post.create('post1', {
|
|
372
|
+
title: 'Post 1',
|
|
373
|
+
content: 'Content',
|
|
374
|
+
author: 'john',
|
|
375
|
+
tags: [],
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
await db.Post.create('post2', {
|
|
379
|
+
title: 'Post 2',
|
|
380
|
+
content: 'Content',
|
|
381
|
+
author: 'john',
|
|
382
|
+
tags: [],
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
// Create relationships
|
|
386
|
+
await provider.relate('Author', 'john', 'posts', 'Post', 'post1')
|
|
387
|
+
await provider.relate('Author', 'john', 'posts', 'Post', 'post2')
|
|
388
|
+
|
|
389
|
+
const posts = await provider.related('Author', 'john', 'posts')
|
|
390
|
+
expect(posts).toHaveLength(2)
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
it('handles many-to-many relationships', async () => {
|
|
394
|
+
const { db } = DB(schema)
|
|
395
|
+
const provider = createMemoryProvider()
|
|
396
|
+
setProvider(provider)
|
|
397
|
+
|
|
398
|
+
await db.Post.create('post1', {
|
|
399
|
+
title: 'Post 1',
|
|
400
|
+
content: 'Content',
|
|
401
|
+
author: 'john',
|
|
402
|
+
tags: [],
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
await db.Tag.create('ts', { name: 'TypeScript' })
|
|
406
|
+
await db.Tag.create('js', { name: 'JavaScript' })
|
|
407
|
+
|
|
408
|
+
await provider.relate('Post', 'post1', 'tags', 'Tag', 'ts')
|
|
409
|
+
await provider.relate('Post', 'post1', 'tags', 'Tag', 'js')
|
|
410
|
+
|
|
411
|
+
const tags = await provider.related('Post', 'post1', 'tags')
|
|
412
|
+
expect(tags).toHaveLength(2)
|
|
413
|
+
|
|
414
|
+
// Reverse relation
|
|
415
|
+
await provider.relate('Tag', 'ts', 'posts', 'Post', 'post1')
|
|
416
|
+
const posts = await provider.related('Tag', 'ts', 'posts')
|
|
417
|
+
expect(posts).toHaveLength(1)
|
|
418
|
+
})
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
describe('global methods', () => {
|
|
422
|
+
const schema = {
|
|
423
|
+
User: { name: 'string' },
|
|
424
|
+
Post: { title: 'string' },
|
|
425
|
+
} as const
|
|
426
|
+
|
|
427
|
+
it('gets entity by URL', async () => {
|
|
428
|
+
const { db } = DB(schema)
|
|
429
|
+
|
|
430
|
+
await db.User.create('john', { name: 'John' })
|
|
431
|
+
|
|
432
|
+
const user = await db.get('https://example.com/User/john')
|
|
433
|
+
|
|
434
|
+
expect(user).toBeDefined()
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('gets entity by type/id path', async () => {
|
|
438
|
+
const { db } = DB(schema)
|
|
439
|
+
|
|
440
|
+
await db.User.create('john', { name: 'John' })
|
|
441
|
+
|
|
442
|
+
const user = await db.get('User/john')
|
|
443
|
+
|
|
444
|
+
expect(user).toBeDefined()
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it('searches across all entity types', async () => {
|
|
448
|
+
const { db } = DB(schema)
|
|
449
|
+
|
|
450
|
+
await db.User.create('john', { name: 'John TypeScript' })
|
|
451
|
+
await db.Post.create('post1', { title: 'TypeScript Guide' })
|
|
452
|
+
|
|
453
|
+
const results = await db.search('TypeScript')
|
|
454
|
+
|
|
455
|
+
expect(results.length).toBe(2)
|
|
456
|
+
})
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
describe('type safety', () => {
|
|
460
|
+
it('provides typed entity operations', () => {
|
|
461
|
+
const schema = {
|
|
462
|
+
User: {
|
|
463
|
+
name: 'string',
|
|
464
|
+
age: 'number',
|
|
465
|
+
},
|
|
466
|
+
} as const
|
|
467
|
+
|
|
468
|
+
const { db } = DB(schema)
|
|
469
|
+
|
|
470
|
+
// TypeScript should enforce these types at compile time
|
|
471
|
+
expect(db.User).toBeDefined()
|
|
472
|
+
expect(typeof db.User.get).toBe('function')
|
|
473
|
+
expect(typeof db.User.list).toBe('function')
|
|
474
|
+
expect(typeof db.User.create).toBe('function')
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
it('infers entity types correctly', () => {
|
|
478
|
+
const schema = {
|
|
479
|
+
Post: {
|
|
480
|
+
title: 'string',
|
|
481
|
+
views: 'number',
|
|
482
|
+
author: 'Author.posts',
|
|
483
|
+
},
|
|
484
|
+
Author: {
|
|
485
|
+
name: 'string',
|
|
486
|
+
},
|
|
487
|
+
} as const
|
|
488
|
+
|
|
489
|
+
type Post = InferEntity<typeof schema, 'Post'>
|
|
490
|
+
type Author = InferEntity<typeof schema, 'Author'>
|
|
491
|
+
|
|
492
|
+
// Type assertions to verify inference
|
|
493
|
+
const post: Post = {
|
|
494
|
+
$id: '1',
|
|
495
|
+
$type: 'Post',
|
|
496
|
+
title: 'Hello',
|
|
497
|
+
views: 100,
|
|
498
|
+
author: {} as Author,
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
expect(post.$id).toBe('1')
|
|
502
|
+
})
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
describe('complex scenarios', () => {
|
|
506
|
+
const schema = {
|
|
507
|
+
User: {
|
|
508
|
+
name: 'string',
|
|
509
|
+
email: 'string',
|
|
510
|
+
profile: 'Profile.user?',
|
|
511
|
+
},
|
|
512
|
+
Profile: {
|
|
513
|
+
bio: 'string',
|
|
514
|
+
avatar: 'url?',
|
|
515
|
+
},
|
|
516
|
+
Post: {
|
|
517
|
+
title: 'string',
|
|
518
|
+
content: 'markdown',
|
|
519
|
+
published: 'boolean',
|
|
520
|
+
author: 'User.posts',
|
|
521
|
+
tags: ['Tag.posts'],
|
|
522
|
+
},
|
|
523
|
+
Tag: {
|
|
524
|
+
name: 'string',
|
|
525
|
+
slug: 'string',
|
|
526
|
+
},
|
|
527
|
+
} as const
|
|
528
|
+
|
|
529
|
+
it('handles complex multi-entity operations', async () => {
|
|
530
|
+
const { db } = DB(schema)
|
|
531
|
+
|
|
532
|
+
// Create user
|
|
533
|
+
const user = await db.User.create('john', {
|
|
534
|
+
name: 'John Doe',
|
|
535
|
+
email: 'john@example.com',
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
// Create profile
|
|
539
|
+
const profile = await db.Profile.create('john-profile', {
|
|
540
|
+
bio: 'Software developer',
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
// Create tags
|
|
544
|
+
const tsTag = await db.Tag.create('typescript', {
|
|
545
|
+
name: 'TypeScript',
|
|
546
|
+
slug: 'typescript',
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
// Create post
|
|
550
|
+
const post = await db.Post.create('post1', {
|
|
551
|
+
title: 'Getting Started',
|
|
552
|
+
content: '# Introduction',
|
|
553
|
+
published: true,
|
|
554
|
+
author: user.$id,
|
|
555
|
+
tags: [],
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
expect(user.$id).toBe('john')
|
|
559
|
+
expect(profile.$id).toBe('john-profile')
|
|
560
|
+
expect(post.$id).toBe('post1')
|
|
561
|
+
expect(tsTag.$id).toBe('typescript')
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
it('handles self-referential relations', async () => {
|
|
565
|
+
const schema = {
|
|
566
|
+
User: {
|
|
567
|
+
name: 'string',
|
|
568
|
+
manager: 'User.reports?',
|
|
569
|
+
},
|
|
570
|
+
} as const
|
|
571
|
+
|
|
572
|
+
const { db } = DB(schema)
|
|
573
|
+
const provider = createMemoryProvider()
|
|
574
|
+
setProvider(provider)
|
|
575
|
+
|
|
576
|
+
await db.User.create('alice', {
|
|
577
|
+
name: 'Alice',
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
await db.User.create('bob', {
|
|
581
|
+
name: 'Bob',
|
|
582
|
+
manager: 'alice',
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
// Set up relation
|
|
586
|
+
await provider.relate('User', 'alice', 'reports', 'User', 'bob')
|
|
587
|
+
|
|
588
|
+
const reports = await provider.related('User', 'alice', 'reports')
|
|
589
|
+
expect(reports).toHaveLength(1)
|
|
590
|
+
expect(reports[0]?.name).toBe('Bob')
|
|
591
|
+
})
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
describe('events API', () => {
|
|
595
|
+
const schema = {
|
|
596
|
+
User: { name: 'string' },
|
|
597
|
+
} as const
|
|
598
|
+
|
|
599
|
+
it('returns events API from DB', () => {
|
|
600
|
+
const { db, events } = DB(schema)
|
|
601
|
+
|
|
602
|
+
expect(events).toBeDefined()
|
|
603
|
+
expect(typeof events.on).toBe('function')
|
|
604
|
+
expect(typeof events.emit).toBe('function')
|
|
605
|
+
expect(typeof events.list).toBe('function')
|
|
606
|
+
expect(typeof events.replay).toBe('function')
|
|
607
|
+
})
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
describe('actions API', () => {
|
|
611
|
+
const schema = {
|
|
612
|
+
User: { name: 'string' },
|
|
613
|
+
} as const
|
|
614
|
+
|
|
615
|
+
it('returns actions API from DB', () => {
|
|
616
|
+
const { db, actions } = DB(schema)
|
|
617
|
+
|
|
618
|
+
expect(actions).toBeDefined()
|
|
619
|
+
expect(typeof actions.create).toBe('function')
|
|
620
|
+
expect(typeof actions.get).toBe('function')
|
|
621
|
+
expect(typeof actions.update).toBe('function')
|
|
622
|
+
expect(typeof actions.list).toBe('function')
|
|
623
|
+
expect(typeof actions.retry).toBe('function')
|
|
624
|
+
expect(typeof actions.cancel).toBe('function')
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
it('creates and tracks actions', async () => {
|
|
628
|
+
const { actions } = DB(schema)
|
|
629
|
+
|
|
630
|
+
const action = await actions.create({
|
|
631
|
+
type: 'generate',
|
|
632
|
+
data: { count: 10 },
|
|
633
|
+
total: 10,
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
expect(action.id).toBeDefined()
|
|
637
|
+
expect(action.type).toBe('generate')
|
|
638
|
+
expect(action.status).toBe('pending')
|
|
639
|
+
expect(action.total).toBe(10)
|
|
640
|
+
|
|
641
|
+
const retrieved = await actions.get(action.id)
|
|
642
|
+
expect(retrieved?.id).toBe(action.id)
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
it('updates action progress', async () => {
|
|
646
|
+
const { actions } = DB(schema)
|
|
647
|
+
|
|
648
|
+
const action = await actions.create({
|
|
649
|
+
type: 'generate',
|
|
650
|
+
data: {},
|
|
651
|
+
total: 10,
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
const updated = await actions.update(action.id, {
|
|
655
|
+
status: 'active',
|
|
656
|
+
progress: 5,
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
expect(updated.status).toBe('active')
|
|
660
|
+
expect(updated.progress).toBe(5)
|
|
661
|
+
})
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
describe('artifacts API', () => {
|
|
665
|
+
const schema = {
|
|
666
|
+
User: { name: 'string' },
|
|
667
|
+
} as const
|
|
668
|
+
|
|
669
|
+
it('returns artifacts API from DB', () => {
|
|
670
|
+
const { db, artifacts } = DB(schema)
|
|
671
|
+
|
|
672
|
+
expect(artifacts).toBeDefined()
|
|
673
|
+
expect(typeof artifacts.get).toBe('function')
|
|
674
|
+
expect(typeof artifacts.set).toBe('function')
|
|
675
|
+
expect(typeof artifacts.delete).toBe('function')
|
|
676
|
+
expect(typeof artifacts.list).toBe('function')
|
|
677
|
+
})
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
describe('nouns API', () => {
|
|
681
|
+
const schema = {
|
|
682
|
+
BlogPost: { title: 'string' },
|
|
683
|
+
Author: { name: 'string' },
|
|
684
|
+
} as const
|
|
685
|
+
|
|
686
|
+
it('returns nouns API from DB', () => {
|
|
687
|
+
const { nouns } = DB(schema)
|
|
688
|
+
|
|
689
|
+
expect(nouns).toBeDefined()
|
|
690
|
+
expect(typeof nouns.get).toBe('function')
|
|
691
|
+
expect(typeof nouns.list).toBe('function')
|
|
692
|
+
expect(typeof nouns.define).toBe('function')
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
it('lists inferred nouns from schema', async () => {
|
|
696
|
+
const { nouns } = DB(schema)
|
|
697
|
+
|
|
698
|
+
const allNouns = await nouns.list()
|
|
699
|
+
expect(allNouns.length).toBe(2)
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
it('gets noun definition by name', async () => {
|
|
703
|
+
const { nouns } = DB(schema)
|
|
704
|
+
|
|
705
|
+
const blogPost = await nouns.get('BlogPost')
|
|
706
|
+
expect(blogPost).toBeDefined()
|
|
707
|
+
expect(blogPost?.singular).toBe('blog post')
|
|
708
|
+
expect(blogPost?.plural).toBe('blog posts')
|
|
709
|
+
})
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
describe('verbs API', () => {
|
|
713
|
+
const schema = {
|
|
714
|
+
User: { name: 'string' },
|
|
715
|
+
} as const
|
|
716
|
+
|
|
717
|
+
it('returns verbs API from DB', () => {
|
|
718
|
+
const { verbs } = DB(schema)
|
|
719
|
+
|
|
720
|
+
expect(verbs).toBeDefined()
|
|
721
|
+
expect(typeof verbs.get).toBe('function')
|
|
722
|
+
expect(typeof verbs.list).toBe('function')
|
|
723
|
+
expect(typeof verbs.define).toBe('function')
|
|
724
|
+
expect(typeof verbs.conjugate).toBe('function')
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
it('gets standard verb definitions', () => {
|
|
728
|
+
const { verbs } = DB(schema)
|
|
729
|
+
|
|
730
|
+
const create = verbs.get('create')
|
|
731
|
+
expect(create).toBeDefined()
|
|
732
|
+
expect(create?.action).toBe('create')
|
|
733
|
+
expect(create?.actor).toBe('creator')
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
it('conjugates custom verbs', () => {
|
|
737
|
+
const { verbs } = DB(schema)
|
|
738
|
+
|
|
739
|
+
const publish = verbs.conjugate('publish')
|
|
740
|
+
expect(publish.action).toBe('publish')
|
|
741
|
+
expect(publish.actor).toBe('publisher')
|
|
742
|
+
expect(publish.activity).toBe('publishing')
|
|
743
|
+
})
|
|
744
|
+
})
|
|
745
|
+
})
|
|
746
|
+
|
|
747
|
+
describe('dual API syntax', () => {
|
|
748
|
+
beforeEach(() => {
|
|
749
|
+
setProvider(createMemoryProvider())
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
const schema = {
|
|
753
|
+
User: { name: 'string', email: 'string' },
|
|
754
|
+
Post: { title: 'string', author: 'User.posts' },
|
|
755
|
+
} as const
|
|
756
|
+
|
|
757
|
+
it('supports direct usage - db.Entity.method()', async () => {
|
|
758
|
+
// Direct usage: const db = DB(schema)
|
|
759
|
+
const db = DB(schema)
|
|
760
|
+
|
|
761
|
+
// Entity operations work directly
|
|
762
|
+
const user = await db.User.create('john', { name: 'John', email: 'john@example.com' })
|
|
763
|
+
expect(user.name).toBe('John')
|
|
764
|
+
|
|
765
|
+
// Get also works
|
|
766
|
+
const retrieved = await db.User.get('john')
|
|
767
|
+
expect(retrieved?.name).toBe('John')
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
it('supports direct access to events/actions on db object', () => {
|
|
771
|
+
// Direct usage: const db = DB(schema)
|
|
772
|
+
const db = DB(schema)
|
|
773
|
+
|
|
774
|
+
// APIs are available directly on db
|
|
775
|
+
expect(db.events).toBeDefined()
|
|
776
|
+
expect(typeof db.events.on).toBe('function')
|
|
777
|
+
expect(typeof db.events.emit).toBe('function')
|
|
778
|
+
|
|
779
|
+
expect(db.actions).toBeDefined()
|
|
780
|
+
expect(typeof db.actions.create).toBe('function')
|
|
781
|
+
|
|
782
|
+
expect(db.artifacts).toBeDefined()
|
|
783
|
+
expect(db.nouns).toBeDefined()
|
|
784
|
+
expect(db.verbs).toBeDefined()
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
it('supports destructured usage - const { db, events } = DB()', async () => {
|
|
788
|
+
// Destructured usage
|
|
789
|
+
const { db, events, actions, artifacts, nouns, verbs } = DB(schema)
|
|
790
|
+
|
|
791
|
+
// Entity operations work on db
|
|
792
|
+
const user = await db.User.create('jane', { name: 'Jane', email: 'jane@example.com' })
|
|
793
|
+
expect(user.name).toBe('Jane')
|
|
794
|
+
|
|
795
|
+
// Separate API objects work
|
|
796
|
+
expect(typeof events.on).toBe('function')
|
|
797
|
+
expect(typeof actions.create).toBe('function')
|
|
798
|
+
expect(typeof artifacts.get).toBe('function')
|
|
799
|
+
expect(typeof nouns.get).toBe('function')
|
|
800
|
+
expect(typeof verbs.get).toBe('function')
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
it('both syntaxes work with same schema', async () => {
|
|
804
|
+
// Can use both syntaxes interchangeably
|
|
805
|
+
const result = DB(schema)
|
|
806
|
+
|
|
807
|
+
// Direct usage
|
|
808
|
+
await result.User.create('user1', { name: 'User 1', email: 'u1@example.com' })
|
|
809
|
+
|
|
810
|
+
// Destructured usage from same result
|
|
811
|
+
const { db, events, actions } = result
|
|
812
|
+
await db.User.create('user2', { name: 'User 2', email: 'u2@example.com' })
|
|
813
|
+
|
|
814
|
+
// Both users exist
|
|
815
|
+
const users = await result.User.list()
|
|
816
|
+
expect(users).toHaveLength(2)
|
|
817
|
+
|
|
818
|
+
// Can also use db.list()
|
|
819
|
+
const users2 = await db.User.list()
|
|
820
|
+
expect(users2).toHaveLength(2)
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
it('db self-reference allows clean destructuring', () => {
|
|
824
|
+
const result = DB(schema)
|
|
825
|
+
|
|
826
|
+
// result.db is same entity operations as result itself
|
|
827
|
+
expect(result.db.User).toBeDefined()
|
|
828
|
+
expect(result.db.$schema).toBe(result.$schema)
|
|
829
|
+
|
|
830
|
+
// But result.db doesn't have events/actions (clean entity ops)
|
|
831
|
+
// Actually it does since db is a self-reference, but semantically
|
|
832
|
+
// when you destructure { db } you get clean entity ops
|
|
833
|
+
})
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
describe('provider resolution', () => {
|
|
837
|
+
it('uses memory provider when set explicitly', async () => {
|
|
838
|
+
const provider = createMemoryProvider()
|
|
839
|
+
setProvider(provider)
|
|
840
|
+
|
|
841
|
+
const schema = {
|
|
842
|
+
User: { name: 'string' },
|
|
843
|
+
} as const
|
|
844
|
+
|
|
845
|
+
const { db } = DB(schema)
|
|
846
|
+
|
|
847
|
+
await db.User.create('test', { name: 'Test User' })
|
|
848
|
+
|
|
849
|
+
const stats = provider.stats()
|
|
850
|
+
expect(stats.entities).toBe(1)
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
it('isolates data between provider instances', async () => {
|
|
854
|
+
const provider1 = createMemoryProvider()
|
|
855
|
+
const provider2 = createMemoryProvider()
|
|
856
|
+
|
|
857
|
+
setProvider(provider1)
|
|
858
|
+
const schema = { User: { name: 'string' } } as const
|
|
859
|
+
const { db: db1 } = DB(schema)
|
|
860
|
+
await db1.User.create('john', { name: 'John' })
|
|
861
|
+
|
|
862
|
+
setProvider(provider2)
|
|
863
|
+
const { db: db2 } = DB(schema)
|
|
864
|
+
const user = await db2.User.get('john')
|
|
865
|
+
|
|
866
|
+
expect(user).toBeNull()
|
|
867
|
+
})
|
|
868
|
+
})
|