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/tests.ts
DELETED
|
@@ -1,725 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified Compliance Test Suite for Database Adapters
|
|
3
|
-
*
|
|
4
|
-
* Provides reusable test factories that any DBClient/DBClientExtended
|
|
5
|
-
* implementation can use to verify compliance with the interface.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```ts
|
|
9
|
-
* import { createTests } from 'ai-database/tests'
|
|
10
|
-
* import { createClickHouseDatabase } from '@mdxdb/clickhouse'
|
|
11
|
-
*
|
|
12
|
-
* createTests('ClickHouse', () => createClickHouseDatabase({ url: '...' }))
|
|
13
|
-
* ```
|
|
14
|
-
*
|
|
15
|
-
* @packageDocumentation
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'
|
|
19
|
-
import type {
|
|
20
|
-
DBClient,
|
|
21
|
-
DBClientExtended,
|
|
22
|
-
Thing,
|
|
23
|
-
Relationship,
|
|
24
|
-
QueryOptions,
|
|
25
|
-
CreateOptions,
|
|
26
|
-
Event,
|
|
27
|
-
Action,
|
|
28
|
-
Artifact,
|
|
29
|
-
} from './types.js'
|
|
30
|
-
|
|
31
|
-
// =============================================================================
|
|
32
|
-
// Test Fixtures
|
|
33
|
-
// =============================================================================
|
|
34
|
-
|
|
35
|
-
export const fixtures = {
|
|
36
|
-
/** Sample namespace for tests */
|
|
37
|
-
ns: 'test.example.com',
|
|
38
|
-
|
|
39
|
-
/** Sample users */
|
|
40
|
-
users: [
|
|
41
|
-
{ id: 'user-1', name: 'Alice', email: 'alice@example.com', role: 'admin' },
|
|
42
|
-
{ id: 'user-2', name: 'Bob', email: 'bob@example.com', role: 'user' },
|
|
43
|
-
{ id: 'user-3', name: 'Charlie', email: 'charlie@example.com', role: 'user' },
|
|
44
|
-
],
|
|
45
|
-
|
|
46
|
-
/** Sample posts */
|
|
47
|
-
posts: [
|
|
48
|
-
{ id: 'post-1', title: 'Hello World', content: 'First post', authorId: 'user-1' },
|
|
49
|
-
{ id: 'post-2', title: 'Second Post', content: 'More content', authorId: 'user-1' },
|
|
50
|
-
{ id: 'post-3', title: 'Bobs Post', content: 'From Bob', authorId: 'user-2' },
|
|
51
|
-
],
|
|
52
|
-
|
|
53
|
-
/** Sample tags */
|
|
54
|
-
tags: [
|
|
55
|
-
{ id: 'tag-1', name: 'typescript' },
|
|
56
|
-
{ id: 'tag-2', name: 'database' },
|
|
57
|
-
{ id: 'tag-3', name: 'testing' },
|
|
58
|
-
],
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// =============================================================================
|
|
62
|
-
// Test Options
|
|
63
|
-
// =============================================================================
|
|
64
|
-
|
|
65
|
-
export interface CreateTestsOptions<T extends DBClient = DBClient> {
|
|
66
|
-
/** Factory function to create the client */
|
|
67
|
-
factory: () => T | Promise<T>
|
|
68
|
-
/** Optional cleanup function called after all tests */
|
|
69
|
-
cleanup?: () => void | Promise<void>
|
|
70
|
-
/** Skip certain test groups */
|
|
71
|
-
skip?: {
|
|
72
|
-
relationships?: boolean
|
|
73
|
-
search?: boolean
|
|
74
|
-
events?: boolean
|
|
75
|
-
actions?: boolean
|
|
76
|
-
artifacts?: boolean
|
|
77
|
-
}
|
|
78
|
-
/** Custom namespace (defaults to fixtures.ns) */
|
|
79
|
-
ns?: string
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// =============================================================================
|
|
83
|
-
// Main Test Factory
|
|
84
|
-
// =============================================================================
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Create a comprehensive test suite for a DBClient implementation
|
|
88
|
-
*
|
|
89
|
-
* @example Basic usage
|
|
90
|
-
* ```ts
|
|
91
|
-
* import { createTests } from 'ai-database/tests'
|
|
92
|
-
*
|
|
93
|
-
* createTests('Memory', {
|
|
94
|
-
* factory: () => new MemoryDBClient()
|
|
95
|
-
* })
|
|
96
|
-
* ```
|
|
97
|
-
*
|
|
98
|
-
* @example With cleanup
|
|
99
|
-
* ```ts
|
|
100
|
-
* createTests('SQLite', {
|
|
101
|
-
* factory: async () => {
|
|
102
|
-
* const db = await createSQLiteClient({ path: ':memory:' })
|
|
103
|
-
* return db
|
|
104
|
-
* },
|
|
105
|
-
* cleanup: async () => {
|
|
106
|
-
* // cleanup temp files
|
|
107
|
-
* }
|
|
108
|
-
* })
|
|
109
|
-
* ```
|
|
110
|
-
*
|
|
111
|
-
* @example Skip certain tests
|
|
112
|
-
* ```ts
|
|
113
|
-
* createTests('Filesystem', {
|
|
114
|
-
* factory: () => createFsClient({ root: tempDir }),
|
|
115
|
-
* skip: { events: true, actions: true, artifacts: true }
|
|
116
|
-
* })
|
|
117
|
-
* ```
|
|
118
|
-
*/
|
|
119
|
-
export function createTests<T extends DBClient>(
|
|
120
|
-
name: string,
|
|
121
|
-
options: CreateTestsOptions<T>
|
|
122
|
-
): void {
|
|
123
|
-
const { factory, cleanup, skip = {}, ns = fixtures.ns } = options
|
|
124
|
-
|
|
125
|
-
describe(`${name} Compliance Tests`, () => {
|
|
126
|
-
let client: T
|
|
127
|
-
|
|
128
|
-
beforeAll(async () => {
|
|
129
|
-
client = await factory()
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
afterAll(async () => {
|
|
133
|
-
await client.close?.()
|
|
134
|
-
await cleanup?.()
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
// =========================================================================
|
|
138
|
-
// Thing CRUD Operations
|
|
139
|
-
// =========================================================================
|
|
140
|
-
|
|
141
|
-
describe('Things - CRUD', () => {
|
|
142
|
-
const testThing = {
|
|
143
|
-
ns,
|
|
144
|
-
type: 'TestEntity',
|
|
145
|
-
id: 'crud-test-1',
|
|
146
|
-
data: { name: 'Test', value: 42 },
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
afterAll(async () => {
|
|
150
|
-
// Cleanup test data
|
|
151
|
-
try {
|
|
152
|
-
await client.delete(`https://${ns}/TestEntity/crud-test-1`)
|
|
153
|
-
await client.delete(`https://${ns}/TestEntity/crud-test-2`)
|
|
154
|
-
} catch {
|
|
155
|
-
// Ignore cleanup errors
|
|
156
|
-
}
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
it('creates a thing', async () => {
|
|
160
|
-
const thing = await client.create(testThing)
|
|
161
|
-
|
|
162
|
-
expect(thing).toBeDefined()
|
|
163
|
-
expect(thing.ns).toBe(ns)
|
|
164
|
-
expect(thing.type).toBe('TestEntity')
|
|
165
|
-
expect(thing.id).toBe('crud-test-1')
|
|
166
|
-
expect(thing.data.name).toBe('Test')
|
|
167
|
-
expect(thing.data.value).toBe(42)
|
|
168
|
-
expect(thing.createdAt).toBeInstanceOf(Date)
|
|
169
|
-
expect(thing.updatedAt).toBeInstanceOf(Date)
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
it('gets a thing by URL', async () => {
|
|
173
|
-
const thing = await client.get(`https://${ns}/TestEntity/crud-test-1`)
|
|
174
|
-
|
|
175
|
-
expect(thing).not.toBeNull()
|
|
176
|
-
expect(thing?.data.name).toBe('Test')
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
it('gets a thing by ID components', async () => {
|
|
180
|
-
const thing = await client.getById(ns, 'TestEntity', 'crud-test-1')
|
|
181
|
-
|
|
182
|
-
expect(thing).not.toBeNull()
|
|
183
|
-
expect(thing?.data.name).toBe('Test')
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
it('returns null for non-existent thing', async () => {
|
|
187
|
-
const thing = await client.get(`https://${ns}/TestEntity/does-not-exist`)
|
|
188
|
-
|
|
189
|
-
expect(thing).toBeNull()
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
it('updates a thing', async () => {
|
|
193
|
-
const updated = await client.update(`https://${ns}/TestEntity/crud-test-1`, {
|
|
194
|
-
data: { name: 'Updated', value: 100 },
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
expect(updated.data.name).toBe('Updated')
|
|
198
|
-
expect(updated.data.value).toBe(100)
|
|
199
|
-
expect(updated.updatedAt.getTime()).toBeGreaterThanOrEqual(updated.createdAt.getTime())
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it('upserts a new thing', async () => {
|
|
203
|
-
const thing = await client.upsert({
|
|
204
|
-
ns,
|
|
205
|
-
type: 'TestEntity',
|
|
206
|
-
id: 'crud-test-2',
|
|
207
|
-
data: { name: 'Upserted', value: 200 },
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
expect(thing.id).toBe('crud-test-2')
|
|
211
|
-
expect(thing.data.name).toBe('Upserted')
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
it('upserts an existing thing', async () => {
|
|
215
|
-
const thing = await client.upsert({
|
|
216
|
-
ns,
|
|
217
|
-
type: 'TestEntity',
|
|
218
|
-
id: 'crud-test-2',
|
|
219
|
-
data: { name: 'Upserted Again', value: 300 },
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
expect(thing.data.name).toBe('Upserted Again')
|
|
223
|
-
expect(thing.data.value).toBe(300)
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
it('deletes a thing', async () => {
|
|
227
|
-
// Create then delete
|
|
228
|
-
await client.create({
|
|
229
|
-
ns,
|
|
230
|
-
type: 'TestEntity',
|
|
231
|
-
id: 'to-delete',
|
|
232
|
-
data: { temp: true },
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
const deleted = await client.delete(`https://${ns}/TestEntity/to-delete`)
|
|
236
|
-
expect(deleted).toBe(true)
|
|
237
|
-
|
|
238
|
-
const thing = await client.get(`https://${ns}/TestEntity/to-delete`)
|
|
239
|
-
expect(thing).toBeNull()
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
it('returns false when deleting non-existent thing', async () => {
|
|
243
|
-
const deleted = await client.delete(`https://${ns}/TestEntity/never-existed`)
|
|
244
|
-
expect(deleted).toBe(false)
|
|
245
|
-
})
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
// =========================================================================
|
|
249
|
-
// Query Operations
|
|
250
|
-
// =========================================================================
|
|
251
|
-
|
|
252
|
-
describe('Things - Queries', () => {
|
|
253
|
-
beforeAll(async () => {
|
|
254
|
-
// Seed test data
|
|
255
|
-
for (const user of fixtures.users) {
|
|
256
|
-
await client.upsert({
|
|
257
|
-
ns,
|
|
258
|
-
type: 'User',
|
|
259
|
-
id: user.id,
|
|
260
|
-
data: user,
|
|
261
|
-
})
|
|
262
|
-
}
|
|
263
|
-
for (const post of fixtures.posts) {
|
|
264
|
-
await client.upsert({
|
|
265
|
-
ns,
|
|
266
|
-
type: 'Post',
|
|
267
|
-
id: post.id,
|
|
268
|
-
data: post,
|
|
269
|
-
})
|
|
270
|
-
}
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
afterAll(async () => {
|
|
274
|
-
// Cleanup
|
|
275
|
-
for (const user of fixtures.users) {
|
|
276
|
-
try { await client.delete(`https://${ns}/User/${user.id}`) } catch { /* ignore */ }
|
|
277
|
-
}
|
|
278
|
-
for (const post of fixtures.posts) {
|
|
279
|
-
try { await client.delete(`https://${ns}/Post/${post.id}`) } catch { /* ignore */ }
|
|
280
|
-
}
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
it('lists all things', async () => {
|
|
284
|
-
const things = await client.list({ ns })
|
|
285
|
-
|
|
286
|
-
expect(things.length).toBeGreaterThanOrEqual(fixtures.users.length + fixtures.posts.length)
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
it('lists things by type', async () => {
|
|
290
|
-
const users = await client.list({ ns, type: 'User' })
|
|
291
|
-
|
|
292
|
-
expect(users.length).toBe(fixtures.users.length)
|
|
293
|
-
expect(users.every(u => u.type === 'User')).toBe(true)
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
it('finds things with where clause', async () => {
|
|
297
|
-
const admins = await client.find({
|
|
298
|
-
ns,
|
|
299
|
-
type: 'User',
|
|
300
|
-
where: { role: 'admin' },
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
expect(admins.length).toBe(1)
|
|
304
|
-
expect(admins[0]?.data.name).toBe('Alice')
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
it('lists with limit', async () => {
|
|
308
|
-
const things = await client.list({ ns, type: 'User', limit: 2 })
|
|
309
|
-
|
|
310
|
-
expect(things.length).toBe(2)
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
it('lists with offset', async () => {
|
|
314
|
-
const all = await client.list({ ns, type: 'User', orderBy: 'id' })
|
|
315
|
-
const offset = await client.list({ ns, type: 'User', orderBy: 'id', offset: 1, limit: 2 })
|
|
316
|
-
|
|
317
|
-
expect(offset.length).toBe(2)
|
|
318
|
-
expect(offset[0]?.id).toBe(all[1]?.id)
|
|
319
|
-
})
|
|
320
|
-
|
|
321
|
-
it('lists with ordering', async () => {
|
|
322
|
-
const asc = await client.list({ ns, type: 'User', orderBy: 'id', order: 'asc' })
|
|
323
|
-
const desc = await client.list({ ns, type: 'User', orderBy: 'id', order: 'desc' })
|
|
324
|
-
|
|
325
|
-
expect(asc[0]?.id).toBe('user-1')
|
|
326
|
-
expect(desc[0]?.id).toBe('user-3')
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
it('iterates with forEach', async () => {
|
|
330
|
-
const ids: string[] = []
|
|
331
|
-
|
|
332
|
-
await client.forEach({ ns, type: 'User' }, (thing) => {
|
|
333
|
-
ids.push(thing.id)
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
expect(ids.length).toBe(fixtures.users.length)
|
|
337
|
-
})
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
// =========================================================================
|
|
341
|
-
// Search Operations
|
|
342
|
-
// =========================================================================
|
|
343
|
-
|
|
344
|
-
if (!skip.search) {
|
|
345
|
-
describe('Things - Search', () => {
|
|
346
|
-
beforeAll(async () => {
|
|
347
|
-
// Ensure test data exists
|
|
348
|
-
for (const post of fixtures.posts) {
|
|
349
|
-
await client.upsert({
|
|
350
|
-
ns,
|
|
351
|
-
type: 'Post',
|
|
352
|
-
id: post.id,
|
|
353
|
-
data: post,
|
|
354
|
-
})
|
|
355
|
-
}
|
|
356
|
-
})
|
|
357
|
-
|
|
358
|
-
it('searches by query string', async () => {
|
|
359
|
-
const results = await client.search({
|
|
360
|
-
ns,
|
|
361
|
-
type: 'Post',
|
|
362
|
-
query: 'Hello',
|
|
363
|
-
})
|
|
364
|
-
|
|
365
|
-
expect(results.length).toBeGreaterThanOrEqual(1)
|
|
366
|
-
expect(results.some(r => r.data.title === 'Hello World')).toBe(true)
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
it('searches across fields', async () => {
|
|
370
|
-
const results = await client.search({
|
|
371
|
-
ns,
|
|
372
|
-
type: 'Post',
|
|
373
|
-
query: 'Bob',
|
|
374
|
-
})
|
|
375
|
-
|
|
376
|
-
expect(results.some(r => r.data.title === 'Bobs Post')).toBe(true)
|
|
377
|
-
})
|
|
378
|
-
})
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// =========================================================================
|
|
382
|
-
// Relationship Operations
|
|
383
|
-
// =========================================================================
|
|
384
|
-
|
|
385
|
-
if (!skip.relationships) {
|
|
386
|
-
describe('Relationships', () => {
|
|
387
|
-
const authorUrl = `https://${ns}/User/user-1`
|
|
388
|
-
const postUrl = `https://${ns}/Post/post-1`
|
|
389
|
-
|
|
390
|
-
beforeAll(async () => {
|
|
391
|
-
// Ensure entities exist
|
|
392
|
-
await client.upsert({ ns, type: 'User', id: 'user-1', data: fixtures.users[0]! })
|
|
393
|
-
await client.upsert({ ns, type: 'Post', id: 'post-1', data: fixtures.posts[0]! })
|
|
394
|
-
})
|
|
395
|
-
|
|
396
|
-
afterAll(async () => {
|
|
397
|
-
try { await client.unrelate(postUrl, 'author', authorUrl) } catch { /* ignore */ }
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
it('creates a relationship', async () => {
|
|
401
|
-
const rel = await client.relate({
|
|
402
|
-
type: 'author',
|
|
403
|
-
from: postUrl,
|
|
404
|
-
to: authorUrl,
|
|
405
|
-
})
|
|
406
|
-
|
|
407
|
-
expect(rel).toBeDefined()
|
|
408
|
-
expect(rel.type).toBe('author')
|
|
409
|
-
expect(rel.from).toBe(postUrl)
|
|
410
|
-
expect(rel.to).toBe(authorUrl)
|
|
411
|
-
})
|
|
412
|
-
|
|
413
|
-
it('queries outbound related things', async () => {
|
|
414
|
-
const authors = await client.related(postUrl, 'author', 'to')
|
|
415
|
-
|
|
416
|
-
expect(authors.length).toBe(1)
|
|
417
|
-
expect(authors[0]?.id).toBe('user-1')
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
it('queries inbound references', async () => {
|
|
421
|
-
const posts = await client.references(authorUrl, 'author')
|
|
422
|
-
|
|
423
|
-
expect(posts.length).toBeGreaterThanOrEqual(1)
|
|
424
|
-
expect(posts.some(p => p.id === 'post-1')).toBe(true)
|
|
425
|
-
})
|
|
426
|
-
|
|
427
|
-
it('lists relationships', async () => {
|
|
428
|
-
const rels = await client.relationships(postUrl, 'author')
|
|
429
|
-
|
|
430
|
-
expect(rels.length).toBeGreaterThanOrEqual(1)
|
|
431
|
-
expect(rels[0]?.type).toBe('author')
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
it('removes a relationship', async () => {
|
|
435
|
-
const removed = await client.unrelate(postUrl, 'author', authorUrl)
|
|
436
|
-
expect(removed).toBe(true)
|
|
437
|
-
|
|
438
|
-
const rels = await client.relationships(postUrl, 'author')
|
|
439
|
-
expect(rels.length).toBe(0)
|
|
440
|
-
})
|
|
441
|
-
})
|
|
442
|
-
}
|
|
443
|
-
})
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// =============================================================================
|
|
447
|
-
// Extended Test Factory (Events, Actions, Artifacts)
|
|
448
|
-
// =============================================================================
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Create extended tests for DBClientExtended implementations
|
|
452
|
-
*
|
|
453
|
-
* Includes all DBClient tests plus Events, Actions, and Artifacts
|
|
454
|
-
*
|
|
455
|
-
* @example
|
|
456
|
-
* ```ts
|
|
457
|
-
* import { createExtendedTests } from 'ai-database/tests'
|
|
458
|
-
*
|
|
459
|
-
* createExtendedTests('ClickHouse', {
|
|
460
|
-
* factory: () => createClickHouseDatabase({ url: '...' })
|
|
461
|
-
* })
|
|
462
|
-
* ```
|
|
463
|
-
*/
|
|
464
|
-
export function createExtendedTests<T extends DBClientExtended>(
|
|
465
|
-
name: string,
|
|
466
|
-
options: CreateTestsOptions<T>
|
|
467
|
-
): void {
|
|
468
|
-
const { factory, cleanup, skip = {}, ns = fixtures.ns } = options
|
|
469
|
-
|
|
470
|
-
// Run base DBClient tests
|
|
471
|
-
createTests(name, options as CreateTestsOptions<DBClient>)
|
|
472
|
-
|
|
473
|
-
describe(`${name} Extended Compliance Tests`, () => {
|
|
474
|
-
let client: T
|
|
475
|
-
|
|
476
|
-
beforeAll(async () => {
|
|
477
|
-
client = await factory()
|
|
478
|
-
})
|
|
479
|
-
|
|
480
|
-
afterAll(async () => {
|
|
481
|
-
await client.close?.()
|
|
482
|
-
await cleanup?.()
|
|
483
|
-
})
|
|
484
|
-
|
|
485
|
-
// =========================================================================
|
|
486
|
-
// Event Operations
|
|
487
|
-
// =========================================================================
|
|
488
|
-
|
|
489
|
-
if (!skip.events) {
|
|
490
|
-
describe('Events', () => {
|
|
491
|
-
let eventId: string
|
|
492
|
-
|
|
493
|
-
it('tracks an event', async () => {
|
|
494
|
-
const event = await client.track({
|
|
495
|
-
type: 'User.created',
|
|
496
|
-
source: 'test-suite',
|
|
497
|
-
data: { userId: 'user-1', action: 'signup' },
|
|
498
|
-
})
|
|
499
|
-
|
|
500
|
-
eventId = event.id
|
|
501
|
-
|
|
502
|
-
expect(event).toBeDefined()
|
|
503
|
-
expect(event.id).toBeDefined()
|
|
504
|
-
expect(event.type).toBe('User.created')
|
|
505
|
-
expect(event.source).toBe('test-suite')
|
|
506
|
-
expect(event.timestamp).toBeInstanceOf(Date)
|
|
507
|
-
})
|
|
508
|
-
|
|
509
|
-
it('gets an event by ID', async () => {
|
|
510
|
-
const event = await client.getEvent(eventId)
|
|
511
|
-
|
|
512
|
-
expect(event).not.toBeNull()
|
|
513
|
-
expect(event?.type).toBe('User.created')
|
|
514
|
-
})
|
|
515
|
-
|
|
516
|
-
it('queries events by type', async () => {
|
|
517
|
-
const events = await client.queryEvents({ type: 'User.created' })
|
|
518
|
-
|
|
519
|
-
expect(events.length).toBeGreaterThanOrEqual(1)
|
|
520
|
-
expect(events.every(e => e.type === 'User.created')).toBe(true)
|
|
521
|
-
})
|
|
522
|
-
|
|
523
|
-
it('queries events by source', async () => {
|
|
524
|
-
const events = await client.queryEvents({ source: 'test-suite' })
|
|
525
|
-
|
|
526
|
-
expect(events.length).toBeGreaterThanOrEqual(1)
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
it('tracks event with correlation ID', async () => {
|
|
530
|
-
const event = await client.track({
|
|
531
|
-
type: 'Order.placed',
|
|
532
|
-
source: 'test-suite',
|
|
533
|
-
data: { orderId: 'order-1' },
|
|
534
|
-
correlationId: 'session-123',
|
|
535
|
-
})
|
|
536
|
-
|
|
537
|
-
expect(event.correlationId).toBe('session-123')
|
|
538
|
-
|
|
539
|
-
const related = await client.queryEvents({ correlationId: 'session-123' })
|
|
540
|
-
expect(related.some(e => e.id === event.id)).toBe(true)
|
|
541
|
-
})
|
|
542
|
-
})
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// =========================================================================
|
|
546
|
-
// Action Operations
|
|
547
|
-
// =========================================================================
|
|
548
|
-
|
|
549
|
-
if (!skip.actions) {
|
|
550
|
-
describe('Actions', () => {
|
|
551
|
-
let actionId: string
|
|
552
|
-
|
|
553
|
-
it('sends an action (pending)', async () => {
|
|
554
|
-
const action = await client.send({
|
|
555
|
-
actor: 'user:test-user',
|
|
556
|
-
object: `https://${ns}/Order/order-1`,
|
|
557
|
-
action: 'approve',
|
|
558
|
-
})
|
|
559
|
-
|
|
560
|
-
actionId = action.id
|
|
561
|
-
|
|
562
|
-
expect(action).toBeDefined()
|
|
563
|
-
expect(action.status).toBe('pending')
|
|
564
|
-
expect(action.actor).toBe('user:test-user')
|
|
565
|
-
expect(action.action).toBe('approve')
|
|
566
|
-
})
|
|
567
|
-
|
|
568
|
-
it('does an action (active)', async () => {
|
|
569
|
-
const action = await client.do({
|
|
570
|
-
actor: 'user:test-user',
|
|
571
|
-
object: `https://${ns}/Order/order-2`,
|
|
572
|
-
action: 'process',
|
|
573
|
-
})
|
|
574
|
-
|
|
575
|
-
expect(action.status).toBe('active')
|
|
576
|
-
expect(action.startedAt).toBeInstanceOf(Date)
|
|
577
|
-
})
|
|
578
|
-
|
|
579
|
-
it('gets an action by ID', async () => {
|
|
580
|
-
const action = await client.getAction(actionId)
|
|
581
|
-
|
|
582
|
-
expect(action).not.toBeNull()
|
|
583
|
-
expect(action?.action).toBe('approve')
|
|
584
|
-
})
|
|
585
|
-
|
|
586
|
-
it('starts a pending action', async () => {
|
|
587
|
-
const started = await client.startAction(actionId)
|
|
588
|
-
|
|
589
|
-
expect(started.status).toBe('active')
|
|
590
|
-
expect(started.startedAt).toBeInstanceOf(Date)
|
|
591
|
-
})
|
|
592
|
-
|
|
593
|
-
it('completes an action', async () => {
|
|
594
|
-
const completed = await client.completeAction(actionId, { approved: true })
|
|
595
|
-
|
|
596
|
-
expect(completed.status).toBe('completed')
|
|
597
|
-
expect(completed.completedAt).toBeInstanceOf(Date)
|
|
598
|
-
expect(completed.result).toEqual({ approved: true })
|
|
599
|
-
})
|
|
600
|
-
|
|
601
|
-
it('fails an action', async () => {
|
|
602
|
-
const action = await client.do({
|
|
603
|
-
actor: 'system',
|
|
604
|
-
object: `https://${ns}/Task/task-1`,
|
|
605
|
-
action: 'process',
|
|
606
|
-
})
|
|
607
|
-
|
|
608
|
-
const failed = await client.failAction(action.id, 'Connection timeout')
|
|
609
|
-
|
|
610
|
-
expect(failed.status).toBe('failed')
|
|
611
|
-
expect(failed.error).toBe('Connection timeout')
|
|
612
|
-
})
|
|
613
|
-
|
|
614
|
-
it('cancels an action', async () => {
|
|
615
|
-
const action = await client.send({
|
|
616
|
-
actor: 'user:test-user',
|
|
617
|
-
object: `https://${ns}/Report/report-1`,
|
|
618
|
-
action: 'generate',
|
|
619
|
-
})
|
|
620
|
-
|
|
621
|
-
const cancelled = await client.cancelAction(action.id)
|
|
622
|
-
|
|
623
|
-
expect(cancelled.status).toBe('cancelled')
|
|
624
|
-
})
|
|
625
|
-
|
|
626
|
-
it('queries actions by status', async () => {
|
|
627
|
-
const completed = await client.queryActions({ status: 'completed' })
|
|
628
|
-
|
|
629
|
-
expect(completed.every(a => a.status === 'completed')).toBe(true)
|
|
630
|
-
})
|
|
631
|
-
|
|
632
|
-
it('queries actions by actor', async () => {
|
|
633
|
-
const actions = await client.queryActions({ actor: 'user:test-user' })
|
|
634
|
-
|
|
635
|
-
expect(actions.every(a => a.actor === 'user:test-user')).toBe(true)
|
|
636
|
-
})
|
|
637
|
-
})
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// =========================================================================
|
|
641
|
-
// Artifact Operations
|
|
642
|
-
// =========================================================================
|
|
643
|
-
|
|
644
|
-
if (!skip.artifacts) {
|
|
645
|
-
describe('Artifacts', () => {
|
|
646
|
-
const artifactKey = 'test-artifact-1'
|
|
647
|
-
|
|
648
|
-
afterAll(async () => {
|
|
649
|
-
try { await client.deleteArtifact(artifactKey) } catch { /* ignore */ }
|
|
650
|
-
})
|
|
651
|
-
|
|
652
|
-
it('stores an artifact', async () => {
|
|
653
|
-
const artifact = await client.storeArtifact({
|
|
654
|
-
key: artifactKey,
|
|
655
|
-
type: 'esm',
|
|
656
|
-
source: `https://${ns}/Module/module-1`,
|
|
657
|
-
sourceHash: 'abc123',
|
|
658
|
-
content: 'export const foo = 42',
|
|
659
|
-
})
|
|
660
|
-
|
|
661
|
-
expect(artifact).toBeDefined()
|
|
662
|
-
expect(artifact.key).toBe(artifactKey)
|
|
663
|
-
expect(artifact.type).toBe('esm')
|
|
664
|
-
expect(artifact.content).toBe('export const foo = 42')
|
|
665
|
-
})
|
|
666
|
-
|
|
667
|
-
it('gets an artifact by key', async () => {
|
|
668
|
-
const artifact = await client.getArtifact(artifactKey)
|
|
669
|
-
|
|
670
|
-
expect(artifact).not.toBeNull()
|
|
671
|
-
expect(artifact?.content).toBe('export const foo = 42')
|
|
672
|
-
})
|
|
673
|
-
|
|
674
|
-
it('gets artifact by source', async () => {
|
|
675
|
-
const artifact = await client.getArtifactBySource(
|
|
676
|
-
`https://${ns}/Module/module-1`,
|
|
677
|
-
'esm'
|
|
678
|
-
)
|
|
679
|
-
|
|
680
|
-
expect(artifact).not.toBeNull()
|
|
681
|
-
expect(artifact?.key).toBe(artifactKey)
|
|
682
|
-
})
|
|
683
|
-
|
|
684
|
-
it('deletes an artifact', async () => {
|
|
685
|
-
await client.storeArtifact({
|
|
686
|
-
key: 'to-delete',
|
|
687
|
-
type: 'ast',
|
|
688
|
-
source: `https://${ns}/File/file-1`,
|
|
689
|
-
sourceHash: 'xyz789',
|
|
690
|
-
content: { type: 'Program', body: [] },
|
|
691
|
-
})
|
|
692
|
-
|
|
693
|
-
const deleted = await client.deleteArtifact('to-delete')
|
|
694
|
-
expect(deleted).toBe(true)
|
|
695
|
-
|
|
696
|
-
const artifact = await client.getArtifact('to-delete')
|
|
697
|
-
expect(artifact).toBeNull()
|
|
698
|
-
})
|
|
699
|
-
|
|
700
|
-
it('stores artifact with TTL', async () => {
|
|
701
|
-
const artifact = await client.storeArtifact({
|
|
702
|
-
key: 'ttl-artifact',
|
|
703
|
-
type: 'html',
|
|
704
|
-
source: `https://${ns}/Page/page-1`,
|
|
705
|
-
sourceHash: 'def456',
|
|
706
|
-
content: '<html></html>',
|
|
707
|
-
ttl: 60000, // 1 minute
|
|
708
|
-
})
|
|
709
|
-
|
|
710
|
-
expect(artifact.expiresAt).toBeInstanceOf(Date)
|
|
711
|
-
expect(artifact.expiresAt!.getTime()).toBeGreaterThan(Date.now())
|
|
712
|
-
|
|
713
|
-
// Cleanup
|
|
714
|
-
await client.deleteArtifact('ttl-artifact')
|
|
715
|
-
})
|
|
716
|
-
})
|
|
717
|
-
}
|
|
718
|
-
})
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// =============================================================================
|
|
722
|
-
// Re-export for convenience
|
|
723
|
-
// =============================================================================
|
|
724
|
-
|
|
725
|
-
export { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'
|