ai-database 0.0.0-development → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-test.log +102 -0
  3. package/README.md +402 -47
  4. package/TESTING.md +410 -0
  5. package/TEST_SUMMARY.md +250 -0
  6. package/TODO.md +128 -0
  7. package/dist/ai-promise-db.d.ts +370 -0
  8. package/dist/ai-promise-db.d.ts.map +1 -0
  9. package/dist/ai-promise-db.js +839 -0
  10. package/dist/ai-promise-db.js.map +1 -0
  11. package/dist/authorization.d.ts +531 -0
  12. package/dist/authorization.d.ts.map +1 -0
  13. package/dist/authorization.js +632 -0
  14. package/dist/authorization.js.map +1 -0
  15. package/dist/durable-clickhouse.d.ts +193 -0
  16. package/dist/durable-clickhouse.d.ts.map +1 -0
  17. package/dist/durable-clickhouse.js +422 -0
  18. package/dist/durable-clickhouse.js.map +1 -0
  19. package/dist/durable-promise.d.ts +182 -0
  20. package/dist/durable-promise.d.ts.map +1 -0
  21. package/dist/durable-promise.js +409 -0
  22. package/dist/durable-promise.js.map +1 -0
  23. package/dist/execution-queue.d.ts +239 -0
  24. package/dist/execution-queue.d.ts.map +1 -0
  25. package/dist/execution-queue.js +400 -0
  26. package/dist/execution-queue.js.map +1 -0
  27. package/dist/index.d.ts +54 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +79 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/linguistic.d.ts +115 -0
  32. package/dist/linguistic.d.ts.map +1 -0
  33. package/dist/linguistic.js +379 -0
  34. package/dist/linguistic.js.map +1 -0
  35. package/dist/memory-provider.d.ts +304 -0
  36. package/dist/memory-provider.d.ts.map +1 -0
  37. package/dist/memory-provider.js +785 -0
  38. package/dist/memory-provider.js.map +1 -0
  39. package/dist/schema.d.ts +899 -0
  40. package/dist/schema.d.ts.map +1 -0
  41. package/dist/schema.js +1165 -0
  42. package/dist/schema.js.map +1 -0
  43. package/dist/tests.d.ts +107 -0
  44. package/dist/tests.d.ts.map +1 -0
  45. package/dist/tests.js +568 -0
  46. package/dist/tests.js.map +1 -0
  47. package/dist/types.d.ts +972 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +126 -0
  50. package/dist/types.js.map +1 -0
  51. package/package.json +37 -23
  52. package/src/ai-promise-db.ts +1243 -0
  53. package/src/authorization.ts +1102 -0
  54. package/src/durable-clickhouse.ts +596 -0
  55. package/src/durable-promise.ts +582 -0
  56. package/src/execution-queue.ts +608 -0
  57. package/src/index.test.ts +868 -0
  58. package/src/index.ts +337 -0
  59. package/src/linguistic.ts +404 -0
  60. package/src/memory-provider.test.ts +1036 -0
  61. package/src/memory-provider.ts +1119 -0
  62. package/src/schema.test.ts +1254 -0
  63. package/src/schema.ts +2296 -0
  64. package/src/tests.ts +725 -0
  65. package/src/types.ts +1177 -0
  66. package/test/README.md +153 -0
  67. package/test/edge-cases.test.ts +646 -0
  68. package/test/provider-resolution.test.ts +402 -0
  69. package/tsconfig.json +9 -0
  70. package/vitest.config.ts +19 -0
  71. package/LICENSE +0 -21
  72. package/dist/types/database.d.ts +0 -46
  73. package/dist/types/document.d.ts +0 -15
  74. package/dist/types/index.d.ts +0 -5
  75. package/dist/types/mdxdb/embedding.d.ts +0 -7
  76. package/dist/types/mdxdb/types.d.ts +0 -59
  77. package/dist/types/synthetic.d.ts +0 -9
  78. package/dist/types/tools.d.ts +0 -10
  79. package/dist/types/vector.d.ts +0 -16
