ai-database 0.1.0 → 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.
- package/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-test.log +102 -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/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 }
|