ai-database 0.1.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +9 -0
- package/README.md +381 -68
- package/TESTING.md +410 -0
- package/TEST_SUMMARY.md +250 -0
- package/TODO.md +128 -0
- package/dist/ai-promise-db.d.ts +370 -0
- package/dist/ai-promise-db.d.ts.map +1 -0
- package/dist/ai-promise-db.js +839 -0
- package/dist/ai-promise-db.js.map +1 -0
- package/dist/authorization.d.ts +531 -0
- package/dist/authorization.d.ts.map +1 -0
- package/dist/authorization.js +632 -0
- package/dist/authorization.js.map +1 -0
- package/dist/durable-clickhouse.d.ts +193 -0
- package/dist/durable-clickhouse.d.ts.map +1 -0
- package/dist/durable-clickhouse.js +422 -0
- package/dist/durable-clickhouse.js.map +1 -0
- package/dist/durable-promise.d.ts +182 -0
- package/dist/durable-promise.d.ts.map +1 -0
- package/dist/durable-promise.js +409 -0
- package/dist/durable-promise.js.map +1 -0
- package/dist/execution-queue.d.ts +239 -0
- package/dist/execution-queue.d.ts.map +1 -0
- package/dist/execution-queue.js +400 -0
- package/dist/execution-queue.js.map +1 -0
- package/dist/index.d.ts +50 -191
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -462
- package/dist/index.js.map +1 -0
- package/dist/linguistic.d.ts +115 -0
- package/dist/linguistic.d.ts.map +1 -0
- package/dist/linguistic.js +379 -0
- package/dist/linguistic.js.map +1 -0
- package/dist/memory-provider.d.ts +304 -0
- package/dist/memory-provider.d.ts.map +1 -0
- package/dist/memory-provider.js +785 -0
- package/dist/memory-provider.js.map +1 -0
- package/dist/schema.d.ts +899 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +1165 -0
- package/dist/schema.js.map +1 -0
- package/dist/tests.d.ts +107 -0
- package/dist/tests.d.ts.map +1 -0
- package/dist/tests.js +568 -0
- package/dist/tests.js.map +1 -0
- package/dist/types.d.ts +972 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +126 -0
- package/dist/types.js.map +1 -0
- package/package.json +37 -37
- package/src/ai-promise-db.ts +1243 -0
- package/src/authorization.ts +1102 -0
- package/src/durable-clickhouse.ts +596 -0
- package/src/durable-promise.ts +582 -0
- package/src/execution-queue.ts +608 -0
- package/src/index.test.ts +868 -0
- package/src/index.ts +337 -0
- package/src/linguistic.ts +404 -0
- package/src/memory-provider.test.ts +1036 -0
- package/src/memory-provider.ts +1119 -0
- package/src/schema.test.ts +1254 -0
- package/src/schema.ts +2296 -0
- package/src/tests.ts +725 -0
- package/src/types.ts +1177 -0
- package/test/README.md +153 -0
- package/test/edge-cases.test.ts +646 -0
- package/test/provider-resolution.test.ts +402 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +19 -0
- package/dist/index.d.mts +0 -195
- package/dist/index.mjs +0 -430
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
|