package/src/schema.ts ADDED
@@ -0,0 +1,2296 @@
1
+ /**
2
+ * Schema-first Database Definition
3
+ *
4
+ * Declarative schema with automatic bi-directional relationships.
5
+ * Uses mdxld conventions for entity structure.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const { db } = DB({
10
+ * Post: {
11
+ * title: 'string',
12
+ * author: 'Author.posts', // one-to-many: Post.author -> Author, Author.posts -> Post[]
13
+ * tags: ['Tag.posts'], // many-to-many: Post.tags -> Tag[], Tag.posts -> Post[]
14
+ * },
15
+ * Author: {
16
+ * name: 'string',
17
+ * // posts: Post[] auto-created from backref
18
+ * },
19
+ * Tag: {
20
+ * name: 'string',
21
+ * // posts: Post[] auto-created from backref
22
+ * }
23
+ * })
24
+ *
25
+ * // Typed access
26
+ * const post = await db.Post.get('123')
27
+ * post.author // Author (single)
28
+ * post.tags // Tag[] (array)
29
+ * ```
30
+ */
31
+
32
+ import type { MDXLD } from 'mdxld'
33
+ import { DBPromise, wrapEntityOperations, type ForEachOptions, type ForEachResult } from './ai-promise-db.js'
34
+
35
+ // =============================================================================
36
+ // Re-exports from modular files
37
+ // =============================================================================
38
+
39
+ // Re-export types from types.ts
40
+ export type {
41
+ ThingFlat,
42
+ ThingExpanded,
43
+ PrimitiveType,
44
+ FieldDefinition,
45
+ EntitySchema,
46
+ DatabaseSchema,
47
+ ParsedField,
48
+ ParsedEntity,
49
+ ParsedSchema,
50
+ Verb,
51
+ Noun,
52
+ NounProperty,
53
+ NounRelationship,
54
+ TypeMeta,
55
+ // Graph Database Types
56
+ EntityId,
57
+ Thing,
58
+ Relationship,
59
+ // Query Types
60
+ QueryOptions,
61
+ ThingSearchOptions,
62
+ CreateOptions,
63
+ UpdateOptions,
64
+ RelateOptions,
65
+ // Event/Action/Artifact Types
66
+ Event,
67
+ ActionStatus,
68
+ Action,
69
+ ArtifactType,
70
+ Artifact,
71
+ // Options Types (Note: CreateEventOptions, CreateActionOptions defined locally below)
72
+ StoreArtifactOptions,
73
+ EventQueryOptions,
74
+ ActionQueryOptions,
75
+ // Client Interfaces
76
+ DBClient,
77
+ DBClientExtended,
78
+ // Import with aliases to avoid conflict with local definitions
79
+ CreateEventOptions as GraphCreateEventOptions,
80
+ CreateActionOptions as GraphCreateActionOptions,
81
+ } from './types.js'
82
+
83
+ export { toExpanded, toFlat, Verbs, resolveUrl, resolveShortUrl, parseUrl } from './types.js'
84
+
85
+ // Re-export linguistic utilities from linguistic.ts
86
+ export {
87
+ conjugate,
88
+ pluralize,
89
+ singularize,
90
+ inferNoun,
91
+ createTypeMeta,
92
+ getTypeMeta,
93
+ Type,
94
+ getVerbFields,
95
+ } from './linguistic.js'
96
+
97
+ // Import for internal use
98
+ import type {
99
+ ThingFlat,
100
+ ThingExpanded,
101
+ PrimitiveType,
102
+ FieldDefinition,
103
+ EntitySchema,
104
+ DatabaseSchema,
105
+ ParsedField,
106
+ ParsedEntity,
107
+ ParsedSchema,
108
+ Verb,
109
+ Noun,
110
+ NounProperty,
111
+ NounRelationship,
112
+ TypeMeta,
113
+ } from './types.js'
114
+
115
+ import { Verbs } from './types.js'
116
+
117
+ import {
118
+ inferNoun,
119
+ getTypeMeta,
120
+ conjugate,
121
+ } from './linguistic.js'
122
+
123
+ /**
124
+ * Create a Noun definition with type inference
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * const Post = defineNoun({
129
+ * singular: 'post',
130
+ * plural: 'posts',
131
+ * description: 'A blog post',
132
+ * properties: {
133
+ * title: { type: 'string', description: 'Post title' },
134
+ * content: { type: 'markdown' },
135
+ * },
136
+ * relationships: {
137
+ * author: { type: 'Author', backref: 'posts' },
138
+ * },
139
+ * })
140
+ * ```
141
+ */
142
+ export function defineNoun<T extends Noun>(noun: T): T {
143
+ return noun
144
+ }
145
+
146
+ /**
147
+ * Create a Verb definition with type inference
148
+ *
149
+ * @example
150
+ * ```ts
151
+ * const publish = defineVerb({
152
+ * action: 'publish',
153
+ * actor: 'publisher',
154
+ * act: 'publishes',
155
+ * activity: 'publishing',
156
+ * result: 'publication',
157
+ * reverse: { at: 'publishedAt', by: 'publishedBy' },
158
+ * inverse: 'unpublish',
159
+ * })
160
+ * ```
161
+ */
162
+ export function defineVerb<T extends Verb>(verb: T): T {
163
+ return verb
164
+ }
165
+
166
+ /**
167
+ * Convert a Noun to an EntitySchema for use with DB()
168
+ *
169
+ * @example
170
+ * ```ts
171
+ * const postNoun = defineNoun({
172
+ * singular: 'post',
173
+ * plural: 'posts',
174
+ * properties: { title: { type: 'string' } },
175
+ * relationships: { author: { type: 'Author', backref: 'posts' } },
176
+ * })
177
+ *
178
+ * const db = DB({
179
+ * Post: nounToSchema(postNoun),
180
+ * })
181
+ * ```
182
+ */
183
+ export function nounToSchema(noun: Noun): EntitySchema {
184
+ const schema: EntitySchema = {}
185
+
186
+ // Add properties
187
+ if (noun.properties) {
188
+ for (const [name, prop] of Object.entries(noun.properties)) {
189
+ let type = prop.type
190
+ if (prop.array) type = `${type}[]`
191
+ if (prop.optional) type = `${type}?`
192
+ schema[name] = type
193
+ }
194
+ }
195
+
196
+ // Add relationships
197
+ if (noun.relationships) {
198
+ for (const [name, rel] of Object.entries(noun.relationships)) {
199
+ const baseType = rel.type.replace('[]', '')
200
+ const isArray = rel.type.endsWith('[]')
201
+
202
+ if (rel.backref) {
203
+ schema[name] = isArray ? [`${baseType}.${rel.backref}`] : `${baseType}.${rel.backref}`
204
+ } else {
205
+ schema[name] = rel.type
206
+ }
207
+ }
208
+ }
209
+
210
+ return schema
211
+ }
212
+
213
+ // =============================================================================
214
+ // Built-in Schema Types - Self-Describing Database
215
+ // =============================================================================
216
+
217
+ /**
218
+ * Built-in Thing schema - base type for all entities
219
+ *
220
+ * Every entity instance is a Thing with a relationship to its Noun.
221
+ * This creates a complete graph: Thing.type -> Noun.things
222
+ *
223
+ * @example
224
+ * ```ts
225
+ * // Every post instance:
226
+ * post.$type // 'Post' (string)
227
+ * post.type // -> Noun('Post') (relationship)
228
+ *
229
+ * // From Noun, get all instances:
230
+ * const postNoun = await db.Noun.get('Post')
231
+ * const allPosts = await postNoun.things // -> Post[]
232
+ * ```
233
+ */
234
+ export const ThingSchema: EntitySchema = {
235
+ // Every Thing has a type that links to its Noun
236
+ type: 'Noun.things', // Thing.type -> Noun, Noun.things -> Thing[]
237
+ }
238
+
239
+ /**
240
+ * Built-in Noun schema for storing type definitions
241
+ *
242
+ * Every Type/Collection automatically gets a Noun record stored in the database.
243
+ * This enables introspection and self-describing schemas.
244
+ *
245
+ * @example
246
+ * ```ts
247
+ * // When you define:
248
+ * const db = DB({ Post: { title: 'string' } })
249
+ *
250
+ * // The database auto-creates:
251
+ * // db.Noun.get('Post') => { singular: 'post', plural: 'posts', ... }
252
+ *
253
+ * // Query all types:
254
+ * const types = await db.Noun.list()
255
+ *
256
+ * // Get all instances of a type:
257
+ * const postNoun = await db.Noun.get('Post')
258
+ * const allPosts = await postNoun.things
259
+ *
260
+ * // Listen for new types:
261
+ * on.Noun.created(noun => console.log(`New type: ${noun.name}`))
262
+ * ```
263
+ */
264
+ export const NounSchema: EntitySchema = {
265
+ // Identity
266
+ name: 'string', // 'Post', 'BlogPost'
267
+ singular: 'string', // 'post', 'blog post'
268
+ plural: 'string', // 'posts', 'blog posts'
269
+ slug: 'string', // 'post', 'blog-post'
270
+ slugPlural: 'string', // 'posts', 'blog-posts'
271
+ description: 'string?', // Human description
272
+
273
+ // Schema
274
+ properties: 'json?', // Property definitions
275
+ relationships: 'json?', // Relationship definitions
276
+
277
+ // Behavior
278
+ actions: 'json?', // Available actions (verbs)
279
+ events: 'json?', // Event types
280
+
281
+ // Metadata
282
+ metadata: 'json?', // Additional metadata
283
+
284
+ // Relationships - auto-created by bi-directional system
285
+ // things: Thing[] // All instances of this type (backref from Thing.type)
286
+ }
287
+
288
+ /**
289
+ * Built-in Verb schema for storing action definitions
290
+ */
291
+ export const VerbSchema: EntitySchema = {
292
+ action: 'string', // 'create', 'publish'
293
+ actor: 'string?', // 'creator', 'publisher'
294
+ act: 'string?', // 'creates', 'publishes'
295
+ activity: 'string?', // 'creating', 'publishing'
296
+ result: 'string?', // 'creation', 'publication'
297
+ reverse: 'json?', // { at, by, in, for }
298
+ inverse: 'string?', // 'delete', 'unpublish'
299
+ description: 'string?',
300
+ }
301
+
302
+ /**
303
+ * Built-in Edge schema for relationships between types
304
+ *
305
+ * Every relationship in a schema creates an Edge record.
306
+ * This enables graph queries across the type system.
307
+ *
308
+ * @example
309
+ * ```ts
310
+ * // Post.author -> Author creates:
311
+ * // Edge { from: 'Post', name: 'author', to: 'Author', backref: 'posts', cardinality: 'many-to-one' }
312
+ *
313
+ * // Query the graph:
314
+ * const edges = await db.Edge.find({ to: 'Author' })
315
+ * // => [{ from: 'Post', name: 'author' }, { from: 'Comment', name: 'author' }]
316
+ *
317
+ * // What types reference Author?
318
+ * const referencing = edges.map(e => e.from) // ['Post', 'Comment']
319
+ * ```
320
+ */
321
+ export const EdgeSchema: EntitySchema = {
322
+ from: 'string', // Source type: 'Post'
323
+ name: 'string', // Field name: 'author'
324
+ to: 'string', // Target type: 'Author'
325
+ backref: 'string?', // Inverse field: 'posts'
326
+ cardinality: 'string', // 'one-to-one', 'one-to-many', 'many-to-one', 'many-to-many'
327
+ required: 'boolean?', // Is this relationship required?
328
+ description: 'string?', // Human description
329
+ }
330
+
331
+ /**
332
+ * System types that are auto-created in every database
333
+ *
334
+ * The graph structure:
335
+ * - Thing.type -> Noun (every instance links to its type)
336
+ * - Noun.things -> Thing[] (every type has its instances)
337
+ * - Edge connects Nouns (relationships between types)
338
+ * - Verb describes actions on Nouns
339
+ */
340
+ export const SystemSchema: DatabaseSchema = {
341
+ Thing: ThingSchema,
342
+ Noun: NounSchema,
343
+ Verb: VerbSchema,
344
+ Edge: EdgeSchema,
345
+ }
346
+
347
+ /**
348
+ * Create Edge records from schema relationships
349
+ *
350
+ * @internal Used by DB() to auto-populate Edge records
351
+ */
352
+ export function createEdgeRecords(
353
+ typeName: string,
354
+ schema: EntitySchema,
355
+ parsedEntity: ParsedEntity
356
+ ): Array<Record<string, unknown>> {
357
+ const edges: Array<Record<string, unknown>> = []
358
+
359
+ for (const [fieldName, field] of parsedEntity.fields) {
360
+ if (field.isRelation && field.relatedType) {
361
+ const cardinality = field.isArray
362
+ ? field.backref ? 'many-to-many' : 'one-to-many'
363
+ : field.backref ? 'many-to-one' : 'one-to-one'
364
+
365
+ edges.push({
366
+ from: typeName,
367
+ name: fieldName,
368
+ to: field.relatedType,
369
+ backref: field.backref,
370
+ cardinality,
371
+ })
372
+ }
373
+ }
374
+
375
+ return edges
376
+ }
377
+
378
+ /**
379
+ * Create a Noun record from a type name and optional schema
380
+ *
381
+ * @internal Used by DB() to auto-populate Noun records
382
+ */
383
+ export function createNounRecord(
384
+ typeName: string,
385
+ schema?: EntitySchema,
386
+ nounDef?: Partial<Noun>
387
+ ): Record<string, unknown> {
388
+ const meta = getTypeMeta(typeName)
389
+ const inferred = inferNoun(typeName)
390
+
391
+ return {
392
+ name: typeName,
393
+ singular: nounDef?.singular ?? meta.singular,
394
+ plural: nounDef?.plural ?? meta.plural,
395
+ slug: meta.slug,
396
+ slugPlural: meta.slugPlural,
397
+ description: nounDef?.description,
398
+ properties: nounDef?.properties ?? (schema ? schemaToProperties(schema) : undefined),
399
+ relationships: nounDef?.relationships,
400
+ actions: nounDef?.actions ?? inferred.actions,
401
+ events: nounDef?.events ?? inferred.events,
402
+ metadata: nounDef?.metadata,
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Convert EntitySchema to NounProperty format
408
+ */
409
+ function schemaToProperties(schema: EntitySchema): Record<string, NounProperty> {
410
+ const properties: Record<string, NounProperty> = {}
411
+
412
+ for (const [name, def] of Object.entries(schema)) {
413
+ const defStr = Array.isArray(def) ? def[0] : def
414
+ const isOptional = defStr.endsWith('?')
415
+ const isArray = defStr.endsWith('[]') || Array.isArray(def)
416
+ const baseType = defStr.replace(/[\?\[\]]/g, '').split('.')[0]!
417
+
418
+ properties[name] = {
419
+ type: baseType,
420
+ optional: isOptional,
421
+ array: isArray,
422
+ }
423
+ }
424
+
425
+ return properties
426
+ }
427
+
428
+ // =============================================================================
429
+ // Schema Parsing
430
+ // =============================================================================
431
+
432
+ /**
433
+ * Parse a single field definition
434
+ */
435
+ function parseField(name: string, definition: FieldDefinition): ParsedField {
436
+ // Handle array literal syntax: ['Author.posts']
437
+ if (Array.isArray(definition)) {
438
+ const inner = parseField(name, definition[0])
439
+ return { ...inner, isArray: true }
440
+ }
441
+
442
+ let type = definition
443
+ let isArray = false
444
+ let isOptional = false
445
+ let isRelation = false
446
+ let relatedType: string | undefined
447
+ let backref: string | undefined
448
+
449
+ // Check for optional modifier
450
+ if (type.endsWith('?')) {
451
+ isOptional = true
452
+ type = type.slice(0, -1)
453
+ }
454
+
455
+ // Check for array modifier (string syntax)
456
+ if (type.endsWith('[]')) {
457
+ isArray = true
458
+ type = type.slice(0, -2)
459
+ }
460
+
461
+ // Check for relation (contains a dot for backref)
462
+ if (type.includes('.')) {
463
+ isRelation = true
464
+ const [entityName, backrefName] = type.split('.')
465
+ relatedType = entityName
466
+ backref = backrefName
467
+ type = entityName!
468
+ } else if (type[0] === type[0]?.toUpperCase() && !isPrimitiveType(type)) {
469
+ // PascalCase non-primitive = relation without explicit backref
470
+ isRelation = true
471
+ relatedType = type
472
+ }
473
+
474
+ return {
475
+ name,
476
+ type,
477
+ isArray,
478
+ isOptional,
479
+ isRelation,
480
+ relatedType,
481
+ backref,
482
+ }
483
+ }
484
+
485
+ /**
486
+ * Check if a type is a primitive
487
+ */
488
+ function isPrimitiveType(type: string): boolean {
489
+ const primitives: PrimitiveType[] = [
490
+ 'string',
491
+ 'number',
492
+ 'boolean',
493
+ 'date',
494
+ 'datetime',
495
+ 'json',
496
+ 'markdown',
497
+ 'url',
498
+ ]
499
+ return primitives.includes(type as PrimitiveType)
500
+ }
501
+
502
+ /**
503
+ * Parse a database schema and resolve bi-directional relationships
504
+ */
505
+ export function parseSchema(schema: DatabaseSchema): ParsedSchema {
506
+ const entities = new Map<string, ParsedEntity>()
507
+
508
+ // First pass: parse all entities and their fields
509
+ for (const [entityName, entitySchema] of Object.entries(schema)) {
510
+ const fields = new Map<string, ParsedField>()
511
+
512
+ for (const [fieldName, fieldDef] of Object.entries(entitySchema)) {
513
+ fields.set(fieldName, parseField(fieldName, fieldDef))
514
+ }
515
+
516
+ entities.set(entityName, { name: entityName, fields })
517
+ }
518
+
519
+ // Second pass: create bi-directional relationships
520
+ for (const [entityName, entity] of entities) {
521
+ for (const [fieldName, field] of entity.fields) {
522
+ if (field.isRelation && field.relatedType && field.backref) {
523
+ const relatedEntity = entities.get(field.relatedType)
524
+ if (relatedEntity && !relatedEntity.fields.has(field.backref)) {
525
+ // Auto-create the inverse relation
526
+ // If Post.author -> Author.posts, then Author.posts -> Post[]
527
+ relatedEntity.fields.set(field.backref, {
528
+ name: field.backref,
529
+ type: entityName,
530
+ isArray: true, // Backref is always an array
531
+ isOptional: false,
532
+ isRelation: true,
533
+ relatedType: entityName,
534
+ backref: fieldName, // Points back to the original field
535
+ })
536
+ }
537
+ }
538
+ }
539
+ }
540
+
541
+ return { entities }
542
+ }
543
+
544
+ // =============================================================================
545
+ // Type Generation (for TypeScript inference)
546
+ // =============================================================================
547
+
548
+ /**
549
+ * Map field type to TypeScript type
550
+ */
551
+ type FieldToTS<T extends string> = T extends 'string'
552
+ ? string
553
+ : T extends 'number'
554
+ ? number
555
+ : T extends 'boolean'
556
+ ? boolean
557
+ : T extends 'date' | 'datetime'
558
+ ? Date
559
+ : T extends 'json'
560
+ ? Record<string, unknown>
561
+ : T extends 'markdown'
562
+ ? string
563
+ : T extends 'url'
564
+ ? string
565
+ : unknown
566
+
567
+ /**
568
+ * Infer entity type from schema definition
569
+ */
570
+ export type InferEntity<
571
+ TSchema extends DatabaseSchema,
572
+ TEntity extends keyof TSchema,
573
+ > = {
574
+ $id: string
575
+ $type: TEntity
576
+ } & {
577
+ [K in keyof TSchema[TEntity]]: TSchema[TEntity][K] extends `${infer Type}.${string}`
578
+ ? Type extends keyof TSchema
579
+ ? InferEntity<TSchema, Type>
580
+ : unknown
581
+ : TSchema[TEntity][K] extends `${infer Type}[]`
582
+ ? Type extends keyof TSchema
583
+ ? InferEntity<TSchema, Type>[]
584
+ : FieldToTS<Type>[]
585
+ : TSchema[TEntity][K] extends `${infer Type}?`
586
+ ? FieldToTS<Type> | undefined
587
+ : FieldToTS<TSchema[TEntity][K] & string>
588
+ }
589
+
590
+ // =============================================================================
591
+ // Typed Operations
592
+ // =============================================================================
593
+
594
+ /**
595
+ * Operations available on each entity type
596
+ */
597
+ export interface EntityOperations<T> {
598
+ /** Get an entity by ID */
599
+ get(id: string): Promise<T | null>
600
+
601
+ /** List all entities */
602
+ list(options?: ListOptions): Promise<T[]>
603
+
604
+ /** Find entities matching criteria */
605
+ find(where: Partial<T>): Promise<T[]>
606
+
607
+ /** Search entities */
608
+ search(query: string, options?: SearchOptions): Promise<T[]>
609
+
610
+ /** Create a new entity */
611
+ create(data: Omit<T, '$id' | '$type'>): Promise<T>
612
+ create(id: string, data: Omit<T, '$id' | '$type'>): Promise<T>
613
+
614
+ /** Update an entity */
615
+ update(id: string, data: Partial<Omit<T, '$id' | '$type'>>): Promise<T>
616
+
617
+ /** Upsert an entity */
618
+ upsert(id: string, data: Omit<T, '$id' | '$type'>): Promise<T>
619
+
620
+ /** Delete an entity */
621
+ delete(id: string): Promise<boolean>
622
+
623
+ /** Iterate over entities */
624
+ forEach(callback: (entity: T) => void | Promise<void>): Promise<void>
625
+ forEach(
626
+ options: ListOptions,
627
+ callback: (entity: T) => void | Promise<void>
628
+ ): Promise<void>
629
+ }
630
+
631
+ /**
632
+ * Operations with promise pipelining support
633
+ *
634
+ * Query methods return DBPromise for chainable operations:
635
+ * - `.map()` with batch optimization
636
+ * - `.filter()`, `.sort()`, `.limit()`
637
+ * - Property access tracking for projections
638
+ *
639
+ * @example
640
+ * ```ts
641
+ * // Chain without await
642
+ * const leads = db.Lead.list()
643
+ * const qualified = await leads
644
+ * .filter(l => l.score > 80)
645
+ * .map(l => ({ name: l.name, company: l.company }))
646
+ *
647
+ * // Batch relationship loading
648
+ * const orders = await db.Order.list().map(o => ({
649
+ * order: o,
650
+ * customer: o.customer, // Batch loaded!
651
+ * }))
652
+ * ```
653
+ */
654
+ export interface PipelineEntityOperations<T> {
655
+ /** Get an entity by ID */
656
+ get(id: string): DBPromise<T | null>
657
+
658
+ /** List all entities */
659
+ list(options?: ListOptions): DBPromise<T[]>
660
+
661
+ /** Find entities matching criteria */
662
+ find(where: Partial<T>): DBPromise<T[]>
663
+
664
+ /** Search entities */
665
+ search(query: string, options?: SearchOptions): DBPromise<T[]>
666
+
667
+ /** Get first matching entity */
668
+ first(): DBPromise<T | null>
669
+
670
+ /** Create a new entity */
671
+ create(data: Omit<T, '$id' | '$type'>): Promise<T>
672
+ create(id: string, data: Omit<T, '$id' | '$type'>): Promise<T>
673
+
674
+ /** Update an entity */
675
+ update(id: string, data: Partial<Omit<T, '$id' | '$type'>>): Promise<T>
676
+
677
+ /** Upsert an entity */
678
+ upsert(id: string, data: Omit<T, '$id' | '$type'>): Promise<T>
679
+
680
+ /** Delete an entity */
681
+ delete(id: string): Promise<boolean>
682
+
683
+ /**
684
+ * Process each entity with concurrency control, progress tracking, and error handling
685
+ *
686
+ * Designed for large-scale operations like AI generations or workflows.
687
+ *
688
+ * @example
689
+ * ```ts
690
+ * // Simple iteration
691
+ * await db.Lead.forEach(lead => console.log(lead.name))
692
+ *
693
+ * // With AI and concurrency
694
+ * const result = await db.Lead.forEach(async lead => {
695
+ * const analysis = await ai`analyze ${lead}`
696
+ * await db.Lead.update(lead.$id, { analysis })
697
+ * }, {
698
+ * concurrency: 10,
699
+ * onProgress: p => console.log(`${p.completed}/${p.total}`),
700
+ * })
701
+ *
702
+ * // With error handling
703
+ * await db.Order.forEach(async order => {
704
+ * await sendInvoice(order)
705
+ * }, {
706
+ * maxRetries: 3,
707
+ * onError: (err, order) => err.code === 'RATE_LIMIT' ? 'retry' : 'continue',
708
+ * })
709
+ * ```
710
+ */
711
+ forEach<U>(
712
+ callback: (entity: T, index: number) => U | Promise<U>,
713
+ options?: ForEachOptions<T>
714
+ ): Promise<ForEachResult>
715
+ }
716
+
717
+ export interface ListOptions {
718
+ where?: Record<string, unknown>
719
+ orderBy?: string
720
+ order?: 'asc' | 'desc'
721
+ limit?: number
722
+ offset?: number
723
+ }
724
+
725
+ export interface SearchOptions extends ListOptions {
726
+ fields?: string[]
727
+ minScore?: number
728
+ }
729
+
730
+ // =============================================================================
731
+ // Database Client Type
732
+ // =============================================================================
733
+
734
+ /**
735
+ * Natural language query result
736
+ */
737
+ export interface NLQueryResult<T = unknown> {
738
+ /** The interpreted query */
739
+ interpretation: string
740
+ /** Confidence in the interpretation (0-1) */
741
+ confidence: number
742
+ /** The results */
743
+ results: T[]
744
+ /** SQL/filter equivalent (for debugging) */
745
+ query?: string
746
+ /** Explanation of what was found */
747
+ explanation?: string
748
+ }
749
+
750
+ /**
751
+ * Tagged template for natural language queries
752
+ *
753
+ * @example
754
+ * ```ts
755
+ * // Query across all types
756
+ * const results = await db`what is happening with joe in ca?`
757
+ *
758
+ * // Query specific type
759
+ * const orders = await db.Orders`what pending orders are delayed?`
760
+ *
761
+ * // With interpolation
762
+ * const name = 'joe'
763
+ * const results = await db`find all orders for ${name}`
764
+ * ```
765
+ */
766
+ export type NLQueryFn<T = unknown> = (
767
+ strings: TemplateStringsArray,
768
+ ...values: unknown[]
769
+ ) => Promise<NLQueryResult<T>>
770
+
771
+ /**
772
+ * Typed database client based on schema
773
+ *
774
+ * Entity operations return DBPromise for chainable queries:
775
+ * ```ts
776
+ * const { db } = DB({ Lead: { name: 'string', company: 'Company.leads' } })
777
+ *
778
+ * // Chain without await
779
+ * const leads = db.Lead.list()
780
+ * const qualified = await leads.filter(l => l.score > 80)
781
+ *
782
+ * // Batch relationship loading
783
+ * const withCompanies = await leads.map(l => ({
784
+ * lead: l,
785
+ * company: l.company, // Batch loaded!
786
+ * }))
787
+ * ```
788
+ */
789
+ export type TypedDB<TSchema extends DatabaseSchema> = {
790
+ [K in keyof TSchema]: PipelineEntityOperations<InferEntity<TSchema, K>> & NLQueryFn<InferEntity<TSchema, K>>
791
+ } & {
792
+ /** The parsed schema */
793
+ readonly $schema: ParsedSchema
794
+
795
+ /** Get any entity by URL */
796
+ get(url: string): Promise<unknown>
797
+
798
+ /** Search across all entities */
799
+ search(query: string, options?: SearchOptions): Promise<unknown[]>
800
+
801
+ /** Count entities of a type */
802
+ count(type: string, where?: Record<string, unknown>): Promise<number>
803
+
804
+ /** Iterate over entities with a callback */
805
+ forEach(
806
+ options: { type: string; where?: Record<string, unknown>; concurrency?: number },
807
+ callback: (entity: unknown) => void | Promise<void>
808
+ ): Promise<void>
809
+
810
+ /** Set entity data by ID (creates or replaces) */
811
+ set(type: string, id: string, data: Record<string, unknown>): Promise<unknown>
812
+
813
+ /** Generate entities using AI */
814
+ generate(options: GenerateOptions): Promise<unknown | { id: string }>
815
+
816
+ /**
817
+ * Natural language query across all types
818
+ *
819
+ * @example
820
+ * ```ts
821
+ * const results = await db`what orders are pending for customers in california?`
822
+ * const results = await db`show me joe's recent activity`
823
+ * const results = await db`what changed in the last hour?`
824
+ * ```
825
+ */
826
+ ask: NLQueryFn
827
+ }
828
+
829
+ /**
830
+ * Options for AI-powered entity generation
831
+ */
832
+ export interface GenerateOptions {
833
+ type: string
834
+ count?: number
835
+ data?: Record<string, unknown>
836
+ mode?: 'sync' | 'background'
837
+ }
838
+
839
+ // =============================================================================
840
+ // Events API (Actor-Event-Object-Result pattern)
841
+ // =============================================================================
842
+
843
+ /**
844
+ * Actor data - who performed the action
845
+ *
846
+ * @example
847
+ * ```ts
848
+ * const actorData: ActorData = {
849
+ * name: 'John Doe',
850
+ * email: 'john@example.com',
851
+ * org: 'Acme Corp',
852
+ * role: 'admin',
853
+ * }
854
+ * ```
855
+ */
856
+ export interface ActorData {
857
+ /** Actor's display name */
858
+ name?: string
859
+ /** Actor's email */
860
+ email?: string
861
+ /** Actor's organization */
862
+ org?: string
863
+ /** Actor's role or access level */
864
+ role?: string
865
+ /** Additional actor metadata */
866
+ [key: string]: unknown
867
+ }
868
+
869
+ /**
870
+ * Event data structure - Actor-Event-Object-Result pattern
871
+ *
872
+ * Following ActivityStreams semantics:
873
+ * - Actor: Who did it (user, system, agent)
874
+ * - Event: What happened (created, updated, published)
875
+ * - Object: What it was done to (the entity)
876
+ * - Result: What was the outcome (optional)
877
+ *
878
+ * @example
879
+ * ```ts
880
+ * const event: DBEvent = {
881
+ * id: '01HGXYZ...',
882
+ * actor: 'user:john',
883
+ * actorData: { name: 'John Doe', email: 'john@example.com' },
884
+ * event: 'Post.published',
885
+ * object: 'https://example.com/Post/hello-world',
886
+ * objectData: { title: 'Hello World' },
887
+ * result: 'https://example.com/Publication/123',
888
+ * resultData: { url: 'https://blog.example.com/hello-world' },
889
+ * timestamp: new Date(),
890
+ * }
891
+ * ```
892
+ */
893
+ export interface DBEvent {
894
+ /** Unique event ID (ULID recommended) */
895
+ id: string
896
+ /** Actor identifier (user:id, system, agent:name) */
897
+ actor: string
898
+ /** Actor metadata */
899
+ actorData?: ActorData
900
+ /** Event type (Entity.action format, e.g., Post.created) */
901
+ event: string
902
+ /** Object URL/identifier that was acted upon */
903
+ object?: string
904
+ /** Object data snapshot at time of event */
905
+ objectData?: Record<string, unknown>
906
+ /** Result URL/identifier (outcome of the action) */
907
+ result?: string
908
+ /** Result data */
909
+ resultData?: Record<string, unknown>
910
+ /** Additional metadata */
911
+ meta?: Record<string, unknown>
912
+ /** When the event occurred */
913
+ timestamp: Date
914
+
915
+ // Legacy compatibility (deprecated)
916
+ /** @deprecated Use 'event' instead */
917
+ type?: string
918
+ /** @deprecated Use 'objectData' instead */
919
+ data?: unknown
920
+ /** @deprecated Use 'object' instead */
921
+ url?: string
922
+ }
923
+
924
+ /**
925
+ * Options for creating an event
926
+ */
927
+ export interface CreateEventOptions {
928
+ /** Actor identifier */
929
+ actor: string
930
+ /** Actor metadata */
931
+ actorData?: ActorData
932
+ /** Event type */
933
+ event: string
934
+ /** Object URL/identifier */
935
+ object?: string
936
+ /** Object data */
937
+ objectData?: Record<string, unknown>
938
+ /** Result URL/identifier */
939
+ result?: string
940
+ /** Result data */
941
+ resultData?: Record<string, unknown>
942
+ /** Additional metadata */
943
+ meta?: Record<string, unknown>
944
+ }
945
+
946
+ /**
947
+ * Events API for subscribing to and emitting events
948
+ */
949
+ export interface EventsAPI {
950
+ /** Subscribe to events matching a pattern */
951
+ on(pattern: string, handler: (event: DBEvent) => void | Promise<void>): () => void
952
+
953
+ /** Emit an event using Actor-Event-Object-Result pattern */
954
+ emit(options: CreateEventOptions): Promise<DBEvent>
955
+
956
+ /** Emit a simple event (legacy compatibility) */
957
+ emit(type: string, data: unknown): Promise<DBEvent>
958
+
959
+ /** List events with optional filters */
960
+ list(options?: {
961
+ event?: string
962
+ actor?: string
963
+ object?: string
964
+ since?: Date
965
+ until?: Date
966
+ limit?: number
967
+ /** @deprecated Use 'event' instead */
968
+ type?: string
969
+ }): Promise<DBEvent[]>
970
+
971
+ /** Replay events through a handler */
972
+ replay(options: {
973
+ event?: string
974
+ actor?: string
975
+ since?: Date
976
+ handler: (event: DBEvent) => void | Promise<void>
977
+ /** @deprecated Use 'event' instead */
978
+ type?: string
979
+ }): Promise<void>
980
+ }
981
+
982
+ // =============================================================================
983
+ // Actions API (Linguistic Verb Pattern)
984
+ // =============================================================================
985
+
986
+ /**
987
+ * Action data structure for durable execution
988
+ *
989
+ * Uses linguistic verb conjugations for semantic clarity:
990
+ * - act: Present tense 3rd person (creates, publishes)
991
+ * - action: Base verb form (create, publish)
992
+ * - activity: Gerund/progressive (creating, publishing)
993
+ *
994
+ * @example
995
+ * ```ts
996
+ * const action: DBAction = {
997
+ * id: '01HGXYZ...',
998
+ * actor: 'user:john',
999
+ * actorData: { name: 'John Doe' },
1000
+ * // Verb conjugations
1001
+ * act: 'generates', // Present tense: "system generates posts"
1002
+ * action: 'generate', // Base form for lookups
1003
+ * activity: 'generating', // Progressive: "currently generating posts"
1004
+ * // Target
1005
+ * object: 'Post',
1006
+ * objectData: { count: 100 },
1007
+ * // Status
1008
+ * status: 'active',
1009
+ * progress: 50,
1010
+ * total: 100,
1011
+ * // Result
1012
+ * result: { created: 50 },
1013
+ * timestamp: new Date(),
1014
+ * }
1015
+ * ```
1016
+ */
1017
+ export interface DBAction {
1018
+ /** Unique action ID (ULID recommended) */
1019
+ id: string
1020
+
1021
+ /** Actor identifier (user:id, system, agent:name) */
1022
+ actor: string
1023
+ /** Actor metadata */
1024
+ actorData?: ActorData
1025
+
1026
+ /** Present tense 3rd person verb (creates, publishes, generates) */
1027
+ act: string
1028
+ /** Base verb form - imperative (create, publish, generate) */
1029
+ action: string
1030
+ /** Gerund/progressive form (creating, publishing, generating) */
1031
+ activity: string
1032
+
1033
+ /** Object being acted upon (type name or URL) */
1034
+ object?: string
1035
+ /** Object data/parameters for the action */
1036
+ objectData?: Record<string, unknown>
1037
+
1038
+ /** Action status */
1039
+ status: 'pending' | 'active' | 'completed' | 'failed' | 'cancelled'
1040
+
1041
+ /** Current progress count */
1042
+ progress?: number
1043
+ /** Total items to process */
1044
+ total?: number
1045
+
1046
+ /** Result data on completion */
1047
+ result?: Record<string, unknown>
1048
+ /** Error message on failure */
1049
+ error?: string
1050
+
1051
+ /** Additional metadata */
1052
+ meta?: Record<string, unknown>
1053
+
1054
+ /** When the action was created */
1055
+ createdAt: Date
1056
+ /** When the action started executing */
1057
+ startedAt?: Date
1058
+ /** When the action completed/failed */
1059
+ completedAt?: Date
1060
+
1061
+ // Legacy compatibility (deprecated)
1062
+ /** @deprecated Use 'action' instead */
1063
+ type?: string
1064
+ /** @deprecated Use 'objectData' instead */
1065
+ data?: unknown
1066
+ }
1067
+
1068
+ /**
1069
+ * Options for creating an action
1070
+ */
1071
+ export interface CreateActionOptions {
1072
+ /** Actor identifier */
1073
+ actor: string
1074
+ /** Actor metadata */
1075
+ actorData?: ActorData
1076
+ /** Base verb (will auto-conjugate to act/activity) */
1077
+ action: string
1078
+ /** Object being acted upon */
1079
+ object?: string
1080
+ /** Object data/parameters */
1081
+ objectData?: Record<string, unknown>
1082
+ /** Total items for progress tracking */
1083
+ total?: number
1084
+ /** Additional metadata */
1085
+ meta?: Record<string, unknown>
1086
+
1087
+ // Legacy compatibility
1088
+ /** @deprecated Use 'action' instead */
1089
+ type?: string
1090
+ /** @deprecated Use 'objectData' instead */
1091
+ data?: unknown
1092
+ }
1093
+
1094
+ /**
1095
+ * Actions API for durable execution tracking
1096
+ *
1097
+ * @example
1098
+ * ```ts
1099
+ * // Create an action with verb conjugation
1100
+ * const action = await actions.create({
1101
+ * actor: 'system',
1102
+ * action: 'generate', // auto-conjugates to act='generates', activity='generating'
1103
+ * object: 'Post',
1104
+ * objectData: { count: 100 },
1105
+ * total: 100,
1106
+ * })
1107
+ *
1108
+ * // Update progress
1109
+ * await actions.update(action.id, { progress: 50 })
1110
+ *
1111
+ * // Complete with result
1112
+ * await actions.update(action.id, {
1113
+ * status: 'completed',
1114
+ * result: { created: 100 },
1115
+ * })
1116
+ * ```
1117
+ */
1118
+ export interface ActionsAPI {
1119
+ /** Create a new action (auto-conjugates verb forms) */
1120
+ create(options: CreateActionOptions): Promise<DBAction>
1121
+
1122
+ /** Create with legacy format (deprecated) */
1123
+ create(data: { type: string; data: unknown; total?: number }): Promise<DBAction>
1124
+
1125
+ /** Get an action by ID */
1126
+ get(id: string): Promise<DBAction | null>
1127
+
1128
+ /** Update action progress/status */
1129
+ update(
1130
+ id: string,
1131
+ updates: Partial<Pick<DBAction, 'status' | 'progress' | 'result' | 'error'>>
1132
+ ): Promise<DBAction>
1133
+
1134
+ /** List actions with optional filters */
1135
+ list(options?: {
1136
+ status?: DBAction['status']
1137
+ action?: string
1138
+ actor?: string
1139
+ object?: string
1140
+ since?: Date
1141
+ until?: Date
1142
+ limit?: number
1143
+ /** @deprecated Use 'action' instead */
1144
+ type?: string
1145
+ }): Promise<DBAction[]>
1146
+
1147
+ /** Retry a failed action */
1148
+ retry(id: string): Promise<DBAction>
1149
+
1150
+ /** Cancel a pending/active action */
1151
+ cancel(id: string): Promise<void>
1152
+
1153
+ /** Conjugate a verb to get all forms */
1154
+ conjugate(action: string): Verb
1155
+ }
1156
+
1157
+ // =============================================================================
1158
+ // Artifacts API
1159
+ // =============================================================================
1160
+
1161
+ /**
1162
+ * Artifact data structure for cached content
1163
+ */
1164
+ export interface DBArtifact {
1165
+ url: string
1166
+ type: string
1167
+ sourceHash: string
1168
+ content: unknown
1169
+ metadata?: Record<string, unknown>
1170
+ createdAt: Date
1171
+ }
1172
+
1173
+ /**
1174
+ * Artifacts API for cached embeddings and computed content
1175
+ */
1176
+ export interface ArtifactsAPI {
1177
+ /** Get an artifact by URL and type */
1178
+ get(url: string, type: string): Promise<DBArtifact | null>
1179
+
1180
+ /** Set an artifact */
1181
+ set(
1182
+ url: string,
1183
+ type: string,
1184
+ data: { content: unknown; sourceHash: string; metadata?: Record<string, unknown> }
1185
+ ): Promise<void>
1186
+
1187
+ /** Delete an artifact */
1188
+ delete(url: string, type?: string): Promise<void>
1189
+
1190
+ /** List artifacts for a URL */
1191
+ list(url: string): Promise<DBArtifact[]>
1192
+ }
1193
+
1194
+ // =============================================================================
1195
+ // Nouns API
1196
+ // =============================================================================
1197
+
1198
+ /**
1199
+ * Nouns API for type introspection
1200
+ */
1201
+ export interface NounsAPI {
1202
+ /** Get a noun definition by type name */
1203
+ get(name: string): Promise<Noun | null>
1204
+
1205
+ /** List all noun definitions */
1206
+ list(): Promise<Noun[]>
1207
+
1208
+ /** Define a new noun */
1209
+ define(noun: Noun): Promise<void>
1210
+ }
1211
+
1212
+ // =============================================================================
1213
+ // Verbs API
1214
+ // =============================================================================
1215
+
1216
+ /**
1217
+ * Verbs API for action introspection
1218
+ */
1219
+ export interface VerbsAPI {
1220
+ /** Get a verb definition by action name */
1221
+ get(action: string): Verb | null
1222
+
1223
+ /** List all verb definitions */
1224
+ list(): Verb[]
1225
+
1226
+ /** Define a new verb */
1227
+ define(verb: Verb): void
1228
+
1229
+ /** Conjugate a verb from base form */
1230
+ conjugate(action: string): Verb
1231
+ }
1232
+
1233
+ // =============================================================================
1234
+ // DB Result Type
1235
+ // =============================================================================
1236
+
1237
+ /**
1238
+ * Result of DB() factory - supports both direct and destructured usage
1239
+ *
1240
+ * @example
1241
+ * ```ts
1242
+ * // Direct usage - everything on one object
1243
+ * const db = DB(schema)
1244
+ * db.User.create(...) // entity operations
1245
+ * db.events.on(...) // events API
1246
+ * db.actions.create(...) // actions API
1247
+ *
1248
+ * // Destructured usage - cleaner separation
1249
+ * const { db, events, actions } = DB(schema)
1250
+ * db.User.create(...) // just entity ops
1251
+ * events.on(...) // separate events
1252
+ * ```
1253
+ */
1254
+ export type DBResult<TSchema extends DatabaseSchema> = TypedDB<TSchema> & {
1255
+ /** Self-reference for destructuring - same as the parent object but cleaner semantically */
1256
+ db: TypedDB<TSchema>
1257
+
1258
+ /** Event subscription and emission */
1259
+ events: EventsAPI
1260
+
1261
+ /** Durable action execution */
1262
+ actions: ActionsAPI
1263
+
1264
+ /** Cached embeddings and computed content */
1265
+ artifacts: ArtifactsAPI
1266
+
1267
+ /** Type introspection */
1268
+ nouns: NounsAPI
1269
+
1270
+ /** Action introspection */
1271
+ verbs: VerbsAPI
1272
+ }
1273
+
1274
+ // =============================================================================
1275
+ // Natural Language Query Implementation
1276
+ // =============================================================================
1277
+
1278
+ /**
1279
+ * AI generator function type for NL queries
1280
+ * This is injected by the user or resolved from environment
1281
+ */
1282
+ export type NLQueryGenerator = (prompt: string, context: NLQueryContext) => Promise<NLQueryPlan>
1283
+
1284
+ /**
1285
+ * Context provided to the AI for query generation
1286
+ */
1287
+ export interface NLQueryContext {
1288
+ /** Available types with their schemas */
1289
+ types: Array<{
1290
+ name: string
1291
+ singular: string
1292
+ plural: string
1293
+ fields: string[]
1294
+ relationships: Array<{ name: string; to: string; cardinality: string }>
1295
+ }>
1296
+ /** The specific type being queried (if any) */
1297
+ targetType?: string
1298
+ /** Recent events for context */
1299
+ recentEvents?: Array<{ type: string; timestamp: Date }>
1300
+ }
1301
+
1302
+ /**
1303
+ * Query plan generated by AI
1304
+ */
1305
+ export interface NLQueryPlan {
1306
+ /** Types to query */
1307
+ types: string[]
1308
+ /** Filters to apply */
1309
+ filters?: Record<string, unknown>
1310
+ /** Search terms */
1311
+ search?: string
1312
+ /** Time range */
1313
+ timeRange?: { since?: Date; until?: Date }
1314
+ /** Relationships to follow */
1315
+ include?: string[]
1316
+ /** How to interpret results */
1317
+ interpretation: string
1318
+ /** Confidence score */
1319
+ confidence: number
1320
+ }
1321
+
1322
+ let nlQueryGenerator: NLQueryGenerator | null = null
1323
+
1324
+ /**
1325
+ * Set the AI generator for natural language queries
1326
+ *
1327
+ * @example
1328
+ * ```ts
1329
+ * import { generate } from 'ai-functions'
1330
+ *
1331
+ * setNLQueryGenerator(async (prompt, context) => {
1332
+ * return generate({
1333
+ * prompt: `Given this schema: ${JSON.stringify(context.types)}
1334
+ * Answer this question: ${prompt}
1335
+ * Return a query plan as JSON.`,
1336
+ * schema: NLQueryPlanSchema
1337
+ * })
1338
+ * })
1339
+ * ```
1340
+ */
1341
+ export function setNLQueryGenerator(generator: NLQueryGenerator): void {
1342
+ nlQueryGenerator = generator
1343
+ }
1344
+
1345
+ /**
1346
+ * Build schema context for NL queries
1347
+ */
1348
+ function buildNLQueryContext(schema: ParsedSchema, targetType?: string): NLQueryContext {
1349
+ const types: NLQueryContext['types'] = []
1350
+
1351
+ for (const [name, entity] of schema.entities) {
1352
+ const fields: string[] = []
1353
+ const relationships: NLQueryContext['types'][0]['relationships'] = []
1354
+
1355
+ for (const [fieldName, field] of entity.fields) {
1356
+ if (field.isRelation && field.relatedType) {
1357
+ relationships.push({
1358
+ name: fieldName,
1359
+ to: field.relatedType,
1360
+ cardinality: field.isArray ? 'many' : 'one',
1361
+ })
1362
+ } else {
1363
+ fields.push(fieldName)
1364
+ }
1365
+ }
1366
+
1367
+ const meta = getTypeMeta(name)
1368
+ types.push({
1369
+ name,
1370
+ singular: meta.singular,
1371
+ plural: meta.plural,
1372
+ fields,
1373
+ relationships,
1374
+ })
1375
+ }
1376
+
1377
+ return { types, targetType }
1378
+ }
1379
+
1380
+ /**
1381
+ * Execute a natural language query
1382
+ */
1383
+ async function executeNLQuery<T>(
1384
+ question: string,
1385
+ schema: ParsedSchema,
1386
+ targetType?: string
1387
+ ): Promise<NLQueryResult<T>> {
1388
+ // If no AI generator configured, fall back to search
1389
+ if (!nlQueryGenerator) {
1390
+ // Simple fallback: search across all types or target type
1391
+ const provider = await resolveProvider()
1392
+ const results: T[] = []
1393
+
1394
+ if (targetType) {
1395
+ const searchResults = await provider.search(targetType, question)
1396
+ results.push(...(searchResults as T[]))
1397
+ } else {
1398
+ for (const [typeName] of schema.entities) {
1399
+ const searchResults = await provider.search(typeName, question)
1400
+ results.push(...(searchResults as T[]))
1401
+ }
1402
+ }
1403
+
1404
+ return {
1405
+ interpretation: `Search for "${question}"`,
1406
+ confidence: 0.5,
1407
+ results,
1408
+ explanation: 'Fallback to keyword search (no AI generator configured)',
1409
+ }
1410
+ }
1411
+
1412
+ // Build context and get AI-generated query plan
1413
+ const context = buildNLQueryContext(schema, targetType)
1414
+ const plan = await nlQueryGenerator(question, context)
1415
+
1416
+ // Execute the plan
1417
+ const provider = await resolveProvider()
1418
+ const results: T[] = []
1419
+
1420
+ for (const typeName of plan.types) {
1421
+ let typeResults: Record<string, unknown>[]
1422
+
1423
+ if (plan.search) {
1424
+ typeResults = await provider.search(typeName, plan.search, {
1425
+ where: plan.filters,
1426
+ })
1427
+ } else {
1428
+ typeResults = await provider.list(typeName, {
1429
+ where: plan.filters,
1430
+ })
1431
+ }
1432
+
1433
+ results.push(...(typeResults as T[]))
1434
+ }
1435
+
1436
+ return {
1437
+ interpretation: plan.interpretation,
1438
+ confidence: plan.confidence,
1439
+ results,
1440
+ query: JSON.stringify({ types: plan.types, filters: plan.filters, search: plan.search }),
1441
+ }
1442
+ }
1443
+
1444
+ /**
1445
+ * Create a natural language query function for a specific type
1446
+ */
1447
+ function createNLQueryFn<T>(
1448
+ schema: ParsedSchema,
1449
+ typeName?: string
1450
+ ): NLQueryFn<T> {
1451
+ return async (strings: TemplateStringsArray, ...values: unknown[]) => {
1452
+ // Interpolate the template
1453
+ const question = strings.reduce((acc, str, i) => {
1454
+ return acc + str + (values[i] !== undefined ? String(values[i]) : '')
1455
+ }, '')
1456
+
1457
+ return executeNLQuery<T>(question, schema, typeName)
1458
+ }
1459
+ }
1460
+
1461
+ // =============================================================================
1462
+ // Provider Interface
1463
+ // =============================================================================
1464
+
1465
+ /**
1466
+ * Database provider interface that adapters must implement
1467
+ */
1468
+ export interface DBProvider {
1469
+ /** Get an entity */
1470
+ get(type: string, id: string): Promise<Record<string, unknown> | null>
1471
+
1472
+ /** List entities */
1473
+ list(type: string, options?: ListOptions): Promise<Record<string, unknown>[]>
1474
+
1475
+ /** Search entities */
1476
+ search(
1477
+ type: string,
1478
+ query: string,
1479
+ options?: SearchOptions
1480
+ ): Promise<Record<string, unknown>[]>
1481
+
1482
+ /** Create an entity */
1483
+ create(
1484
+ type: string,
1485
+ id: string | undefined,
1486
+ data: Record<string, unknown>
1487
+ ): Promise<Record<string, unknown>>
1488
+
1489
+ /** Update an entity */
1490
+ update(
1491
+ type: string,
1492
+ id: string,
1493
+ data: Record<string, unknown>
1494
+ ): Promise<Record<string, unknown>>
1495
+
1496
+ /** Delete an entity */
1497
+ delete(type: string, id: string): Promise<boolean>
1498
+
1499
+ /** Get related entities */
1500
+ related(
1501
+ type: string,
1502
+ id: string,
1503
+ relation: string
1504
+ ): Promise<Record<string, unknown>[]>
1505
+
1506
+ /** Create a relationship */
1507
+ relate(
1508
+ fromType: string,
1509
+ fromId: string,
1510
+ relation: string,
1511
+ toType: string,
1512
+ toId: string
1513
+ ): Promise<void>
1514
+
1515
+ /** Remove a relationship */
1516
+ unrelate(
1517
+ fromType: string,
1518
+ fromId: string,
1519
+ relation: string,
1520
+ toType: string,
1521
+ toId: string
1522
+ ): Promise<void>
1523
+ }
1524
+
1525
+ // =============================================================================
1526
+ // Provider Resolution
1527
+ // =============================================================================
1528
+
1529
+ let globalProvider: DBProvider | null = null
1530
+ let providerPromise: Promise<DBProvider> | null = null
1531
+
1532
+ /** File count threshold for suggesting ClickHouse upgrade */
1533
+ const FILE_COUNT_THRESHOLD = 10_000
1534
+
1535
+ /**
1536
+ * Set the global database provider
1537
+ */
1538
+ export function setProvider(provider: DBProvider): void {
1539
+ globalProvider = provider
1540
+ providerPromise = null
1541
+ }
1542
+
1543
+ /**
1544
+ * Parsed DATABASE_URL
1545
+ */
1546
+ interface ParsedDatabaseUrl {
1547
+ provider: 'fs' | 'sqlite' | 'clickhouse' | 'memory'
1548
+ /** Content root directory */
1549
+ root: string
1550
+ /** Remote URL for Turso/ClickHouse HTTP */
1551
+ remoteUrl?: string
1552
+ }
1553
+
1554
+ /**
1555
+ * Parse DATABASE_URL into provider type and paths
1556
+ *
1557
+ * Local storage (all use .db/ folder):
1558
+ * - `./content` → fs (default)
1559
+ * - `sqlite://./content` → sqlite stored in ./content/.db/index.sqlite
1560
+ * - `chdb://./content` → clickhouse stored in ./content/.db/clickhouse/
1561
+ *
1562
+ * Remote:
1563
+ * - `libsql://your-db.turso.io` → Turso SQLite
1564
+ * - `clickhouse://host:8123` → ClickHouse HTTP
1565
+ * - `:memory:` → in-memory
1566
+ */
1567
+ function parseDatabaseUrl(url: string): ParsedDatabaseUrl {
1568
+ if (!url) return { provider: 'fs', root: './content' }
1569
+
1570
+ // In-memory
1571
+ if (url === ':memory:') {
1572
+ return { provider: 'memory', root: '' }
1573
+ }
1574
+
1575
+ // Remote Turso
1576
+ if (url.startsWith('libsql://') || url.includes('.turso.io')) {
1577
+ return { provider: 'sqlite', root: '', remoteUrl: url }
1578
+ }
1579
+
1580
+ // Remote ClickHouse
1581
+ if (url.startsWith('clickhouse://') && url.includes(':')) {
1582
+ // clickhouse://host:port/db
1583
+ return { provider: 'clickhouse', root: '', remoteUrl: url.replace('clickhouse://', 'https://') }
1584
+ }
1585
+
1586
+ // Local SQLite: sqlite://./content → ./content/.db/index.sqlite
1587
+ if (url.startsWith('sqlite://')) {
1588
+ const root = url.replace('sqlite://', '') || './content'
1589
+ return { provider: 'sqlite', root }
1590
+ }
1591
+
1592
+ // Local ClickHouse (chDB): chdb://./content → ./content/.db/clickhouse/
1593
+ if (url.startsWith('chdb://')) {
1594
+ const root = url.replace('chdb://', '') || './content'
1595
+ return { provider: 'clickhouse', root }
1596
+ }
1597
+
1598
+ // Default: filesystem
1599
+ return { provider: 'fs', root: url }
1600
+ }
1601
+
1602
+ /**
1603
+ * Resolve provider from DATABASE_URL environment variable
1604
+ *
1605
+ * @example
1606
+ * ```bash
1607
+ * # Filesystem (default) - stores in ./content with .db/ metadata
1608
+ * DATABASE_URL=./content
1609
+ *
1610
+ * # Local SQLite - stores in ./content/.db/index.sqlite
1611
+ * DATABASE_URL=sqlite://./content
1612
+ *
1613
+ * # Remote Turso
1614
+ * DATABASE_URL=libsql://your-db.turso.io
1615
+ *
1616
+ * # Local ClickHouse (chDB) - stores in ./content/.db/clickhouse/
1617
+ * DATABASE_URL=chdb://./content
1618
+ *
1619
+ * # Remote ClickHouse
1620
+ * DATABASE_URL=clickhouse://localhost:8123
1621
+ *
1622
+ * # In-memory (testing)
1623
+ * DATABASE_URL=:memory:
1624
+ * ```
1625
+ */
1626
+ async function resolveProvider(): Promise<DBProvider> {
1627
+ if (globalProvider) return globalProvider
1628
+
1629
+ if (providerPromise) return providerPromise
1630
+
1631
+ providerPromise = (async () => {
1632
+ const databaseUrl =
1633
+ (typeof process !== 'undefined' && process.env?.DATABASE_URL) || './content'
1634
+
1635
+ const parsed = parseDatabaseUrl(databaseUrl)
1636
+
1637
+ switch (parsed.provider) {
1638
+ case 'memory': {
1639
+ const { createMemoryProvider } = await import('./memory-provider.js')
1640
+ globalProvider = createMemoryProvider()
1641
+ break
1642
+ }
1643
+
1644
+ case 'fs': {
1645
+ try {
1646
+ const { createFsProvider } = await import('@mdxdb/fs' as any)
1647
+ globalProvider = createFsProvider({ root: parsed.root })
1648
+
1649
+ // Check file count and warn if approaching threshold
1650
+ checkFileCountThreshold(parsed.root)
1651
+ } catch (err) {
1652
+ console.warn('@mdxdb/fs not available, falling back to memory provider')
1653
+ const { createMemoryProvider } = await import('./memory-provider.js')
1654
+ globalProvider = createMemoryProvider()
1655
+ }
1656
+ break
1657
+ }
1658
+
1659
+ case 'sqlite': {
1660
+ try {
1661
+ const { createSqliteProvider } = await import('@mdxdb/sqlite' as any)
1662
+
1663
+ if (parsed.remoteUrl) {
1664
+ // Remote Turso
1665
+ globalProvider = await createSqliteProvider({ url: parsed.remoteUrl })
1666
+ } else {
1667
+ // Local SQLite in .db folder
1668
+ const dbPath = `${parsed.root}/.db/index.sqlite`
1669
+ globalProvider = await createSqliteProvider({ url: `file:${dbPath}` })
1670
+ }
1671
+ } catch (err) {
1672
+ console.warn('@mdxdb/sqlite not available, falling back to memory provider')
1673
+ const { createMemoryProvider } = await import('./memory-provider.js')
1674
+ globalProvider = createMemoryProvider()
1675
+ }
1676
+ break
1677
+ }
1678
+
1679
+ case 'clickhouse': {
1680
+ try {
1681
+ const { createClickhouseProvider } = await import('@mdxdb/clickhouse' as any)
1682
+
1683
+ if (parsed.remoteUrl) {
1684
+ // Remote ClickHouse
1685
+ globalProvider = await createClickhouseProvider({
1686
+ mode: 'http',
1687
+ url: parsed.remoteUrl,
1688
+ })
1689
+ } else {
1690
+ // Local chDB in .db folder
1691
+ const dbPath = `${parsed.root}/.db/clickhouse`
1692
+ globalProvider = await createClickhouseProvider({
1693
+ mode: 'chdb',
1694
+ url: dbPath,
1695
+ })
1696
+ }
1697
+ } catch (err) {
1698
+ console.warn('@mdxdb/clickhouse not available, falling back to memory provider')
1699
+ const { createMemoryProvider } = await import('./memory-provider.js')
1700
+ globalProvider = createMemoryProvider()
1701
+ }
1702
+ break
1703
+ }
1704
+
1705
+ default: {
1706
+ const { createMemoryProvider } = await import('./memory-provider.js')
1707
+ globalProvider = createMemoryProvider()
1708
+ }
1709
+ }
1710
+
1711
+ return globalProvider!
1712
+ })()
1713
+
1714
+ return providerPromise
1715
+ }
1716
+
1717
+ /**
1718
+ * Check file count and warn if approaching threshold
1719
+ */
1720
+ async function checkFileCountThreshold(root: string): Promise<void> {
1721
+ try {
1722
+ const fs = await import('node:fs/promises')
1723
+ const path = await import('node:path')
1724
+
1725
+ async function countFiles(dir: string): Promise<number> {
1726
+ let count = 0
1727
+ try {
1728
+ const entries = await fs.readdir(dir, { withFileTypes: true })
1729
+ for (const entry of entries) {
1730
+ if (entry.name.startsWith('.')) continue
1731
+ if (entry.isDirectory()) {
1732
+ count += await countFiles(path.join(dir, entry.name))
1733
+ } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {
1734
+ count++
1735
+ }
1736
+ }
1737
+ } catch {
1738
+ // Directory doesn't exist yet
1739
+ }
1740
+ return count
1741
+ }
1742
+
1743
+ const count = await countFiles(root)
1744
+ if (count > FILE_COUNT_THRESHOLD) {
1745
+ console.warn(
1746
+ `\n⚠️ You have ${count.toLocaleString()} MDX files. ` +
1747
+ `Consider upgrading to ClickHouse for better performance:\n` +
1748
+ ` DATABASE_URL=chdb://./data/clickhouse\n`
1749
+ )
1750
+ }
1751
+ } catch {
1752
+ // Ignore errors in file counting
1753
+ }
1754
+ }
1755
+
1756
+ // =============================================================================
1757
+ // DB Factory
1758
+ // =============================================================================
1759
+
1760
+ /**
1761
+ * Create a typed database from a schema definition
1762
+ *
1763
+ * Supports both direct usage and destructuring for flexibility:
1764
+ *
1765
+ * @example Direct usage - everything on one object
1766
+ * ```ts
1767
+ * const db = DB({
1768
+ * Post: { title: 'string', author: 'Author.posts' },
1769
+ * Author: { name: 'string' },
1770
+ * })
1771
+ *
1772
+ * // Entity operations
1773
+ * const post = await db.Post.create({ title: 'Hello' })
1774
+ *
1775
+ * // Events, actions, etc. are also available directly
1776
+ * db.events.on('Post.created', (event) => console.log(event))
1777
+ * db.actions.create({ type: 'generate', data: {} })
1778
+ * ```
1779
+ *
1780
+ * @example Destructured usage - cleaner separation
1781
+ * ```ts
1782
+ * const { db, events, actions, artifacts, nouns, verbs } = DB({
1783
+ * Post: { title: 'string', author: 'Author.posts' },
1784
+ * Author: { name: 'string' },
1785
+ * })
1786
+ *
1787
+ * // CRUD operations on db
1788
+ * const post = await db.Post.create({ title: 'Hello' })
1789
+ * await db.Post.update(post.$id, { title: 'Updated' })
1790
+ *
1791
+ * // Separate events API
1792
+ * events.on('Post.created', (event) => console.log(event))
1793
+ *
1794
+ * // Separate actions API
1795
+ * const action = await actions.create({ type: 'generate', data: {} })
1796
+ * ```
1797
+ */
1798
+ export function DB<TSchema extends DatabaseSchema>(
1799
+ schema: TSchema
1800
+ ): DBResult<TSchema> {
1801
+ const parsedSchema = parseSchema(schema)
1802
+
1803
+ // Create Actions API early so it can be injected into entity operations
1804
+ const actionsAPI = {
1805
+ async create(options: CreateActionOptions | { type: string; data: unknown; total?: number }) {
1806
+ const provider = await resolveProvider()
1807
+ if ('createAction' in provider) {
1808
+ return (provider as any).createAction(options)
1809
+ }
1810
+ throw new Error('Provider does not support actions')
1811
+ },
1812
+ async get(id: string) {
1813
+ const provider = await resolveProvider()
1814
+ if ('getAction' in provider) {
1815
+ return (provider as any).getAction(id)
1816
+ }
1817
+ return null
1818
+ },
1819
+ async update(id: string, updates: unknown) {
1820
+ const provider = await resolveProvider()
1821
+ if ('updateAction' in provider) {
1822
+ return (provider as any).updateAction(id, updates)
1823
+ }
1824
+ throw new Error('Provider does not support actions')
1825
+ },
1826
+ }
1827
+
1828
+ // Create entity operations for each type with promise pipelining
1829
+ const entityOperations: Record<string, PipelineEntityOperations<unknown>> = {}
1830
+
1831
+ for (const [entityName, entity] of parsedSchema.entities) {
1832
+ const baseOps = createEntityOperations(entityName, entity, parsedSchema)
1833
+ // Wrap with DBPromise for chainable queries, inject actions for forEach persistence
1834
+ entityOperations[entityName] = wrapEntityOperations(entityName, baseOps, actionsAPI)
1835
+ }
1836
+
1837
+ // Noun definitions cache
1838
+ const nounDefinitions = new Map<string, Noun>()
1839
+
1840
+ // Initialize nouns from schema
1841
+ for (const [entityName] of parsedSchema.entities) {
1842
+ const noun = inferNoun(entityName)
1843
+ nounDefinitions.set(entityName, noun)
1844
+ }
1845
+
1846
+ // Verb definitions cache
1847
+ const verbDefinitions = new Map<string, Verb>(
1848
+ Object.entries(Verbs).map(([k, v]) => [k, v])
1849
+ )
1850
+
1851
+ // Create the typed DB object
1852
+ const db = {
1853
+ $schema: parsedSchema,
1854
+
1855
+ async get(url: string) {
1856
+ const provider = await resolveProvider()
1857
+ const parsed = parseUrl(url)
1858
+ return provider.get(parsed.type, parsed.id)
1859
+ },
1860
+
1861
+ async search(query: string, options?: SearchOptions) {
1862
+ const provider = await resolveProvider()
1863
+ const results: unknown[] = []
1864
+ for (const [typeName] of parsedSchema.entities) {
1865
+ const typeResults = await provider.search(typeName, query, options)
1866
+ results.push(...typeResults)
1867
+ }
1868
+ return results
1869
+ },
1870
+
1871
+ async count(type: string, where?: Record<string, unknown>) {
1872
+ const provider = await resolveProvider()
1873
+ const results = await provider.list(type, { where })
1874
+ return results.length
1875
+ },
1876
+
1877
+ async forEach(
1878
+ options: { type: string; where?: Record<string, unknown>; concurrency?: number },
1879
+ callback: (entity: unknown) => void | Promise<void>
1880
+ ) {
1881
+ const provider = await resolveProvider()
1882
+ const results = await provider.list(options.type, { where: options.where })
1883
+ const concurrency = options.concurrency ?? 1
1884
+
1885
+ if (concurrency === 1) {
1886
+ for (const entity of results) {
1887
+ await callback(entity)
1888
+ }
1889
+ } else {
1890
+ // Process in batches with concurrency
1891
+ const { Semaphore } = await import('./memory-provider.js')
1892
+ const semaphore = new Semaphore(concurrency)
1893
+ await semaphore.map(results, callback as (item: Record<string, unknown>) => Promise<void>)
1894
+ }
1895
+ },
1896
+
1897
+ async set(type: string, id: string, data: Record<string, unknown>) {
1898
+ const provider = await resolveProvider()
1899
+ const existing = await provider.get(type, id)
1900
+ if (existing) {
1901
+ // Replace entirely (not merge)
1902
+ return provider.update(type, id, data)
1903
+ }
1904
+ return provider.create(type, id, data)
1905
+ },
1906
+
1907
+ async generate(options: GenerateOptions) {
1908
+ // Placeholder - actual AI generation would be implemented here
1909
+ // For now, just create with provided data
1910
+ const provider = await resolveProvider()
1911
+ if (options.mode === 'background') {
1912
+ // Return action ID for tracking
1913
+ const { createMemoryProvider } = await import('./memory-provider.js')
1914
+ const memProvider = provider as ReturnType<typeof createMemoryProvider>
1915
+ if ('createAction' in memProvider) {
1916
+ return memProvider.createAction({
1917
+ type: 'generate',
1918
+ data: options,
1919
+ total: options.count ?? 1,
1920
+ })
1921
+ }
1922
+ }
1923
+ // Sync mode - create single entity
1924
+ return provider.create(options.type, undefined, options.data ?? {})
1925
+ },
1926
+
1927
+ ask: createNLQueryFn(parsedSchema),
1928
+
1929
+ ...entityOperations,
1930
+ } as TypedDB<TSchema>
1931
+
1932
+ // Create Events API
1933
+ const events: EventsAPI = {
1934
+ on(pattern, handler) {
1935
+ // Get provider and delegate - need async resolution
1936
+ let unsubscribe = () => {}
1937
+ resolveProvider().then((provider) => {
1938
+ if ('on' in provider) {
1939
+ unsubscribe = (provider as any).on(pattern, handler)
1940
+ }
1941
+ })
1942
+ return () => unsubscribe()
1943
+ },
1944
+
1945
+ async emit(optionsOrType: CreateEventOptions | string, data?: unknown): Promise<DBEvent> {
1946
+ const provider = await resolveProvider()
1947
+ if ('emit' in provider) {
1948
+ return (provider as any).emit(optionsOrType, data)
1949
+ }
1950
+ // Return minimal event if provider doesn't support emit
1951
+ const now = new Date()
1952
+ if (typeof optionsOrType === 'string') {
1953
+ return {
1954
+ id: crypto.randomUUID(),
1955
+ actor: 'system',
1956
+ event: optionsOrType,
1957
+ objectData: data as Record<string, unknown> | undefined,
1958
+ timestamp: now,
1959
+ }
1960
+ }
1961
+ return {
1962
+ id: crypto.randomUUID(),
1963
+ actor: optionsOrType.actor,
1964
+ actorData: optionsOrType.actorData,
1965
+ event: optionsOrType.event,
1966
+ object: optionsOrType.object,
1967
+ objectData: optionsOrType.objectData,
1968
+ result: optionsOrType.result,
1969
+ resultData: optionsOrType.resultData,
1970
+ meta: optionsOrType.meta,
1971
+ timestamp: now,
1972
+ }
1973
+ },
1974
+
1975
+ async list(options) {
1976
+ const provider = await resolveProvider()
1977
+ if ('listEvents' in provider) {
1978
+ return (provider as any).listEvents(options)
1979
+ }
1980
+ return []
1981
+ },
1982
+
1983
+ async replay(options) {
1984
+ const provider = await resolveProvider()
1985
+ if ('replayEvents' in provider) {
1986
+ await (provider as any).replayEvents(options)
1987
+ }
1988
+ },
1989
+ }
1990
+
1991
+ // Create Actions API (extends actionsAPI with list, retry, cancel)
1992
+ const actions: ActionsAPI = {
1993
+ ...actionsAPI,
1994
+
1995
+ async list(options) {
1996
+ const provider = await resolveProvider()
1997
+ if ('listActions' in provider) {
1998
+ return (provider as any).listActions(options)
1999
+ }
2000
+ return []
2001
+ },
2002
+
2003
+ async retry(id) {
2004
+ const provider = await resolveProvider()
2005
+ if ('retryAction' in provider) {
2006
+ return (provider as any).retryAction(id)
2007
+ }
2008
+ throw new Error('Provider does not support actions')
2009
+ },
2010
+
2011
+ async cancel(id) {
2012
+ const provider = await resolveProvider()
2013
+ if ('cancelAction' in provider) {
2014
+ await (provider as any).cancelAction(id)
2015
+ }
2016
+ },
2017
+
2018
+ conjugate,
2019
+ }
2020
+
2021
+ // Create Artifacts API
2022
+ const artifacts: ArtifactsAPI = {
2023
+ async get(url, type) {
2024
+ const provider = await resolveProvider()
2025
+ if ('getArtifact' in provider) {
2026
+ return (provider as any).getArtifact(url, type)
2027
+ }
2028
+ return null
2029
+ },
2030
+
2031
+ async set(url, type, data) {
2032
+ const provider = await resolveProvider()
2033
+ if ('setArtifact' in provider) {
2034
+ await (provider as any).setArtifact(url, type, data)
2035
+ }
2036
+ },
2037
+
2038
+ async delete(url, type) {
2039
+ const provider = await resolveProvider()
2040
+ if ('deleteArtifact' in provider) {
2041
+ await (provider as any).deleteArtifact(url, type)
2042
+ }
2043
+ },
2044
+
2045
+ async list(url) {
2046
+ const provider = await resolveProvider()
2047
+ if ('listArtifacts' in provider) {
2048
+ return (provider as any).listArtifacts(url)
2049
+ }
2050
+ return []
2051
+ },
2052
+ }
2053
+
2054
+ // Create Nouns API
2055
+ const nouns: NounsAPI = {
2056
+ async get(name) {
2057
+ return nounDefinitions.get(name) ?? null
2058
+ },
2059
+
2060
+ async list() {
2061
+ return Array.from(nounDefinitions.values())
2062
+ },
2063
+
2064
+ async define(noun) {
2065
+ nounDefinitions.set(noun.singular, noun)
2066
+ },
2067
+ }
2068
+
2069
+ // Create Verbs API
2070
+ const verbs: VerbsAPI = {
2071
+ get(action) {
2072
+ return verbDefinitions.get(action) ?? null
2073
+ },
2074
+
2075
+ list() {
2076
+ return Array.from(verbDefinitions.values())
2077
+ },
2078
+
2079
+ define(verb) {
2080
+ verbDefinitions.set(verb.action, verb)
2081
+ },
2082
+
2083
+ conjugate,
2084
+ }
2085
+
2086
+ // Return combined object that supports both direct usage and destructuring
2087
+ // db.User.create() works, db.events.on() works
2088
+ // const { db, events } = DB(...) also works
2089
+ return Object.assign(db, {
2090
+ db, // self-reference for destructuring
2091
+ events,
2092
+ actions,
2093
+ artifacts,
2094
+ nouns,
2095
+ verbs,
2096
+ }) as DBResult<TSchema>
2097
+ }
2098
+
2099
+ /**
2100
+ * Parse a URL into type and id
2101
+ */
2102
+ function parseUrl(url: string): { type: string; id: string } {
2103
+ // Handle full URLs
2104
+ if (url.includes('://')) {
2105
+ const parsed = new URL(url)
2106
+ const parts = parsed.pathname.split('/').filter(Boolean)
2107
+ return {
2108
+ type: parts[0] || '',
2109
+ id: parts.slice(1).join('/'),
2110
+ }
2111
+ }
2112
+
2113
+ // Handle type/id format
2114
+ if (url.includes('/')) {
2115
+ const parts = url.split('/')
2116
+ return {
2117
+ type: parts[0]!,
2118
+ id: parts.slice(1).join('/'),
2119
+ }
2120
+ }
2121
+
2122
+ // Just id
2123
+ return { type: '', id: url }
2124
+ }
2125
+
2126
+ /**
2127
+ * Create operations for a single entity type
2128
+ */
2129
+ function createEntityOperations<T>(
2130
+ typeName: string,
2131
+ entity: ParsedEntity,
2132
+ schema: ParsedSchema
2133
+ ): EntityOperations<T> {
2134
+ return {
2135
+ async get(id: string): Promise<T | null> {
2136
+ const provider = await resolveProvider()
2137
+ const result = await provider.get(typeName, id)
2138
+ if (!result) return null
2139
+ return hydrateEntity(result, entity, schema) as T
2140
+ },
2141
+
2142
+ async list(options?: ListOptions): Promise<T[]> {
2143
+ const provider = await resolveProvider()
2144
+ const results = await provider.list(typeName, options)
2145
+ return Promise.all(
2146
+ results.map((r) => hydrateEntity(r, entity, schema) as T)
2147
+ )
2148
+ },
2149
+
2150
+ async find(where: Partial<T>): Promise<T[]> {
2151
+ const provider = await resolveProvider()
2152
+ const results = await provider.list(typeName, {
2153
+ where: where as Record<string, unknown>,
2154
+ })
2155
+ return Promise.all(
2156
+ results.map((r) => hydrateEntity(r, entity, schema) as T)
2157
+ )
2158
+ },
2159
+
2160
+ async search(query: string, options?: SearchOptions): Promise<T[]> {
2161
+ const provider = await resolveProvider()
2162
+ const results = await provider.search(typeName, query, options)
2163
+ return Promise.all(
2164
+ results.map((r) => hydrateEntity(r, entity, schema) as T)
2165
+ )
2166
+ },
2167
+
2168
+ async create(
2169
+ idOrData: string | Omit<T, '$id' | '$type'>,
2170
+ maybeData?: Omit<T, '$id' | '$type'>
2171
+ ): Promise<T> {
2172
+ const provider = await resolveProvider()
2173
+ const id = typeof idOrData === 'string' ? idOrData : undefined
2174
+ const data =
2175
+ typeof idOrData === 'string'
2176
+ ? (maybeData as Record<string, unknown>)
2177
+ : (idOrData as Record<string, unknown>)
2178
+
2179
+ const result = await provider.create(typeName, id, data)
2180
+ return hydrateEntity(result, entity, schema) as T
2181
+ },
2182
+
2183
+ async update(
2184
+ id: string,
2185
+ data: Partial<Omit<T, '$id' | '$type'>>
2186
+ ): Promise<T> {
2187
+ const provider = await resolveProvider()
2188
+ const result = await provider.update(
2189
+ typeName,
2190
+ id,
2191
+ data as Record<string, unknown>
2192
+ )
2193
+ return hydrateEntity(result, entity, schema) as T
2194
+ },
2195
+
2196
+ async upsert(id: string, data: Omit<T, '$id' | '$type'>): Promise<T> {
2197
+ const provider = await resolveProvider()
2198
+ const existing = await provider.get(typeName, id)
2199
+ if (existing) {
2200
+ const result = await provider.update(
2201
+ typeName,
2202
+ id,
2203
+ data as Record<string, unknown>
2204
+ )
2205
+ return hydrateEntity(result, entity, schema) as T
2206
+ }
2207
+ const result = await provider.create(
2208
+ typeName,
2209
+ id,
2210
+ data as Record<string, unknown>
2211
+ )
2212
+ return hydrateEntity(result, entity, schema) as T
2213
+ },
2214
+
2215
+ async delete(id: string): Promise<boolean> {
2216
+ const provider = await resolveProvider()
2217
+ return provider.delete(typeName, id)
2218
+ },
2219
+
2220
+ async forEach(
2221
+ optionsOrCallback:
2222
+ | ListOptions
2223
+ | ((entity: T) => void | Promise<void>),
2224
+ maybeCallback?: (entity: T) => void | Promise<void>
2225
+ ): Promise<void> {
2226
+ const options =
2227
+ typeof optionsOrCallback === 'function' ? undefined : optionsOrCallback
2228
+ const callback =
2229
+ typeof optionsOrCallback === 'function'
2230
+ ? optionsOrCallback
2231
+ : maybeCallback!
2232
+
2233
+ const items = await this.list(options)
2234
+ for (const item of items) {
2235
+ await callback(item)
2236
+ }
2237
+ },
2238
+ }
2239
+ }
2240
+
2241
+ /**
2242
+ * Hydrate an entity with lazy-loaded relations
2243
+ */
2244
+ function hydrateEntity(
2245
+ data: Record<string, unknown>,
2246
+ entity: ParsedEntity,
2247
+ schema: ParsedSchema
2248
+ ): Record<string, unknown> {
2249
+ const hydrated: Record<string, unknown> = { ...data }
2250
+ const id = (data.$id || data.id) as string
2251
+
2252
+ // Add lazy getters for relations
2253
+ for (const [fieldName, field] of entity.fields) {
2254
+ if (field.isRelation && field.relatedType) {
2255
+ const relatedEntity = schema.entities.get(field.relatedType)
2256
+ if (!relatedEntity) continue
2257
+
2258
+ // Define lazy getter
2259
+ Object.defineProperty(hydrated, fieldName, {
2260
+ get: async () => {
2261
+ const provider = await resolveProvider()
2262
+
2263
+ if (field.isArray) {
2264
+ // Array relation - get related entities
2265
+ const results = await provider.related(
2266
+ entity.name,
2267
+ id,
2268
+ fieldName
2269
+ )
2270
+ return Promise.all(
2271
+ results.map((r) => hydrateEntity(r, relatedEntity, schema))
2272
+ )
2273
+ } else {
2274
+ // Single relation - get the stored ID and fetch
2275
+ const relatedId = data[fieldName] as string | undefined
2276
+ if (!relatedId) return null
2277
+ const result = await provider.get(field.relatedType!, relatedId)
2278
+ return result
2279
+ ? hydrateEntity(result, relatedEntity, schema)
2280
+ : null
2281
+ }
2282
+ },
2283
+ enumerable: true,
2284
+ configurable: true,
2285
+ })
2286
+ }
2287
+ }
2288
+
2289
+ return hydrated
2290
+ }
2291
+
2292
+ // =============================================================================
2293
+ // Re-export for convenience
2294
+ // =============================================================================
2295
+
2296
+ export { parseSchema as parse }