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.
- package/CHANGELOG.md +36 -0
- package/dist/actions.d.ts +247 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +260 -0
- package/dist/actions.js.map +1 -0
- package/dist/ai-promise-db.d.ts +34 -2
- package/dist/ai-promise-db.d.ts.map +1 -1
- package/dist/ai-promise-db.js +511 -66
- package/dist/ai-promise-db.js.map +1 -1
- package/dist/constants.d.ts +16 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +16 -0
- package/dist/constants.js.map +1 -0
- package/dist/events.d.ts +153 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +154 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/memory-provider.d.ts +144 -2
- package/dist/memory-provider.d.ts.map +1 -1
- package/dist/memory-provider.js +569 -13
- package/dist/memory-provider.js.map +1 -1
- package/dist/schema/cascade.d.ts +96 -0
- package/dist/schema/cascade.d.ts.map +1 -0
- package/dist/schema/cascade.js +528 -0
- package/dist/schema/cascade.js.map +1 -0
- package/dist/schema/index.d.ts +197 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +1211 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/parse.d.ts +225 -0
- package/dist/schema/parse.d.ts.map +1 -0
- package/dist/schema/parse.js +732 -0
- package/dist/schema/parse.js.map +1 -0
- package/dist/schema/provider.d.ts +176 -0
- package/dist/schema/provider.d.ts.map +1 -0
- package/dist/schema/provider.js +258 -0
- package/dist/schema/provider.js.map +1 -0
- package/dist/schema/resolve.d.ts +87 -0
- package/dist/schema/resolve.d.ts.map +1 -0
- package/dist/schema/resolve.js +474 -0
- package/dist/schema/resolve.js.map +1 -0
- package/dist/schema/semantic.d.ts +53 -0
- package/dist/schema/semantic.d.ts.map +1 -0
- package/dist/schema/semantic.js +247 -0
- package/dist/schema/semantic.js.map +1 -0
- package/dist/schema/types.d.ts +528 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +9 -0
- package/dist/schema/types.js.map +1 -0
- package/dist/schema.d.ts +24 -867
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +41 -1124
- package/dist/schema.js.map +1 -1
- package/dist/semantic.d.ts +175 -0
- package/dist/semantic.d.ts.map +1 -0
- package/dist/semantic.js +338 -0
- package/dist/semantic.js.map +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +13 -4
- package/.turbo/turbo-build.log +0 -5
- package/TESTING.md +0 -410
- package/TEST_SUMMARY.md +0 -250
- package/TODO.md +0 -128
- package/src/ai-promise-db.ts +0 -1243
- package/src/authorization.ts +0 -1102
- package/src/durable-clickhouse.ts +0 -596
- package/src/durable-promise.ts +0 -582
- package/src/execution-queue.ts +0 -608
- package/src/index.test.ts +0 -868
- package/src/index.ts +0 -337
- package/src/linguistic.ts +0 -404
- package/src/memory-provider.test.ts +0 -1036
- package/src/memory-provider.ts +0 -1119
- package/src/schema.test.ts +0 -1254
- package/src/schema.ts +0 -2296
- package/src/tests.ts +0 -725
- package/src/types.ts +0 -1177
- package/test/README.md +0 -153
- package/test/edge-cases.test.ts +0 -646
- package/test/provider-resolution.test.ts +0 -402
- package/tsconfig.json +0 -9
- 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
|