db4ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +438 -0
  2. package/dist/cli/bin.d.ts +50 -0
  3. package/dist/cli/bin.d.ts.map +1 -0
  4. package/dist/cli/bin.js +418 -0
  5. package/dist/cli/bin.js.map +1 -0
  6. package/dist/cli/dashboard/App.d.ts +16 -0
  7. package/dist/cli/dashboard/App.d.ts.map +1 -0
  8. package/dist/cli/dashboard/App.js +116 -0
  9. package/dist/cli/dashboard/App.js.map +1 -0
  10. package/dist/cli/dashboard/components/index.d.ts +70 -0
  11. package/dist/cli/dashboard/components/index.d.ts.map +1 -0
  12. package/dist/cli/dashboard/components/index.js +192 -0
  13. package/dist/cli/dashboard/components/index.js.map +1 -0
  14. package/dist/cli/dashboard/hooks/index.d.ts +76 -0
  15. package/dist/cli/dashboard/hooks/index.d.ts.map +1 -0
  16. package/dist/cli/dashboard/hooks/index.js +201 -0
  17. package/dist/cli/dashboard/hooks/index.js.map +1 -0
  18. package/dist/cli/dashboard/index.d.ts +17 -0
  19. package/dist/cli/dashboard/index.d.ts.map +1 -0
  20. package/dist/cli/dashboard/index.js +16 -0
  21. package/dist/cli/dashboard/index.js.map +1 -0
  22. package/dist/cli/dashboard/types.d.ts +84 -0
  23. package/dist/cli/dashboard/types.d.ts.map +1 -0
  24. package/dist/cli/dashboard/types.js +5 -0
  25. package/dist/cli/dashboard/types.js.map +1 -0
  26. package/dist/cli/dashboard/views/index.d.ts +51 -0
  27. package/dist/cli/dashboard/views/index.d.ts.map +1 -0
  28. package/dist/cli/dashboard/views/index.js +72 -0
  29. package/dist/cli/dashboard/views/index.js.map +1 -0
  30. package/dist/cli/index.d.ts +16 -0
  31. package/dist/cli/index.d.ts.map +1 -0
  32. package/dist/cli/index.js +48 -0
  33. package/dist/cli/index.js.map +1 -0
  34. package/dist/cli/runtime/index.d.ts +236 -0
  35. package/dist/cli/runtime/index.d.ts.map +1 -0
  36. package/dist/cli/runtime/index.js +705 -0
  37. package/dist/cli/runtime/index.js.map +1 -0
  38. package/dist/cli/scanner/index.d.ts +90 -0
  39. package/dist/cli/scanner/index.d.ts.map +1 -0
  40. package/dist/cli/scanner/index.js +640 -0
  41. package/dist/cli/scanner/index.js.map +1 -0
  42. package/dist/cli/seed/index.d.ts +160 -0
  43. package/dist/cli/seed/index.d.ts.map +1 -0
  44. package/dist/cli/seed/index.js +774 -0
  45. package/dist/cli/seed/index.js.map +1 -0
  46. package/dist/cli/sync/index.d.ts +197 -0
  47. package/dist/cli/sync/index.d.ts.map +1 -0
  48. package/dist/cli/sync/index.js +706 -0
  49. package/dist/cli/sync/index.js.map +1 -0
  50. package/dist/cli/terminal.d.ts +60 -0
  51. package/dist/cli/terminal.d.ts.map +1 -0
  52. package/dist/cli/terminal.js +210 -0
  53. package/dist/cli/terminal.js.map +1 -0
  54. package/dist/cli/workflow/index.d.ts +152 -0
  55. package/dist/cli/workflow/index.d.ts.map +1 -0
  56. package/dist/cli/workflow/index.js +308 -0
  57. package/dist/cli/workflow/index.js.map +1 -0
  58. package/dist/errors.d.ts +43 -0
  59. package/dist/errors.d.ts.map +1 -0
  60. package/dist/errors.js +47 -0
  61. package/dist/errors.js.map +1 -0
  62. package/dist/handlers.d.ts +147 -0
  63. package/dist/handlers.d.ts.map +1 -0
  64. package/dist/handlers.js +39 -0
  65. package/dist/handlers.js.map +1 -0
  66. package/dist/index.d.ts +1281 -0
  67. package/dist/index.d.ts.map +1 -0
  68. package/dist/index.js +3164 -0
  69. package/dist/index.js.map +1 -0
  70. package/dist/types.d.ts +215 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +12 -0
  73. package/dist/types.js.map +1 -0
  74. package/docs/api-reference.mdx +3 -0
  75. package/docs/examples.mdx +3 -0
  76. package/docs/getting-started.mdx +3 -0
  77. package/docs/index.mdx +3 -0
  78. package/docs/schema-dsl.mdx +3 -0
  79. package/package.json +121 -0
