ai-database 2.0.2 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/actions.d.ts +247 -0
  3. package/dist/actions.d.ts.map +1 -0
  4. package/dist/actions.js +260 -0
  5. package/dist/actions.js.map +1 -0
  6. package/dist/ai-promise-db.d.ts +34 -2
  7. package/dist/ai-promise-db.d.ts.map +1 -1
  8. package/dist/ai-promise-db.js +511 -66
  9. package/dist/ai-promise-db.js.map +1 -1
  10. package/dist/constants.d.ts +16 -0
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/constants.js +16 -0
  13. package/dist/constants.js.map +1 -0
  14. package/dist/events.d.ts +153 -0
  15. package/dist/events.d.ts.map +1 -0
  16. package/dist/events.js +154 -0
  17. package/dist/events.js.map +1 -0
  18. package/dist/index.d.ts +8 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +13 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/memory-provider.d.ts +144 -2
  23. package/dist/memory-provider.d.ts.map +1 -1
  24. package/dist/memory-provider.js +569 -13
  25. package/dist/memory-provider.js.map +1 -1
  26. package/dist/schema/cascade.d.ts +96 -0
  27. package/dist/schema/cascade.d.ts.map +1 -0
  28. package/dist/schema/cascade.js +528 -0
  29. package/dist/schema/cascade.js.map +1 -0
  30. package/dist/schema/index.d.ts +197 -0
  31. package/dist/schema/index.d.ts.map +1 -0
  32. package/dist/schema/index.js +1211 -0
  33. package/dist/schema/index.js.map +1 -0
  34. package/dist/schema/parse.d.ts +225 -0
  35. package/dist/schema/parse.d.ts.map +1 -0
  36. package/dist/schema/parse.js +732 -0
  37. package/dist/schema/parse.js.map +1 -0
  38. package/dist/schema/provider.d.ts +176 -0
  39. package/dist/schema/provider.d.ts.map +1 -0
  40. package/dist/schema/provider.js +258 -0
  41. package/dist/schema/provider.js.map +1 -0
  42. package/dist/schema/resolve.d.ts +87 -0
  43. package/dist/schema/resolve.d.ts.map +1 -0
  44. package/dist/schema/resolve.js +474 -0
  45. package/dist/schema/resolve.js.map +1 -0
  46. package/dist/schema/semantic.d.ts +53 -0
  47. package/dist/schema/semantic.d.ts.map +1 -0
  48. package/dist/schema/semantic.js +247 -0
  49. package/dist/schema/semantic.js.map +1 -0
  50. package/dist/schema/types.d.ts +528 -0
  51. package/dist/schema/types.d.ts.map +1 -0
  52. package/dist/schema/types.js +9 -0
  53. package/dist/schema/types.js.map +1 -0
  54. package/dist/schema.d.ts +24 -867
  55. package/dist/schema.d.ts.map +1 -1
  56. package/dist/schema.js +41 -1124
  57. package/dist/schema.js.map +1 -1
  58. package/dist/semantic.d.ts +175 -0
  59. package/dist/semantic.d.ts.map +1 -0
  60. package/dist/semantic.js +338 -0
  61. package/dist/semantic.js.map +1 -0
  62. package/dist/types.d.ts +14 -0
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/types.js.map +1 -1
  65. package/package.json +13 -4
  66. package/.turbo/turbo-build.log +0 -5
  67. package/TESTING.md +0 -410
  68. package/TEST_SUMMARY.md +0 -250
  69. package/TODO.md +0 -128
  70. package/src/ai-promise-db.ts +0 -1243
  71. package/src/authorization.ts +0 -1102
  72. package/src/durable-clickhouse.ts +0 -596
  73. package/src/durable-promise.ts +0 -582
  74. package/src/execution-queue.ts +0 -608
  75. package/src/index.test.ts +0 -868
  76. package/src/index.ts +0 -337
  77. package/src/linguistic.ts +0 -404
  78. package/src/memory-provider.test.ts +0 -1036
  79. package/src/memory-provider.ts +0 -1119
  80. package/src/schema.test.ts +0 -1254
  81. package/src/schema.ts +0 -2296
  82. package/src/tests.ts +0 -725
  83. package/src/types.ts +0 -1177
  84. package/test/README.md +0 -153
  85. package/test/edge-cases.test.ts +0 -646
  86. package/test/provider-resolution.test.ts +0 -402
  87. package/tsconfig.json +0 -9
  88. package/vitest.config.ts +0 -19
