ai-database 0.1.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +381 -68
  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 +50 -191
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +79 -462
  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 -37
  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/dist/index.d.mts +0 -195
  72. package/dist/index.mjs +0 -430
package/dist/schema.js ADDED
@@ -0,0 +1,1165 @@
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
+ import { DBPromise, wrapEntityOperations } from './ai-promise-db.js';
32
+ export { toExpanded, toFlat, Verbs, resolveUrl, resolveShortUrl, parseUrl } from './types.js';
33
+ // Re-export linguistic utilities from linguistic.ts
34
+ export { conjugate, pluralize, singularize, inferNoun, createTypeMeta, getTypeMeta, Type, getVerbFields, } from './linguistic.js';
35
+ import { Verbs } from './types.js';
36
+ import { inferNoun, getTypeMeta, conjugate, } from './linguistic.js';
37
+ /**
38
+ * Create a Noun definition with type inference
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * const Post = defineNoun({
43
+ * singular: 'post',
44
+ * plural: 'posts',
45
+ * description: 'A blog post',
46
+ * properties: {
47
+ * title: { type: 'string', description: 'Post title' },
48
+ * content: { type: 'markdown' },
49
+ * },
50
+ * relationships: {
51
+ * author: { type: 'Author', backref: 'posts' },
52
+ * },
53
+ * })
54
+ * ```
55
+ */
56
+ export function defineNoun(noun) {
57
+ return noun;
58
+ }
59
+ /**
60
+ * Create a Verb definition with type inference
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * const publish = defineVerb({
65
+ * action: 'publish',
66
+ * actor: 'publisher',
67
+ * act: 'publishes',
68
+ * activity: 'publishing',
69
+ * result: 'publication',
70
+ * reverse: { at: 'publishedAt', by: 'publishedBy' },
71
+ * inverse: 'unpublish',
72
+ * })
73
+ * ```
74
+ */
75
+ export function defineVerb(verb) {
76
+ return verb;
77
+ }
78
+ /**
79
+ * Convert a Noun to an EntitySchema for use with DB()
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * const postNoun = defineNoun({
84
+ * singular: 'post',
85
+ * plural: 'posts',
86
+ * properties: { title: { type: 'string' } },
87
+ * relationships: { author: { type: 'Author', backref: 'posts' } },
88
+ * })
89
+ *
90
+ * const db = DB({
91
+ * Post: nounToSchema(postNoun),
92
+ * })
93
+ * ```
94
+ */
95
+ export function nounToSchema(noun) {
96
+ const schema = {};
97
+ // Add properties
98
+ if (noun.properties) {
99
+ for (const [name, prop] of Object.entries(noun.properties)) {
100
+ let type = prop.type;
101
+ if (prop.array)
102
+ type = `${type}[]`;
103
+ if (prop.optional)
104
+ type = `${type}?`;
105
+ schema[name] = type;
106
+ }
107
+ }
108
+ // Add relationships
109
+ if (noun.relationships) {
110
+ for (const [name, rel] of Object.entries(noun.relationships)) {
111
+ const baseType = rel.type.replace('[]', '');
112
+ const isArray = rel.type.endsWith('[]');
113
+ if (rel.backref) {
114
+ schema[name] = isArray ? [`${baseType}.${rel.backref}`] : `${baseType}.${rel.backref}`;
115
+ }
116
+ else {
117
+ schema[name] = rel.type;
118
+ }
119
+ }
120
+ }
121
+ return schema;
122
+ }
123
+ // =============================================================================
124
+ // Built-in Schema Types - Self-Describing Database
125
+ // =============================================================================
126
+ /**
127
+ * Built-in Thing schema - base type for all entities
128
+ *
129
+ * Every entity instance is a Thing with a relationship to its Noun.
130
+ * This creates a complete graph: Thing.type -> Noun.things
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * // Every post instance:
135
+ * post.$type // 'Post' (string)
136
+ * post.type // -> Noun('Post') (relationship)
137
+ *
138
+ * // From Noun, get all instances:
139
+ * const postNoun = await db.Noun.get('Post')
140
+ * const allPosts = await postNoun.things // -> Post[]
141
+ * ```
142
+ */
143
+ export const ThingSchema = {
144
+ // Every Thing has a type that links to its Noun
145
+ type: 'Noun.things', // Thing.type -> Noun, Noun.things -> Thing[]
146
+ };
147
+ /**
148
+ * Built-in Noun schema for storing type definitions
149
+ *
150
+ * Every Type/Collection automatically gets a Noun record stored in the database.
151
+ * This enables introspection and self-describing schemas.
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * // When you define:
156
+ * const db = DB({ Post: { title: 'string' } })
157
+ *
158
+ * // The database auto-creates:
159
+ * // db.Noun.get('Post') => { singular: 'post', plural: 'posts', ... }
160
+ *
161
+ * // Query all types:
162
+ * const types = await db.Noun.list()
163
+ *
164
+ * // Get all instances of a type:
165
+ * const postNoun = await db.Noun.get('Post')
166
+ * const allPosts = await postNoun.things
167
+ *
168
+ * // Listen for new types:
169
+ * on.Noun.created(noun => console.log(`New type: ${noun.name}`))
170
+ * ```
171
+ */
172
+ export const NounSchema = {
173
+ // Identity
174
+ name: 'string', // 'Post', 'BlogPost'
175
+ singular: 'string', // 'post', 'blog post'
176
+ plural: 'string', // 'posts', 'blog posts'
177
+ slug: 'string', // 'post', 'blog-post'
178
+ slugPlural: 'string', // 'posts', 'blog-posts'
179
+ description: 'string?', // Human description
180
+ // Schema
181
+ properties: 'json?', // Property definitions
182
+ relationships: 'json?', // Relationship definitions
183
+ // Behavior
184
+ actions: 'json?', // Available actions (verbs)
185
+ events: 'json?', // Event types
186
+ // Metadata
187
+ metadata: 'json?', // Additional metadata
188
+ // Relationships - auto-created by bi-directional system
189
+ // things: Thing[] // All instances of this type (backref from Thing.type)
190
+ };
191
+ /**
192
+ * Built-in Verb schema for storing action definitions
193
+ */
194
+ export const VerbSchema = {
195
+ action: 'string', // 'create', 'publish'
196
+ actor: 'string?', // 'creator', 'publisher'
197
+ act: 'string?', // 'creates', 'publishes'
198
+ activity: 'string?', // 'creating', 'publishing'
199
+ result: 'string?', // 'creation', 'publication'
200
+ reverse: 'json?', // { at, by, in, for }
201
+ inverse: 'string?', // 'delete', 'unpublish'
202
+ description: 'string?',
203
+ };
204
+ /**
205
+ * Built-in Edge schema for relationships between types
206
+ *
207
+ * Every relationship in a schema creates an Edge record.
208
+ * This enables graph queries across the type system.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * // Post.author -> Author creates:
213
+ * // Edge { from: 'Post', name: 'author', to: 'Author', backref: 'posts', cardinality: 'many-to-one' }
214
+ *
215
+ * // Query the graph:
216
+ * const edges = await db.Edge.find({ to: 'Author' })
217
+ * // => [{ from: 'Post', name: 'author' }, { from: 'Comment', name: 'author' }]
218
+ *
219
+ * // What types reference Author?
220
+ * const referencing = edges.map(e => e.from) // ['Post', 'Comment']
221
+ * ```
222
+ */
223
+ export const EdgeSchema = {
224
+ from: 'string', // Source type: 'Post'
225
+ name: 'string', // Field name: 'author'
226
+ to: 'string', // Target type: 'Author'
227
+ backref: 'string?', // Inverse field: 'posts'
228
+ cardinality: 'string', // 'one-to-one', 'one-to-many', 'many-to-one', 'many-to-many'
229
+ required: 'boolean?', // Is this relationship required?
230
+ description: 'string?', // Human description
231
+ };
232
+ /**
233
+ * System types that are auto-created in every database
234
+ *
235
+ * The graph structure:
236
+ * - Thing.type -> Noun (every instance links to its type)
237
+ * - Noun.things -> Thing[] (every type has its instances)
238
+ * - Edge connects Nouns (relationships between types)
239
+ * - Verb describes actions on Nouns
240
+ */
241
+ export const SystemSchema = {
242
+ Thing: ThingSchema,
243
+ Noun: NounSchema,
244
+ Verb: VerbSchema,
245
+ Edge: EdgeSchema,
246
+ };
247
+ /**
248
+ * Create Edge records from schema relationships
249
+ *
250
+ * @internal Used by DB() to auto-populate Edge records
251
+ */
252
+ export function createEdgeRecords(typeName, schema, parsedEntity) {
253
+ const edges = [];
254
+ for (const [fieldName, field] of parsedEntity.fields) {
255
+ if (field.isRelation && field.relatedType) {
256
+ const cardinality = field.isArray
257
+ ? field.backref ? 'many-to-many' : 'one-to-many'
258
+ : field.backref ? 'many-to-one' : 'one-to-one';
259
+ edges.push({
260
+ from: typeName,
261
+ name: fieldName,
262
+ to: field.relatedType,
263
+ backref: field.backref,
264
+ cardinality,
265
+ });
266
+ }
267
+ }
268
+ return edges;
269
+ }
270
+ /**
271
+ * Create a Noun record from a type name and optional schema
272
+ *
273
+ * @internal Used by DB() to auto-populate Noun records
274
+ */
275
+ export function createNounRecord(typeName, schema, nounDef) {
276
+ const meta = getTypeMeta(typeName);
277
+ const inferred = inferNoun(typeName);
278
+ return {
279
+ name: typeName,
280
+ singular: nounDef?.singular ?? meta.singular,
281
+ plural: nounDef?.plural ?? meta.plural,
282
+ slug: meta.slug,
283
+ slugPlural: meta.slugPlural,
284
+ description: nounDef?.description,
285
+ properties: nounDef?.properties ?? (schema ? schemaToProperties(schema) : undefined),
286
+ relationships: nounDef?.relationships,
287
+ actions: nounDef?.actions ?? inferred.actions,
288
+ events: nounDef?.events ?? inferred.events,
289
+ metadata: nounDef?.metadata,
290
+ };
291
+ }
292
+ /**
293
+ * Convert EntitySchema to NounProperty format
294
+ */
295
+ function schemaToProperties(schema) {
296
+ const properties = {};
297
+ for (const [name, def] of Object.entries(schema)) {
298
+ const defStr = Array.isArray(def) ? def[0] : def;
299
+ const isOptional = defStr.endsWith('?');
300
+ const isArray = defStr.endsWith('[]') || Array.isArray(def);
301
+ const baseType = defStr.replace(/[\?\[\]]/g, '').split('.')[0];
302
+ properties[name] = {
303
+ type: baseType,
304
+ optional: isOptional,
305
+ array: isArray,
306
+ };
307
+ }
308
+ return properties;
309
+ }
310
+ // =============================================================================
311
+ // Schema Parsing
312
+ // =============================================================================
313
+ /**
314
+ * Parse a single field definition
315
+ */
316
+ function parseField(name, definition) {
317
+ // Handle array literal syntax: ['Author.posts']
318
+ if (Array.isArray(definition)) {
319
+ const inner = parseField(name, definition[0]);
320
+ return { ...inner, isArray: true };
321
+ }
322
+ let type = definition;
323
+ let isArray = false;
324
+ let isOptional = false;
325
+ let isRelation = false;
326
+ let relatedType;
327
+ let backref;
328
+ // Check for optional modifier
329
+ if (type.endsWith('?')) {
330
+ isOptional = true;
331
+ type = type.slice(0, -1);
332
+ }
333
+ // Check for array modifier (string syntax)
334
+ if (type.endsWith('[]')) {
335
+ isArray = true;
336
+ type = type.slice(0, -2);
337
+ }
338
+ // Check for relation (contains a dot for backref)
339
+ if (type.includes('.')) {
340
+ isRelation = true;
341
+ const [entityName, backrefName] = type.split('.');
342
+ relatedType = entityName;
343
+ backref = backrefName;
344
+ type = entityName;
345
+ }
346
+ else if (type[0] === type[0]?.toUpperCase() && !isPrimitiveType(type)) {
347
+ // PascalCase non-primitive = relation without explicit backref
348
+ isRelation = true;
349
+ relatedType = type;
350
+ }
351
+ return {
352
+ name,
353
+ type,
354
+ isArray,
355
+ isOptional,
356
+ isRelation,
357
+ relatedType,
358
+ backref,
359
+ };
360
+ }
361
+ /**
362
+ * Check if a type is a primitive
363
+ */
364
+ function isPrimitiveType(type) {
365
+ const primitives = [
366
+ 'string',
367
+ 'number',
368
+ 'boolean',
369
+ 'date',
370
+ 'datetime',
371
+ 'json',
372
+ 'markdown',
373
+ 'url',
374
+ ];
375
+ return primitives.includes(type);
376
+ }
377
+ /**
378
+ * Parse a database schema and resolve bi-directional relationships
379
+ */
380
+ export function parseSchema(schema) {
381
+ const entities = new Map();
382
+ // First pass: parse all entities and their fields
383
+ for (const [entityName, entitySchema] of Object.entries(schema)) {
384
+ const fields = new Map();
385
+ for (const [fieldName, fieldDef] of Object.entries(entitySchema)) {
386
+ fields.set(fieldName, parseField(fieldName, fieldDef));
387
+ }
388
+ entities.set(entityName, { name: entityName, fields });
389
+ }
390
+ // Second pass: create bi-directional relationships
391
+ for (const [entityName, entity] of entities) {
392
+ for (const [fieldName, field] of entity.fields) {
393
+ if (field.isRelation && field.relatedType && field.backref) {
394
+ const relatedEntity = entities.get(field.relatedType);
395
+ if (relatedEntity && !relatedEntity.fields.has(field.backref)) {
396
+ // Auto-create the inverse relation
397
+ // If Post.author -> Author.posts, then Author.posts -> Post[]
398
+ relatedEntity.fields.set(field.backref, {
399
+ name: field.backref,
400
+ type: entityName,
401
+ isArray: true, // Backref is always an array
402
+ isOptional: false,
403
+ isRelation: true,
404
+ relatedType: entityName,
405
+ backref: fieldName, // Points back to the original field
406
+ });
407
+ }
408
+ }
409
+ }
410
+ }
411
+ return { entities };
412
+ }
413
+ let nlQueryGenerator = null;
414
+ /**
415
+ * Set the AI generator for natural language queries
416
+ *
417
+ * @example
418
+ * ```ts
419
+ * import { generate } from 'ai-functions'
420
+ *
421
+ * setNLQueryGenerator(async (prompt, context) => {
422
+ * return generate({
423
+ * prompt: `Given this schema: ${JSON.stringify(context.types)}
424
+ * Answer this question: ${prompt}
425
+ * Return a query plan as JSON.`,
426
+ * schema: NLQueryPlanSchema
427
+ * })
428
+ * })
429
+ * ```
430
+ */
431
+ export function setNLQueryGenerator(generator) {
432
+ nlQueryGenerator = generator;
433
+ }
434
+ /**
435
+ * Build schema context for NL queries
436
+ */
437
+ function buildNLQueryContext(schema, targetType) {
438
+ const types = [];
439
+ for (const [name, entity] of schema.entities) {
440
+ const fields = [];
441
+ const relationships = [];
442
+ for (const [fieldName, field] of entity.fields) {
443
+ if (field.isRelation && field.relatedType) {
444
+ relationships.push({
445
+ name: fieldName,
446
+ to: field.relatedType,
447
+ cardinality: field.isArray ? 'many' : 'one',
448
+ });
449
+ }
450
+ else {
451
+ fields.push(fieldName);
452
+ }
453
+ }
454
+ const meta = getTypeMeta(name);
455
+ types.push({
456
+ name,
457
+ singular: meta.singular,
458
+ plural: meta.plural,
459
+ fields,
460
+ relationships,
461
+ });
462
+ }
463
+ return { types, targetType };
464
+ }
465
+ /**
466
+ * Execute a natural language query
467
+ */
468
+ async function executeNLQuery(question, schema, targetType) {
469
+ // If no AI generator configured, fall back to search
470
+ if (!nlQueryGenerator) {
471
+ // Simple fallback: search across all types or target type
472
+ const provider = await resolveProvider();
473
+ const results = [];
474
+ if (targetType) {
475
+ const searchResults = await provider.search(targetType, question);
476
+ results.push(...searchResults);
477
+ }
478
+ else {
479
+ for (const [typeName] of schema.entities) {
480
+ const searchResults = await provider.search(typeName, question);
481
+ results.push(...searchResults);
482
+ }
483
+ }
484
+ return {
485
+ interpretation: `Search for "${question}"`,
486
+ confidence: 0.5,
487
+ results,
488
+ explanation: 'Fallback to keyword search (no AI generator configured)',
489
+ };
490
+ }
491
+ // Build context and get AI-generated query plan
492
+ const context = buildNLQueryContext(schema, targetType);
493
+ const plan = await nlQueryGenerator(question, context);
494
+ // Execute the plan
495
+ const provider = await resolveProvider();
496
+ const results = [];
497
+ for (const typeName of plan.types) {
498
+ let typeResults;
499
+ if (plan.search) {
500
+ typeResults = await provider.search(typeName, plan.search, {
501
+ where: plan.filters,
502
+ });
503
+ }
504
+ else {
505
+ typeResults = await provider.list(typeName, {
506
+ where: plan.filters,
507
+ });
508
+ }
509
+ results.push(...typeResults);
510
+ }
511
+ return {
512
+ interpretation: plan.interpretation,
513
+ confidence: plan.confidence,
514
+ results,
515
+ query: JSON.stringify({ types: plan.types, filters: plan.filters, search: plan.search }),
516
+ };
517
+ }
518
+ /**
519
+ * Create a natural language query function for a specific type
520
+ */
521
+ function createNLQueryFn(schema, typeName) {
522
+ return async (strings, ...values) => {
523
+ // Interpolate the template
524
+ const question = strings.reduce((acc, str, i) => {
525
+ return acc + str + (values[i] !== undefined ? String(values[i]) : '');
526
+ }, '');
527
+ return executeNLQuery(question, schema, typeName);
528
+ };
529
+ }
530
+ // =============================================================================
531
+ // Provider Resolution
532
+ // =============================================================================
533
+ let globalProvider = null;
534
+ let providerPromise = null;
535
+ /** File count threshold for suggesting ClickHouse upgrade */
536
+ const FILE_COUNT_THRESHOLD = 10_000;
537
+ /**
538
+ * Set the global database provider
539
+ */
540
+ export function setProvider(provider) {
541
+ globalProvider = provider;
542
+ providerPromise = null;
543
+ }
544
+ /**
545
+ * Parse DATABASE_URL into provider type and paths
546
+ *
547
+ * Local storage (all use .db/ folder):
548
+ * - `./content` → fs (default)
549
+ * - `sqlite://./content` → sqlite stored in ./content/.db/index.sqlite
550
+ * - `chdb://./content` → clickhouse stored in ./content/.db/clickhouse/
551
+ *
552
+ * Remote:
553
+ * - `libsql://your-db.turso.io` → Turso SQLite
554
+ * - `clickhouse://host:8123` → ClickHouse HTTP
555
+ * - `:memory:` → in-memory
556
+ */
557
+ function parseDatabaseUrl(url) {
558
+ if (!url)
559
+ return { provider: 'fs', root: './content' };
560
+ // In-memory
561
+ if (url === ':memory:') {
562
+ return { provider: 'memory', root: '' };
563
+ }
564
+ // Remote Turso
565
+ if (url.startsWith('libsql://') || url.includes('.turso.io')) {
566
+ return { provider: 'sqlite', root: '', remoteUrl: url };
567
+ }
568
+ // Remote ClickHouse
569
+ if (url.startsWith('clickhouse://') && url.includes(':')) {
570
+ // clickhouse://host:port/db
571
+ return { provider: 'clickhouse', root: '', remoteUrl: url.replace('clickhouse://', 'https://') };
572
+ }
573
+ // Local SQLite: sqlite://./content → ./content/.db/index.sqlite
574
+ if (url.startsWith('sqlite://')) {
575
+ const root = url.replace('sqlite://', '') || './content';
576
+ return { provider: 'sqlite', root };
577
+ }
578
+ // Local ClickHouse (chDB): chdb://./content → ./content/.db/clickhouse/
579
+ if (url.startsWith('chdb://')) {
580
+ const root = url.replace('chdb://', '') || './content';
581
+ return { provider: 'clickhouse', root };
582
+ }
583
+ // Default: filesystem
584
+ return { provider: 'fs', root: url };
585
+ }
586
+ /**
587
+ * Resolve provider from DATABASE_URL environment variable
588
+ *
589
+ * @example
590
+ * ```bash
591
+ * # Filesystem (default) - stores in ./content with .db/ metadata
592
+ * DATABASE_URL=./content
593
+ *
594
+ * # Local SQLite - stores in ./content/.db/index.sqlite
595
+ * DATABASE_URL=sqlite://./content
596
+ *
597
+ * # Remote Turso
598
+ * DATABASE_URL=libsql://your-db.turso.io
599
+ *
600
+ * # Local ClickHouse (chDB) - stores in ./content/.db/clickhouse/
601
+ * DATABASE_URL=chdb://./content
602
+ *
603
+ * # Remote ClickHouse
604
+ * DATABASE_URL=clickhouse://localhost:8123
605
+ *
606
+ * # In-memory (testing)
607
+ * DATABASE_URL=:memory:
608
+ * ```
609
+ */
610
+ async function resolveProvider() {
611
+ if (globalProvider)
612
+ return globalProvider;
613
+ if (providerPromise)
614
+ return providerPromise;
615
+ providerPromise = (async () => {
616
+ const databaseUrl = (typeof process !== 'undefined' && process.env?.DATABASE_URL) || './content';
617
+ const parsed = parseDatabaseUrl(databaseUrl);
618
+ switch (parsed.provider) {
619
+ case 'memory': {
620
+ const { createMemoryProvider } = await import('./memory-provider.js');
621
+ globalProvider = createMemoryProvider();
622
+ break;
623
+ }
624
+ case 'fs': {
625
+ try {
626
+ const { createFsProvider } = await import('@mdxdb/fs');
627
+ globalProvider = createFsProvider({ root: parsed.root });
628
+ // Check file count and warn if approaching threshold
629
+ checkFileCountThreshold(parsed.root);
630
+ }
631
+ catch (err) {
632
+ console.warn('@mdxdb/fs not available, falling back to memory provider');
633
+ const { createMemoryProvider } = await import('./memory-provider.js');
634
+ globalProvider = createMemoryProvider();
635
+ }
636
+ break;
637
+ }
638
+ case 'sqlite': {
639
+ try {
640
+ const { createSqliteProvider } = await import('@mdxdb/sqlite');
641
+ if (parsed.remoteUrl) {
642
+ // Remote Turso
643
+ globalProvider = await createSqliteProvider({ url: parsed.remoteUrl });
644
+ }
645
+ else {
646
+ // Local SQLite in .db folder
647
+ const dbPath = `${parsed.root}/.db/index.sqlite`;
648
+ globalProvider = await createSqliteProvider({ url: `file:${dbPath}` });
649
+ }
650
+ }
651
+ catch (err) {
652
+ console.warn('@mdxdb/sqlite not available, falling back to memory provider');
653
+ const { createMemoryProvider } = await import('./memory-provider.js');
654
+ globalProvider = createMemoryProvider();
655
+ }
656
+ break;
657
+ }
658
+ case 'clickhouse': {
659
+ try {
660
+ const { createClickhouseProvider } = await import('@mdxdb/clickhouse');
661
+ if (parsed.remoteUrl) {
662
+ // Remote ClickHouse
663
+ globalProvider = await createClickhouseProvider({
664
+ mode: 'http',
665
+ url: parsed.remoteUrl,
666
+ });
667
+ }
668
+ else {
669
+ // Local chDB in .db folder
670
+ const dbPath = `${parsed.root}/.db/clickhouse`;
671
+ globalProvider = await createClickhouseProvider({
672
+ mode: 'chdb',
673
+ url: dbPath,
674
+ });
675
+ }
676
+ }
677
+ catch (err) {
678
+ console.warn('@mdxdb/clickhouse not available, falling back to memory provider');
679
+ const { createMemoryProvider } = await import('./memory-provider.js');
680
+ globalProvider = createMemoryProvider();
681
+ }
682
+ break;
683
+ }
684
+ default: {
685
+ const { createMemoryProvider } = await import('./memory-provider.js');
686
+ globalProvider = createMemoryProvider();
687
+ }
688
+ }
689
+ return globalProvider;
690
+ })();
691
+ return providerPromise;
692
+ }
693
+ /**
694
+ * Check file count and warn if approaching threshold
695
+ */
696
+ async function checkFileCountThreshold(root) {
697
+ try {
698
+ const fs = await import('node:fs/promises');
699
+ const path = await import('node:path');
700
+ async function countFiles(dir) {
701
+ let count = 0;
702
+ try {
703
+ const entries = await fs.readdir(dir, { withFileTypes: true });
704
+ for (const entry of entries) {
705
+ if (entry.name.startsWith('.'))
706
+ continue;
707
+ if (entry.isDirectory()) {
708
+ count += await countFiles(path.join(dir, entry.name));
709
+ }
710
+ else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {
711
+ count++;
712
+ }
713
+ }
714
+ }
715
+ catch {
716
+ // Directory doesn't exist yet
717
+ }
718
+ return count;
719
+ }
720
+ const count = await countFiles(root);
721
+ if (count > FILE_COUNT_THRESHOLD) {
722
+ console.warn(`\n⚠️ You have ${count.toLocaleString()} MDX files. ` +
723
+ `Consider upgrading to ClickHouse for better performance:\n` +
724
+ ` DATABASE_URL=chdb://./data/clickhouse\n`);
725
+ }
726
+ }
727
+ catch {
728
+ // Ignore errors in file counting
729
+ }
730
+ }
731
+ // =============================================================================
732
+ // DB Factory
733
+ // =============================================================================
734
+ /**
735
+ * Create a typed database from a schema definition
736
+ *
737
+ * Supports both direct usage and destructuring for flexibility:
738
+ *
739
+ * @example Direct usage - everything on one object
740
+ * ```ts
741
+ * const db = DB({
742
+ * Post: { title: 'string', author: 'Author.posts' },
743
+ * Author: { name: 'string' },
744
+ * })
745
+ *
746
+ * // Entity operations
747
+ * const post = await db.Post.create({ title: 'Hello' })
748
+ *
749
+ * // Events, actions, etc. are also available directly
750
+ * db.events.on('Post.created', (event) => console.log(event))
751
+ * db.actions.create({ type: 'generate', data: {} })
752
+ * ```
753
+ *
754
+ * @example Destructured usage - cleaner separation
755
+ * ```ts
756
+ * const { db, events, actions, artifacts, nouns, verbs } = DB({
757
+ * Post: { title: 'string', author: 'Author.posts' },
758
+ * Author: { name: 'string' },
759
+ * })
760
+ *
761
+ * // CRUD operations on db
762
+ * const post = await db.Post.create({ title: 'Hello' })
763
+ * await db.Post.update(post.$id, { title: 'Updated' })
764
+ *
765
+ * // Separate events API
766
+ * events.on('Post.created', (event) => console.log(event))
767
+ *
768
+ * // Separate actions API
769
+ * const action = await actions.create({ type: 'generate', data: {} })
770
+ * ```
771
+ */
772
+ export function DB(schema) {
773
+ const parsedSchema = parseSchema(schema);
774
+ // Create Actions API early so it can be injected into entity operations
775
+ const actionsAPI = {
776
+ async create(options) {
777
+ const provider = await resolveProvider();
778
+ if ('createAction' in provider) {
779
+ return provider.createAction(options);
780
+ }
781
+ throw new Error('Provider does not support actions');
782
+ },
783
+ async get(id) {
784
+ const provider = await resolveProvider();
785
+ if ('getAction' in provider) {
786
+ return provider.getAction(id);
787
+ }
788
+ return null;
789
+ },
790
+ async update(id, updates) {
791
+ const provider = await resolveProvider();
792
+ if ('updateAction' in provider) {
793
+ return provider.updateAction(id, updates);
794
+ }
795
+ throw new Error('Provider does not support actions');
796
+ },
797
+ };
798
+ // Create entity operations for each type with promise pipelining
799
+ const entityOperations = {};
800
+ for (const [entityName, entity] of parsedSchema.entities) {
801
+ const baseOps = createEntityOperations(entityName, entity, parsedSchema);
802
+ // Wrap with DBPromise for chainable queries, inject actions for forEach persistence
803
+ entityOperations[entityName] = wrapEntityOperations(entityName, baseOps, actionsAPI);
804
+ }
805
+ // Noun definitions cache
806
+ const nounDefinitions = new Map();
807
+ // Initialize nouns from schema
808
+ for (const [entityName] of parsedSchema.entities) {
809
+ const noun = inferNoun(entityName);
810
+ nounDefinitions.set(entityName, noun);
811
+ }
812
+ // Verb definitions cache
813
+ const verbDefinitions = new Map(Object.entries(Verbs).map(([k, v]) => [k, v]));
814
+ // Create the typed DB object
815
+ const db = {
816
+ $schema: parsedSchema,
817
+ async get(url) {
818
+ const provider = await resolveProvider();
819
+ const parsed = parseUrl(url);
820
+ return provider.get(parsed.type, parsed.id);
821
+ },
822
+ async search(query, options) {
823
+ const provider = await resolveProvider();
824
+ const results = [];
825
+ for (const [typeName] of parsedSchema.entities) {
826
+ const typeResults = await provider.search(typeName, query, options);
827
+ results.push(...typeResults);
828
+ }
829
+ return results;
830
+ },
831
+ async count(type, where) {
832
+ const provider = await resolveProvider();
833
+ const results = await provider.list(type, { where });
834
+ return results.length;
835
+ },
836
+ async forEach(options, callback) {
837
+ const provider = await resolveProvider();
838
+ const results = await provider.list(options.type, { where: options.where });
839
+ const concurrency = options.concurrency ?? 1;
840
+ if (concurrency === 1) {
841
+ for (const entity of results) {
842
+ await callback(entity);
843
+ }
844
+ }
845
+ else {
846
+ // Process in batches with concurrency
847
+ const { Semaphore } = await import('./memory-provider.js');
848
+ const semaphore = new Semaphore(concurrency);
849
+ await semaphore.map(results, callback);
850
+ }
851
+ },
852
+ async set(type, id, data) {
853
+ const provider = await resolveProvider();
854
+ const existing = await provider.get(type, id);
855
+ if (existing) {
856
+ // Replace entirely (not merge)
857
+ return provider.update(type, id, data);
858
+ }
859
+ return provider.create(type, id, data);
860
+ },
861
+ async generate(options) {
862
+ // Placeholder - actual AI generation would be implemented here
863
+ // For now, just create with provided data
864
+ const provider = await resolveProvider();
865
+ if (options.mode === 'background') {
866
+ // Return action ID for tracking
867
+ const { createMemoryProvider } = await import('./memory-provider.js');
868
+ const memProvider = provider;
869
+ if ('createAction' in memProvider) {
870
+ return memProvider.createAction({
871
+ type: 'generate',
872
+ data: options,
873
+ total: options.count ?? 1,
874
+ });
875
+ }
876
+ }
877
+ // Sync mode - create single entity
878
+ return provider.create(options.type, undefined, options.data ?? {});
879
+ },
880
+ ask: createNLQueryFn(parsedSchema),
881
+ ...entityOperations,
882
+ };
883
+ // Create Events API
884
+ const events = {
885
+ on(pattern, handler) {
886
+ // Get provider and delegate - need async resolution
887
+ let unsubscribe = () => { };
888
+ resolveProvider().then((provider) => {
889
+ if ('on' in provider) {
890
+ unsubscribe = provider.on(pattern, handler);
891
+ }
892
+ });
893
+ return () => unsubscribe();
894
+ },
895
+ async emit(optionsOrType, data) {
896
+ const provider = await resolveProvider();
897
+ if ('emit' in provider) {
898
+ return provider.emit(optionsOrType, data);
899
+ }
900
+ // Return minimal event if provider doesn't support emit
901
+ const now = new Date();
902
+ if (typeof optionsOrType === 'string') {
903
+ return {
904
+ id: crypto.randomUUID(),
905
+ actor: 'system',
906
+ event: optionsOrType,
907
+ objectData: data,
908
+ timestamp: now,
909
+ };
910
+ }
911
+ return {
912
+ id: crypto.randomUUID(),
913
+ actor: optionsOrType.actor,
914
+ actorData: optionsOrType.actorData,
915
+ event: optionsOrType.event,
916
+ object: optionsOrType.object,
917
+ objectData: optionsOrType.objectData,
918
+ result: optionsOrType.result,
919
+ resultData: optionsOrType.resultData,
920
+ meta: optionsOrType.meta,
921
+ timestamp: now,
922
+ };
923
+ },
924
+ async list(options) {
925
+ const provider = await resolveProvider();
926
+ if ('listEvents' in provider) {
927
+ return provider.listEvents(options);
928
+ }
929
+ return [];
930
+ },
931
+ async replay(options) {
932
+ const provider = await resolveProvider();
933
+ if ('replayEvents' in provider) {
934
+ await provider.replayEvents(options);
935
+ }
936
+ },
937
+ };
938
+ // Create Actions API (extends actionsAPI with list, retry, cancel)
939
+ const actions = {
940
+ ...actionsAPI,
941
+ async list(options) {
942
+ const provider = await resolveProvider();
943
+ if ('listActions' in provider) {
944
+ return provider.listActions(options);
945
+ }
946
+ return [];
947
+ },
948
+ async retry(id) {
949
+ const provider = await resolveProvider();
950
+ if ('retryAction' in provider) {
951
+ return provider.retryAction(id);
952
+ }
953
+ throw new Error('Provider does not support actions');
954
+ },
955
+ async cancel(id) {
956
+ const provider = await resolveProvider();
957
+ if ('cancelAction' in provider) {
958
+ await provider.cancelAction(id);
959
+ }
960
+ },
961
+ conjugate,
962
+ };
963
+ // Create Artifacts API
964
+ const artifacts = {
965
+ async get(url, type) {
966
+ const provider = await resolveProvider();
967
+ if ('getArtifact' in provider) {
968
+ return provider.getArtifact(url, type);
969
+ }
970
+ return null;
971
+ },
972
+ async set(url, type, data) {
973
+ const provider = await resolveProvider();
974
+ if ('setArtifact' in provider) {
975
+ await provider.setArtifact(url, type, data);
976
+ }
977
+ },
978
+ async delete(url, type) {
979
+ const provider = await resolveProvider();
980
+ if ('deleteArtifact' in provider) {
981
+ await provider.deleteArtifact(url, type);
982
+ }
983
+ },
984
+ async list(url) {
985
+ const provider = await resolveProvider();
986
+ if ('listArtifacts' in provider) {
987
+ return provider.listArtifacts(url);
988
+ }
989
+ return [];
990
+ },
991
+ };
992
+ // Create Nouns API
993
+ const nouns = {
994
+ async get(name) {
995
+ return nounDefinitions.get(name) ?? null;
996
+ },
997
+ async list() {
998
+ return Array.from(nounDefinitions.values());
999
+ },
1000
+ async define(noun) {
1001
+ nounDefinitions.set(noun.singular, noun);
1002
+ },
1003
+ };
1004
+ // Create Verbs API
1005
+ const verbs = {
1006
+ get(action) {
1007
+ return verbDefinitions.get(action) ?? null;
1008
+ },
1009
+ list() {
1010
+ return Array.from(verbDefinitions.values());
1011
+ },
1012
+ define(verb) {
1013
+ verbDefinitions.set(verb.action, verb);
1014
+ },
1015
+ conjugate,
1016
+ };
1017
+ // Return combined object that supports both direct usage and destructuring
1018
+ // db.User.create() works, db.events.on() works
1019
+ // const { db, events } = DB(...) also works
1020
+ return Object.assign(db, {
1021
+ db, // self-reference for destructuring
1022
+ events,
1023
+ actions,
1024
+ artifacts,
1025
+ nouns,
1026
+ verbs,
1027
+ });
1028
+ }
1029
+ /**
1030
+ * Parse a URL into type and id
1031
+ */
1032
+ function parseUrl(url) {
1033
+ // Handle full URLs
1034
+ if (url.includes('://')) {
1035
+ const parsed = new URL(url);
1036
+ const parts = parsed.pathname.split('/').filter(Boolean);
1037
+ return {
1038
+ type: parts[0] || '',
1039
+ id: parts.slice(1).join('/'),
1040
+ };
1041
+ }
1042
+ // Handle type/id format
1043
+ if (url.includes('/')) {
1044
+ const parts = url.split('/');
1045
+ return {
1046
+ type: parts[0],
1047
+ id: parts.slice(1).join('/'),
1048
+ };
1049
+ }
1050
+ // Just id
1051
+ return { type: '', id: url };
1052
+ }
1053
+ /**
1054
+ * Create operations for a single entity type
1055
+ */
1056
+ function createEntityOperations(typeName, entity, schema) {
1057
+ return {
1058
+ async get(id) {
1059
+ const provider = await resolveProvider();
1060
+ const result = await provider.get(typeName, id);
1061
+ if (!result)
1062
+ return null;
1063
+ return hydrateEntity(result, entity, schema);
1064
+ },
1065
+ async list(options) {
1066
+ const provider = await resolveProvider();
1067
+ const results = await provider.list(typeName, options);
1068
+ return Promise.all(results.map((r) => hydrateEntity(r, entity, schema)));
1069
+ },
1070
+ async find(where) {
1071
+ const provider = await resolveProvider();
1072
+ const results = await provider.list(typeName, {
1073
+ where: where,
1074
+ });
1075
+ return Promise.all(results.map((r) => hydrateEntity(r, entity, schema)));
1076
+ },
1077
+ async search(query, options) {
1078
+ const provider = await resolveProvider();
1079
+ const results = await provider.search(typeName, query, options);
1080
+ return Promise.all(results.map((r) => hydrateEntity(r, entity, schema)));
1081
+ },
1082
+ async create(idOrData, maybeData) {
1083
+ const provider = await resolveProvider();
1084
+ const id = typeof idOrData === 'string' ? idOrData : undefined;
1085
+ const data = typeof idOrData === 'string'
1086
+ ? maybeData
1087
+ : idOrData;
1088
+ const result = await provider.create(typeName, id, data);
1089
+ return hydrateEntity(result, entity, schema);
1090
+ },
1091
+ async update(id, data) {
1092
+ const provider = await resolveProvider();
1093
+ const result = await provider.update(typeName, id, data);
1094
+ return hydrateEntity(result, entity, schema);
1095
+ },
1096
+ async upsert(id, data) {
1097
+ const provider = await resolveProvider();
1098
+ const existing = await provider.get(typeName, id);
1099
+ if (existing) {
1100
+ const result = await provider.update(typeName, id, data);
1101
+ return hydrateEntity(result, entity, schema);
1102
+ }
1103
+ const result = await provider.create(typeName, id, data);
1104
+ return hydrateEntity(result, entity, schema);
1105
+ },
1106
+ async delete(id) {
1107
+ const provider = await resolveProvider();
1108
+ return provider.delete(typeName, id);
1109
+ },
1110
+ async forEach(optionsOrCallback, maybeCallback) {
1111
+ const options = typeof optionsOrCallback === 'function' ? undefined : optionsOrCallback;
1112
+ const callback = typeof optionsOrCallback === 'function'
1113
+ ? optionsOrCallback
1114
+ : maybeCallback;
1115
+ const items = await this.list(options);
1116
+ for (const item of items) {
1117
+ await callback(item);
1118
+ }
1119
+ },
1120
+ };
1121
+ }
1122
+ /**
1123
+ * Hydrate an entity with lazy-loaded relations
1124
+ */
1125
+ function hydrateEntity(data, entity, schema) {
1126
+ const hydrated = { ...data };
1127
+ const id = (data.$id || data.id);
1128
+ // Add lazy getters for relations
1129
+ for (const [fieldName, field] of entity.fields) {
1130
+ if (field.isRelation && field.relatedType) {
1131
+ const relatedEntity = schema.entities.get(field.relatedType);
1132
+ if (!relatedEntity)
1133
+ continue;
1134
+ // Define lazy getter
1135
+ Object.defineProperty(hydrated, fieldName, {
1136
+ get: async () => {
1137
+ const provider = await resolveProvider();
1138
+ if (field.isArray) {
1139
+ // Array relation - get related entities
1140
+ const results = await provider.related(entity.name, id, fieldName);
1141
+ return Promise.all(results.map((r) => hydrateEntity(r, relatedEntity, schema)));
1142
+ }
1143
+ else {
1144
+ // Single relation - get the stored ID and fetch
1145
+ const relatedId = data[fieldName];
1146
+ if (!relatedId)
1147
+ return null;
1148
+ const result = await provider.get(field.relatedType, relatedId);
1149
+ return result
1150
+ ? hydrateEntity(result, relatedEntity, schema)
1151
+ : null;
1152
+ }
1153
+ },
1154
+ enumerable: true,
1155
+ configurable: true,
1156
+ });
1157
+ }
1158
+ }
1159
+ return hydrated;
1160
+ }
1161
+ // =============================================================================
1162
+ // Re-export for convenience
1163
+ // =============================================================================
1164
+ export { parseSchema as parse };
1165
+ //# sourceMappingURL=schema.js.map