package/dist/index.js ADDED
@@ -0,0 +1,3164 @@
1
+ // Import and re-export shared handler types
2
+ export { serializeFunction, } from './handlers';
3
+ import { serializeFunction } from './handlers';
4
+ /**
5
+ * Error thrown when schema validation fails.
6
+ *
7
+ * @example
8
+ * try {
9
+ * DB({ User: { name: 'User name' } }) // Missing $id or $context
10
+ * } catch (error) {
11
+ * if (error instanceof SchemaValidationError) {
12
+ * console.log(error.code) // 'MISSING_CONTEXT'
13
+ * }
14
+ * }
15
+ */
16
+ export class SchemaValidationError extends Error {
17
+ code;
18
+ constructor(code, message) {
19
+ super(message);
20
+ this.name = 'SchemaValidationError';
21
+ this.code = code;
22
+ }
23
+ }
24
+ /**
25
+ * Error thrown when API operations fail.
26
+ *
27
+ * @example
28
+ * try {
29
+ * await db.User('john-doe')
30
+ * } catch (error) {
31
+ * if (error instanceof APIError) {
32
+ * console.log(error.code) // 'GENERATION_FAILED'
33
+ * console.log(error.statusCode) // 404
34
+ * }
35
+ * }
36
+ */
37
+ export class APIError extends Error {
38
+ code;
39
+ statusCode;
40
+ constructor(code, message, statusCode) {
41
+ super(message);
42
+ this.name = 'APIError';
43
+ this.code = code;
44
+ this.statusCode = statusCode;
45
+ }
46
+ }
47
+ export function UI(components) {
48
+ return components;
49
+ }
50
+ /**
51
+ * Parse namespace from a $id URL.
52
+ *
53
+ * @example
54
+ * parseNamespaceFromId('https://db.sb/sales') // 'sales' (from path)
55
+ * parseNamespaceFromId('https://db4.ai/startups') // 'startups' (from path)
56
+ * parseNamespaceFromId('https://db.sb/org/team') // 'org/team' (from path)
57
+ * parseNamespaceFromId('https://startup.db.sb') // 'startup' (from subdomain)
58
+ * parseNamespaceFromId('https://startups.db.sb') // 'startups' (from subdomain)
59
+ * parseNamespaceFromId('https://db.sb/') // null
60
+ * parseNamespaceFromId('invalid') // null
61
+ */
62
+ export function parseNamespaceFromId(id) {
63
+ try {
64
+ const url = new URL(id);
65
+ // Remove leading slash and trailing slash
66
+ const path = url.pathname.replace(/^\//, '').replace(/\/$/, '');
67
+ if (path)
68
+ return path;
69
+ // No path - try to extract namespace from subdomain
70
+ // e.g., 'startup.db.sb' -> 'startup', 'startups.db4.ai' -> 'startups'
71
+ const hostParts = url.hostname.split('.');
72
+ // Need at least 3 parts for subdomain (e.g., 'startup.db.sb' or 'startups.db4.ai')
73
+ if (hostParts.length >= 3) {
74
+ const subdomain = hostParts[0];
75
+ // Exclude common non-namespace subdomains
76
+ if (subdomain && subdomain !== 'www' && subdomain !== 'api') {
77
+ return subdomain;
78
+ }
79
+ }
80
+ return null;
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ // ============================================================================
87
+ // Reference Operator Constants
88
+ // ============================================================================
89
+ /**
90
+ * Reference operators used in the schema DSL.
91
+ *
92
+ * - `->` (FORWARD_EXACT): Forward exact reference - resolves by ID
93
+ * - `~>` (FORWARD_FUZZY): Forward fuzzy reference - resolves by semantic matching
94
+ * - `<-` (BACKWARD_EXACT): Backward exact reference - reverse lookup by ID
95
+ * - `<~` (BACKWARD_FUZZY): Backward fuzzy reference - reverse lookup by semantic matching
96
+ *
97
+ * @example
98
+ * // Forward references (existing)
99
+ * { author: '->User' } // Exact forward ref
100
+ * { avatar: '~>Image' } // Fuzzy forward ref
101
+ *
102
+ * // Backward references (new)
103
+ * { posts: '<-Post' } // Find Posts that reference this entity
104
+ * { related: '<~Article' } // Find semantically related Articles
105
+ */
106
+ export const OPERATORS = {
107
+ /** Forward exact reference: `->Type` - resolves to exact ID match */
108
+ FORWARD_EXACT: '->',
109
+ /** Forward fuzzy reference: `~>Type` - resolves by semantic similarity */
110
+ FORWARD_FUZZY: '~>',
111
+ /** Backward exact reference: `<-Type` - finds entities referencing this one */
112
+ BACKWARD_EXACT: '<-',
113
+ /** Backward fuzzy reference: `<~Type` - finds semantically related entities */
114
+ BACKWARD_FUZZY: '<~',
115
+ };
116
+ /**
117
+ * Parse a reference operator prefix into its direction and mode/fuzzy properties.
118
+ *
119
+ * This helper function converts the two-character operator prefix into structured
120
+ * properties for building ReferenceField objects.
121
+ *
122
+ * @param prefix - The operator prefix (`->`, `~>`, `<-`, or `<~`)
123
+ * @returns Parsed operator with direction and either fuzzy (forward) or mode (backward)
124
+ *
125
+ * @example
126
+ * parseOperator('->') // { direction: 'forward', fuzzy: false }
127
+ * parseOperator('~>') // { direction: 'forward', fuzzy: true }
128
+ * parseOperator('<-') // { direction: 'backward', mode: 'exact' }
129
+ * parseOperator('<~') // { direction: 'backward', mode: 'fuzzy' }
130
+ */
131
+ export function parseOperator(prefix) {
132
+ switch (prefix) {
133
+ case OPERATORS.FORWARD_EXACT:
134
+ return { direction: 'forward', fuzzy: false };
135
+ case OPERATORS.FORWARD_FUZZY:
136
+ return { direction: 'forward', fuzzy: true };
137
+ case OPERATORS.BACKWARD_EXACT:
138
+ return { direction: 'backward', mode: 'exact' };
139
+ case OPERATORS.BACKWARD_FUZZY:
140
+ return { direction: 'backward', mode: 'fuzzy' };
141
+ default:
142
+ // Default to forward exact for unknown operators
143
+ return { direction: 'forward', fuzzy: false };
144
+ }
145
+ }
146
+ /**
147
+ * Parse $id field which can be:
148
+ * - Simple JSONPath: $.slug
149
+ * - Transform with path: PascalCase($.example)
150
+ * - Column name (legacy): slug
151
+ *
152
+ * @example
153
+ * parseIdConfig('$.slug') // { path: '$.slug' }
154
+ * parseIdConfig('PascalCase($.example)') // { path: '$.example', transform: 'PascalCase' }
155
+ * parseIdConfig('kebab-case($.title)') // { path: '$.title', transform: 'kebab-case' }
156
+ * parseIdConfig('slug') // { path: 'slug' }
157
+ */
158
+ export function parseIdConfig(idField) {
159
+ // Check for transform pattern: Transform($.path)
160
+ // Supports transforms like PascalCase, camelCase, kebab-case, UPPERCASE, lowercase
161
+ const transformMatch = idField.match(/^([A-Za-z][A-Za-z0-9-]*)\((\$\..+)\)$/);
162
+ if (transformMatch) {
163
+ const [, transform, path] = transformMatch;
164
+ return { path, transform };
165
+ }
166
+ // Plain JSONPath or column name
167
+ return { path: idField };
168
+ }
169
+ // ============================================================================
170
+ // JSONPath Validation
171
+ // ============================================================================
172
+ /**
173
+ * Validates that a string is a valid JSONPath expression.
174
+ *
175
+ * A valid JSONPath must:
176
+ * - Start with `$.` (root reference)
177
+ * - Contain valid property names, array indices, or bracket notation
178
+ *
179
+ * Supported patterns:
180
+ * - `$.field` - Simple property access
181
+ * - `$.nested.field` - Nested property access
182
+ * - `$.array[0]` - Array index access
183
+ * - `$.array[*]` - Array wildcard
184
+ * - `$.items[?(@.active)]` - Filter expressions
185
+ *
186
+ * @example
187
+ * isValidJSONPath('$.title') // true
188
+ * isValidJSONPath('$.data.items[0]') // true
189
+ * isValidJSONPath('$.tags[*]') // true
190
+ * isValidJSONPath('title') // false (missing $.)
191
+ * isValidJSONPath('$50 price') // false (not a valid path)
192
+ */
193
+ export function isValidJSONPath(path) {
194
+ // Must start with $.
195
+ if (!path.startsWith('$.')) {
196
+ return false;
197
+ }
198
+ // Basic validation regex for common JSONPath patterns:
199
+ // - Property access: .propertyName or ['property name']
200
+ // - Array access: [0], [*], [?(@.condition)]
201
+ // - Nested paths: $.a.b.c
202
+ const jsonPathPattern = /^\$\.[\w\[\].*@?()'"=!<>|&\s-]+$/;
203
+ return jsonPathPattern.test(path);
204
+ }
205
+ // ============================================================================
206
+ // Type Guards for ParsedField
207
+ // ============================================================================
208
+ /**
209
+ * Type guard to check if a ParsedField is a JSONPathField.
210
+ *
211
+ * @example
212
+ * const field = parseFieldType('$.title')
213
+ * if (isJSONPathField(field)) {
214
+ * console.log(field.path) // TypeScript knows field.path exists
215
+ * }
216
+ */
217
+ export function isJSONPathField(field) {
218
+ return field.type === 'jsonpath';
219
+ }
220
+ /**
221
+ * Type guard to check if a ParsedField is a JSONPathEmbeddedField.
222
+ *
223
+ * @example
224
+ * const field = parseFieldType('$.author ->Author')
225
+ * if (isJSONPathEmbeddedField(field)) {
226
+ * console.log(field.embeddedType) // TypeScript knows field.embeddedType exists
227
+ * }
228
+ */
229
+ export function isJSONPathEmbeddedField(field) {
230
+ return field.type === 'jsonpath-embedded';
231
+ }
232
+ /**
233
+ * Type guard to check if a ParsedField is a PromptField.
234
+ *
235
+ * @example
236
+ * const field = parsed.types.Task.fields.description
237
+ * if (isPromptField(field)) {
238
+ * console.log(field.prompt) // TypeScript knows field.prompt exists
239
+ * }
240
+ */
241
+ export function isPromptField(field) {
242
+ return field.type === 'prompt';
243
+ }
244
+ /**
245
+ * Type guard to check if a ParsedField is a StringField.
246
+ */
247
+ export function isStringField(field) {
248
+ return field.type === 'string';
249
+ }
250
+ /**
251
+ * Validate a raw state machine configuration.
252
+ * Checks for: valid $initial, reachable states, no orphan states, valid transitions.
253
+ */
254
+ export function validateStateConfig(config) {
255
+ const errors = [];
256
+ // Extract state names (exclude $ prefixed keys like $initial, $version, etc.)
257
+ const stateNames = Object.keys(config).filter((k) => !k.startsWith('$'));
258
+ // Check if $initial is missing or empty
259
+ if (!('$initial' in config) || config.$initial === undefined) {
260
+ errors.push({
261
+ code: 'MISSING_INITIAL_STATE',
262
+ message: 'State machine configuration is missing $initial',
263
+ });
264
+ }
265
+ else if (config.$initial === '' || !stateNames.includes(config.$initial)) {
266
+ // $initial is present but invalid (empty string or references non-existent state)
267
+ errors.push({
268
+ code: 'INVALID_INITIAL_STATE',
269
+ message: `Initial state '${config.$initial}' is not a valid state`,
270
+ });
271
+ }
272
+ // Helper to get transition target from a transition value
273
+ const getTransitionTarget = (value) => {
274
+ if (typeof value === 'string')
275
+ return value;
276
+ if (value && typeof value === 'object' && 'to' in value && typeof value.to === 'string') {
277
+ return value.to;
278
+ }
279
+ return null;
280
+ };
281
+ // Check all transition targets exist and have valid syntax
282
+ for (const stateName of stateNames) {
283
+ const stateConfig = config[stateName];
284
+ // null means terminal state - valid
285
+ if (stateConfig === null)
286
+ continue;
287
+ // Empty object means terminal state - valid
288
+ if (typeof stateConfig === 'object' && Object.keys(stateConfig).length === 0)
289
+ continue;
290
+ if (typeof stateConfig !== 'object') {
291
+ errors.push({
292
+ code: 'INVALID_STATE_CONFIG',
293
+ message: `State '${stateName}' has invalid configuration`,
294
+ state: stateName,
295
+ });
296
+ continue;
297
+ }
298
+ // Check each transition
299
+ for (const [eventName, transitionValue] of Object.entries(stateConfig)) {
300
+ // Skip handler functions ($entry, $exit)
301
+ if (eventName.startsWith('$'))
302
+ continue;
303
+ // Check transition syntax
304
+ if (Array.isArray(transitionValue)) {
305
+ errors.push({
306
+ code: 'INVALID_TRANSITION_SYNTAX',
307
+ message: `Transition '${eventName}' in state '${stateName}' cannot be an array`,
308
+ state: stateName,
309
+ event: eventName,
310
+ });
311
+ continue;
312
+ }
313
+ // String transition: { submit: 'pending' }
314
+ if (typeof transitionValue === 'string') {
315
+ if (!stateNames.includes(transitionValue)) {
316
+ errors.push({
317
+ code: 'INVALID_TRANSITION_TARGET',
318
+ message: `Transition '${eventName}' in state '${stateName}' targets unknown state '${transitionValue}'`,
319
+ state: stateName,
320
+ event: eventName,
321
+ });
322
+ }
323
+ continue;
324
+ }
325
+ // null transition: terminal (goes nowhere, like archive/delete)
326
+ if (transitionValue === null)
327
+ continue;
328
+ // Object transition: { submit: { to: 'pending', if: ..., do: ... } }
329
+ if (typeof transitionValue === 'object') {
330
+ const tv = transitionValue;
331
+ // Must have 'to' property
332
+ if (!('to' in tv)) {
333
+ errors.push({
334
+ code: 'INVALID_TRANSITION_SYNTAX',
335
+ message: `Transition '${eventName}' in state '${stateName}' is missing 'to' property`,
336
+ state: stateName,
337
+ event: eventName,
338
+ });
339
+ continue;
340
+ }
341
+ // Check target exists if 'to' is specified
342
+ if (typeof tv.to === 'string' && !stateNames.includes(tv.to)) {
343
+ errors.push({
344
+ code: 'INVALID_TRANSITION_TARGET',
345
+ message: `Transition '${eventName}' in state '${stateName}' targets unknown state '${tv.to}'`,
346
+ state: stateName,
347
+ event: eventName,
348
+ });
349
+ }
350
+ continue;
351
+ }
352
+ // Unknown transition format (e.g., number)
353
+ errors.push({
354
+ code: 'INVALID_TRANSITION_SYNTAX',
355
+ message: `Transition '${eventName}' in state '${stateName}' has invalid format`,
356
+ state: stateName,
357
+ event: eventName,
358
+ });
359
+ }
360
+ }
361
+ // Check for orphan states (states not reachable from $initial)
362
+ // Only do this check if we have a valid $initial state
363
+ if (config.$initial && stateNames.includes(config.$initial)) {
364
+ const reachable = new Set();
365
+ const toVisit = [config.$initial];
366
+ while (toVisit.length > 0) {
367
+ const current = toVisit.pop();
368
+ if (reachable.has(current))
369
+ continue;
370
+ reachable.add(current);
371
+ // Find all states reachable from current
372
+ const stateConfig = config[current];
373
+ if (stateConfig === null || typeof stateConfig !== 'object')
374
+ continue;
375
+ for (const [eventName, transitionValue] of Object.entries(stateConfig)) {
376
+ if (eventName.startsWith('$'))
377
+ continue;
378
+ const target = getTransitionTarget(transitionValue);
379
+ if (target && stateNames.includes(target) && !reachable.has(target)) {
380
+ toVisit.push(target);
381
+ }
382
+ }
383
+ }
384
+ // Any state not in reachable set is an orphan
385
+ for (const stateName of stateNames) {
386
+ if (!reachable.has(stateName)) {
387
+ errors.push({
388
+ code: 'ORPHAN_STATE',
389
+ message: `State '${stateName}' is not reachable from initial state '${config.$initial}'`,
390
+ state: stateName,
391
+ });
392
+ }
393
+ }
394
+ }
395
+ return { valid: errors.length === 0, errors };
396
+ }
397
+ /**
398
+ * Parse a raw state machine configuration into the normalized format.
399
+ * Converts string functions to SerializedFunction objects.
400
+ *
401
+ * @param config - Raw state machine config from schema
402
+ * @returns Parsed state machine configuration
403
+ *
404
+ * @example
405
+ * const parsed = parseStateConfig({
406
+ * $initial: 'draft',
407
+ * draft: {
408
+ * submit: 'pending',
409
+ * $entry: (entity) => { console.log('entered draft') }
410
+ * },
411
+ * pending: {
412
+ * approve: { to: 'approved', if: (e) => e.score >= 80 }
413
+ * },
414
+ * approved: null // terminal state
415
+ * })
416
+ */
417
+ export function parseStateConfig(config) {
418
+ const states = {};
419
+ for (const [stateName, stateValue] of Object.entries(config)) {
420
+ // Skip $initial - it's not a state
421
+ if (stateName === '$initial')
422
+ continue;
423
+ // Terminal state (null or empty object)
424
+ if (stateValue === null || (typeof stateValue === 'object' && Object.keys(stateValue).length === 0)) {
425
+ states[stateName] = { transitions: {} };
426
+ continue;
427
+ }
428
+ if (typeof stateValue !== 'object') {
429
+ continue; // Skip invalid state configs
430
+ }
431
+ const stateConfig = { transitions: {} };
432
+ const stateObj = stateValue;
433
+ for (const [key, value] of Object.entries(stateObj)) {
434
+ // Handle $entry handler
435
+ if (key === '$entry') {
436
+ if (typeof value === 'function') {
437
+ stateConfig.$entry = serializeFunction('$entry', value);
438
+ }
439
+ else if (typeof value === 'string') {
440
+ // String function body - parse it
441
+ stateConfig.$entry = {
442
+ name: '$entry',
443
+ body: value,
444
+ params: ['entity'],
445
+ async: value.includes('await'),
446
+ };
447
+ }
448
+ continue;
449
+ }
450
+ // Handle $exit handler
451
+ if (key === '$exit') {
452
+ if (typeof value === 'function') {
453
+ stateConfig.$exit = serializeFunction('$exit', value);
454
+ }
455
+ else if (typeof value === 'string') {
456
+ stateConfig.$exit = {
457
+ name: '$exit',
458
+ body: value,
459
+ params: ['entity'],
460
+ async: value.includes('await'),
461
+ };
462
+ }
463
+ continue;
464
+ }
465
+ // Handle transitions
466
+ const transitionName = key;
467
+ // Simple string transition: { submit: 'pending' }
468
+ if (typeof value === 'string') {
469
+ stateConfig.transitions[transitionName] = { to: value };
470
+ continue;
471
+ }
472
+ // null transition (shouldn't happen, but handle it)
473
+ if (value === null) {
474
+ stateConfig.transitions[transitionName] = { to: null };
475
+ continue;
476
+ }
477
+ // Complex transition with to/if/do: { approve: { to: 'approved', if: ..., do: ... } }
478
+ if (typeof value === 'object') {
479
+ const transObj = value;
480
+ const transitionConfig = {
481
+ to: transObj.to ?? null,
482
+ };
483
+ // Parse 'if' guard
484
+ if (transObj.if) {
485
+ if (typeof transObj.if === 'function') {
486
+ transitionConfig.if = serializeFunction('if', transObj.if);
487
+ }
488
+ else if (typeof transObj.if === 'string') {
489
+ transitionConfig.if = {
490
+ name: 'if',
491
+ body: transObj.if,
492
+ params: ['entity'],
493
+ async: transObj.if.includes('await'),
494
+ };
495
+ }
496
+ }
497
+ // Parse 'do' action
498
+ if (transObj.do) {
499
+ if (typeof transObj.do === 'function') {
500
+ transitionConfig.do = serializeFunction('do', transObj.do);
501
+ }
502
+ else if (typeof transObj.do === 'string') {
503
+ transitionConfig.do = {
504
+ name: 'do',
505
+ body: transObj.do,
506
+ params: ['entity'],
507
+ async: transObj.do.includes('await'),
508
+ };
509
+ }
510
+ }
511
+ stateConfig.transitions[transitionName] = transitionConfig;
512
+ }
513
+ }
514
+ states[stateName] = stateConfig;
515
+ }
516
+ return {
517
+ $initial: config.$initial,
518
+ states,
519
+ };
520
+ }
521
+ // Extract count from array description
522
+ // Supports patterns like:
523
+ // - "List 5 items" -> 5
524
+ // - "Generate 10 samples" -> 10
525
+ // - "Create exactly 7 categories" -> 7
526
+ // - "List 3-5 keywords" -> { min: 3, max: 5 }
527
+ export function extractCountFromDescription(description) {
528
+ // First check for range pattern: N-M (e.g., "3-5")
529
+ const rangeMatch = description.match(/\b(\d+)-(\d+)\b/);
530
+ if (rangeMatch) {
531
+ return { min: parseInt(rangeMatch[1], 10), max: parseInt(rangeMatch[2], 10) };
532
+ }
533
+ // Check for patterns like "List N", "Generate N", "Create exactly N"
534
+ // Match word followed by optional "exactly" and a number
535
+ const countMatch = description.match(/\b(?:List|Generate|Create)\s+(?:exactly\s+)?(\d+)\b/i);
536
+ if (countMatch) {
537
+ return parseInt(countMatch[1], 10);
538
+ }
539
+ return undefined;
540
+ }
541
+ /**
542
+ * Parse a field value into its structured field type representation.
543
+ *
544
+ * Converts the shorthand field notation used in schema definitions into
545
+ * a fully parsed field object with type information, references, and options.
546
+ *
547
+ * ## Reference Operators
548
+ *
549
+ * This function parses all four reference operators defined in {@link OPERATORS}:
550
+ *
551
+ * | Operator | Direction | Mode | Description |
552
+ * |----------|-----------|-------|--------------------------------|
553
+ * | `->` | forward | exact | Resolves to exact ID match |
554
+ * | `~>` | forward | fuzzy | Resolves by semantic similarity|
555
+ * | `<-` | backward | exact | Finds entities referencing this|
556
+ * | `<~` | backward | fuzzy | Finds semantically related |
557
+ *
558
+ * @param value - The field value to parse. Can be:
559
+ * - `string` - A field description or reference pattern (e.g., `'User name'`, `'->User'`, `'~>Image?'`, `'<-Post[]'`)
560
+ * - `string[]` - An array field with description (e.g., `['Tag labels']`, `['<-Post']`)
561
+ * - `Record<string, string>` - A nested object with field descriptions
562
+ * - `Function` - A computed field
563
+ *
564
+ * @returns A ParsedField object representing the field type:
565
+ * - `StringField` - Plain text field with description
566
+ * - `ReferenceField` - Reference to another type (forward or backward)
567
+ * - `ArrayField` - Array of strings or references
568
+ * - `ObjectField` - Nested object with properties
569
+ * - `EmbeddedField` - Embedded type (detected in second pass)
570
+ * - `JSONPathField` - JSONPath expression for data mapping
571
+ * - `ComputedField` - Field computed from a function
572
+ *
573
+ * @example
574
+ * // String field
575
+ * parseFieldType('User name')
576
+ * // { type: 'string', description: 'User name' }
577
+ *
578
+ * @example
579
+ * // Forward exact reference
580
+ * parseFieldType('->User')
581
+ * // { type: 'reference', ref: 'User', isArray: false, optional: false, fuzzy: false }
582
+ *
583
+ * @example
584
+ * // Forward fuzzy reference (optional)
585
+ * parseFieldType('~>Image?')
586
+ * // { type: 'reference', ref: 'Image', isArray: false, optional: true, fuzzy: true }
587
+ *
588
+ * @example
589
+ * // Forward array reference
590
+ * parseFieldType('->Tag[]')
591
+ * // { type: 'reference', ref: 'Tag', isArray: true, optional: false, fuzzy: false }
592
+ *
593
+ * @example
594
+ * // Backward exact reference - finds Posts that reference this entity
595
+ * parseFieldType('<-Post')
596
+ * // { type: 'reference', ref: 'Post', isArray: false, optional: false, direction: 'backward', mode: 'exact' }
597
+ *
598
+ * @example
599
+ * // Backward fuzzy reference array - finds semantically related Articles
600
+ * parseFieldType('<~Article[]')
601
+ * // { type: 'reference', ref: 'Article', isArray: true, optional: false, direction: 'backward', mode: 'fuzzy' }
602
+ *
603
+ * @example
604
+ * // Union type reference
605
+ * parseFieldType('->User|Organization')
606
+ * // { type: 'reference', refs: ['User', 'Organization'], isArray: false, optional: false, fuzzy: false }
607
+ *
608
+ * @example
609
+ * // String array with count hint
610
+ * parseFieldType(['List 5 keywords'])
611
+ * // { type: 'array', items: { type: 'string' }, description: 'List 5 keywords', count: 5 }
612
+ *
613
+ * @example
614
+ * // Nested object
615
+ * parseFieldType({ street: 'Street address', city: 'City name' })
616
+ * // { type: 'object', properties: { street: {...}, city: {...} } }
617
+ */
618
+ export function parseFieldType(value) {
619
+ // Handle computed fields (functions)
620
+ if (typeof value === 'function') {
621
+ return {
622
+ type: 'computed',
623
+ source: value.toString(),
624
+ };
625
+ }
626
+ // Handle array descriptions: ['description'], ['... ->Type'], ['<-Type']
627
+ if (Array.isArray(value)) {
628
+ const desc = value[0] || '';
629
+ const count = extractCountFromDescription(desc);
630
+ // Check for back reference: ['<-Type']
631
+ const backRefMatch = desc.match(/^<-(\w+)$/);
632
+ if (backRefMatch) {
633
+ const result = {
634
+ type: 'array',
635
+ items: { type: 'string' },
636
+ description: desc,
637
+ backRef: backRefMatch[1],
638
+ };
639
+ if (count !== undefined)
640
+ result.count = count;
641
+ return result;
642
+ }
643
+ // Check for inline references: ['... ->Type'] or ['... ->Type1 ->Type2']
644
+ const inlineRefMatches = desc.match(/->(\w+)/g);
645
+ if (inlineRefMatches) {
646
+ const refs = inlineRefMatches.map((m) => m.slice(2)); // Remove '->' prefix
647
+ const result = {
648
+ type: 'array',
649
+ items: { type: 'string' },
650
+ description: desc,
651
+ refs,
652
+ };
653
+ if (count !== undefined)
654
+ result.count = count;
655
+ return result;
656
+ }
657
+ const result = {
658
+ type: 'array',
659
+ items: { type: 'string' },
660
+ description: desc,
661
+ };
662
+ if (count !== undefined)
663
+ result.count = count;
664
+ return result;
665
+ }
666
+ // Handle nested objects: { field: 'description', ... }
667
+ if (typeof value === 'object' && value !== null) {
668
+ const properties = {};
669
+ for (const [key, desc] of Object.entries(value)) {
670
+ properties[key] = { type: 'string', description: desc };
671
+ }
672
+ return {
673
+ type: 'object',
674
+ properties,
675
+ };
676
+ }
677
+ // Handle string patterns
678
+ const str = value;
679
+ // Check for JSONPath with transform: Transform($.path)
680
+ // Supports transforms like PascalCase, camelCase, kebab-case, UPPERCASE, lowercase
681
+ const transformMatch = str.match(/^([A-Za-z][A-Za-z0-9-]*)\((\$\..+)\)$/);
682
+ if (transformMatch) {
683
+ const [, transform, path] = transformMatch;
684
+ return {
685
+ type: 'jsonpath',
686
+ path,
687
+ transform,
688
+ };
689
+ }
690
+ // Check for JSONPath embedded pattern: $.path ->Type or $.path ->Type?
691
+ // Must check this BEFORE plain JSONPath to properly detect embedded mappings
692
+ // Pattern: JSONPath followed by optional whitespace, ->, optional whitespace, TypeName, optional ?
693
+ const jsonpathEmbeddedMatch = str.match(/^(\$\.[^\s]+?)\s*->\s*([A-Za-z][A-Za-z0-9]*)(\?)?$/);
694
+ if (jsonpathEmbeddedMatch) {
695
+ const [, path, embeddedType, optionalMarker] = jsonpathEmbeddedMatch;
696
+ // Determine if it's an array based on path containing [*] or [?(...)]
697
+ const isArray = /\[\*\]|\[\?/.test(path);
698
+ return {
699
+ type: 'jsonpath-embedded',
700
+ path,
701
+ embeddedType,
702
+ isArray,
703
+ ...(optionalMarker && { optional: true }),
704
+ };
705
+ }
706
+ // Check for JSONPath pattern: $.xxx (must start with $. to avoid matching "$50 price")
707
+ if (str.startsWith('$.')) {
708
+ return {
709
+ type: 'jsonpath',
710
+ path: str,
711
+ };
712
+ }
713
+ // Check for reference pattern: ->Type, ~>Type, <-Type, <~Type, with optional [], ?, and unions
714
+ // Pattern: (<-|<~|->|~>)Type1|Type2|...[]?
715
+ const refMatch = str.match(/^(<-|<~|->|~>)(.+?)(\[\])?(\?)?$/);
716
+ if (refMatch) {
717
+ const [, prefix, typesPart, arrayMarker, optionalMarker] = refMatch;
718
+ const isArray = !!arrayMarker;
719
+ const optional = !!optionalMarker;
720
+ // Check for union types (Type1|Type2|...)
721
+ const types = typesPart.split('|').map((t) => t.trim());
722
+ // Parse the operator to get direction and mode/fuzzy
723
+ const parsedOp = parseOperator(prefix);
724
+ if (parsedOp.direction === 'backward') {
725
+ // Backward references use direction/mode properties
726
+ const baseRef = {
727
+ type: 'reference',
728
+ isArray,
729
+ optional,
730
+ direction: parsedOp.direction,
731
+ mode: parsedOp.mode,
732
+ };
733
+ return types.length > 1 ? { ...baseRef, refs: types } : { ...baseRef, ref: types[0] };
734
+ }
735
+ else {
736
+ // Forward references use fuzzy property (backward compatibility)
737
+ const baseRef = {
738
+ type: 'reference',
739
+ isArray,
740
+ optional,
741
+ fuzzy: parsedOp.fuzzy,
742
+ };
743
+ return types.length > 1 ? { ...baseRef, refs: types } : { ...baseRef, ref: types[0] };
744
+ }
745
+ }
746
+ // Plain string field
747
+ return {
748
+ type: 'string',
749
+ description: str,
750
+ };
751
+ }
752
+ /**
753
+ * Parse a reference field definition for generation.
754
+ *
755
+ * Handles patterns like:
756
+ * - `->Person` - exact reference
757
+ * - `~>Industry` - fuzzy reference
758
+ * - `->Type[]` - array of exact references
759
+ * - `~>Type?` - optional fuzzy reference
760
+ * - `->User|Organization` - union type reference
761
+ *
762
+ * @param fieldDef - The field definition string (e.g., '->Person', '~>Industry?')
763
+ * @returns ParsedGenerationReference or null if not a reference field
764
+ *
765
+ * @example
766
+ * parseGenerationReference('->Person')
767
+ * // { mode: 'exact', targetType: 'Person', isOptional: false, isArray: false }
768
+ *
769
+ * @example
770
+ * parseGenerationReference('~>Industry?')
771
+ * // { mode: 'fuzzy', targetType: 'Industry', isOptional: true, isArray: false }
772
+ *
773
+ * @example
774
+ * parseGenerationReference('->User|Organization')
775
+ * // { mode: 'exact', targetType: 'User', targetTypes: ['User', 'Organization'], isOptional: false, isArray: false }
776
+ */
777
+ export function parseGenerationReference(fieldDef) {
778
+ // Match reference patterns: ->, ~>, with optional ? and []
779
+ // Pattern supports union types: ->Type1|Type2|Type3
780
+ const match = fieldDef.match(/^(->|~>)([A-Za-z][A-Za-z0-9]*(?:\|[A-Za-z][A-Za-z0-9]*)*)(\[\])?(\?)?$/);
781
+ if (!match)
782
+ return null;
783
+ const [, operator, typesPart, arrayMarker, optionalMarker] = match;
784
+ const types = typesPart.split('|').map(t => t.trim());
785
+ return {
786
+ mode: operator === '->' ? 'exact' : 'fuzzy',
787
+ targetType: types[0],
788
+ ...(types.length > 1 && { targetTypes: types }),
789
+ isOptional: optionalMarker === '?',
790
+ isArray: arrayMarker === '[]',
791
+ };
792
+ }
793
+ /**
794
+ * Parse array field definition for generation.
795
+ *
796
+ * Handles array shorthand like `['~>Investor']` or `['->Person']`.
797
+ *
798
+ * @param fieldDef - The field definition array (e.g., ['~>Investor'])
799
+ * @returns ParsedGenerationReference or null if not a reference array
800
+ *
801
+ * @example
802
+ * parseGenerationArrayReference(['~>Investor'])
803
+ * // { mode: 'fuzzy', targetType: 'Investor', isOptional: false, isArray: true }
804
+ */
805
+ export function parseGenerationArrayReference(fieldDef) {
806
+ if (!Array.isArray(fieldDef) || fieldDef.length !== 1)
807
+ return null;
808
+ const inner = fieldDef[0];
809
+ if (typeof inner !== 'string')
810
+ return null;
811
+ const parsed = parseGenerationReference(inner);
812
+ if (!parsed)
813
+ return null;
814
+ return {
815
+ ...parsed,
816
+ isArray: true,
817
+ };
818
+ }
819
+ /**
820
+ * Check if a field definition is a reference field.
821
+ *
822
+ * @param fieldDef - The field definition (string or array)
823
+ * @returns true if this is a reference field
824
+ *
825
+ * @example
826
+ * isReferenceField('->User') // true
827
+ * isReferenceField('~>Industry?') // true
828
+ * isReferenceField(['~>Investor']) // true
829
+ * isReferenceField('User name') // false
830
+ * isReferenceField(['tags']) // false
831
+ */
832
+ export function isReferenceField(fieldDef) {
833
+ if (typeof fieldDef === 'string') {
834
+ return parseGenerationReference(fieldDef) !== null;
835
+ }
836
+ if (Array.isArray(fieldDef)) {
837
+ return parseGenerationArrayReference(fieldDef) !== null;
838
+ }
839
+ return false;
840
+ }
841
+ /**
842
+ * Extract reference mode from a field definition.
843
+ *
844
+ * @param fieldDef - The field definition
845
+ * @returns 'exact' | 'fuzzy' | null
846
+ */
847
+ export function getReferenceMode(fieldDef) {
848
+ if (typeof fieldDef === 'string') {
849
+ return parseGenerationReference(fieldDef)?.mode ?? null;
850
+ }
851
+ if (Array.isArray(fieldDef)) {
852
+ return parseGenerationArrayReference(fieldDef)?.mode ?? null;
853
+ }
854
+ return null;
855
+ }
856
+ /**
857
+ * Extract target type(s) from a reference field.
858
+ *
859
+ * @param fieldDef - The field definition
860
+ * @returns Target type string, array of types for unions, or null if not a reference
861
+ */
862
+ export function getReferenceTargetType(fieldDef) {
863
+ let parsed = null;
864
+ if (typeof fieldDef === 'string') {
865
+ parsed = parseGenerationReference(fieldDef);
866
+ }
867
+ else if (Array.isArray(fieldDef)) {
868
+ parsed = parseGenerationArrayReference(fieldDef);
869
+ }
870
+ if (!parsed)
871
+ return null;
872
+ return parsed.targetTypes ?? parsed.targetType;
873
+ }
874
+ /**
875
+ * Parse seed configuration from a schema type's `$seed` field.
876
+ *
877
+ * Seed configuration tells the system where to fetch initial data for a type,
878
+ * supporting TSV, CSV, and JSON formats with pagination support.
879
+ *
880
+ * @param value - The seed configuration. Can be:
881
+ * - `string` - A URL (format inferred from extension: `.csv`, `.json`, or default `.tsv`)
882
+ * - `Record<string, string>` - Object with explicit options
883
+ *
884
+ * @returns A SeedConfig object with:
885
+ * - `url` - The data source URL
886
+ * - `format` - Data format ('tsv', 'csv', or 'json')
887
+ * - `idField` - Optional field to use as entity ID
888
+ * - `next` - Optional JSONPath to pagination URL
889
+ * - `data` - Optional JSONPath to data array in response
890
+ *
891
+ * @example
892
+ * // Simple URL (format inferred)
893
+ * parseSeedConfig('https://example.com/data.csv')
894
+ * // { url: 'https://example.com/data.csv', format: 'csv' }
895
+ *
896
+ * @example
897
+ * // URL with TSV default
898
+ * parseSeedConfig('https://example.com/data')
899
+ * // { url: 'https://example.com/data', format: 'tsv' }
900
+ *
901
+ * @example
902
+ * // Object config with pagination
903
+ * parseSeedConfig({
904
+ * url: 'https://api.example.com/items',
905
+ * format: 'json',
906
+ * data: '$.items',
907
+ * next: '$.pagination.next'
908
+ * })
909
+ * // { url: '...', format: 'json', data: '$.items', next: '$.pagination.next' }
910
+ */
911
+ export function parseSeedConfig(value) {
912
+ if (typeof value === 'string') {
913
+ // Infer format from URL extension
914
+ const url = value;
915
+ let format = 'tsv';
916
+ if (url.endsWith('.csv'))
917
+ format = 'csv';
918
+ else if (url.endsWith('.json'))
919
+ format = 'json';
920
+ return { url, format };
921
+ }
922
+ // Object config with explicit options
923
+ const config = {
924
+ url: value.url || value.$seed,
925
+ format: value.format || 'tsv',
926
+ };
927
+ if (value.idField)
928
+ config.idField = value.idField;
929
+ if (value.next)
930
+ config.next = value.next;
931
+ if (value.data)
932
+ config.data = value.data;
933
+ return config;
934
+ }
935
+ /**
936
+ * Converts string fields to prompt fields in a seeded type's field map.
937
+ *
938
+ * In seeded schemas, plain string values represent prompts for AI generation
939
+ * rather than simple field descriptions. This function performs the conversion
940
+ * after initial field parsing, ensuring that JSONPath fields and reference
941
+ * fields remain unchanged while string fields become prompt fields.
942
+ *
943
+ * @param fields - The parsed fields map from parseFieldType calls
944
+ * @returns A new fields map with string fields converted to prompt fields
945
+ *
946
+ * @example
947
+ * // Input: fields from a seeded type
948
+ * const fields = {
949
+ * title: { type: 'string', description: 'Some title' },
950
+ * jsonField: { type: 'jsonpath', path: '$.name' },
951
+ * ref: { type: 'reference', ref: 'Other', isArray: false },
952
+ * }
953
+ *
954
+ * // Output: string -> prompt, others unchanged
955
+ * const converted = convertStringsToPromptsInSeededType(fields)
956
+ * // converted.title = { type: 'prompt', prompt: 'Some title' }
957
+ * // converted.jsonField = { type: 'jsonpath', path: '$.name' }
958
+ * // converted.ref = { type: 'reference', ref: 'Other', isArray: false }
959
+ *
960
+ * @remarks
961
+ * This function is called during parseSchema when a type has `$seed` defined.
962
+ * Only StringField types are converted; all other field types pass through unchanged.
963
+ */
964
+ export function convertStringsToPromptsInSeededType(fields) {
965
+ const result = {};
966
+ for (const [fieldName, field] of Object.entries(fields)) {
967
+ if (isStringField(field)) {
968
+ result[fieldName] = {
969
+ type: 'prompt',
970
+ prompt: field.description,
971
+ };
972
+ }
973
+ else {
974
+ result[fieldName] = field;
975
+ }
976
+ }
977
+ return result;
978
+ }
979
+ /**
980
+ * Parse an entire schema definition into a structured ParsedSchema.
981
+ *
982
+ * This is the main parsing function that takes a schema definition object and
983
+ * produces a fully parsed representation with all types, fields, references,
984
+ * and metadata resolved.
985
+ *
986
+ * @param schema - The schema definition object containing type definitions and metadata.
987
+ * Metadata fields are prefixed with `$` (e.g., `$id`, `$context`, `$settings`).
988
+ * All other keys are treated as type definitions.
989
+ *
990
+ * @returns A ParsedSchema object containing:
991
+ * - `types` - Record of parsed type definitions with fields and references
992
+ * - `hash` - Deterministic hash of the schema for versioning
993
+ * - `settings` - Optional schema-level settings (e.g., iconLibrary)
994
+ *
995
+ * @example
996
+ * const schema = parseSchema({
997
+ * $id: 'https://db.sb/blog',
998
+ * Post: {
999
+ * title: 'Post title',
1000
+ * content: 'Main content',
1001
+ * author: '->User',
1002
+ * tags: ['Tag labels']
1003
+ * },
1004
+ * User: {
1005
+ * name: 'User name',
1006
+ * email: 'Email address',
1007
+ * posts: '->Post[]'
1008
+ * }
1009
+ * })
1010
+ *
1011
+ * // schema.types.Post.fields.title
1012
+ * // { type: 'string', description: 'Post title' }
1013
+ *
1014
+ * // schema.types.Post.references
1015
+ * // [{ field: 'author', ref: 'User', isArray: false }]
1016
+ *
1017
+ * @example
1018
+ * // Schema with seed configuration and icons
1019
+ * const schema = parseSchema({
1020
+ * $context: 'startups',
1021
+ * $settings: { iconLibrary: 'lucide' },
1022
+ * Company: {
1023
+ * $icon: 'building',
1024
+ * $seed: 'https://api.example.com/companies.json',
1025
+ * name: 'Company name',
1026
+ * website: 'Website URL'
1027
+ * }
1028
+ * })
1029
+ *
1030
+ * // schema.types.Company.icon === 'building'
1031
+ * // schema.types.Company.seed.url === 'https://api.example.com/companies.json'
1032
+ */
1033
+ export function parseSchema(schema) {
1034
+ const types = {};
1035
+ // Extract schema-level $settings
1036
+ let settings;
1037
+ const settingsValue = schema.$settings;
1038
+ if (settingsValue && typeof settingsValue === 'object' && settingsValue !== null) {
1039
+ const settingsObj = settingsValue;
1040
+ if (settingsObj.iconLibrary || Object.keys(settingsObj).length > 0) {
1041
+ settings = {};
1042
+ if (typeof settingsObj.iconLibrary === 'string') {
1043
+ settings.iconLibrary = settingsObj.iconLibrary;
1044
+ }
1045
+ }
1046
+ }
1047
+ // First pass: parse all types
1048
+ for (const [typeName, fields] of Object.entries(schema)) {
1049
+ // Skip metadata fields ($ prefixed)
1050
+ if (typeName.startsWith('$'))
1051
+ continue;
1052
+ // Skip if not an object (metadata string values)
1053
+ if (typeof fields !== 'object' || fields === null)
1054
+ continue;
1055
+ const parsedFields = {};
1056
+ const references = [];
1057
+ let seed;
1058
+ let icon;
1059
+ let group;
1060
+ let seedNext;
1061
+ let seedData;
1062
+ let seedId;
1063
+ let seedIdTransform;
1064
+ // Entity lifecycle handlers
1065
+ const entityHandlers = {};
1066
+ // State machine configuration
1067
+ let parsedState;
1068
+ // Data source configurations
1069
+ let source;
1070
+ let input;
1071
+ let from;
1072
+ let cache;
1073
+ // Generative format configurations
1074
+ let image;
1075
+ let speech;
1076
+ let diagram;
1077
+ let conventions;
1078
+ let code;
1079
+ // Entity-level eval and experiment
1080
+ let entityEval;
1081
+ let entityExperiment;
1082
+ for (const [fieldName, fieldValue] of Object.entries(fields)) {
1083
+ // Handle $seed metadata
1084
+ if (fieldName === '$seed') {
1085
+ seed = parseSeedConfig(fieldValue);
1086
+ continue;
1087
+ }
1088
+ // Handle $next metadata (JSONPath to next page URL)
1089
+ if (fieldName === '$next') {
1090
+ if (typeof fieldValue === 'string') {
1091
+ seedNext = fieldValue;
1092
+ }
1093
+ continue;
1094
+ }
1095
+ // Handle $data metadata (JSONPath to data array)
1096
+ if (fieldName === '$data') {
1097
+ if (typeof fieldValue === 'string') {
1098
+ seedData = fieldValue;
1099
+ }
1100
+ continue;
1101
+ }
1102
+ // Handle $id metadata (JSONPath or transform for ID field)
1103
+ // Supports: '$.taskID', 'PascalCase($.example)', etc.
1104
+ if (fieldName === '$id') {
1105
+ if (typeof fieldValue === 'string') {
1106
+ const idConfig = parseIdConfig(fieldValue);
1107
+ seedId = idConfig.path;
1108
+ seedIdTransform = idConfig.transform;
1109
+ }
1110
+ continue;
1111
+ }
1112
+ // Handle $icon metadata
1113
+ if (fieldName === '$icon') {
1114
+ if (typeof fieldValue === 'string') {
1115
+ icon = fieldValue;
1116
+ }
1117
+ continue;
1118
+ }
1119
+ // Handle $group metadata
1120
+ if (fieldName === '$group') {
1121
+ if (typeof fieldValue === 'string') {
1122
+ group = fieldValue;
1123
+ }
1124
+ continue;
1125
+ }
1126
+ // Handle entity lifecycle handlers ($created, $updated, $deleted)
1127
+ if (fieldName === '$created' || fieldName === '$updated' || fieldName === '$deleted') {
1128
+ if (typeof fieldValue === 'function') {
1129
+ entityHandlers[fieldName] = serializeFunction(fieldName, fieldValue);
1130
+ }
1131
+ continue;
1132
+ }
1133
+ // Handle $state (state machine definition)
1134
+ if (fieldName === '$state') {
1135
+ if (typeof fieldValue === 'object' && fieldValue !== null && '$initial' in fieldValue) {
1136
+ parsedState = parseStateConfig(fieldValue);
1137
+ }
1138
+ continue;
1139
+ }
1140
+ // Handle $source (external API source configuration)
1141
+ if (fieldName === '$source') {
1142
+ if (typeof fieldValue === 'object' && fieldValue !== null && !Array.isArray(fieldValue)) {
1143
+ source = fieldValue;
1144
+ }
1145
+ continue;
1146
+ }
1147
+ // Handle $input (input parameters for source)
1148
+ if (fieldName === '$input') {
1149
+ if (typeof fieldValue === 'object' && fieldValue !== null && !Array.isArray(fieldValue)) {
1150
+ input = fieldValue;
1151
+ }
1152
+ continue;
1153
+ }
1154
+ // Handle $from (multi-source aggregation)
1155
+ if (fieldName === '$from') {
1156
+ if (Array.isArray(fieldValue)) {
1157
+ from = fieldValue;
1158
+ }
1159
+ continue;
1160
+ }
1161
+ // Handle $cache (cache duration)
1162
+ if (fieldName === '$cache') {
1163
+ if (typeof fieldValue === 'string') {
1164
+ cache = fieldValue;
1165
+ }
1166
+ continue;
1167
+ }
1168
+ // Handle $image (image generation configuration)
1169
+ if (fieldName === '$image') {
1170
+ if (typeof fieldValue === 'object' && fieldValue !== null && !Array.isArray(fieldValue)) {
1171
+ image = fieldValue;
1172
+ }
1173
+ continue;
1174
+ }
1175
+ // Handle $speech (speech/TTS generation configuration)
1176
+ if (fieldName === '$speech') {
1177
+ if (typeof fieldValue === 'object' && fieldValue !== null && !Array.isArray(fieldValue)) {
1178
+ speech = fieldValue;
1179
+ }
1180
+ continue;
1181
+ }
1182
+ // Handle $diagram (diagram generation configuration)
1183
+ if (fieldName === '$diagram') {
1184
+ if (typeof fieldValue === 'object' && fieldValue !== null && !Array.isArray(fieldValue)) {
1185
+ diagram = fieldValue;
1186
+ }
1187
+ continue;
1188
+ }
1189
+ // Handle $code (code generation configuration)
1190
+ if (fieldName === '$code') {
1191
+ if (typeof fieldValue === 'object' && fieldValue !== null && !Array.isArray(fieldValue)) {
1192
+ code = fieldValue;
1193
+ }
1194
+ continue;
1195
+ }
1196
+ // Handle $conventions (naming conventions)
1197
+ if (fieldName === '$conventions') {
1198
+ if (typeof fieldValue === 'object' && fieldValue !== null && !Array.isArray(fieldValue)) {
1199
+ conventions = fieldValue;
1200
+ }
1201
+ continue;
1202
+ }
1203
+ // Handle $eval (entity-level evaluation definitions)
1204
+ if (fieldName === '$eval') {
1205
+ if (typeof fieldValue === 'object' && fieldValue !== null && !Array.isArray(fieldValue)) {
1206
+ entityEval = fieldValue;
1207
+ }
1208
+ continue;
1209
+ }
1210
+ // Handle $experiment (entity-level experiment definition)
1211
+ if (fieldName === '$experiment') {
1212
+ if (typeof fieldValue === 'object' && fieldValue !== null && !Array.isArray(fieldValue)) {
1213
+ entityExperiment = fieldValue;
1214
+ }
1215
+ continue;
1216
+ }
1217
+ const parsed = parseFieldType(fieldValue);
1218
+ parsedFields[fieldName] = parsed;
1219
+ if (parsed.type === 'reference') {
1220
+ const refField = parsed;
1221
+ references.push({
1222
+ field: fieldName,
1223
+ ref: refField.ref || refField.refs?.[0] || '',
1224
+ refs: refField.refs,
1225
+ isArray: refField.isArray,
1226
+ optional: refField.optional,
1227
+ fuzzy: refField.fuzzy,
1228
+ });
1229
+ }
1230
+ else if (parsed.type === 'array') {
1231
+ const arrField = parsed;
1232
+ // Track inline references from arrays
1233
+ if (arrField.refs) {
1234
+ for (const ref of arrField.refs) {
1235
+ references.push({
1236
+ field: fieldName,
1237
+ ref,
1238
+ isArray: true,
1239
+ optional: true, // Inline refs are always optional
1240
+ });
1241
+ }
1242
+ }
1243
+ // Track back references (for schema completeness, but these are read-only)
1244
+ if (arrField.backRef) {
1245
+ references.push({
1246
+ field: fieldName,
1247
+ ref: arrField.backRef,
1248
+ isArray: true,
1249
+ optional: true, // Back refs are collections
1250
+ backRef: true, // Mark as back reference - should not trigger generation
1251
+ });
1252
+ }
1253
+ }
1254
+ }
1255
+ // Merge $next, $data, $id into seed config if present
1256
+ if (seed) {
1257
+ if (seedNext)
1258
+ seed.next = seedNext;
1259
+ if (seedData)
1260
+ seed.data = seedData;
1261
+ if (seedId)
1262
+ seed.id = seedId;
1263
+ if (seedIdTransform)
1264
+ seed.idTransform = seedIdTransform;
1265
+ }
1266
+ // Post-process fields for seeded types: convert string fields to prompt fields
1267
+ // In seeded schemas, plain strings are prompts for AI generation, not string descriptions
1268
+ if (seed) {
1269
+ for (const [fieldName, field] of Object.entries(parsedFields)) {
1270
+ if (field.type === 'string') {
1271
+ const stringField = field;
1272
+ parsedFields[fieldName] = {
1273
+ type: 'prompt',
1274
+ prompt: stringField.description,
1275
+ };
1276
+ }
1277
+ }
1278
+ }
1279
+ // Determine if this is a source function type (provider:Type pattern)
1280
+ const isSourceFunction = typeName.includes(':') && !!source;
1281
+ // Determine if this is a generative type
1282
+ const isGenerative = !!(image || speech || diagram || code);
1283
+ types[typeName] = {
1284
+ name: typeName,
1285
+ fields: parsedFields,
1286
+ references,
1287
+ ...(seed && { seed }),
1288
+ ...(icon && { icon }),
1289
+ ...(group && { group }),
1290
+ // Direct lifecycle handler access
1291
+ ...(entityHandlers['$created'] && { $created: entityHandlers['$created'] }),
1292
+ ...(entityHandlers['$updated'] && { $updated: entityHandlers['$updated'] }),
1293
+ ...(entityHandlers['$deleted'] && { $deleted: entityHandlers['$deleted'] }),
1294
+ // Grouped handlers (deprecated)
1295
+ ...(Object.keys(entityHandlers).length > 0 && { handlers: entityHandlers }),
1296
+ // State machine configuration
1297
+ ...(parsedState && { $state: parsedState }),
1298
+ // Data source configurations
1299
+ ...(source && { source }),
1300
+ ...(input && { input }),
1301
+ ...(from && { from }),
1302
+ ...(cache && { cache }),
1303
+ ...(isSourceFunction && { isSourceFunction }),
1304
+ // Generative format configurations
1305
+ ...(image && { image }),
1306
+ ...(speech && { speech }),
1307
+ ...(diagram && { diagram }),
1308
+ ...(code && { code }),
1309
+ ...(conventions && { conventions }),
1310
+ ...(isGenerative && { isGenerative }),
1311
+ // Entity-level eval and experiment
1312
+ ...(entityEval && { eval: entityEval }),
1313
+ ...(entityExperiment && { experiment: entityExperiment }),
1314
+ };
1315
+ }
1316
+ // Second pass: validate references (warn but don't crash on undefined types)
1317
+ for (const type of Object.values(types)) {
1318
+ for (const ref of type.references) {
1319
+ // Check all refs in union types
1320
+ const refsToCheck = ref.refs || [ref.ref];
1321
+ for (const refType of refsToCheck) {
1322
+ if (refType && !types[refType]) {
1323
+ // Warn instead of throwing - allows app to load with schema errors
1324
+ console.warn(`Reference to undefined type: ${refType} (in type ${type.name})`);
1325
+ }
1326
+ }
1327
+ }
1328
+ // Also validate jsonpath-embedded fields
1329
+ for (const [fieldName, field] of Object.entries(type.fields)) {
1330
+ if (field.type === 'jsonpath-embedded') {
1331
+ const embeddedType = field.embeddedType;
1332
+ if (!types[embeddedType]) {
1333
+ console.warn(`JSONPath embedded reference to undefined type: ${embeddedType} (in type ${type.name}.${fieldName})`);
1334
+ }
1335
+ }
1336
+ }
1337
+ }
1338
+ // Third pass: detect embedded types (string fields that exactly match a type name)
1339
+ for (const type of Object.values(types)) {
1340
+ for (const [fieldName, field] of Object.entries(type.fields)) {
1341
+ if (field.type === 'string') {
1342
+ const desc = field.description;
1343
+ // Check for embedded type pattern: TypeName, TypeName?, TypeName[]
1344
+ // Only match if the entire string is a type name (possibly with ? or [] suffix)
1345
+ const embeddedMatch = desc.match(/^(\w+)(\[\])?(\?)?$/);
1346
+ if (embeddedMatch) {
1347
+ const [, typeName, arrayMarker, optionalMarker] = embeddedMatch;
1348
+ // Check if the type name exists in our schema
1349
+ if (types[typeName]) {
1350
+ // Convert to embedded field
1351
+ const embeddedField = {
1352
+ type: 'embedded',
1353
+ embeddedType: typeName,
1354
+ isArray: !!arrayMarker,
1355
+ optional: !!optionalMarker,
1356
+ };
1357
+ type.fields[fieldName] = embeddedField;
1358
+ }
1359
+ }
1360
+ }
1361
+ }
1362
+ }
1363
+ // Parse schema-level directives
1364
+ let fn;
1365
+ let on;
1366
+ let evalDefs;
1367
+ let experiment;
1368
+ let analytics;
1369
+ const schemaObj = schema;
1370
+ // Parse $fn - schema-level functions
1371
+ if (schemaObj.$fn && typeof schemaObj.$fn === 'object') {
1372
+ fn = {};
1373
+ for (const [name, func] of Object.entries(schemaObj.$fn)) {
1374
+ if (typeof func === 'function') {
1375
+ fn[name] = serializeFunction(name, func);
1376
+ }
1377
+ }
1378
+ }
1379
+ // Parse $on - schema-level event handlers
1380
+ if (schemaObj.$on && typeof schemaObj.$on === 'object') {
1381
+ on = {};
1382
+ for (const [name, func] of Object.entries(schemaObj.$on)) {
1383
+ if (typeof func === 'function') {
1384
+ on[name] = serializeFunction(name, func);
1385
+ }
1386
+ }
1387
+ }
1388
+ // Parse $eval - evaluation definitions
1389
+ if (schemaObj.$eval && typeof schemaObj.$eval === 'object') {
1390
+ evalDefs = schemaObj.$eval;
1391
+ }
1392
+ // Parse $experiment - experiment definitions
1393
+ if (schemaObj.$experiment && typeof schemaObj.$experiment === 'object') {
1394
+ experiment = schemaObj.$experiment;
1395
+ }
1396
+ // Parse $analytics - analytics configuration
1397
+ if (schemaObj.$analytics && typeof schemaObj.$analytics === 'object') {
1398
+ analytics = schemaObj.$analytics;
1399
+ }
1400
+ // Parse schema-level lifecycle handlers ($seeded, $ready, $error)
1401
+ const schemaHandlers = {};
1402
+ for (const key of ['$seeded', '$ready', '$error']) {
1403
+ if (typeof schemaObj[key] === 'function') {
1404
+ schemaHandlers[key] = serializeFunction(key, schemaObj[key]);
1405
+ }
1406
+ }
1407
+ return {
1408
+ types,
1409
+ hash: schemaHash(schema),
1410
+ ...(settings && { settings }),
1411
+ ...(fn && { fn }),
1412
+ ...(on && { on }),
1413
+ ...(evalDefs && { eval: evalDefs }),
1414
+ ...(experiment && { experiment }),
1415
+ ...(analytics && { analytics }),
1416
+ ...(Object.keys(schemaHandlers).length > 0 && { handlers: schemaHandlers }),
1417
+ };
1418
+ }
1419
+ /**
1420
+ * Convert a parsed type definition to a JSON Schema representation.
1421
+ *
1422
+ * Transforms the internal ParsedType format into a standard JSON Schema object
1423
+ * that can be used for validation, documentation, or AI generation prompts.
1424
+ *
1425
+ * @param parsedType - The parsed type definition to convert
1426
+ * @param parsedSchema - Optional full schema for resolving embedded types.
1427
+ * When provided, embedded types (e.g., `Address` in `address: 'Address'`) are
1428
+ * recursively expanded into their full JSON Schema representation.
1429
+ * @param seen - Internal set for tracking visited types to prevent infinite recursion
1430
+ * in circular reference scenarios. Do not pass this parameter manually.
1431
+ *
1432
+ * @returns A JSON Schema object with:
1433
+ * - `type: 'object'` - Always an object schema
1434
+ * - `properties` - Map of field names to their JSON Schema definitions
1435
+ * - `required` - Array of required field names (non-optional fields)
1436
+ * - `additionalProperties: false` - Strict schema, no extra fields allowed
1437
+ *
1438
+ * @example
1439
+ * const parsed = parseSchema({
1440
+ * $context: 'blog',
1441
+ * Post: {
1442
+ * title: 'Post title',
1443
+ * author: '->User',
1444
+ * tags: ['List 3-5 keywords']
1445
+ * }
1446
+ * })
1447
+ *
1448
+ * const jsonSchema = toJSONSchema(parsed.types.Post, parsed)
1449
+ * // {
1450
+ * // type: 'object',
1451
+ * // properties: {
1452
+ * // title: { type: 'string', description: 'Post title' },
1453
+ * // author: { type: 'string', description: 'Reference to User (ID)' },
1454
+ * // tags: { type: 'array', items: { type: 'string' }, minItems: 3, maxItems: 5 }
1455
+ * // },
1456
+ * // required: ['title', 'author', 'tags'],
1457
+ * // additionalProperties: false
1458
+ * // }
1459
+ *
1460
+ * @example
1461
+ * // With embedded types expanded
1462
+ * const schema = parseSchema({
1463
+ * $context: 'app',
1464
+ * User: {
1465
+ * name: 'User name',
1466
+ * address: 'Address' // Embedded type
1467
+ * },
1468
+ * Address: {
1469
+ * street: 'Street',
1470
+ * city: 'City'
1471
+ * }
1472
+ * })
1473
+ *
1474
+ * const jsonSchema = toJSONSchema(schema.types.User, schema)
1475
+ * // properties.address will contain the full Address schema inline
1476
+ */
1477
+ export function toJSONSchema(parsedType, parsedSchemaOrIncludeRefs, seen = new Set()) {
1478
+ // Support both old API (boolean for includeReferences) and new API (ParsedSchema)
1479
+ const parsedSchema = typeof parsedSchemaOrIncludeRefs === 'object' ? parsedSchemaOrIncludeRefs : undefined;
1480
+ const includeReferences = typeof parsedSchemaOrIncludeRefs === 'boolean' ? parsedSchemaOrIncludeRefs : true;
1481
+ const properties = {};
1482
+ const required = [];
1483
+ for (const [fieldName, field] of Object.entries(parsedType.fields)) {
1484
+ // Skip computed fields - they're derived, not generated
1485
+ if (field.type === 'computed') {
1486
+ continue;
1487
+ }
1488
+ const isOptional = 'optional' in field && field.optional;
1489
+ if (field.type === 'string') {
1490
+ properties[fieldName] = {
1491
+ type: 'string',
1492
+ description: field.description,
1493
+ };
1494
+ }
1495
+ else if (field.type === 'reference' && includeReferences) {
1496
+ const refName = field.refs ? field.refs.join(' | ') : field.ref;
1497
+ if (field.isArray) {
1498
+ properties[fieldName] = {
1499
+ type: 'array',
1500
+ items: { type: 'string' },
1501
+ description: `Array of ${refName} references (IDs)`,
1502
+ };
1503
+ }
1504
+ else {
1505
+ properties[fieldName] = {
1506
+ type: 'string',
1507
+ description: `Reference to ${refName} (ID)`,
1508
+ };
1509
+ }
1510
+ }
1511
+ else if (field.type === 'array') {
1512
+ const arrayProp = {
1513
+ type: 'array',
1514
+ items: { type: 'string' },
1515
+ description: field.description,
1516
+ };
1517
+ // Wire up minItems/maxItems from parsed count
1518
+ if (field.count !== undefined) {
1519
+ if (typeof field.count === 'number') {
1520
+ arrayProp.minItems = field.count;
1521
+ arrayProp.maxItems = field.count;
1522
+ }
1523
+ else {
1524
+ arrayProp.minItems = field.count.min;
1525
+ arrayProp.maxItems = field.count.max;
1526
+ }
1527
+ }
1528
+ properties[fieldName] = arrayProp;
1529
+ }
1530
+ else if (field.type === 'object') {
1531
+ const nestedProps = {};
1532
+ const nestedRequired = [];
1533
+ for (const [propName, propField] of Object.entries(field.properties)) {
1534
+ nestedProps[propName] = {
1535
+ type: 'string',
1536
+ description: propField.description,
1537
+ };
1538
+ nestedRequired.push(propName);
1539
+ }
1540
+ properties[fieldName] = {
1541
+ type: 'object',
1542
+ properties: nestedProps,
1543
+ required: nestedRequired,
1544
+ };
1545
+ }
1546
+ else if (field.type === 'embedded' && parsedSchema) {
1547
+ // Recursively embed the referenced type's JSON schema
1548
+ const embeddedTypeName = field.embeddedType;
1549
+ const embeddedType = parsedSchema.types[embeddedTypeName];
1550
+ if (embeddedType) {
1551
+ // Determine if we're in a circular reference situation
1552
+ const isCircular = seen.has(embeddedTypeName);
1553
+ // Get the embedded type's JSON schema
1554
+ // For circular refs, stop recursion by not passing parsedSchema
1555
+ // For normal refs, continue with updated seen set
1556
+ const newSeen = isCircular ? seen : new Set([...seen, embeddedTypeName]);
1557
+ const embeddedSchema = toJSONSchema(embeddedType, isCircular ? undefined : parsedSchema, newSeen);
1558
+ // Build the embedded object schema
1559
+ const embeddedObjectSchema = {
1560
+ type: 'object',
1561
+ properties: embeddedSchema.properties,
1562
+ required: embeddedSchema.required,
1563
+ additionalProperties: false,
1564
+ };
1565
+ if (field.isArray) {
1566
+ properties[fieldName] = {
1567
+ type: 'array',
1568
+ items: embeddedObjectSchema,
1569
+ };
1570
+ }
1571
+ else {
1572
+ properties[fieldName] = embeddedObjectSchema;
1573
+ }
1574
+ }
1575
+ }
1576
+ else if (field.type === 'embedded') {
1577
+ // If no parsedSchema provided, fall back to string representation
1578
+ properties[fieldName] = {
1579
+ type: 'string',
1580
+ description: field.embeddedType,
1581
+ };
1582
+ }
1583
+ // Only add to required if:
1584
+ // - not optional AND
1585
+ // - (not a reference OR includeReferences is true)
1586
+ const isReference = field.type === 'reference';
1587
+ if (!isOptional && (!isReference || includeReferences)) {
1588
+ required.push(fieldName);
1589
+ }
1590
+ }
1591
+ return {
1592
+ type: 'object',
1593
+ properties,
1594
+ required,
1595
+ additionalProperties: false,
1596
+ };
1597
+ }
1598
+ /**
1599
+ * Generate a deterministic hash for a schema definition.
1600
+ *
1601
+ * Creates a consistent 8-character hexadecimal hash that uniquely identifies
1602
+ * a schema's structure. The hash is computed using the FNV-1a algorithm on
1603
+ * the JSON-serialized schema with sorted keys, ensuring the same schema
1604
+ * always produces the same hash regardless of property order.
1605
+ *
1606
+ * @param schema - The schema definition to hash
1607
+ *
1608
+ * @returns An 8-character hexadecimal hash string
1609
+ *
1610
+ * @example
1611
+ * const hash1 = schemaHash({
1612
+ * $context: 'blog',
1613
+ * Post: { title: 'Title' }
1614
+ * })
1615
+ * // e.g., 'a1b2c3d4'
1616
+ *
1617
+ * // Same schema, different property order = same hash
1618
+ * const hash2 = schemaHash({
1619
+ * Post: { title: 'Title' },
1620
+ * $context: 'blog'
1621
+ * })
1622
+ * // hash1 === hash2
1623
+ *
1624
+ * @example
1625
+ * // Used for schema versioning
1626
+ * const schema = parseSchema({ $context: 'app', User: { name: 'Name' } })
1627
+ * console.log(schema.hash)
1628
+ * // The hash can be used to detect schema changes and trigger migrations
1629
+ */
1630
+ export function schemaHash(schema) {
1631
+ // Sort keys recursively for consistent hashing
1632
+ const sortedSchema = sortObject(schema);
1633
+ const json = JSON.stringify(sortedSchema);
1634
+ // Simple hash function (FNV-1a)
1635
+ let hash = 2166136261;
1636
+ for (let i = 0; i < json.length; i++) {
1637
+ hash ^= json.charCodeAt(i);
1638
+ hash = (hash * 16777619) >>> 0;
1639
+ }
1640
+ return hash.toString(16).padStart(8, '0');
1641
+ }
1642
+ function sortObject(obj) {
1643
+ if (typeof obj !== 'object' || obj === null) {
1644
+ return obj;
1645
+ }
1646
+ if (Array.isArray(obj)) {
1647
+ return obj.map(sortObject);
1648
+ }
1649
+ const sorted = {};
1650
+ for (const key of Object.keys(obj).sort()) {
1651
+ sorted[key] = sortObject(obj[key]);
1652
+ }
1653
+ return sorted;
1654
+ }
1655
+ // ============================================================================
1656
+ // SDK Configuration
1657
+ // ============================================================================
1658
+ /**
1659
+ * Get the default API base URL. Reads environment variable dynamically
1660
+ * to support test mocking.
1661
+ */
1662
+ function getDefaultApiBase() {
1663
+ return (typeof process !== 'undefined' && process.env?.DB4AI_API_BASE) || 'https://db4ai.dev';
1664
+ }
1665
+ function createJob() {
1666
+ return {
1667
+ id: `job_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`,
1668
+ status: 'queued',
1669
+ };
1670
+ }
1671
+ /**
1672
+ * Internal class for managing entity collection state
1673
+ */
1674
+ class EntityCollectionImpl {
1675
+ $type;
1676
+ _items = [];
1677
+ _baseUrl;
1678
+ _namespace;
1679
+ _registerSchema;
1680
+ constructor(typeName, baseUrl, namespace, registerSchema) {
1681
+ this.$type = typeName;
1682
+ this._baseUrl = baseUrl;
1683
+ this._namespace = namespace;
1684
+ this._registerSchema = registerSchema;
1685
+ }
1686
+ get $count() {
1687
+ return this._items.length;
1688
+ }
1689
+ _addItem(item) {
1690
+ this._items.push(item);
1691
+ }
1692
+ _setItems(items) {
1693
+ this._items = items;
1694
+ }
1695
+ _getItems() {
1696
+ return this._items;
1697
+ }
1698
+ [Symbol.iterator]() {
1699
+ return this._items[Symbol.iterator]();
1700
+ }
1701
+ forEach(fn) {
1702
+ const job = createJob();
1703
+ job.status = 'running';
1704
+ try {
1705
+ for (const item of this._items) {
1706
+ fn(item);
1707
+ }
1708
+ job.status = 'completed';
1709
+ }
1710
+ catch (error) {
1711
+ job.status = 'failed';
1712
+ job.error = error instanceof Error ? error : new Error(String(error));
1713
+ }
1714
+ return job;
1715
+ }
1716
+ map(fn) {
1717
+ const items = this._items;
1718
+ return new Combinable(function* () {
1719
+ for (const item of items) {
1720
+ yield fn(item);
1721
+ }
1722
+ });
1723
+ }
1724
+ where(predicate) {
1725
+ const items = this._items;
1726
+ return new Combinable(function* () {
1727
+ for (const item of items) {
1728
+ if (predicate(item)) {
1729
+ yield item;
1730
+ }
1731
+ }
1732
+ });
1733
+ }
1734
+ async fetchAll() {
1735
+ await this._registerSchema();
1736
+ const url = `${this._baseUrl}/api/${this._namespace}/${this.$type}`;
1737
+ const res = await fetch(url);
1738
+ if (!res.ok) {
1739
+ throw new APIError('GENERATION_FAILED', `Failed to fetch ${this.$type}: ${await res.text()}`, res.status);
1740
+ }
1741
+ const data = await res.json();
1742
+ const items = Array.isArray(data) ? data : data.items || [];
1743
+ this._setItems(items);
1744
+ return items;
1745
+ }
1746
+ }
1747
+ /**
1748
+ * Create a callable EntityCollection using a Proxy.
1749
+ * The returned object is both a function (factory) and has collection methods/properties.
1750
+ */
1751
+ function createEntityCollection(typeName, baseUrl, namespace, registerSchema, factory) {
1752
+ const collection = new EntityCollectionImpl(typeName, baseUrl, namespace, registerSchema);
1753
+ // Create a proxy around the factory function
1754
+ return new Proxy(factory, {
1755
+ get(target, prop) {
1756
+ // Handle Symbol.iterator for iteration
1757
+ if (prop === Symbol.iterator) {
1758
+ return () => collection[Symbol.iterator]();
1759
+ }
1760
+ // Return collection properties and methods
1761
+ if (prop === '$type')
1762
+ return collection.$type;
1763
+ if (prop === '$count')
1764
+ return collection.$count;
1765
+ if (prop === 'forEach')
1766
+ return collection.forEach.bind(collection);
1767
+ if (prop === 'map')
1768
+ return collection.map.bind(collection);
1769
+ if (prop === 'where')
1770
+ return collection.where.bind(collection);
1771
+ if (prop === 'fetchAll')
1772
+ return collection.fetchAll.bind(collection);
1773
+ // Internal methods for testing/manipulation
1774
+ if (prop === '_addItem')
1775
+ return collection._addItem.bind(collection);
1776
+ if (prop === '_setItems')
1777
+ return collection._setItems.bind(collection);
1778
+ if (prop === '_getItems')
1779
+ return collection._getItems.bind(collection);
1780
+ return undefined;
1781
+ },
1782
+ apply(target, thisArg, args) {
1783
+ // When called as a function, invoke the factory
1784
+ return factory(args[0], args[1]);
1785
+ },
1786
+ });
1787
+ }
1788
+ /**
1789
+ * Extract API base URL from $id
1790
+ * @example
1791
+ * getApiBaseFromId('https://db.sb/blogs') // 'https://db.sb'
1792
+ * getApiBaseFromId('https://startups.db.sb') // 'https://startups.db.sb'
1793
+ * getApiBaseFromId('my-namespace') // undefined (use default)
1794
+ */
1795
+ function getApiBaseFromId(id) {
1796
+ // If it's not a URL, return undefined to use default
1797
+ if (!id.startsWith('http://') && !id.startsWith('https://')) {
1798
+ return undefined;
1799
+ }
1800
+ try {
1801
+ const url = new URL(id);
1802
+ return `${url.protocol}//${url.host}`;
1803
+ }
1804
+ catch {
1805
+ return undefined;
1806
+ }
1807
+ }
1808
+ /**
1809
+ * Parse namespace from $id - supports both full URLs and plain namespace strings
1810
+ * @example
1811
+ * parseIdNamespace('https://db.sb/blogs') // 'blogs'
1812
+ * parseIdNamespace('my-namespace') // 'my-namespace'
1813
+ * parseIdNamespace('https://db.sb/') // null
1814
+ */
1815
+ function parseIdNamespace(id) {
1816
+ // If it's not a URL, treat the whole string as the namespace
1817
+ if (!id.startsWith('http://') && !id.startsWith('https://')) {
1818
+ const trimmed = id.trim();
1819
+ return trimmed || null;
1820
+ }
1821
+ // Otherwise parse as URL
1822
+ return parseNamespaceFromId(id);
1823
+ }
1824
+ /**
1825
+ * Resolve all references in a generated object recursively
1826
+ */
1827
+ async function resolveReferences(obj, typeName, parsed, baseUrl, namespace, cache) {
1828
+ const type = parsed.types[typeName];
1829
+ if (!type)
1830
+ return obj;
1831
+ const resolved = { ...obj };
1832
+ for (const [fieldName, field] of Object.entries(type.fields)) {
1833
+ const value = obj[fieldName];
1834
+ if (value === undefined || value === null)
1835
+ continue;
1836
+ if (field.type === 'array' && field.refs && Array.isArray(value)) {
1837
+ // Inline references: ['... ->Type'] - resolve each ID to full object
1838
+ const refType = field.refs[0];
1839
+ const resolvedItems = await Promise.all(value.map(async (refId) => {
1840
+ const cacheKey = `${refType}:${refId}`;
1841
+ if (cache.has(cacheKey))
1842
+ return cache.get(cacheKey);
1843
+ // If refId is already a URL, use it directly; otherwise construct URL
1844
+ const refUrl = refId.startsWith('http://') || refId.startsWith('https://')
1845
+ ? refId
1846
+ : `${baseUrl}/api/${namespace}/${refType}/${refId}`;
1847
+ const refRes = await fetch(refUrl);
1848
+ if (!refRes.ok)
1849
+ return { id: refId, _error: 'Failed to resolve' };
1850
+ const refObj = await refRes.json();
1851
+ cache.set(cacheKey, refObj);
1852
+ // Recursively resolve nested references
1853
+ return resolveReferences(refObj, refType, parsed, baseUrl, namespace, cache);
1854
+ }));
1855
+ resolved[fieldName] = resolvedItems;
1856
+ }
1857
+ else if (field.type === 'reference' && !field.isArray && typeof value === 'string') {
1858
+ // Single reference: ->Type - resolve to full object
1859
+ const refType = field.ref || field.refs?.[0];
1860
+ if (refType) {
1861
+ const cacheKey = `${refType}:${value}`;
1862
+ if (cache.has(cacheKey)) {
1863
+ resolved[fieldName] = cache.get(cacheKey);
1864
+ }
1865
+ else {
1866
+ // If value is already a URL, use it directly; otherwise construct URL
1867
+ const refUrl = value.startsWith('http://') || value.startsWith('https://')
1868
+ ? value
1869
+ : `${baseUrl}/api/${namespace}/${refType}/${value}`;
1870
+ const refRes = await fetch(refUrl);
1871
+ if (refRes.ok) {
1872
+ const refObj = await refRes.json();
1873
+ cache.set(cacheKey, refObj);
1874
+ resolved[fieldName] = await resolveReferences(refObj, refType, parsed, baseUrl, namespace, cache);
1875
+ }
1876
+ }
1877
+ }
1878
+ }
1879
+ else if (field.type === 'reference' && field.isArray && Array.isArray(value)) {
1880
+ // Array reference: ->Type[] - resolve each ID to full object
1881
+ const refType = field.ref || field.refs?.[0];
1882
+ if (refType) {
1883
+ const resolvedItems = await Promise.all(value.map(async (refId) => {
1884
+ const cacheKey = `${refType}:${refId}`;
1885
+ if (cache.has(cacheKey))
1886
+ return cache.get(cacheKey);
1887
+ // If refId is already a URL, use it directly; otherwise construct URL
1888
+ const refUrl = refId.startsWith('http://') || refId.startsWith('https://')
1889
+ ? refId
1890
+ : `${baseUrl}/api/${namespace}/${refType}/${refId}`;
1891
+ const refRes = await fetch(refUrl);
1892
+ if (!refRes.ok)
1893
+ return { id: refId, _error: 'Failed to resolve' };
1894
+ const refObj = await refRes.json();
1895
+ cache.set(cacheKey, refObj);
1896
+ return resolveReferences(refObj, refType, parsed, baseUrl, namespace, cache);
1897
+ }));
1898
+ resolved[fieldName] = resolvedItems;
1899
+ }
1900
+ }
1901
+ // Back references (['<-Type']) are populated by the API, no client-side resolution needed
1902
+ }
1903
+ return resolved;
1904
+ }
1905
+ /**
1906
+ * Create a database instance from a schema definition.
1907
+ *
1908
+ * This is the main entry point for defining and working with a db4.ai schema.
1909
+ * It parses the schema, registers it with the API, and returns a typed object
1910
+ * that provides access to all defined types as EntityCollections.
1911
+ *
1912
+ * ## API Base URL Configuration
1913
+ *
1914
+ * The SDK determines the API base URL in the following priority order:
1915
+ * 1. Host from `$id` if it's a full URL (e.g., `'https://custom.api.com/myapp'`)
1916
+ * 2. `DB4AI_API_BASE` environment variable
1917
+ * 3. Default: `'https://db4ai.dev'`
1918
+ *
1919
+ * @param schema - The schema definition object. Must include either:
1920
+ * - `$id` - A URL or namespace identifier (e.g., `'https://db.sb/blog'` or `'blog'`)
1921
+ * - `$context` - A namespace string (e.g., `'my-app'`)
1922
+ *
1923
+ * Type definitions are records where keys are type names and values define fields.
1924
+ *
1925
+ * @returns A DBResult object providing:
1926
+ * - `schema` - The original schema definition
1927
+ * - `types` - Parsed type definitions
1928
+ * - `hash` - Schema version hash
1929
+ * - `namespace` - The resolved namespace
1930
+ * - `baseUrl` - API base URL
1931
+ * - `toJSONSchema(typeName)` - Convert a type to JSON Schema
1932
+ * - `registerSchema()` - Manually register the schema with the API
1933
+ * - Dynamic type accessors (e.g., `db.Post`, `db.User`) as EntityCollections
1934
+ *
1935
+ * @throws Error if neither `$id` nor `$context` is provided
1936
+ *
1937
+ * @example
1938
+ * // Define a blog schema
1939
+ * const db = DB({
1940
+ * $context: 'blog',
1941
+ * Post: {
1942
+ * title: 'Post title',
1943
+ * content: 'Main content',
1944
+ * author: '->User',
1945
+ * tags: ['List 3-5 keywords']
1946
+ * },
1947
+ * User: {
1948
+ * name: 'User name',
1949
+ * email: 'Email address'
1950
+ * }
1951
+ * })
1952
+ *
1953
+ * @example
1954
+ * // Generate a new post
1955
+ * const post = await db.Post('my-first-post', { topic: 'AI' })
1956
+ * console.log(post.title, post.content)
1957
+ *
1958
+ * @example
1959
+ * // Fetch all posts and iterate
1960
+ * await db.Post.fetchAll()
1961
+ * for (const post of db.Post) {
1962
+ * console.log(post.title)
1963
+ * }
1964
+ *
1965
+ * @example
1966
+ * // Use with full URL
1967
+ * const db = DB({
1968
+ * $id: 'https://db.sb/startups',
1969
+ * Company: {
1970
+ * name: 'Company name',
1971
+ * description: 'Company description'
1972
+ * }
1973
+ * })
1974
+ */
1975
+ export function DB(schema) {
1976
+ const parsed = parseSchema(schema);
1977
+ const metadata = schema;
1978
+ const schemaId = metadata.$id;
1979
+ const schemaContext = metadata.$context;
1980
+ // Either $id or $context is required
1981
+ if (!schemaId && !schemaContext) {
1982
+ throw new SchemaValidationError('MISSING_CONTEXT', 'Schema must have $id or $context set (e.g., $context: "my-app" or $id: "https://db4ai.dev/my-app")');
1983
+ }
1984
+ // Parse namespace from $id (URL or plain string) or use $context directly
1985
+ let namespace = null;
1986
+ let baseUrl = getDefaultApiBase();
1987
+ if (schemaContext) {
1988
+ // $context is always just a namespace, uses default base URL
1989
+ namespace = schemaContext.trim() || null;
1990
+ }
1991
+ if (schemaId) {
1992
+ // $id can be a full URL or plain namespace
1993
+ namespace = parseIdNamespace(schemaId);
1994
+ // Use $id's host if it's a URL (overrides default/env var)
1995
+ const idBaseUrl = getApiBaseFromId(schemaId);
1996
+ if (idBaseUrl) {
1997
+ baseUrl = idBaseUrl;
1998
+ }
1999
+ }
2000
+ if (!namespace) {
2001
+ throw new SchemaValidationError('MISSING_NAMESPACE', 'Schema must specify a namespace via $context or $id (e.g., $context: "my-app")');
2002
+ }
2003
+ const instances = [];
2004
+ // Track schema registration state
2005
+ let schemaRegistered = false;
2006
+ let schemaRegistrationPromise = null;
2007
+ // Register schema with API (idempotent, cached)
2008
+ async function registerSchema() {
2009
+ if (schemaRegistered)
2010
+ return;
2011
+ if (schemaRegistrationPromise)
2012
+ return schemaRegistrationPromise;
2013
+ schemaRegistrationPromise = (async () => {
2014
+ const res = await fetch(`${baseUrl}/api/${namespace}/schema`, {
2015
+ method: 'POST',
2016
+ headers: { 'Content-Type': 'application/json' },
2017
+ body: JSON.stringify(schema),
2018
+ });
2019
+ if (!res.ok) {
2020
+ const error = await res.text();
2021
+ throw new APIError('REGISTRATION_FAILED', `Failed to register schema: ${error}`, res.status);
2022
+ }
2023
+ schemaRegistered = true;
2024
+ })();
2025
+ return schemaRegistrationPromise;
2026
+ }
2027
+ // Create base result object
2028
+ const base = {
2029
+ schema,
2030
+ types: parsed.types,
2031
+ hash: parsed.hash,
2032
+ id: schemaId,
2033
+ context: schemaContext,
2034
+ namespace,
2035
+ baseUrl,
2036
+ instances,
2037
+ registerSchema,
2038
+ toJSONSchema: (typeName) => {
2039
+ const type = parsed.types[typeName];
2040
+ if (!type)
2041
+ throw new SchemaValidationError('INVALID_FIELD_TYPE', `Unknown type: ${typeName}`);
2042
+ return toJSONSchema(type, parsed);
2043
+ },
2044
+ };
2045
+ // Cache EntityCollections so the same instance is returned each time
2046
+ const collectionCache = new Map();
2047
+ // Create a proxy that handles dynamic type access
2048
+ return new Proxy(base, {
2049
+ get(target, prop) {
2050
+ // Return known properties directly
2051
+ if (prop in target) {
2052
+ return target[prop];
2053
+ }
2054
+ // For type names, return an EntityCollection (cached)
2055
+ const typeName = String(prop);
2056
+ if (parsed.types[typeName]) {
2057
+ // Return cached collection if exists
2058
+ if (collectionCache.has(typeName)) {
2059
+ return collectionCache.get(typeName);
2060
+ }
2061
+ // Create factory function for this type
2062
+ const factory = async (id, context) => {
2063
+ // Ensure schema is registered
2064
+ await registerSchema();
2065
+ // Build URL with context as query params if provided
2066
+ let url = `${baseUrl}/api/${namespace}/${typeName}/${id}`;
2067
+ if (context && Object.keys(context).length > 0) {
2068
+ const params = new URLSearchParams();
2069
+ for (const [key, value] of Object.entries(context)) {
2070
+ if (value !== undefined && value !== null) {
2071
+ params.set(key, String(value));
2072
+ }
2073
+ }
2074
+ url += `?${params.toString()}`;
2075
+ }
2076
+ // Fetch/generate the object with extended timeout for long-running generations
2077
+ // Default Node.js undici timeout is 300s, but Blog generation can take 80-150s
2078
+ // We use 10 minutes to handle complex hierarchies with AI latency variance
2079
+ const controller = new AbortController();
2080
+ const timeoutId = setTimeout(() => controller.abort(), 600000); // 10 minute timeout
2081
+ try {
2082
+ const res = await fetch(url, { signal: controller.signal });
2083
+ clearTimeout(timeoutId);
2084
+ if (!res.ok) {
2085
+ const error = await res.text();
2086
+ throw new APIError('GENERATION_FAILED', `Failed to generate ${typeName}/${id}: ${error}`, res.status);
2087
+ }
2088
+ const obj = await res.json();
2089
+ // Resolve all references recursively
2090
+ const cache = new Map();
2091
+ return resolveReferences(obj, typeName, parsed, baseUrl, namespace, cache);
2092
+ }
2093
+ finally {
2094
+ clearTimeout(timeoutId);
2095
+ }
2096
+ };
2097
+ // Create and cache the EntityCollection
2098
+ const collection = createEntityCollection(typeName, baseUrl, namespace, registerSchema, factory);
2099
+ collectionCache.set(typeName, collection);
2100
+ return collection;
2101
+ }
2102
+ return undefined;
2103
+ },
2104
+ });
2105
+ }
2106
+ // ============================================================================
2107
+ // Combinable: Lazy cartesian product pipeline builder
2108
+ // ============================================================================
2109
+ /**
2110
+ * A lazy pipeline builder for cartesian products and collection transformations.
2111
+ *
2112
+ * Combinable provides a fluent API for building transformation pipelines where
2113
+ * no computation happens until a terminal operation is invoked. This enables
2114
+ * efficient processing of large datasets and cartesian products.
2115
+ *
2116
+ * Key features:
2117
+ * - Lazy evaluation: transformations are only applied when results are consumed
2118
+ * - Chainable: all intermediate operations return new Combinable instances
2119
+ * - Iterable: works with for...of loops and spread operator
2120
+ *
2121
+ * Intermediate operations (return Combinable):
2122
+ * - `where(predicate)` - Filter items by predicate
2123
+ * - `map(fn)` - Transform each item
2124
+ * - `take(n)` - Limit to first n items
2125
+ * - `skip(n)` - Skip first n items
2126
+ *
2127
+ * Terminal operations (trigger evaluation):
2128
+ * - `all()` - Collect all results into an array
2129
+ * - `first()` - Get the first result or undefined
2130
+ * - `count()` - Count total results
2131
+ * - Iteration via for...of or spread
2132
+ *
2133
+ * @example
2134
+ * // Basic cartesian product
2135
+ * const pairs = combine([1, 2, 3], ['a', 'b']).all()
2136
+ * // [[1, 'a'], [1, 'b'], [2, 'a'], [2, 'b'], [3, 'a'], [3, 'b']]
2137
+ *
2138
+ * @example
2139
+ * // Chained transformations
2140
+ * const result = combine([1, 2, 3], [10, 20, 30])
2141
+ * .where(([a, b]) => a + b > 15)
2142
+ * .map(([a, b]) => a * b)
2143
+ * .take(3)
2144
+ * .all()
2145
+ * // [20, 30, 40]
2146
+ *
2147
+ * @example
2148
+ * // Lazy evaluation with for...of
2149
+ * for (const [x, y] of combine(range(1000), range(1000)).take(10)) {
2150
+ * console.log(x, y) // Only computes first 10 pairs
2151
+ * }
2152
+ *
2153
+ * @example
2154
+ * // From EntityCollection
2155
+ * await db.User.fetchAll()
2156
+ * const activeUsers = db.User
2157
+ * .where(user => user.isActive)
2158
+ * .map(user => user.email)
2159
+ * .all()
2160
+ */
2161
+ export class Combinable {
2162
+ source;
2163
+ constructor(source) {
2164
+ this.source = source;
2165
+ }
2166
+ /**
2167
+ * Filter combinations by predicate
2168
+ */
2169
+ where(predicate) {
2170
+ const source = this.source;
2171
+ return new Combinable(function* () {
2172
+ for (const item of source()) {
2173
+ if (predicate(item)) {
2174
+ yield item;
2175
+ }
2176
+ }
2177
+ });
2178
+ }
2179
+ /**
2180
+ * Transform each combination
2181
+ */
2182
+ map(fn) {
2183
+ const source = this.source;
2184
+ return new Combinable(function* () {
2185
+ for (const item of source()) {
2186
+ yield fn(item);
2187
+ }
2188
+ });
2189
+ }
2190
+ /**
2191
+ * Limit to first n items
2192
+ */
2193
+ take(n) {
2194
+ const source = this.source;
2195
+ return new Combinable(function* () {
2196
+ let count = 0;
2197
+ for (const item of source()) {
2198
+ if (count >= n)
2199
+ break;
2200
+ yield item;
2201
+ count++;
2202
+ }
2203
+ });
2204
+ }
2205
+ /**
2206
+ * Skip first n items
2207
+ */
2208
+ skip(n) {
2209
+ const source = this.source;
2210
+ return new Combinable(function* () {
2211
+ let count = 0;
2212
+ for (const item of source()) {
2213
+ if (count >= n) {
2214
+ yield item;
2215
+ }
2216
+ count++;
2217
+ }
2218
+ });
2219
+ }
2220
+ /**
2221
+ * Collect all results into an array (terminal operation)
2222
+ */
2223
+ all() {
2224
+ return [...this.source()];
2225
+ }
2226
+ /**
2227
+ * Get the first result or undefined (terminal operation)
2228
+ */
2229
+ first() {
2230
+ const iterator = this.source();
2231
+ const result = iterator.next();
2232
+ return result.done ? undefined : result.value;
2233
+ }
2234
+ /**
2235
+ * Count total results (terminal operation)
2236
+ */
2237
+ count() {
2238
+ let count = 0;
2239
+ for (const _ of this.source()) {
2240
+ count++;
2241
+ }
2242
+ return count;
2243
+ }
2244
+ /**
2245
+ * Make Combinable iterable with for...of and spread operator
2246
+ */
2247
+ [Symbol.iterator]() {
2248
+ return this.source();
2249
+ }
2250
+ }
2251
+ /**
2252
+ * Create cartesian product of multiple iterables.
2253
+ * Returns a lazy Combinable that only computes when iterated or terminal operation is called.
2254
+ *
2255
+ * @example
2256
+ * const result = combine([1, 2], ['a', 'b']).all()
2257
+ * // [[1, 'a'], [1, 'b'], [2, 'a'], [2, 'b']]
2258
+ *
2259
+ * @example
2260
+ * const filtered = combine([1, 2, 3], [10, 20])
2261
+ * .where(([a, b]) => a + b > 20)
2262
+ * .map(([a, b]) => a * b)
2263
+ * .all()
2264
+ */
2265
+ export function combine(...iterables) {
2266
+ // Convert iterables to arrays lazily (we need to iterate multiple times)
2267
+ // This is done inside the generator to maintain laziness
2268
+ return new Combinable(function* () {
2269
+ if (iterables.length === 0)
2270
+ return;
2271
+ // Convert to arrays (required for cartesian product - need multiple passes)
2272
+ const arrays = iterables.map((it) => [...it]);
2273
+ // Check for empty arrays - cartesian product with empty set is empty
2274
+ if (arrays.some((arr) => arr.length === 0))
2275
+ return;
2276
+ // Generate cartesian product using indices
2277
+ const indices = new Array(arrays.length).fill(0);
2278
+ const maxIndices = arrays.map((arr) => arr.length);
2279
+ while (true) {
2280
+ // Yield current combination
2281
+ yield indices.map((i, j) => arrays[j][i]);
2282
+ // Increment indices (like counting in mixed radix)
2283
+ let carry = true;
2284
+ for (let i = indices.length - 1; i >= 0 && carry; i--) {
2285
+ indices[i]++;
2286
+ if (indices[i] >= maxIndices[i]) {
2287
+ indices[i] = 0;
2288
+ }
2289
+ else {
2290
+ carry = false;
2291
+ }
2292
+ }
2293
+ // If we carried all the way, we're done
2294
+ if (carry)
2295
+ break;
2296
+ }
2297
+ });
2298
+ }
2299
+ /**
2300
+ * Simple YAML parser for frontmatter
2301
+ * Handles the subset of YAML commonly used in MDX frontmatter:
2302
+ * - Key-value pairs (string values)
2303
+ * - Nested objects (indentation-based)
2304
+ * - Arrays with bracket notation [item1, item2]
2305
+ * - Comments (# ...)
2306
+ * - Multiline strings with | and >
2307
+ */
2308
+ function parseYAML(yaml) {
2309
+ // Normalize line endings to LF
2310
+ const normalized = yaml.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
2311
+ const lines = normalized.split('\n');
2312
+ const result = {};
2313
+ // Stack for tracking nested objects: [{ obj, indent }]
2314
+ const stack = [{ obj: result, indent: -1 }];
2315
+ // State for multiline strings
2316
+ let multilineKey = null;
2317
+ let multilineIndent = 0;
2318
+ let multilineLines = [];
2319
+ let multilineStyle = null;
2320
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
2321
+ const rawLine = lines[lineNum];
2322
+ // Handle multiline string continuation
2323
+ if (multilineKey !== null) {
2324
+ // Check if this line continues the multiline string
2325
+ const lineIndent = rawLine.match(/^(\s*)/)?.[1].length ?? 0;
2326
+ if (lineIndent >= multilineIndent && (rawLine.trim() !== '' || multilineLines.length > 0)) {
2327
+ // This is a continuation of the multiline string
2328
+ multilineLines.push(rawLine.slice(multilineIndent));
2329
+ continue;
2330
+ }
2331
+ else {
2332
+ // End of multiline string - set the value
2333
+ const current = stack[stack.length - 1];
2334
+ if (multilineStyle === '|') {
2335
+ // Literal block - preserve newlines
2336
+ current.obj[multilineKey] = multilineLines.join('\n');
2337
+ }
2338
+ else {
2339
+ // Folded block - join with spaces (single newlines become spaces)
2340
+ current.obj[multilineKey] = multilineLines.join(' ').replace(/\s+/g, ' ').trim();
2341
+ }
2342
+ multilineKey = null;
2343
+ multilineLines = [];
2344
+ multilineStyle = null;
2345
+ }
2346
+ }
2347
+ // Remove comments (but not inside quoted strings)
2348
+ let line = rawLine;
2349
+ // Simple comment removal - find # that's not inside quotes
2350
+ const hashIndex = line.indexOf('#');
2351
+ if (hashIndex !== -1) {
2352
+ // Check if it's inside quotes by counting quotes before it
2353
+ const beforeHash = line.slice(0, hashIndex);
2354
+ const singleQuotes = (beforeHash.match(/'/g) || []).length;
2355
+ const doubleQuotes = (beforeHash.match(/"/g) || []).length;
2356
+ if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0) {
2357
+ line = line.slice(0, hashIndex);
2358
+ }
2359
+ }
2360
+ // Skip empty lines
2361
+ if (line.trim() === '')
2362
+ continue;
2363
+ // Calculate indentation
2364
+ const indentMatch = line.match(/^(\s*)/);
2365
+ const indent = indentMatch ? indentMatch[1].length : 0;
2366
+ const content = line.trim();
2367
+ // Pop stack back to appropriate level
2368
+ while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
2369
+ stack.pop();
2370
+ }
2371
+ // Parse key-value pair
2372
+ const colonIndex = content.indexOf(':');
2373
+ if (colonIndex === -1) {
2374
+ throw new Error(`Invalid YAML at line ${lineNum + 1}: expected key-value pair, got "${content}"`);
2375
+ }
2376
+ const key = content.slice(0, colonIndex).trim();
2377
+ let value = content.slice(colonIndex + 1).trim();
2378
+ // Validate key
2379
+ if (!key) {
2380
+ throw new Error(`Invalid YAML at line ${lineNum + 1}: empty key`);
2381
+ }
2382
+ const current = stack[stack.length - 1];
2383
+ // Handle multiline string indicators
2384
+ if (value === '|' || value === '>') {
2385
+ multilineKey = key;
2386
+ multilineStyle = value;
2387
+ multilineIndent = indent + 2; // Expect content indented from the key
2388
+ multilineLines = [];
2389
+ continue;
2390
+ }
2391
+ if (value === '') {
2392
+ // No value - this is a nested object
2393
+ const nested = {};
2394
+ current.obj[key] = nested;
2395
+ stack.push({ obj: nested, indent });
2396
+ }
2397
+ else if (value.startsWith('[') && !value.endsWith(']')) {
2398
+ // Unclosed array bracket
2399
+ throw new Error(`Invalid YAML at line ${lineNum + 1}: unclosed array bracket in "${content}"`);
2400
+ }
2401
+ else if (value.startsWith('[') && value.endsWith(']')) {
2402
+ // Array notation: [item1, item2, ...]
2403
+ const arrayContent = value.slice(1, -1).trim();
2404
+ if (arrayContent === '') {
2405
+ current.obj[key] = [];
2406
+ }
2407
+ else {
2408
+ // Parse array items - handle quoted strings and unquoted values
2409
+ const items = [];
2410
+ let currentItem = '';
2411
+ let inQuote = null;
2412
+ for (let i = 0; i < arrayContent.length; i++) {
2413
+ const char = arrayContent[i];
2414
+ if (inQuote) {
2415
+ if (char === inQuote) {
2416
+ inQuote = null;
2417
+ }
2418
+ else {
2419
+ currentItem += char;
2420
+ }
2421
+ }
2422
+ else if (char === '"' || char === "'") {
2423
+ inQuote = char;
2424
+ }
2425
+ else if (char === ',') {
2426
+ items.push(currentItem.trim());
2427
+ currentItem = '';
2428
+ }
2429
+ else {
2430
+ currentItem += char;
2431
+ }
2432
+ }
2433
+ if (currentItem.trim()) {
2434
+ items.push(currentItem.trim());
2435
+ }
2436
+ current.obj[key] = items;
2437
+ }
2438
+ }
2439
+ else {
2440
+ // String value - remove surrounding quotes if present
2441
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
2442
+ value = value.slice(1, -1);
2443
+ }
2444
+ current.obj[key] = value;
2445
+ }
2446
+ }
2447
+ // Handle any remaining multiline string
2448
+ if (multilineKey !== null) {
2449
+ const current = stack[stack.length - 1];
2450
+ if (multilineStyle === '|') {
2451
+ current.obj[multilineKey] = multilineLines.join('\n');
2452
+ }
2453
+ else {
2454
+ current.obj[multilineKey] = multilineLines.join(' ').replace(/\s+/g, ' ').trim();
2455
+ }
2456
+ }
2457
+ return result;
2458
+ }
2459
+ /**
2460
+ * Parse MDX frontmatter and extract schema definition.
2461
+ *
2462
+ * @param mdx - The MDX content with optional YAML frontmatter
2463
+ * @returns Object with parsed schema and remaining body
2464
+ * @throws Error if YAML syntax is invalid
2465
+ *
2466
+ * @example
2467
+ * const mdx = `---
2468
+ * $context: https://db.sb
2469
+ * Idea:
2470
+ * concept: What is the concept?
2471
+ * ---
2472
+ *
2473
+ * <App>content</App>`
2474
+ *
2475
+ * const { schema, body } = parseMDX(mdx)
2476
+ * // schema = { $context: 'https://db.sb', Idea: { concept: 'What is the concept?' } }
2477
+ * // body = '\n<App>content</App>'
2478
+ */
2479
+ export function parseMDX(mdx) {
2480
+ // Normalize line endings
2481
+ const normalized = mdx.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
2482
+ // Check if MDX starts with frontmatter delimiter
2483
+ if (!normalized.startsWith('---')) {
2484
+ // No frontmatter - return empty schema and full content as body
2485
+ return {
2486
+ schema: {},
2487
+ body: mdx,
2488
+ };
2489
+ }
2490
+ // Find the closing frontmatter delimiter
2491
+ // Start searching after the first ---
2492
+ const startIndex = 3; // Length of '---'
2493
+ let endIndex = -1;
2494
+ // Look for --- at the start of a line (after a newline)
2495
+ let searchStart = startIndex;
2496
+ while (searchStart < normalized.length) {
2497
+ const newlineIndex = normalized.indexOf('\n', searchStart);
2498
+ if (newlineIndex === -1)
2499
+ break;
2500
+ // Check if the next line starts with ---
2501
+ if (normalized.slice(newlineIndex + 1, newlineIndex + 4) === '---') {
2502
+ // Verify it's actually the closing delimiter (followed by newline or EOF)
2503
+ const afterDash = normalized[newlineIndex + 4];
2504
+ if (afterDash === undefined || afterDash === '\n' || afterDash === '\r') {
2505
+ endIndex = newlineIndex + 1;
2506
+ break;
2507
+ }
2508
+ }
2509
+ searchStart = newlineIndex + 1;
2510
+ }
2511
+ if (endIndex === -1) {
2512
+ throw new Error('Invalid MDX: unclosed frontmatter (missing closing ---)');
2513
+ }
2514
+ // Extract frontmatter YAML (between the two --- markers)
2515
+ const frontmatter = normalized.slice(startIndex, endIndex).trim();
2516
+ // Extract body (everything after the closing ---)
2517
+ const bodyStart = endIndex + 3; // Skip past ---
2518
+ let body = normalized.slice(bodyStart);
2519
+ // Remove leading newline from body if present (but preserve rest of whitespace)
2520
+ if (body.startsWith('\n')) {
2521
+ body = body.slice(1);
2522
+ }
2523
+ // Parse YAML frontmatter
2524
+ let schema;
2525
+ if (frontmatter === '') {
2526
+ // Empty frontmatter
2527
+ schema = {};
2528
+ }
2529
+ else {
2530
+ try {
2531
+ schema = parseYAML(frontmatter);
2532
+ }
2533
+ catch (error) {
2534
+ if (error instanceof Error) {
2535
+ throw new Error(`Invalid YAML in frontmatter: ${error.message}`);
2536
+ }
2537
+ throw error;
2538
+ }
2539
+ }
2540
+ return { schema, body };
2541
+ }
2542
+ // ============================================================================
2543
+ // JSX UI Specification Parser
2544
+ // ============================================================================
2545
+ import { parse as babelParse } from '@babel/parser';
2546
+ function isJSXElement(node) {
2547
+ return node?.type === 'JSXElement';
2548
+ }
2549
+ function isObjectExpression(node) {
2550
+ return node?.type === 'ObjectExpression';
2551
+ }
2552
+ function isArrayExpression(node) {
2553
+ return node?.type === 'ArrayExpression';
2554
+ }
2555
+ function isStringLiteral(node) {
2556
+ return node?.type === 'StringLiteral';
2557
+ }
2558
+ function isNumericLiteral(node) {
2559
+ return node?.type === 'NumericLiteral';
2560
+ }
2561
+ function isBooleanLiteral(node) {
2562
+ return node?.type === 'BooleanLiteral';
2563
+ }
2564
+ function isNullLiteral(node) {
2565
+ return node?.type === 'NullLiteral';
2566
+ }
2567
+ function isIdentifier(node) {
2568
+ return node?.type === 'Identifier';
2569
+ }
2570
+ function isArrowFunctionExpression(node) {
2571
+ return node?.type === 'ArrowFunctionExpression';
2572
+ }
2573
+ function isFunctionExpression(node) {
2574
+ return node?.type === 'FunctionExpression';
2575
+ }
2576
+ function isObjectProperty(node) {
2577
+ return node?.type === 'ObjectProperty';
2578
+ }
2579
+ /**
2580
+ * Get the element name from a JSX element
2581
+ */
2582
+ function getElementName(element) {
2583
+ const name = element.openingElement.name;
2584
+ if (name.type === 'JSXIdentifier') {
2585
+ return name.name;
2586
+ }
2587
+ if (name.type === 'JSXMemberExpression') {
2588
+ // Handle Member.Expression style names
2589
+ const parts = [];
2590
+ let current = name;
2591
+ while (current.type === 'JSXMemberExpression') {
2592
+ parts.unshift(current.property.name);
2593
+ current = current.object;
2594
+ }
2595
+ if (current.type === 'JSXIdentifier') {
2596
+ parts.unshift(current.name);
2597
+ }
2598
+ return parts.join('.');
2599
+ }
2600
+ return '';
2601
+ }
2602
+ /**
2603
+ * Extract props from a JSX element
2604
+ */
2605
+ function extractProps(element, sourceCode) {
2606
+ const props = {};
2607
+ for (const attr of element.openingElement.attributes) {
2608
+ if (attr.type === 'JSXAttribute') {
2609
+ const name = attr.name.type === 'JSXIdentifier' ? attr.name.name : String(attr.name.name);
2610
+ const value = attr.value;
2611
+ if (value === null || value === undefined) {
2612
+ // Boolean prop: <Field searchable />
2613
+ props[name] = true;
2614
+ }
2615
+ else if (value.type === 'StringLiteral') {
2616
+ props[name] = value.value;
2617
+ }
2618
+ else if (value.type === 'JSXExpressionContainer') {
2619
+ props[name] = evaluateExpression(value.expression, sourceCode);
2620
+ }
2621
+ }
2622
+ else if (attr.type === 'JSXSpreadAttribute') {
2623
+ // Handle spread attributes if needed
2624
+ const spreadValue = evaluateExpression(attr.argument, sourceCode);
2625
+ if (typeof spreadValue === 'object' && spreadValue !== null) {
2626
+ Object.assign(props, spreadValue);
2627
+ }
2628
+ }
2629
+ }
2630
+ return props;
2631
+ }
2632
+ /**
2633
+ * Evaluate a JSX expression to a JavaScript value
2634
+ */
2635
+ function evaluateExpression(expr, sourceCode) {
2636
+ if (expr.type === 'JSXEmptyExpression') {
2637
+ return undefined;
2638
+ }
2639
+ if (isStringLiteral(expr)) {
2640
+ return expr.value;
2641
+ }
2642
+ if (isNumericLiteral(expr)) {
2643
+ return expr.value;
2644
+ }
2645
+ if (isBooleanLiteral(expr)) {
2646
+ return expr.value;
2647
+ }
2648
+ if (isNullLiteral(expr)) {
2649
+ return null;
2650
+ }
2651
+ if (isIdentifier(expr)) {
2652
+ // Handle special identifiers
2653
+ if (expr.name === 'undefined')
2654
+ return undefined;
2655
+ if (expr.name === 'true')
2656
+ return true;
2657
+ if (expr.name === 'false')
2658
+ return false;
2659
+ // Return identifier name as string (for field references)
2660
+ return expr.name;
2661
+ }
2662
+ if (isArrayExpression(expr)) {
2663
+ return expr.elements.map((el) => {
2664
+ if (el === null)
2665
+ return null;
2666
+ if (el.type === 'SpreadElement') {
2667
+ return evaluateExpression(el.argument, sourceCode);
2668
+ }
2669
+ return evaluateExpression(el, sourceCode);
2670
+ });
2671
+ }
2672
+ if (isObjectExpression(expr)) {
2673
+ const obj = {};
2674
+ for (const prop of expr.properties) {
2675
+ if (isObjectProperty(prop)) {
2676
+ const key = isIdentifier(prop.key)
2677
+ ? prop.key.name
2678
+ : isStringLiteral(prop.key)
2679
+ ? prop.key.value
2680
+ : String(prop.key);
2681
+ obj[key] = evaluateExpression(prop.value, sourceCode);
2682
+ }
2683
+ // ObjectMethod handling for hooks
2684
+ if (prop.type === 'ObjectMethod') {
2685
+ const key = isIdentifier(prop.key)
2686
+ ? prop.key.name
2687
+ : isStringLiteral(prop.key)
2688
+ ? prop.key.value
2689
+ : String(prop.key);
2690
+ // Preserve function as source code string
2691
+ if (prop.start !== null && prop.end !== null) {
2692
+ obj[key] = sourceCode.slice(prop.start, prop.end);
2693
+ }
2694
+ }
2695
+ }
2696
+ return obj;
2697
+ }
2698
+ if (isArrowFunctionExpression(expr) || isFunctionExpression(expr)) {
2699
+ // Preserve function source for hooks
2700
+ if (expr.start !== null && expr.end !== null) {
2701
+ return sourceCode.slice(expr.start, expr.end);
2702
+ }
2703
+ return '[Function]';
2704
+ }
2705
+ // For complex expressions, preserve as string
2706
+ if (expr.start !== null && expr.end !== null) {
2707
+ return sourceCode.slice(expr.start, expr.end);
2708
+ }
2709
+ return undefined;
2710
+ }
2711
+ /**
2712
+ * Get child elements of a JSX element, filtering out whitespace-only text
2713
+ */
2714
+ function getChildElements(element) {
2715
+ return element.children.filter((child) => {
2716
+ if (!isJSXElement(child))
2717
+ return false;
2718
+ return true;
2719
+ });
2720
+ }
2721
+ /**
2722
+ * Parse Field configuration from a JSX element
2723
+ */
2724
+ function parseFieldConfig(element, sourceCode) {
2725
+ const props = extractProps(element, sourceCode);
2726
+ const config = {
2727
+ name: String(props.name || ''),
2728
+ };
2729
+ if (typeof props.lines === 'number')
2730
+ config.lines = props.lines;
2731
+ if (typeof props.placeholder === 'string')
2732
+ config.placeholder = props.placeholder;
2733
+ if (typeof props.display === 'string')
2734
+ config.display = props.display;
2735
+ if (props.allowCreate === true)
2736
+ config.allowCreate = true;
2737
+ if (props.searchable === true)
2738
+ config.searchable = true;
2739
+ if (Array.isArray(props.preview))
2740
+ config.preview = props.preview;
2741
+ if (props.collapsed === true)
2742
+ config.collapsed = true;
2743
+ if (props.readOnly === true)
2744
+ config.readOnly = true;
2745
+ if (props.required === true)
2746
+ config.required = true;
2747
+ if (typeof props.min === 'number')
2748
+ config.min = props.min;
2749
+ if (typeof props.max === 'number')
2750
+ config.max = props.max;
2751
+ if (typeof props.span === 'number')
2752
+ config.span = props.span;
2753
+ if (props.autoFocus === true)
2754
+ config.autoFocus = true;
2755
+ return config;
2756
+ }
2757
+ /**
2758
+ * Parse Section configuration from a JSX element
2759
+ */
2760
+ function parseSectionConfig(element, sourceCode) {
2761
+ const props = extractProps(element, sourceCode);
2762
+ const config = {
2763
+ label: String(props.label || ''),
2764
+ };
2765
+ if (props.collapsed === true)
2766
+ config.collapsed = true;
2767
+ // Parse child fields and sections
2768
+ const children = [];
2769
+ for (const child of getChildElements(element)) {
2770
+ const childName = getElementName(child);
2771
+ if (childName === 'Field') {
2772
+ children.push(parseFieldConfig(child, sourceCode));
2773
+ }
2774
+ else if (childName === 'Section') {
2775
+ children.push(parseSectionConfig(child, sourceCode));
2776
+ }
2777
+ }
2778
+ if (children.length > 0) {
2779
+ config.children = children;
2780
+ }
2781
+ return config;
2782
+ }
2783
+ /**
2784
+ * Parse Form configuration from a JSX element
2785
+ */
2786
+ function parseFormConfig(element, sourceCode) {
2787
+ const props = extractProps(element, sourceCode);
2788
+ const config = {};
2789
+ if (typeof props.layout === 'string')
2790
+ config.layout = props.layout;
2791
+ if (Array.isArray(props.fields))
2792
+ config.fields = props.fields;
2793
+ // Parse child fields and sections
2794
+ const children = [];
2795
+ for (const child of getChildElements(element)) {
2796
+ const childName = getElementName(child);
2797
+ if (childName === 'Field') {
2798
+ children.push(parseFieldConfig(child, sourceCode));
2799
+ }
2800
+ else if (childName === 'Section') {
2801
+ children.push(parseSectionConfig(child, sourceCode));
2802
+ }
2803
+ }
2804
+ if (children.length > 0) {
2805
+ config.children = children;
2806
+ }
2807
+ return config;
2808
+ }
2809
+ /**
2810
+ * Parse Template configuration, preserving JSX for runtime
2811
+ */
2812
+ function parseTemplateConfig(element, sourceCode) {
2813
+ // Preserve the entire template JSX as a string
2814
+ if (element.start !== null && element.end !== null) {
2815
+ // Get just the children content
2816
+ const openingEnd = element.openingElement.end ?? element.start;
2817
+ const closingStart = element.closingElement?.start ?? element.end;
2818
+ const jsx = sourceCode.slice(openingEnd, closingStart).trim();
2819
+ return { jsx };
2820
+ }
2821
+ return { jsx: '' };
2822
+ }
2823
+ /**
2824
+ * Parse display component (Table, Grid, Cards)
2825
+ */
2826
+ function parseDisplayConfig(element, sourceCode) {
2827
+ const name = getElementName(element);
2828
+ const props = extractProps(element, sourceCode);
2829
+ const config = {
2830
+ type: name.toLowerCase(),
2831
+ };
2832
+ if (Array.isArray(props.columns))
2833
+ config.columns = props.columns;
2834
+ if (props.sortable === true)
2835
+ config.sortable = true;
2836
+ // Look for Template child
2837
+ for (const child of getChildElements(element)) {
2838
+ if (getElementName(child) === 'Template') {
2839
+ config.template = parseTemplateConfig(child, sourceCode);
2840
+ break;
2841
+ }
2842
+ }
2843
+ return config;
2844
+ }
2845
+ /**
2846
+ * Parse Search configuration
2847
+ */
2848
+ function parseSearchConfig(element, sourceCode) {
2849
+ const props = extractProps(element, sourceCode);
2850
+ const config = {};
2851
+ if (Array.isArray(props.fields))
2852
+ config.fields = props.fields;
2853
+ return config;
2854
+ }
2855
+ /**
2856
+ * Parse Filter configuration
2857
+ */
2858
+ function parseFilterConfig(element, sourceCode) {
2859
+ const props = extractProps(element, sourceCode);
2860
+ return {
2861
+ field: String(props.field || ''),
2862
+ exists: props.exists === true ? true : undefined,
2863
+ };
2864
+ }
2865
+ /**
2866
+ * Parse view configuration (List, Detail, Edit, Create)
2867
+ */
2868
+ function parseViewConfig(element, sourceCode) {
2869
+ const config = {};
2870
+ for (const child of getChildElements(element)) {
2871
+ const childName = getElementName(child);
2872
+ switch (childName) {
2873
+ case 'Search':
2874
+ config.search = parseSearchConfig(child, sourceCode);
2875
+ break;
2876
+ case 'Filters': {
2877
+ config.filters = [];
2878
+ for (const filterChild of getChildElements(child)) {
2879
+ if (getElementName(filterChild) === 'Filter') {
2880
+ config.filters.push(parseFilterConfig(filterChild, sourceCode));
2881
+ }
2882
+ }
2883
+ break;
2884
+ }
2885
+ case 'Table':
2886
+ case 'Grid':
2887
+ case 'Cards':
2888
+ config.display = parseDisplayConfig(child, sourceCode);
2889
+ break;
2890
+ case 'Form':
2891
+ config.form = parseFormConfig(child, sourceCode);
2892
+ break;
2893
+ case 'Template':
2894
+ config.template = parseTemplateConfig(child, sourceCode);
2895
+ break;
2896
+ case 'Editor': {
2897
+ const props = extractProps(child, sourceCode);
2898
+ config.editor = { language: String(props.language || 'yaml') };
2899
+ break;
2900
+ }
2901
+ }
2902
+ }
2903
+ return config;
2904
+ }
2905
+ /**
2906
+ * Parse Actions container
2907
+ */
2908
+ function parseActionsConfig(element, schemaTypes, sourceCode) {
2909
+ const actions = [];
2910
+ for (const child of getChildElements(element)) {
2911
+ const childName = getElementName(child);
2912
+ const props = extractProps(child, sourceCode);
2913
+ if (childName === 'Delete') {
2914
+ actions.push({
2915
+ type: 'delete',
2916
+ confirm: typeof props.confirm === 'string' ? props.confirm : undefined,
2917
+ });
2918
+ }
2919
+ else if (childName === 'Duplicate') {
2920
+ actions.push({ type: 'duplicate' });
2921
+ }
2922
+ else if (childName === 'Export') {
2923
+ actions.push({
2924
+ type: 'export',
2925
+ formats: Array.isArray(props.formats) ? props.formats : undefined,
2926
+ });
2927
+ }
2928
+ else if (childName === 'Import') {
2929
+ actions.push({ type: 'import' });
2930
+ }
2931
+ else if (schemaTypes.has(childName)) {
2932
+ // Entity reference = generate action
2933
+ actions.push({
2934
+ type: 'generate',
2935
+ entity: childName,
2936
+ context: Array.isArray(props.context) ? props.context : undefined,
2937
+ });
2938
+ }
2939
+ }
2940
+ return actions;
2941
+ }
2942
+ /**
2943
+ * Parse Permissions container
2944
+ */
2945
+ function parsePermissionsConfig(element, sourceCode) {
2946
+ const roles = [];
2947
+ for (const child of getChildElements(element)) {
2948
+ if (getElementName(child) === 'Role') {
2949
+ const props = extractProps(child, sourceCode);
2950
+ roles.push({
2951
+ name: String(props.name || ''),
2952
+ actions: Array.isArray(props.actions) ? props.actions : [],
2953
+ });
2954
+ }
2955
+ }
2956
+ return roles;
2957
+ }
2958
+ /**
2959
+ * Parse a collection element (schema type)
2960
+ */
2961
+ function parseCollectionConfig(element, schemaTypes, sourceCode) {
2962
+ const props = extractProps(element, sourceCode);
2963
+ const config = {};
2964
+ // Top-level collection props
2965
+ if (typeof props.view === 'string')
2966
+ config.view = props.view;
2967
+ if (Array.isArray(props.columns))
2968
+ config.columns = props.columns;
2969
+ if (props.searchable === true)
2970
+ config.searchable = true;
2971
+ if (typeof props.path === 'string')
2972
+ config.path = props.path;
2973
+ if (typeof props.slug === 'string')
2974
+ config.slug = props.slug;
2975
+ // Parse child views and configurations
2976
+ for (const child of getChildElements(element)) {
2977
+ const childName = getElementName(child);
2978
+ switch (childName) {
2979
+ case 'List':
2980
+ config.list = parseViewConfig(child, sourceCode);
2981
+ break;
2982
+ case 'Detail':
2983
+ config.detail = parseViewConfig(child, sourceCode);
2984
+ break;
2985
+ case 'Edit':
2986
+ config.edit = parseViewConfig(child, sourceCode);
2987
+ break;
2988
+ case 'Create':
2989
+ config.create = parseViewConfig(child, sourceCode);
2990
+ break;
2991
+ case 'Actions':
2992
+ config.actions = parseActionsConfig(child, schemaTypes, sourceCode);
2993
+ break;
2994
+ case 'Permissions':
2995
+ config.permissions = parsePermissionsConfig(child, sourceCode);
2996
+ break;
2997
+ }
2998
+ }
2999
+ return config;
3000
+ }
3001
+ /**
3002
+ * Parse navigation structure, distinguishing between groups and collections
3003
+ */
3004
+ function parseNavigationElement(element, schemaTypes, collections, sourceCode) {
3005
+ const name = getElementName(element);
3006
+ const props = extractProps(element, sourceCode);
3007
+ // If this is a schema type, it's a collection
3008
+ if (schemaTypes.has(name)) {
3009
+ // Parse and store collection config
3010
+ collections[name] = parseCollectionConfig(element, schemaTypes, sourceCode);
3011
+ return name;
3012
+ }
3013
+ // Otherwise it's a navigation group
3014
+ const group = {
3015
+ name,
3016
+ children: [],
3017
+ };
3018
+ if (typeof props.icon === 'string') {
3019
+ group.icon = props.icon;
3020
+ }
3021
+ // Parse children
3022
+ for (const child of getChildElements(element)) {
3023
+ const childResult = parseNavigationElement(child, schemaTypes, collections, sourceCode);
3024
+ group.children.push(childResult);
3025
+ }
3026
+ return group;
3027
+ }
3028
+ /**
3029
+ * Parse hooks from the App element's hooks prop
3030
+ */
3031
+ function parseHooksFromProp(hooksProp, sourceCode) {
3032
+ const hooks = {};
3033
+ if (typeof hooksProp !== 'object' || hooksProp === null) {
3034
+ return hooks;
3035
+ }
3036
+ for (const [typeName, typeHooks] of Object.entries(hooksProp)) {
3037
+ if (typeof typeHooks !== 'object' || typeHooks === null)
3038
+ continue;
3039
+ const entityHooks = {};
3040
+ const hooksObj = typeHooks;
3041
+ // Preserve hooks as function source strings
3042
+ if (hooksObj.onCreate !== undefined) {
3043
+ entityHooks.onCreate = hooksObj.onCreate;
3044
+ }
3045
+ if (hooksObj.onUpdate !== undefined) {
3046
+ entityHooks.onUpdate = hooksObj.onUpdate;
3047
+ }
3048
+ if (hooksObj.onDelete !== undefined) {
3049
+ entityHooks.onDelete = hooksObj.onDelete;
3050
+ }
3051
+ if (Object.keys(entityHooks).length > 0) {
3052
+ hooks[typeName] = entityHooks;
3053
+ }
3054
+ }
3055
+ return hooks;
3056
+ }
3057
+ /**
3058
+ * Parse JSX UI specification into UIConfig
3059
+ *
3060
+ * @param jsx - The JSX string to parse
3061
+ * @param schema - The parsed schema to determine which elements are collections
3062
+ * @returns Parsed UI configuration
3063
+ * @throws Error if JSX syntax is invalid or no App root element
3064
+ *
3065
+ * @example
3066
+ * const jsx = `
3067
+ * <App hooks={{ Startup: { onCreate: async () => {} } }}>
3068
+ * <Planning icon="lightbulb">
3069
+ * <Idea view="cards" />
3070
+ * </Planning>
3071
+ * </App>
3072
+ * `
3073
+ * const config = parseJSXUI(jsx, schema)
3074
+ * // config.hooks = { Startup: { onCreate: ... } }
3075
+ * // config.navigation = [{ name: 'Planning', icon: 'lightbulb', children: ['Idea'] }]
3076
+ * // config.collections = { Idea: { view: 'cards' } }
3077
+ */
3078
+ export function parseJSXUI(jsx, schema) {
3079
+ // Parse JSX using Babel
3080
+ let ast;
3081
+ try {
3082
+ ast = babelParse(jsx, {
3083
+ plugins: ['jsx'],
3084
+ sourceType: 'module',
3085
+ });
3086
+ }
3087
+ catch (error) {
3088
+ if (error instanceof Error) {
3089
+ throw new Error(`Invalid JSX syntax: ${error.message}`);
3090
+ }
3091
+ throw new Error('Invalid JSX syntax');
3092
+ }
3093
+ // Get schema type names for distinguishing collections from groups
3094
+ const schemaTypes = new Set(Object.keys(schema.types));
3095
+ // Find the root App element
3096
+ let appElement = null;
3097
+ // Walk the AST to find JSX elements
3098
+ function findApp(node) {
3099
+ if (isJSXElement(node)) {
3100
+ if (getElementName(node) === 'App') {
3101
+ appElement = node;
3102
+ return;
3103
+ }
3104
+ // Check children
3105
+ for (const child of node.children) {
3106
+ findApp(child);
3107
+ }
3108
+ }
3109
+ // Handle ExpressionStatement wrapping JSX
3110
+ if (node.type === 'ExpressionStatement' && isJSXElement(node.expression)) {
3111
+ findApp(node.expression);
3112
+ }
3113
+ // Handle Program body
3114
+ if (node.type === 'Program') {
3115
+ for (const stmt of node.body) {
3116
+ findApp(stmt);
3117
+ }
3118
+ }
3119
+ // Handle File
3120
+ if (node.type === 'File') {
3121
+ findApp(node.program);
3122
+ }
3123
+ }
3124
+ findApp(ast);
3125
+ if (!appElement) {
3126
+ throw new Error('No <App> root element found in JSX');
3127
+ }
3128
+ // Initialize result
3129
+ const result = {
3130
+ hooks: {},
3131
+ navigation: [],
3132
+ collections: {},
3133
+ };
3134
+ // Extract hooks from App props
3135
+ const appProps = extractProps(appElement, jsx);
3136
+ if (appProps.hooks) {
3137
+ result.hooks = parseHooksFromProp(appProps.hooks, jsx);
3138
+ }
3139
+ // Parse children of App as navigation/collections
3140
+ for (const child of getChildElements(appElement)) {
3141
+ const parsed = parseNavigationElement(child, schemaTypes, result.collections, jsx);
3142
+ if (typeof parsed === 'string') {
3143
+ // Top-level collection (unusual but possible)
3144
+ result.navigation.push({
3145
+ name: parsed,
3146
+ children: [],
3147
+ });
3148
+ }
3149
+ else {
3150
+ result.navigation.push(parsed);
3151
+ }
3152
+ }
3153
+ return result;
3154
+ }
3155
+ /**
3156
+ * Process an MDX file and return all parsed components.
3157
+ */
3158
+ export function processMDX(mdx) {
3159
+ const { schema, body } = parseMDX(mdx);
3160
+ const parsedSchema = parseSchema(schema);
3161
+ const uiConfig = parseJSXUI(body, parsedSchema);
3162
+ return { schema, parsedSchema, uiConfig, rawBody: body };
3163
+ }
3164
+ //# sourceMappingURL=index.js.map