@@ -0,0 +1,732 @@
1
+ /**
2
+ * Schema Parsing Functions
3
+ *
4
+ * Contains parseOperator, parseField, parseSchema, and related parsing utilities.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+ // =============================================================================
9
+ // Schema Validation Error
10
+ // =============================================================================
11
+ /**
12
+ * Custom error class for schema validation errors
13
+ */
14
+ export class SchemaValidationError extends Error {
15
+ /** Error code for programmatic handling */
16
+ code;
17
+ /** Path to the problematic element (e.g., 'User.name') */
18
+ path;
19
+ /** Additional details about the error */
20
+ details;
21
+ constructor(message, code, path, details) {
22
+ super(message);
23
+ this.name = 'SchemaValidationError';
24
+ this.code = code;
25
+ this.path = path;
26
+ this.details = details;
27
+ }
28
+ }
29
+ // =============================================================================
30
+ // Validation Constants
31
+ // =============================================================================
32
+ /** Valid primitive types */
33
+ const VALID_PRIMITIVE_TYPES = [
34
+ 'string',
35
+ 'number',
36
+ 'boolean',
37
+ 'date',
38
+ 'datetime',
39
+ 'json',
40
+ 'markdown',
41
+ 'url',
42
+ ];
43
+ /** Maximum entity name length */
44
+ const MAX_ENTITY_NAME_LENGTH = 64;
45
+ /** Maximum field name length */
46
+ const MAX_FIELD_NAME_LENGTH = 64;
47
+ /** Pattern for valid entity names: starts with letter, followed by letters/numbers/underscores */
48
+ const VALID_ENTITY_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
49
+ /** Pattern for valid field names: starts with letter or underscore, followed by letters/numbers/underscores */
50
+ const VALID_FIELD_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
51
+ // =============================================================================
52
+ // Validation Functions
53
+ // =============================================================================
54
+ /**
55
+ * Validate an entity name
56
+ *
57
+ * @param name - The entity name to validate
58
+ * @throws SchemaValidationError if the name is invalid
59
+ */
60
+ export function validateEntityName(name) {
61
+ // Check for empty name
62
+ if (!name || name.trim().length === 0) {
63
+ throw new SchemaValidationError(`Invalid entity name: name cannot be empty. Entity names must start with a letter and contain only letters, numbers, and underscores.`, 'INVALID_ENTITY_NAME', name);
64
+ }
65
+ // Check length
66
+ if (name.length > MAX_ENTITY_NAME_LENGTH) {
67
+ throw new SchemaValidationError(`Invalid entity name '${name}': name exceeds maximum length of ${MAX_ENTITY_NAME_LENGTH} characters. Entity names must start with a letter and contain only letters, numbers, and underscores.`, 'INVALID_ENTITY_NAME', name);
68
+ }
69
+ // Check for SQL injection patterns (semicolons, comments, keywords)
70
+ if (/[;'"`]|--|\bDROP\b|\bSELECT\b|\bUNION\b|\bINSERT\b|\bDELETE\b|\bUPDATE\b/i.test(name)) {
71
+ throw new SchemaValidationError(`Invalid entity name '${name}': contains potentially dangerous characters or SQL keywords. Entity names must start with a letter and contain only letters, numbers, and underscores.`, 'INVALID_ENTITY_NAME', name);
72
+ }
73
+ // Check for XSS patterns (script tags, HTML, JavaScript protocol)
74
+ if (/<[^>]*>|javascript:|onerror|onclick|onload/i.test(name)) {
75
+ throw new SchemaValidationError(`Invalid entity name '${name}': contains potentially dangerous HTML or JavaScript. Entity names must start with a letter and contain only letters, numbers, and underscores.`, 'INVALID_ENTITY_NAME', name);
76
+ }
77
+ // Check for angle brackets specifically
78
+ if (/<|>/.test(name)) {
79
+ throw new SchemaValidationError(`Invalid entity name '${name}': contains special characters (< or >). Entity names must start with a letter and contain only letters, numbers, and underscores.`, 'INVALID_ENTITY_NAME', name);
80
+ }
81
+ // Check for spaces
82
+ if (/\s/.test(name)) {
83
+ throw new SchemaValidationError(`Invalid entity name '${name}': contains spaces. Entity names must start with a letter and contain only letters, numbers, and underscores.`, 'INVALID_ENTITY_NAME', name);
84
+ }
85
+ // Check the pattern (alphanumeric + underscores, must start with letter)
86
+ if (!VALID_ENTITY_NAME_PATTERN.test(name)) {
87
+ throw new SchemaValidationError(`Invalid entity name '${name}': must start with a letter and contain only letters, numbers, and underscores.`, 'INVALID_ENTITY_NAME', name);
88
+ }
89
+ }
90
+ /**
91
+ * Validate a field name
92
+ *
93
+ * @param name - The field name to validate
94
+ * @param entityName - The entity the field belongs to (for error messages)
95
+ * @throws SchemaValidationError if the name is invalid
96
+ */
97
+ export function validateFieldName(name, entityName) {
98
+ const path = `${entityName}.${name}`;
99
+ // Check for empty name
100
+ if (!name || name.trim().length === 0) {
101
+ throw new SchemaValidationError(`Invalid field name in '${entityName}': field name cannot be empty.`, 'INVALID_FIELD_NAME', path);
102
+ }
103
+ // Check length
104
+ if (name.length > MAX_FIELD_NAME_LENGTH) {
105
+ throw new SchemaValidationError(`Invalid field name '${name}' in '${entityName}': name exceeds maximum length of ${MAX_FIELD_NAME_LENGTH} characters.`, 'INVALID_FIELD_NAME', path);
106
+ }
107
+ // Check for SQL injection patterns
108
+ if (/[;'"`]|--|\bDROP\b|\bSELECT\b|\bUNION\b|\bINSERT\b|\bDELETE\b|\bUPDATE\b/i.test(name)) {
109
+ throw new SchemaValidationError(`Invalid field name '${name}' in '${entityName}': contains potentially dangerous characters or SQL keywords.`, 'INVALID_FIELD_NAME', path);
110
+ }
111
+ // Check for special characters (including @)
112
+ if (!VALID_FIELD_NAME_PATTERN.test(name)) {
113
+ throw new SchemaValidationError(`Invalid field name '${name}' in '${entityName}': must start with a letter or underscore and contain only letters, numbers, and underscores.`, 'INVALID_FIELD_NAME', path);
114
+ }
115
+ }
116
+ /**
117
+ * Validate a field type
118
+ *
119
+ * @param typeDef - The field type definition to validate
120
+ * @param fieldName - The field name (for error messages)
121
+ * @param entityName - The entity name (for error messages)
122
+ * @throws SchemaValidationError if the type is invalid
123
+ */
124
+ export function validateFieldType(typeDef, fieldName, entityName) {
125
+ const path = `${entityName}.${fieldName}`;
126
+ // Check for empty type
127
+ if (!typeDef || typeDef.trim().length === 0) {
128
+ throw new SchemaValidationError(`Invalid field type for '${path}': type cannot be empty. Valid types are: ${VALID_PRIMITIVE_TYPES.join(', ')}, or a PascalCase entity reference.`, 'INVALID_FIELD_TYPE', path);
129
+ }
130
+ // Strip modifiers to get the base type
131
+ let baseType = typeDef.trim();
132
+ // Check for double optional (string??)
133
+ if (baseType.includes('??')) {
134
+ throw new SchemaValidationError(`Invalid field type '${typeDef}' for '${path}': double optional modifier (??) is not allowed.`, 'INVALID_FIELD_TYPE', path);
135
+ }
136
+ // Remove optional modifier
137
+ if (baseType.endsWith('?')) {
138
+ baseType = baseType.slice(0, -1);
139
+ }
140
+ // Handle array suffix notation (e.g., string[]?)
141
+ if (baseType.endsWith('[]?')) {
142
+ baseType = baseType.slice(0, -3);
143
+ }
144
+ if (baseType.endsWith('[]')) {
145
+ baseType = baseType.slice(0, -2);
146
+ }
147
+ // If it's an operator-based definition, we validate the target type separately
148
+ if (/^(->|~>|<-|<~)/.test(baseType) || baseType.includes('->') || baseType.includes('~>') || baseType.includes('<-') || baseType.includes('<~')) {
149
+ // This will be validated in parseOperator
150
+ return;
151
+ }
152
+ // Handle backref syntax (Type.field)
153
+ if (baseType.includes('.')) {
154
+ const parts = baseType.split('.');
155
+ baseType = parts[0];
156
+ // Validate the backref field name if there are multiple dots
157
+ if (parts.length > 2) {
158
+ throw new SchemaValidationError(`Invalid field type '${typeDef}' for '${path}': multiple dots in backref syntax are not allowed.`, 'INVALID_FIELD_TYPE', path);
159
+ }
160
+ // Validate the backref field name
161
+ const backrefName = parts[1];
162
+ if (!VALID_FIELD_NAME_PATTERN.test(backrefName)) {
163
+ throw new SchemaValidationError(`Invalid backref field name '${backrefName}' in '${typeDef}' for '${path}': must start with a letter or underscore.`, 'INVALID_FIELD_TYPE', path);
164
+ }
165
+ }
166
+ // Check for invalid SQL types
167
+ const sqlTypes = ['int', 'varchar', 'text', 'blob', 'integer', 'real', 'float', 'double'];
168
+ if (sqlTypes.includes(baseType.toLowerCase())) {
169
+ const suggestion = baseType.toLowerCase() === 'int' || baseType.toLowerCase() === 'integer' ? 'number' :
170
+ baseType.toLowerCase() === 'text' || baseType.toLowerCase() === 'varchar' ? 'string' :
171
+ baseType.toLowerCase() === 'real' || baseType.toLowerCase() === 'float' || baseType.toLowerCase() === 'double' ? 'number' : 'string';
172
+ throw new SchemaValidationError(`Invalid field type '${baseType}' for '${path}': SQL types are not supported. Did you mean '${suggestion}'? Valid types are: ${VALID_PRIMITIVE_TYPES.join(', ')}.`, 'INVALID_FIELD_TYPE', path);
173
+ }
174
+ // Check for invalid JavaScript types
175
+ const jsTypes = ['object', 'array', 'function', 'symbol', 'bigint', 'undefined', 'null'];
176
+ if (jsTypes.includes(baseType.toLowerCase())) {
177
+ throw new SchemaValidationError(`Invalid field type '${baseType}' for '${path}': JavaScript types are not supported. Valid types are: ${VALID_PRIMITIVE_TYPES.join(', ')}.`, 'INVALID_FIELD_TYPE', path);
178
+ }
179
+ // Check if it's a valid primitive or PascalCase entity reference
180
+ const isPrimitive = VALID_PRIMITIVE_TYPES.includes(baseType);
181
+ const isPascalCase = /^[A-Z][A-Za-z0-9_]*$/.test(baseType);
182
+ if (!isPrimitive && !isPascalCase) {
183
+ throw new SchemaValidationError(`Invalid field type '${baseType}' for '${path}': unknown type. Valid types are: ${VALID_PRIMITIVE_TYPES.join(', ')}, or a PascalCase entity reference.`, 'INVALID_FIELD_TYPE', path);
184
+ }
185
+ }
186
+ /**
187
+ * Validate array field definition
188
+ *
189
+ * @param definition - The array field definition
190
+ * @param fieldName - The field name (for error messages)
191
+ * @param entityName - The entity name (for error messages)
192
+ * @throws SchemaValidationError if the array syntax is invalid
193
+ */
194
+ export function validateArrayDefinition(definition, fieldName, entityName) {
195
+ const path = `${entityName}.${fieldName}`;
196
+ // Check for empty array
197
+ if (definition.length === 0) {
198
+ throw new SchemaValidationError(`Invalid array field definition for '${path}': empty array syntax is not allowed. Use ['Type'] for array of Type.`, 'INVALID_FIELD_TYPE', path);
199
+ }
200
+ // Check for multiple elements
201
+ if (definition.length > 1) {
202
+ throw new SchemaValidationError(`Invalid array field definition for '${path}': array syntax only supports single element. Use ['Type'] not ['Type1', 'Type2'].`, 'INVALID_FIELD_TYPE', path);
203
+ }
204
+ // Check for nested arrays
205
+ if (Array.isArray(definition[0])) {
206
+ throw new SchemaValidationError(`Invalid array field definition for '${path}': nested array syntax is not allowed. Use ['Type'] not [['Type']].`, 'INVALID_FIELD_TYPE', path);
207
+ }
208
+ // Check that the inner element is a string
209
+ if (typeof definition[0] !== 'string') {
210
+ throw new SchemaValidationError(`Invalid array field definition for '${path}': array element must be a string type definition.`, 'INVALID_FIELD_TYPE', path);
211
+ }
212
+ }
213
+ /**
214
+ * Validate operator target type
215
+ *
216
+ * @param targetType - The target type from the operator
217
+ * @param operator - The operator used
218
+ * @param fieldName - The field name (for error messages)
219
+ * @throws SchemaValidationError if the target type is invalid
220
+ */
221
+ export function validateOperatorTarget(targetType, operator, fieldName) {
222
+ // Check for empty target type
223
+ if (!targetType || targetType.trim().length === 0) {
224
+ throw new SchemaValidationError(`Invalid operator '${operator}' for field '${fieldName}': missing target type. Use '${operator}Type' syntax.`, 'INVALID_OPERATOR', fieldName);
225
+ }
226
+ // Strip modifiers for validation
227
+ let baseType = targetType.trim();
228
+ if (baseType.endsWith('?'))
229
+ baseType = baseType.slice(0, -1);
230
+ if (baseType.endsWith('[]'))
231
+ baseType = baseType.slice(0, -2);
232
+ // Handle threshold syntax (Type(0.8) or malformed Type(0.8)
233
+ // Match either complete threshold (Type(0.8)) or incomplete (Type(0.8)
234
+ const incompleteThresholdMatch = baseType.match(/^([A-Za-z][A-Za-z0-9_]*)\([^)]*$/);
235
+ if (incompleteThresholdMatch) {
236
+ // Unclosed parenthesis - this is a malformed threshold
237
+ // The test expects this to parse without error but not extract threshold
238
+ // So we strip the malformed part and use just the type name
239
+ baseType = incompleteThresholdMatch[1];
240
+ }
241
+ else {
242
+ const fullThresholdMatch = baseType.match(/^([^(]+)\(([^)]+)\)$/);
243
+ if (fullThresholdMatch) {
244
+ baseType = fullThresholdMatch[1];
245
+ }
246
+ }
247
+ // Handle backref syntax
248
+ if (baseType.includes('.')) {
249
+ const parts = baseType.split('.');
250
+ const [typePart, backrefPart] = parts;
251
+ baseType = typePart;
252
+ // Validate backref name
253
+ if (backrefPart && !VALID_FIELD_NAME_PATTERN.test(backrefPart)) {
254
+ throw new SchemaValidationError(`Invalid backref name '${backrefPart}' in operator target '${targetType}' for field '${fieldName}': must start with a letter or underscore.`, 'INVALID_OPERATOR', fieldName);
255
+ }
256
+ // Check for multiple dots
257
+ if (parts.length > 2) {
258
+ throw new SchemaValidationError(`Invalid operator target '${targetType}' for field '${fieldName}': multiple dots in backref syntax are not allowed.`, 'INVALID_OPERATOR', fieldName);
259
+ }
260
+ }
261
+ // Handle union types
262
+ if (baseType.includes('|')) {
263
+ const unionTypes = baseType.split('|').map(t => t.trim());
264
+ // Check for empty union members
265
+ if (unionTypes.some(t => !t)) {
266
+ throw new SchemaValidationError(`Invalid union type '${targetType}' for field '${fieldName}': empty union members are not allowed.`, 'INVALID_OPERATOR', fieldName);
267
+ }
268
+ // Validate each union type
269
+ for (const unionType of unionTypes) {
270
+ if (!/^[A-Z][A-Za-z0-9_]*$/.test(unionType)) {
271
+ throw new SchemaValidationError(`Invalid union type '${unionType}' in '${targetType}' for field '${fieldName}': type names must be PascalCase.`, 'INVALID_OPERATOR', fieldName);
272
+ }
273
+ // Check for SQL injection in union types
274
+ if (/[;'"`]|--|\bDROP\b|\bSELECT\b|\bUNION\b/i.test(unionType)) {
275
+ throw new SchemaValidationError(`Invalid union type '${unionType}' in '${targetType}' for field '${fieldName}': contains potentially dangerous characters.`, 'INVALID_OPERATOR', fieldName);
276
+ }
277
+ }
278
+ }
279
+ else {
280
+ // Single type validation
281
+ // Check for SQL injection
282
+ if (/[;'"`]|--|\bDROP\b|\bSELECT\b|\bUNION\b/i.test(baseType)) {
283
+ throw new SchemaValidationError(`Invalid operator target '${targetType}' for field '${fieldName}': contains potentially dangerous characters or SQL keywords.`, 'INVALID_OPERATOR', fieldName);
284
+ }
285
+ // Check for XSS
286
+ if (/<[^>]*>|javascript:|onerror|onclick/i.test(baseType)) {
287
+ throw new SchemaValidationError(`Invalid operator target '${targetType}' for field '${fieldName}': contains potentially dangerous HTML or JavaScript.`, 'INVALID_OPERATOR', fieldName);
288
+ }
289
+ // Validate PascalCase
290
+ if (baseType && !/^[A-Z][A-Za-z0-9_]*$/.test(baseType)) {
291
+ throw new SchemaValidationError(`Invalid operator target '${targetType}' for field '${fieldName}': type names must be PascalCase.`, 'INVALID_OPERATOR', fieldName);
292
+ }
293
+ }
294
+ }
295
+ /**
296
+ * Validate operator syntax
297
+ *
298
+ * @param definition - The field definition containing the operator
299
+ * @param fieldName - The field name (for error messages)
300
+ * @throws SchemaValidationError if the operator syntax is invalid
301
+ */
302
+ export function validateOperatorSyntax(definition, fieldName) {
303
+ // Check for invalid operator combinations
304
+ if (/<>|><|~~>|-->>|<~~/.test(definition)) {
305
+ throw new SchemaValidationError(`Invalid operator in field '${fieldName}': '${definition}' contains invalid operator syntax. Valid operators are: ->, ~>, <-, <~.`, 'INVALID_OPERATOR', fieldName);
306
+ }
307
+ }
308
+ // =============================================================================
309
+ // Operator Parsing
310
+ // =============================================================================
311
+ /**
312
+ * Parse relationship operator from field definition
313
+ *
314
+ * Extracts operator semantics from a field definition string. Supports
315
+ * four relationship operators with different semantics:
316
+ *
317
+ * ## Operators
318
+ *
319
+ * | Operator | Direction | Match Mode | Description |
320
+ * |----------|-----------|------------|-------------|
321
+ * | `->` | forward | exact | Strict foreign key reference |
322
+ * | `~>` | forward | fuzzy | AI-matched semantic reference |
323
+ * | `<-` | backward | exact | Strict backlink reference |
324
+ * | `<~` | backward | fuzzy | AI-matched backlink reference |
325
+ *
326
+ * ## Supported Formats
327
+ *
328
+ * - `'->Type'` - Forward exact reference to Type
329
+ * - `'~>Type'` - Forward fuzzy (semantic search) to Type
330
+ * - `'<-Type'` - Backward exact reference from Type
331
+ * - `'<~Type'` - Backward fuzzy reference from Type
332
+ * - `'Prompt text ->Type'` - With generation prompt (text before operator)
333
+ * - `'->TypeA|TypeB'` - Union types (polymorphic reference)
334
+ * - `'->Type.backref'` - With explicit backref field name
335
+ * - `'->Type?'` - Optional reference
336
+ * - `'->Type[]'` - Array of references
337
+ *
338
+ * @param definition - The field definition string to parse
339
+ * @returns Parsed operator result, or null if no operator found
340
+ *
341
+ * @example Basic usage
342
+ * ```ts
343
+ * parseOperator('->Author')
344
+ * // => { operator: '->', direction: 'forward', matchMode: 'exact', targetType: 'Author' }
345
+ *
346
+ * parseOperator('~>Category')
347
+ * // => { operator: '~>', direction: 'forward', matchMode: 'fuzzy', targetType: 'Category' }
348
+ *
349
+ * parseOperator('<-Post')
350
+ * // => { operator: '<-', direction: 'backward', matchMode: 'exact', targetType: 'Post' }
351
+ * ```
352
+ *
353
+ * @example With prompt
354
+ * ```ts
355
+ * parseOperator('What is the main category? ~>Category')
356
+ * // => {
357
+ * // prompt: 'What is the main category?',
358
+ * // operator: '~>',
359
+ * // direction: 'forward',
360
+ * // matchMode: 'fuzzy',
361
+ * // targetType: 'Category'
362
+ * // }
363
+ * ```
364
+ *
365
+ * @example Union types
366
+ * ```ts
367
+ * parseOperator('->Person|Company|Organization')
368
+ * // => {
369
+ * // operator: '->',
370
+ * // direction: 'forward',
371
+ * // matchMode: 'exact',
372
+ * // targetType: 'Person',
373
+ * // unionTypes: ['Person', 'Company', 'Organization']
374
+ * // }
375
+ * ```
376
+ */
377
+ export function parseOperator(definition) {
378
+ // Supported operators in order of specificity (longer operators first)
379
+ const operators = ['~>', '<~', '->', '<-'];
380
+ for (const op of operators) {
381
+ const opIndex = definition.indexOf(op);
382
+ if (opIndex !== -1) {
383
+ // Extract prompt (text before operator)
384
+ const beforeOp = definition.slice(0, opIndex).trim();
385
+ const prompt = beforeOp || undefined;
386
+ // Extract target type (text after operator)
387
+ let targetType = definition.slice(opIndex + op.length).trim();
388
+ // Determine direction: < = backward, otherwise forward
389
+ const direction = op.startsWith('<') ? 'backward' : 'forward';
390
+ // Determine match mode: ~ = fuzzy, otherwise exact
391
+ const matchMode = op.includes('~') ? 'fuzzy' : 'exact';
392
+ // Parse field-level threshold from ~>Type(0.9) syntax
393
+ let threshold;
394
+ const thresholdMatch = targetType.match(/^([^(]+)\(([0-9.]+)\)(.*)$/);
395
+ if (thresholdMatch) {
396
+ const [, typePart, thresholdStr, suffix] = thresholdMatch;
397
+ threshold = parseFloat(thresholdStr);
398
+ if (!isNaN(threshold) && threshold >= 0 && threshold <= 1) {
399
+ // Reconstruct targetType without the threshold
400
+ targetType = (typePart || '') + (suffix || '');
401
+ }
402
+ else {
403
+ threshold = undefined;
404
+ }
405
+ }
406
+ else {
407
+ // Handle malformed threshold syntax (missing closing paren)
408
+ const malformedThresholdMatch = targetType.match(/^([A-Za-z][A-Za-z0-9_]*)\([^)]*$/);
409
+ if (malformedThresholdMatch) {
410
+ // Strip the malformed threshold part, keep just the type name
411
+ targetType = malformedThresholdMatch[1];
412
+ // threshold stays undefined
413
+ }
414
+ }
415
+ // Parse union types (A|B|C syntax)
416
+ // First, strip off any modifiers (?, [], .backref) to get clean types
417
+ let cleanType = targetType;
418
+ // Remove optional modifier for union parsing
419
+ if (cleanType.endsWith('?')) {
420
+ cleanType = cleanType.slice(0, -1);
421
+ }
422
+ // Remove array modifier for union parsing
423
+ if (cleanType.endsWith('[]')) {
424
+ cleanType = cleanType.slice(0, -2);
425
+ }
426
+ // Remove backref for union parsing (take only part before dot)
427
+ const dotIndex = cleanType.indexOf('.');
428
+ if (dotIndex !== -1) {
429
+ cleanType = cleanType.slice(0, dotIndex);
430
+ }
431
+ // Check for union types
432
+ let unionTypes;
433
+ if (cleanType.includes('|')) {
434
+ unionTypes = cleanType.split('|').map(t => t.trim()).filter(Boolean);
435
+ // The primary targetType is the first union type
436
+ // But we keep targetType as the full string for backward compatibility
437
+ // with modifier parsing in parseField
438
+ }
439
+ return {
440
+ prompt,
441
+ operator: op,
442
+ direction,
443
+ matchMode,
444
+ targetType,
445
+ unionTypes,
446
+ threshold,
447
+ };
448
+ }
449
+ }
450
+ return null;
451
+ }
452
+ // =============================================================================
453
+ // Field Parsing
454
+ // =============================================================================
455
+ /**
456
+ * Check if a type string represents a primitive database type
457
+ *
458
+ * Primitive types are the basic scalar types that don't represent
459
+ * relationships to other entities.
460
+ *
461
+ * @param type - The type string to check
462
+ * @returns True if the type is a primitive (string, number, boolean, date, datetime, json, markdown, url)
463
+ *
464
+ * @example
465
+ * ```ts
466
+ * isPrimitiveType('string') // => true
467
+ * isPrimitiveType('Author') // => false (entity reference)
468
+ * isPrimitiveType('markdown') // => true
469
+ * ```
470
+ */
471
+ export function isPrimitiveType(type) {
472
+ const primitives = [
473
+ 'string',
474
+ 'number',
475
+ 'boolean',
476
+ 'date',
477
+ 'datetime',
478
+ 'json',
479
+ 'markdown',
480
+ 'url',
481
+ ];
482
+ return primitives.includes(type);
483
+ }
484
+ /**
485
+ * Parse a single field definition into a structured ParsedField object
486
+ *
487
+ * Converts a field definition string into a structured ParsedField object,
488
+ * handling primitives, relations, arrays, optionals, and operator syntax.
489
+ *
490
+ * ## Processing Order
491
+ *
492
+ * 1. Handle array literal syntax `['Type']`
493
+ * 2. Extract operators (`->`, `~>`, `<-`, `<~`) using parseOperator
494
+ * 3. Parse optional modifier (`?`)
495
+ * 4. Parse array modifier (`[]`)
496
+ * 5. Parse backref syntax (`Type.field`)
497
+ * 6. Detect PascalCase relations
498
+ *
499
+ * @param name - The field name
500
+ * @param definition - The field definition (string or array literal)
501
+ * @returns Parsed field information including type, modifiers, and relation metadata
502
+ *
503
+ * @example Primitive field
504
+ * ```ts
505
+ * parseField('title', 'string')
506
+ * // => { name: 'title', type: 'string', isArray: false, isOptional: false, isRelation: false }
507
+ * ```
508
+ *
509
+ * @example Relation with backref
510
+ * ```ts
511
+ * parseField('author', 'Author.posts')
512
+ * // => { name: 'author', type: 'Author', isRelation: true, relatedType: 'Author', backref: 'posts' }
513
+ * ```
514
+ *
515
+ * @example Forward fuzzy relation
516
+ * ```ts
517
+ * parseField('category', '~>Category')
518
+ * // => { name: 'category', operator: '~>', matchMode: 'fuzzy', direction: 'forward', ... }
519
+ * ```
520
+ */
521
+ export function parseField(name, definition) {
522
+ // Handle array literal syntax: ['Author.posts']
523
+ if (Array.isArray(definition)) {
524
+ const inner = parseField(name, definition[0]);
525
+ return { ...inner, isArray: true };
526
+ }
527
+ let type = definition;
528
+ // Validate operator syntax first (check for invalid operators)
529
+ validateOperatorSyntax(type, name);
530
+ let isArray = false;
531
+ let isOptional = false;
532
+ let isRelation = false;
533
+ let relatedType;
534
+ let backref;
535
+ let operator;
536
+ let direction;
537
+ let matchMode;
538
+ let prompt;
539
+ let unionTypes;
540
+ // Use the dedicated operator parser
541
+ const operatorResult = parseOperator(type);
542
+ if (operatorResult && operatorResult.operator) {
543
+ // Validate the operator target type
544
+ validateOperatorTarget(operatorResult.targetType, operatorResult.operator, name);
545
+ operator = operatorResult.operator;
546
+ direction = operatorResult.direction;
547
+ matchMode = operatorResult.matchMode;
548
+ prompt = operatorResult.prompt;
549
+ type = operatorResult.targetType;
550
+ unionTypes = operatorResult.unionTypes;
551
+ }
552
+ // Check for optional modifier
553
+ if (type.endsWith('?')) {
554
+ isOptional = true;
555
+ type = type.slice(0, -1);
556
+ }
557
+ // Check for array modifier (string syntax)
558
+ if (type.endsWith('[]')) {
559
+ isArray = true;
560
+ type = type.slice(0, -2);
561
+ }
562
+ // Check for relation (contains a dot for backref)
563
+ if (type.includes('.')) {
564
+ isRelation = true;
565
+ const [entityName, backrefName] = type.split('.');
566
+ relatedType = entityName;
567
+ backref = backrefName;
568
+ type = entityName;
569
+ }
570
+ else if (type[0] === type[0]?.toUpperCase() &&
571
+ !isPrimitiveType(type) &&
572
+ !type.includes(' ') // Type names don't have spaces - strings with spaces are prompts/descriptions
573
+ ) {
574
+ // PascalCase non-primitive = relation without explicit backref
575
+ isRelation = true;
576
+ // For union types (A|B|C), set relatedType to the first type
577
+ if (unionTypes && unionTypes.length > 0) {
578
+ relatedType = unionTypes[0];
579
+ }
580
+ else {
581
+ relatedType = type;
582
+ }
583
+ }
584
+ // Build result object
585
+ const result = {
586
+ name,
587
+ type,
588
+ isArray,
589
+ isOptional,
590
+ isRelation,
591
+ relatedType,
592
+ backref,
593
+ };
594
+ // Only add operator properties if an operator was found
595
+ if (operator) {
596
+ result.operator = operator;
597
+ result.direction = direction;
598
+ result.matchMode = matchMode;
599
+ if (prompt) {
600
+ result.prompt = prompt;
601
+ }
602
+ if (operatorResult?.threshold !== undefined) {
603
+ result.threshold = operatorResult.threshold;
604
+ }
605
+ // Add union types if present (more than one type)
606
+ if (unionTypes && unionTypes.length > 1) {
607
+ result.unionTypes = unionTypes;
608
+ }
609
+ }
610
+ return result;
611
+ }
612
+ // =============================================================================
613
+ // Schema Parsing
614
+ // =============================================================================
615
+ /**
616
+ * Parse a database schema definition and resolve bi-directional relationships
617
+ *
618
+ * This is the main schema parsing function that transforms a raw DatabaseSchema
619
+ * into a fully resolved ParsedSchema with automatic backref creation.
620
+ *
621
+ * ## Processing Phases
622
+ *
623
+ * 1. **First pass**: Parse all entities and their fields, skipping metadata fields (`$*`)
624
+ * 2. **Validation pass**: Verify all operator-based references point to existing types
625
+ * 3. **Second pass**: Create bi-directional relationships from backrefs
626
+ *
627
+ * ## Automatic Backref Creation
628
+ *
629
+ * When a field specifies a backref (e.g., `author: 'Author.posts'`), the inverse
630
+ * relation is automatically created on the related entity if it doesn't exist.
631
+ *
632
+ * @param schema - The raw database schema definition
633
+ * @returns Parsed schema with resolved entities and bi-directional relationships
634
+ * @throws Error if a field references a non-existent type
635
+ *
636
+ * @example
637
+ * ```ts
638
+ * const parsed = parseSchema({
639
+ * Post: { title: 'string', author: 'Author.posts' },
640
+ * Author: { name: 'string' }
641
+ * })
642
+ * // Author.posts is auto-created as Post[]
643
+ * ```
644
+ */
645
+ export function parseSchema(schema) {
646
+ const entities = new Map();
647
+ // First pass: parse all entities and their fields
648
+ for (const [entityName, entitySchema] of Object.entries(schema)) {
649
+ // Validate entity name
650
+ validateEntityName(entityName);
651
+ const fields = new Map();
652
+ for (const [fieldName, fieldDef] of Object.entries(entitySchema)) {
653
+ // Skip metadata fields (prefixed with $) like $fuzzyThreshold, $instructions
654
+ if (fieldName.startsWith('$')) {
655
+ continue;
656
+ }
657
+ // Validate field name
658
+ validateFieldName(fieldName, entityName);
659
+ // Validate field definition type
660
+ if (typeof fieldDef !== 'string' && !Array.isArray(fieldDef)) {
661
+ // Object-type field definitions are invalid
662
+ throw new SchemaValidationError(`Invalid field type for '${entityName}.${fieldName}': nested objects are not supported. Use a reference to another entity instead.`, 'INVALID_FIELD_TYPE', `${entityName}.${fieldName}`);
663
+ }
664
+ // Validate array syntax
665
+ if (Array.isArray(fieldDef)) {
666
+ validateArrayDefinition(fieldDef, fieldName, entityName);
667
+ }
668
+ else {
669
+ // Validate field type (string definition)
670
+ validateFieldType(fieldDef, fieldName, entityName);
671
+ }
672
+ fields.set(fieldName, parseField(fieldName, fieldDef));
673
+ }
674
+ // Store raw schema for accessing metadata like $fuzzyThreshold
675
+ entities.set(entityName, { name: entityName, fields, schema: entitySchema });
676
+ }
677
+ // Validation pass: check that all operator-based references (->, ~>, <-, <~) point to existing types
678
+ // For implicit backrefs (Author.posts), we silently skip if the type doesn't exist
679
+ for (const [entityName, entity] of entities) {
680
+ for (const [fieldName, field] of entity.fields) {
681
+ if (field.isRelation && field.relatedType && field.operator) {
682
+ // Only validate fields with explicit operators
683
+ // Skip self-references (valid)
684
+ if (field.relatedType === entityName)
685
+ continue;
686
+ // For union types, validate each type in the union individually
687
+ // But only if at least one union type exists in the schema
688
+ // (allows "external" types when none are defined)
689
+ if (field.unionTypes && field.unionTypes.length > 0) {
690
+ const existingTypes = field.unionTypes.filter(t => entities.has(t));
691
+ // Only validate if at least one union type exists in schema
692
+ if (existingTypes.length > 0) {
693
+ for (const unionType of field.unionTypes) {
694
+ if (unionType !== entityName && !entities.has(unionType)) {
695
+ throw new Error(`Invalid schema: ${entityName}.${fieldName} references non-existent type '${unionType}'`);
696
+ }
697
+ }
698
+ }
699
+ }
700
+ else {
701
+ // Check if referenced type exists (non-union case)
702
+ if (!entities.has(field.relatedType)) {
703
+ throw new Error(`Invalid schema: ${entityName}.${fieldName} references non-existent type '${field.relatedType}'`);
704
+ }
705
+ }
706
+ }
707
+ }
708
+ }
709
+ // Second pass: create bi-directional relationships
710
+ for (const [entityName, entity] of entities) {
711
+ for (const [fieldName, field] of entity.fields) {
712
+ if (field.isRelation && field.relatedType && field.backref) {
713
+ const relatedEntity = entities.get(field.relatedType);
714
+ if (relatedEntity && !relatedEntity.fields.has(field.backref)) {
715
+ // Auto-create the inverse relation
716
+ // If Post.author -> Author.posts, then Author.posts -> Post[]
717
+ relatedEntity.fields.set(field.backref, {
718
+ name: field.backref,
719
+ type: entityName,
720
+ isArray: true, // Backref is always an array
721
+ isOptional: false,
722
+ isRelation: true,
723
+ relatedType: entityName,
724
+ backref: fieldName, // Points back to the original field
725
+ });
726
+ }
727
+ }
728
+ }
729
+ }
730
+ return { entities };
731
+ }
732
+ //# sourceMappingURL=parse.js.map