bunsane 0.3.1 → 0.3.2
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/.claude/scheduled_tasks.lock +1 -0
- package/CHANGELOG.md +52 -0
- package/config/cache.config.ts +35 -1
- package/core/App.ts +24 -1064
- package/core/ArcheType.ts +78 -2110
- package/core/Entity.ts +10 -33
- package/core/RequestContext.ts +85 -36
- package/core/RequestLoaders.ts +89 -31
- package/core/app/bootstrap.ts +133 -0
- package/core/app/cors.ts +94 -0
- package/core/app/graphqlSetup.ts +56 -0
- package/core/app/healthEndpoints.ts +31 -0
- package/core/app/metricsCollector.ts +27 -0
- package/core/app/preparedStatementWarmup.ts +55 -0
- package/core/app/processHandlers.ts +43 -0
- package/core/app/requestRouter.ts +309 -0
- package/core/app/restRegistry.ts +72 -0
- package/core/app/shutdown.ts +97 -0
- package/core/app/studioRouter.ts +83 -0
- package/core/archetype/customTypes.ts +100 -0
- package/core/archetype/decorators.ts +171 -0
- package/core/archetype/fieldResolvers.ts +621 -0
- package/core/archetype/helpers.ts +29 -0
- package/core/archetype/relationLoader.ts +118 -0
- package/core/archetype/schemaBuilder.ts +141 -0
- package/core/archetype/weaver.ts +218 -0
- package/core/archetype/zodSchemaBuilder.ts +527 -0
- package/core/cache/CacheManager.ts +126 -9
- package/core/middleware/AccessLog.ts +8 -1
- package/database/PreparedStatementCache.ts +12 -3
- package/database/cancellable.ts +22 -0
- package/database/instrumentedDb.ts +141 -0
- package/docs/RFC_APP_REFACTOR.md +248 -0
- package/docs/RFC_REFACTOR_TARGETS.md +251 -0
- package/package.json +1 -1
- package/query/Query.ts +53 -20
- package/tests/integration/loaders/RequestLoaders.abort.test.ts +82 -0
- package/tests/integration/query/Query.abort.test.ts +66 -0
- package/tests/unit/cache/CacheManager.test.ts +132 -1
- package/tests/unit/database/cancellable.test.ts +81 -0
- package/tests/unit/database/instrumentedDb.test.ts +160 -0
package/core/ArcheType.ts
CHANGED
|
@@ -10,547 +10,47 @@ import { ZodWeaver, asEnumType, asUnionType, asObjectType } from "@gqloom/zod";
|
|
|
10
10
|
import { printSchema } from "graphql";
|
|
11
11
|
import "reflect-metadata";
|
|
12
12
|
import { Query, type FilterSchema } from "../query";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
let _generateZodStructuralSignature: ((schema: any) => string) | null = null;
|
|
55
|
-
|
|
56
|
-
function getSignatureGenerator(): (schema: any) => string {
|
|
57
|
-
if (!_generateZodStructuralSignature) {
|
|
58
|
-
const { generateZodStructuralSignature } = require('../gql/utils/TypeSignature');
|
|
59
|
-
_generateZodStructuralSignature = generateZodStructuralSignature;
|
|
60
|
-
}
|
|
61
|
-
return _generateZodStructuralSignature!;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Component-level schema cache
|
|
65
|
-
const componentSchemaCache = new Map<string, ZodObject<any>>(); // componentId -> Zod schema
|
|
66
|
-
|
|
67
|
-
// Enum schema cache to prevent duplicate registrations
|
|
68
|
-
const enumSchemaCache = new Map<string, any>(); // enumTypeName -> Zod enum schema
|
|
69
|
-
|
|
70
|
-
const archetypeSchemaCache = new Map<
|
|
71
|
-
string,
|
|
72
|
-
{ zodSchema: ZodObject<any>; graphqlSchema: string }
|
|
73
|
-
>();
|
|
74
|
-
const allArchetypeZodObjects = new Map<string, ZodObject<any>>();
|
|
75
|
-
|
|
76
|
-
export function registerCustomZodType(
|
|
77
|
-
type: any,
|
|
78
|
-
schema: any,
|
|
79
|
-
typeName?: string,
|
|
80
|
-
inputTypeName?: string
|
|
81
|
-
) {
|
|
82
|
-
// If a type name is provided and it's a ZodObject, add __typename to control GraphQL naming
|
|
83
|
-
if (typeName && schema instanceof ZodObject) {
|
|
84
|
-
// Extend the schema with __typename literal to control the GraphQL type name
|
|
85
|
-
const shape = schema.shape;
|
|
86
|
-
const namedSchema = z.object({
|
|
87
|
-
__typename: z.literal(typeName).nullish(),
|
|
88
|
-
...shape,
|
|
89
|
-
});
|
|
90
|
-
customTypeRegistry.set(type, namedSchema);
|
|
91
|
-
if (typeName) {
|
|
92
|
-
customTypeNameRegistry.set(type, typeName);
|
|
93
|
-
registeredCustomTypes.set(typeName, namedSchema);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Register input type if provided (for use in GraphQL arguments)
|
|
97
|
-
if (inputTypeName) {
|
|
98
|
-
// Create input type schema (without __typename, as input types don't have it)
|
|
99
|
-
const inputSchema = z.object(shape).register(asObjectType, { name: inputTypeName });
|
|
100
|
-
registeredCustomTypes.set(inputTypeName, inputSchema);
|
|
101
|
-
inputTypeRegistry.set(type, inputTypeName);
|
|
102
|
-
|
|
103
|
-
// Register structural signature for input type deduplication
|
|
104
|
-
try {
|
|
105
|
-
const generateSignature = getSignatureGenerator();
|
|
106
|
-
const signature = generateSignature(z.object(shape));
|
|
107
|
-
structuralSignatureRegistry.set(signature, inputTypeName);
|
|
108
|
-
} catch (e) {
|
|
109
|
-
// Signature registration is optional, don't fail if it errors
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
customTypeRegistry.set(type, schema);
|
|
114
|
-
if (typeName) {
|
|
115
|
-
customTypeNameRegistry.set(type, typeName);
|
|
116
|
-
registeredCustomTypes.set(typeName, schema);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Register input type if provided
|
|
120
|
-
if (inputTypeName && schema instanceof ZodObject) {
|
|
121
|
-
const inputSchema = schema.register(asObjectType, { name: inputTypeName });
|
|
122
|
-
registeredCustomTypes.set(inputTypeName, inputSchema);
|
|
123
|
-
inputTypeRegistry.set(type, inputTypeName);
|
|
124
|
-
|
|
125
|
-
// Register structural signature for input type deduplication
|
|
126
|
-
try {
|
|
127
|
-
const generateSignature = getSignatureGenerator();
|
|
128
|
-
const signature = generateSignature(schema);
|
|
129
|
-
structuralSignatureRegistry.set(signature, inputTypeName);
|
|
130
|
-
} catch (e) {
|
|
131
|
-
// Signature registration is optional, don't fail if it errors
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function getArchetypeSchema(archetypeName: string, excludeRelations = false, excludeFunctions = false) {
|
|
138
|
-
const cacheKey = `${archetypeName}_${excludeRelations}_${excludeFunctions}`;
|
|
139
|
-
return archetypeSchemaCache.get(cacheKey);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function getAllArchetypeSchemas() {
|
|
143
|
-
return Array.from(archetypeSchemaCache.entries())
|
|
144
|
-
.filter(([key]) => key.endsWith('_false_false'))
|
|
145
|
-
.map(([, value]) => value);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function getRegisteredCustomTypes() {
|
|
149
|
-
return registeredCustomTypes;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Find a matching registered input type for a given Zod schema based on structural equivalence.
|
|
154
|
-
* This enables deduplication of input types that have the same structure but were created
|
|
155
|
-
* through different transformations (.omit(), .extend(), etc.)
|
|
156
|
-
*
|
|
157
|
-
* @param schema - The Zod schema to find a match for
|
|
158
|
-
* @returns The registered input type name if found, null otherwise
|
|
159
|
-
*/
|
|
160
|
-
export function findMatchingInputType(schema: any): string | null {
|
|
161
|
-
if (!schema) return null;
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
const generateSignature = getSignatureGenerator();
|
|
165
|
-
const signature = generateSignature(schema);
|
|
166
|
-
return structuralSignatureRegistry.get(signature) || null;
|
|
167
|
-
} catch (e) {
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Get the structural signature registry (for debugging/testing purposes)
|
|
174
|
-
*/
|
|
175
|
-
export function getStructuralSignatureRegistry(): Map<string, string> {
|
|
176
|
-
return structuralSignatureRegistry;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export function weaveAllArchetypes() {
|
|
180
|
-
// First, ensure all archetype schemas are generated
|
|
181
|
-
const storage = getMetadataStorage();
|
|
182
|
-
const archetypeNames: string[] = [];
|
|
183
|
-
|
|
184
|
-
for (const archetypeMetadata of storage.archetypes) {
|
|
185
|
-
const archetypeName = archetypeMetadata.name;
|
|
186
|
-
archetypeNames.push(archetypeName);
|
|
187
|
-
const fullSchemaCacheKey = `${archetypeName}_false_false`;
|
|
188
|
-
if (!archetypeSchemaCache.has(fullSchemaCacheKey)) {
|
|
189
|
-
try {
|
|
190
|
-
const ArchetypeClass = archetypeMetadata.target as any;
|
|
191
|
-
const instance = new ArchetypeClass();
|
|
192
|
-
instance.getZodObjectSchema(); // Generate and cache the schema
|
|
193
|
-
} catch (error) {
|
|
194
|
-
console.warn(
|
|
195
|
-
`Could not generate schema for archetype ${archetypeName}:`,
|
|
196
|
-
error
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (allArchetypeZodObjects.size === 0) {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
// Weave all archetype schemas together along with all component schemas
|
|
206
|
-
// This ensures that nested component types are also included in the unified schema
|
|
207
|
-
const archetypeSchemas = Array.from(allArchetypeZodObjects.values());
|
|
208
|
-
const componentSchemas = Array.from(componentSchemaCache.values());
|
|
209
|
-
|
|
210
|
-
// Combine both archetype and component schemas for weaving
|
|
211
|
-
const allSchemas = archetypeSchemas;
|
|
212
|
-
|
|
213
|
-
try {
|
|
214
|
-
const schema = weave(ZodWeaver, ...allSchemas);
|
|
215
|
-
let schemaString = printSchema(schema);
|
|
216
|
-
|
|
217
|
-
// Add Date scalar if not present
|
|
218
|
-
if (!schemaString.includes('scalar Date')) {
|
|
219
|
-
schemaString = 'scalar Date\n\n' + schemaString;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Post-process: Replace 'id: String' with 'id: ID' for all id fields
|
|
223
|
-
schemaString = schemaString.replace(/\bid:\s*String\b/g, "id: ID");
|
|
224
|
-
|
|
225
|
-
// Post-process: Replace date fields (start_at, end_at, created_at, updated_at, etc.) with Date scalar
|
|
226
|
-
// Match common date field patterns
|
|
227
|
-
schemaString = schemaString.replace(/\b(\w*_at|\w*_date|\w*Date|date\w*):\s*String(!?)/gi, (match, fieldName, nullable) => {
|
|
228
|
-
return `${fieldName}: Date${nullable}`;
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
// Post-process: Replace relation String fields with proper GraphQL type references
|
|
232
|
-
// Collect all relation metadata from all archetypes
|
|
233
|
-
for (const archetypeMetadata of storage.archetypes) {
|
|
234
|
-
const archetypeName = archetypeMetadata.name;
|
|
235
|
-
try {
|
|
236
|
-
const ArchetypeClass = archetypeMetadata.target as any;
|
|
237
|
-
const instance = new ArchetypeClass();
|
|
238
|
-
|
|
239
|
-
// Process each relation field
|
|
240
|
-
for (const [field, relatedArcheType] of Object.entries(instance.relationMap)) {
|
|
241
|
-
const relationType = instance.relationTypes[field];
|
|
242
|
-
const isArray = relationType === "hasMany" || relationType === "belongsToMany";
|
|
243
|
-
|
|
244
|
-
let relatedTypeName: string;
|
|
245
|
-
if (typeof relatedArcheType === "string") {
|
|
246
|
-
relatedTypeName = relatedArcheType;
|
|
247
|
-
} else {
|
|
248
|
-
const relatedArchetypeId = storage.getComponentId((relatedArcheType as any).name);
|
|
249
|
-
const relatedArchetypeMetadata = storage.archetypes.find(
|
|
250
|
-
(a) => a.typeId === relatedArchetypeId
|
|
251
|
-
);
|
|
252
|
-
relatedTypeName = relatedArchetypeMetadata?.name || (relatedArcheType as any).name.replace(/ArcheType$/, "");
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (isArray) {
|
|
256
|
-
// Step 1: Add description if it doesn't exist
|
|
257
|
-
const hasDescription = new RegExp(`"""Reference to ${relatedTypeName} type"""[\\s\\S]{0,50}${field}:`).test(schemaString);
|
|
258
|
-
if (!hasDescription) {
|
|
259
|
-
const addDescPattern = new RegExp(
|
|
260
|
-
`(type ${archetypeName} \\{[\\s\\S]*?)(\\n\\s+)(${field}:\\s*\\[String!?\\]!?)`,
|
|
261
|
-
"g"
|
|
262
|
-
);
|
|
263
|
-
schemaString = schemaString.replace(
|
|
264
|
-
addDescPattern,
|
|
265
|
-
`$1$2"""Reference to ${relatedTypeName} type"""$2$3`
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Step 2: Replace [String!] with [TypeName!]
|
|
270
|
-
const shouldBeRequired = instance.relationOptions[field]?.nullable === false;
|
|
271
|
-
const suffix = shouldBeRequired ? "!" : "";
|
|
272
|
-
const replacePattern = new RegExp(
|
|
273
|
-
`(type ${archetypeName} \\{[\\s\\S]*?${field}:\\s*)\\[String!?\\](!?)`,
|
|
274
|
-
"g"
|
|
275
|
-
);
|
|
276
|
-
schemaString = schemaString.replace(
|
|
277
|
-
replacePattern,
|
|
278
|
-
`$1[${relatedTypeName}!]${suffix}`
|
|
279
|
-
);
|
|
280
|
-
} else {
|
|
281
|
-
// Singular relations already have descriptions from Zod, just replace type
|
|
282
|
-
const pattern = new RegExp(
|
|
283
|
-
`(type ${archetypeName} \\{[\\s\\S]*?${field}:\\s*)String(!?)`,
|
|
284
|
-
"g"
|
|
285
|
-
);
|
|
286
|
-
const isNullable = instance.relationOptions[field]?.nullable;
|
|
287
|
-
const suffix = isNullable ? "" : "!";
|
|
288
|
-
schemaString = schemaString.replace(
|
|
289
|
-
pattern,
|
|
290
|
-
`$1${relatedTypeName}${suffix}`
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
} catch (error) {
|
|
295
|
-
console.warn(`Could not process relations for archetype ${archetypeMetadata.name}:`, error);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Process each function field
|
|
299
|
-
if (archetypeMetadata.functions) {
|
|
300
|
-
for (const { propertyKey, options } of archetypeMetadata.functions) {
|
|
301
|
-
|
|
302
|
-
// Add arguments if present
|
|
303
|
-
if (options?.args && options.args.length > 0) {
|
|
304
|
-
const argDefs: string[] = [];
|
|
305
|
-
for (const arg of options.args) {
|
|
306
|
-
let argTypeName: string;
|
|
307
|
-
|
|
308
|
-
const inputTypeName = inputTypeRegistry.get(arg.type);
|
|
309
|
-
if (inputTypeName) {
|
|
310
|
-
argTypeName = inputTypeName;
|
|
311
|
-
} else {
|
|
312
|
-
const registeredTypeName = customTypeNameRegistry.get(arg.type);
|
|
313
|
-
if (registeredTypeName) {
|
|
314
|
-
argTypeName = registeredTypeName;
|
|
315
|
-
} else if (arg.type === String) {
|
|
316
|
-
argTypeName = 'String';
|
|
317
|
-
} else if (arg.type === Number) {
|
|
318
|
-
argTypeName = 'Float';
|
|
319
|
-
} else if (arg.type === Boolean) {
|
|
320
|
-
argTypeName = 'Boolean';
|
|
321
|
-
} else if (arg.type === Date) {
|
|
322
|
-
argTypeName = 'Date';
|
|
323
|
-
} else if (arg.type?.name) {
|
|
324
|
-
argTypeName = arg.type.name;
|
|
325
|
-
} else {
|
|
326
|
-
argTypeName = 'String';
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const nullable = arg.nullable ? '' : '!';
|
|
331
|
-
argDefs.push(`${arg.name}: ${argTypeName}${nullable}`);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const argsString = argDefs.join(', ');
|
|
335
|
-
const escapedKey = propertyKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
336
|
-
|
|
337
|
-
// Pattern to add arguments: fieldName: Type -> fieldName(args): Type
|
|
338
|
-
// Capture leading whitespace separately to preserve it
|
|
339
|
-
const argPattern = new RegExp(
|
|
340
|
-
`(\\s+)(${escapedKey}\\??\\s*:\\s*)([^\\n]+)`,
|
|
341
|
-
'g'
|
|
342
|
-
);
|
|
343
|
-
|
|
344
|
-
schemaString = schemaString.replace(
|
|
345
|
-
argPattern,
|
|
346
|
-
(match, leadingSpace, fieldDef, returnType) => {
|
|
347
|
-
return `${leadingSpace}${fieldDef.trim().replace(':', '')}(${argsString}): ${returnType.trim()}`;
|
|
348
|
-
}
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (options?.returnType && !['string', 'number', 'boolean'].includes(options.returnType)) {
|
|
353
|
-
// Find the archetype type definition first
|
|
354
|
-
const typePattern = new RegExp(`type ${archetypeName}\\s*\\{([\\s\\S]*?)\\n\\}`, 'g');
|
|
355
|
-
const typeMatch = typePattern.exec(schemaString);
|
|
356
|
-
|
|
357
|
-
if (typeMatch) {
|
|
358
|
-
const typeBody = typeMatch[1]!;
|
|
359
|
-
|
|
360
|
-
// Find the field line in the type body
|
|
361
|
-
const fieldIndex = typeBody.indexOf(` ${propertyKey}`);
|
|
362
|
-
if (fieldIndex !== -1) {
|
|
363
|
-
const lineStart = fieldIndex;
|
|
364
|
-
const lineEnd = typeBody.indexOf('\n', fieldIndex);
|
|
365
|
-
const fieldLine = typeBody.substring(lineStart, lineEnd !== -1 ? lineEnd : typeBody.length);
|
|
366
|
-
|
|
367
|
-
// Replace String with the actual return type in this line
|
|
368
|
-
const updatedLine = fieldLine.replace(/:\s*String(\??)(\s*)$/, `: ${options.returnType}$1$2`);
|
|
369
|
-
|
|
370
|
-
if (updatedLine !== fieldLine) {
|
|
371
|
-
// Replace in the full schema
|
|
372
|
-
const fullFieldIndex = schemaString.indexOf(typeMatch[0]) + typeMatch[0].indexOf(fieldLine);
|
|
373
|
-
schemaString = schemaString.substring(0, fullFieldIndex) +
|
|
374
|
-
updatedLine +
|
|
375
|
-
schemaString.substring(fullFieldIndex + fieldLine.length);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
return schemaString;
|
|
385
|
-
} catch (error) {
|
|
386
|
-
console.warn(
|
|
387
|
-
`Failed to weave all archetypes due to duplicate types.\n` +
|
|
388
|
-
`Archetypes being processed: ${archetypeNames.join(', ')}\n` +
|
|
389
|
-
`Error: ${error}`
|
|
390
|
-
);
|
|
391
|
-
return null;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Generate Zod schema for a component and cache it
|
|
396
|
-
function getOrCreateComponentSchema(
|
|
397
|
-
componentCtor: new (...args: any[]) => BaseComponent,
|
|
398
|
-
componentId: string,
|
|
399
|
-
fieldOptions?: ArcheTypeFieldOptions
|
|
400
|
-
): any | null {
|
|
401
|
-
// Check cache first
|
|
402
|
-
if (componentSchemaCache.has(componentId)) {
|
|
403
|
-
return componentSchemaCache.get(componentId)!;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const storage = getMetadataStorage();
|
|
407
|
-
const props = storage.getComponentProperties(componentId);
|
|
408
|
-
|
|
409
|
-
// Return null if no properties - caller should skip this component
|
|
410
|
-
if (props.length === 0) {
|
|
411
|
-
return null;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const zodFields: Record<string, any> = {
|
|
415
|
-
__typename: z
|
|
416
|
-
.literal(compNameToFieldName(componentCtor.name))
|
|
417
|
-
.nullish(),
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
for (const prop of props) {
|
|
421
|
-
if (prop.isPrimitive) {
|
|
422
|
-
switch (prop.propertyType) {
|
|
423
|
-
case String:
|
|
424
|
-
zodFields[prop.propertyKey] = z.string();
|
|
425
|
-
break;
|
|
426
|
-
case Number:
|
|
427
|
-
zodFields[prop.propertyKey] = z.number();
|
|
428
|
-
break;
|
|
429
|
-
case Boolean:
|
|
430
|
-
zodFields[prop.propertyKey] = z.boolean();
|
|
431
|
-
break;
|
|
432
|
-
case Date:
|
|
433
|
-
zodFields[prop.propertyKey] = z.date();
|
|
434
|
-
break;
|
|
435
|
-
default:
|
|
436
|
-
console.warn(`[ArcheType] Unknown primitive type for ${componentCtor.name}.${prop.propertyKey}: ${prop.propertyType?.name}. Falling back to z.string()`);
|
|
437
|
-
zodFields[prop.propertyKey] = z.string();
|
|
438
|
-
}
|
|
439
|
-
if (prop.isOptional) {
|
|
440
|
-
zodFields[prop.propertyKey] =
|
|
441
|
-
zodFields[prop.propertyKey].optional();
|
|
442
|
-
}
|
|
443
|
-
} else if (prop.isEnum && prop.enumValues && prop.enumKeys) {
|
|
444
|
-
const enumTypeName =
|
|
445
|
-
prop.propertyType?.name ||
|
|
446
|
-
`${componentCtor.name}_${prop.propertyKey}_Enum`;
|
|
447
|
-
|
|
448
|
-
// Check if this enum has already been registered
|
|
449
|
-
let enumSchema = enumSchemaCache.get(enumTypeName);
|
|
450
|
-
|
|
451
|
-
if (!enumSchema) {
|
|
452
|
-
// Register the enum for the first time
|
|
453
|
-
enumSchema = z
|
|
454
|
-
.enum(prop.enumValues as any)
|
|
455
|
-
.register(asEnumType, {
|
|
456
|
-
name: enumTypeName,
|
|
457
|
-
valuesConfig: prop.enumKeys.reduce(
|
|
458
|
-
(
|
|
459
|
-
acc: Record<string, { description: string }>,
|
|
460
|
-
key,
|
|
461
|
-
idx
|
|
462
|
-
) => {
|
|
463
|
-
acc[key] = { description: prop.enumValues![idx]! };
|
|
464
|
-
return acc;
|
|
465
|
-
},
|
|
466
|
-
{}
|
|
467
|
-
),
|
|
468
|
-
});
|
|
469
|
-
// Cache it for reuse
|
|
470
|
-
enumSchemaCache.set(enumTypeName, enumSchema);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
zodFields[prop.propertyKey] = enumSchema;
|
|
474
|
-
if (prop.isOptional) {
|
|
475
|
-
zodFields[prop.propertyKey] =
|
|
476
|
-
zodFields[prop.propertyKey].optional();
|
|
477
|
-
}
|
|
478
|
-
} else if (customTypeRegistry.has(prop.propertyType)) {
|
|
479
|
-
zodFields[prop.propertyKey] = customTypeRegistry.get(
|
|
480
|
-
prop.propertyType
|
|
481
|
-
)!;
|
|
482
|
-
if (prop.isOptional) {
|
|
483
|
-
zodFields[prop.propertyKey] =
|
|
484
|
-
zodFields[prop.propertyKey].optional();
|
|
485
|
-
}
|
|
486
|
-
} else if (prop.arrayOf) {
|
|
487
|
-
if (customTypeRegistry.has(prop.arrayOf)) {
|
|
488
|
-
zodFields[prop.propertyKey] = z.array(customTypeRegistry.get(prop.arrayOf)!);
|
|
489
|
-
} else if (primitiveTypes.includes(prop.arrayOf)) {
|
|
490
|
-
if (prop.arrayOf === String) {
|
|
491
|
-
zodFields[prop.propertyKey] = z.array(z.string());
|
|
492
|
-
} else if (prop.arrayOf === Number) {
|
|
493
|
-
zodFields[prop.propertyKey] = z.array(z.number());
|
|
494
|
-
} else if (prop.arrayOf === Boolean) {
|
|
495
|
-
zodFields[prop.propertyKey] = z.array(z.boolean());
|
|
496
|
-
} else if (prop.arrayOf === Date) {
|
|
497
|
-
zodFields[prop.propertyKey] = z.array(z.date());
|
|
498
|
-
}
|
|
499
|
-
} else {
|
|
500
|
-
console.warn(`[ArcheType] Unknown array element type for ${componentCtor.name}.${prop.propertyKey}: ${prop.arrayOf?.name}. Falling back to z.array(z.string())`);
|
|
501
|
-
zodFields[prop.propertyKey] = z.array(z.string());
|
|
502
|
-
}
|
|
503
|
-
if (prop.isOptional) {
|
|
504
|
-
zodFields[prop.propertyKey] = zodFields[prop.propertyKey].optional();
|
|
505
|
-
}
|
|
506
|
-
} else {
|
|
507
|
-
console.warn(`[ArcheType] Unknown type for ${componentCtor.name}.${prop.propertyKey}: ${prop.propertyType?.name}. Falling back to z.string()`);
|
|
508
|
-
zodFields[prop.propertyKey] = z.string();
|
|
509
|
-
if (prop.isOptional) {
|
|
510
|
-
zodFields[prop.propertyKey] =
|
|
511
|
-
zodFields[prop.propertyKey].optional();
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
if (fieldOptions?.nullable) {
|
|
516
|
-
zodFields[prop.propertyKey] = zodFields[prop.propertyKey].nullish();
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const componentSchema = z.object(zodFields);
|
|
521
|
-
|
|
522
|
-
// Cache the component schema for reuse
|
|
523
|
-
componentSchemaCache.set(componentId, componentSchema);
|
|
524
|
-
|
|
525
|
-
return componentSchema;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function compNameToFieldName(compName: string): string {
|
|
529
|
-
return (
|
|
530
|
-
compName.charAt(0).toLowerCase() +
|
|
531
|
-
compName.slice(1).replace(/Component$/, "Component")
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Helper to determine if a component should be unwrapped to a scalar value.
|
|
537
|
-
* Returns true if the component has a single 'value' property and the field type is primitive.
|
|
538
|
-
*/
|
|
539
|
-
function shouldUnwrapComponent(
|
|
540
|
-
componentProps: ComponentPropertyMetadata[],
|
|
541
|
-
fieldType: any
|
|
542
|
-
): boolean {
|
|
543
|
-
// If field type is a primitive, unwrap the component to that primitive
|
|
544
|
-
if (
|
|
545
|
-
fieldType === String ||
|
|
546
|
-
fieldType === Number ||
|
|
547
|
-
fieldType === Boolean ||
|
|
548
|
-
fieldType === Date
|
|
549
|
-
) {
|
|
550
|
-
return true;
|
|
551
|
-
}
|
|
552
|
-
return false;
|
|
553
|
-
}
|
|
13
|
+
import { compNameToFieldName, shouldUnwrapComponent, primitiveTypes } from "./archetype/helpers";
|
|
14
|
+
import {
|
|
15
|
+
customTypeRegistry,
|
|
16
|
+
customTypeNameRegistry,
|
|
17
|
+
registeredCustomTypes,
|
|
18
|
+
customTypeSilks,
|
|
19
|
+
customTypeResolvers,
|
|
20
|
+
inputTypeRegistry,
|
|
21
|
+
structuralSignatureRegistry,
|
|
22
|
+
registerCustomZodType,
|
|
23
|
+
findMatchingInputType,
|
|
24
|
+
getRegisteredCustomTypes,
|
|
25
|
+
getStructuralSignatureRegistry,
|
|
26
|
+
} from "./archetype/customTypes";
|
|
27
|
+
import {
|
|
28
|
+
componentSchemaCache,
|
|
29
|
+
enumSchemaCache,
|
|
30
|
+
getOrCreateComponentSchema,
|
|
31
|
+
} from "./archetype/schemaBuilder";
|
|
32
|
+
import {
|
|
33
|
+
archetypeSchemaCache,
|
|
34
|
+
allArchetypeZodObjects,
|
|
35
|
+
getArchetypeSchema,
|
|
36
|
+
getAllArchetypeSchemas,
|
|
37
|
+
weaveAllArchetypes,
|
|
38
|
+
} from "./archetype/weaver";
|
|
39
|
+
import {
|
|
40
|
+
archetypeFunctionsSymbol,
|
|
41
|
+
archetypeFieldsSymbol,
|
|
42
|
+
archetypeUnionFieldsSymbol,
|
|
43
|
+
archetypeRelationsSymbol,
|
|
44
|
+
ArcheTypeFunction,
|
|
45
|
+
ArcheType,
|
|
46
|
+
ArcheTypeField,
|
|
47
|
+
ArcheTypeUnionField,
|
|
48
|
+
HasMany,
|
|
49
|
+
BelongsTo,
|
|
50
|
+
HasOne,
|
|
51
|
+
BelongsToMany,
|
|
52
|
+
ArcheTypeRelation,
|
|
53
|
+
} from "./archetype/decorators";
|
|
554
54
|
|
|
555
55
|
export type ArcheTypeOptions = {
|
|
556
56
|
name?: string;
|
|
@@ -563,6 +63,37 @@ export interface RelationOptions {
|
|
|
563
63
|
cascade?: boolean;
|
|
564
64
|
}
|
|
565
65
|
|
|
66
|
+
const InputFilterSchema = z.object({
|
|
67
|
+
field: z.string(),
|
|
68
|
+
op: z.string().default("eq"),
|
|
69
|
+
value: z.string(),
|
|
70
|
+
}).register(asObjectType, { name: "InputFilter" });
|
|
71
|
+
|
|
72
|
+
export {asEnumType, asUnionType, asObjectType};
|
|
73
|
+
export {
|
|
74
|
+
ArcheTypeFunction,
|
|
75
|
+
ArcheType,
|
|
76
|
+
ArcheTypeField,
|
|
77
|
+
ArcheTypeUnionField,
|
|
78
|
+
HasMany,
|
|
79
|
+
BelongsTo,
|
|
80
|
+
HasOne,
|
|
81
|
+
BelongsToMany,
|
|
82
|
+
ArcheTypeRelation,
|
|
83
|
+
} from "./archetype/decorators";
|
|
84
|
+
export { compNameToFieldName, shouldUnwrapComponent } from "./archetype/helpers";
|
|
85
|
+
export {
|
|
86
|
+
registerCustomZodType,
|
|
87
|
+
findMatchingInputType,
|
|
88
|
+
getRegisteredCustomTypes,
|
|
89
|
+
getStructuralSignatureRegistry,
|
|
90
|
+
} from "./archetype/customTypes";
|
|
91
|
+
export {
|
|
92
|
+
getArchetypeSchema,
|
|
93
|
+
getAllArchetypeSchemas,
|
|
94
|
+
weaveAllArchetypes,
|
|
95
|
+
} from "./archetype/weaver";
|
|
96
|
+
|
|
566
97
|
export interface HasManyOptions extends RelationOptions {
|
|
567
98
|
// Additional HasMany specific options
|
|
568
99
|
}
|
|
@@ -579,156 +110,6 @@ export interface BelongsToManyOptions extends RelationOptions {
|
|
|
579
110
|
through: string; // Required for many-to-many
|
|
580
111
|
}
|
|
581
112
|
|
|
582
|
-
export function ArcheType<T extends new () => BaseArcheType>(
|
|
583
|
-
nameOrOptions?: string | ArcheTypeOptions
|
|
584
|
-
) {
|
|
585
|
-
return function (target: T): T {
|
|
586
|
-
const storage = getMetadataStorage();
|
|
587
|
-
const typeId = storage.getComponentId(target.name);
|
|
588
|
-
|
|
589
|
-
let archetype_name = target.name;
|
|
590
|
-
|
|
591
|
-
if (typeof nameOrOptions === "string") {
|
|
592
|
-
archetype_name = nameOrOptions;
|
|
593
|
-
} else if (nameOrOptions) {
|
|
594
|
-
archetype_name = nameOrOptions.name || target.name;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
storage.collectArcheTypeMetadata({
|
|
598
|
-
name: archetype_name,
|
|
599
|
-
typeId: typeId,
|
|
600
|
-
target: target,
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
const prototype = target.prototype;
|
|
604
|
-
const fields = prototype[archetypeFieldsSymbol];
|
|
605
|
-
if (fields) {
|
|
606
|
-
for (const { propertyKey, component, options } of fields) {
|
|
607
|
-
const type = Reflect.getMetadata(
|
|
608
|
-
"design:type",
|
|
609
|
-
target.prototype,
|
|
610
|
-
propertyKey
|
|
611
|
-
);
|
|
612
|
-
storage.collectArchetypeField(
|
|
613
|
-
archetype_name,
|
|
614
|
-
propertyKey,
|
|
615
|
-
component,
|
|
616
|
-
options,
|
|
617
|
-
type
|
|
618
|
-
);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
const unions = prototype[archetypeUnionFieldsSymbol];
|
|
623
|
-
if (unions) {
|
|
624
|
-
for (const { propertyKey, components, options } of unions) {
|
|
625
|
-
storage.collectArchetypeUnion(
|
|
626
|
-
archetype_name,
|
|
627
|
-
propertyKey,
|
|
628
|
-
components,
|
|
629
|
-
options,
|
|
630
|
-
"union"
|
|
631
|
-
);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Process relations
|
|
636
|
-
const relations = prototype[archetypeRelationsSymbol];
|
|
637
|
-
if (relations) {
|
|
638
|
-
for (const {
|
|
639
|
-
propertyKey,
|
|
640
|
-
relatedArcheType,
|
|
641
|
-
relationType,
|
|
642
|
-
options,
|
|
643
|
-
} of relations) {
|
|
644
|
-
const type = Reflect.getMetadata(
|
|
645
|
-
"design:type",
|
|
646
|
-
target.prototype,
|
|
647
|
-
propertyKey
|
|
648
|
-
);
|
|
649
|
-
storage.collectArchetypeRelation(
|
|
650
|
-
archetype_name,
|
|
651
|
-
propertyKey,
|
|
652
|
-
relatedArcheType,
|
|
653
|
-
relationType,
|
|
654
|
-
options,
|
|
655
|
-
type
|
|
656
|
-
);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// Process functions
|
|
661
|
-
const functions = prototype[archetypeFunctionsSymbol];
|
|
662
|
-
if (functions) {
|
|
663
|
-
storage.collectArcheTypeMetadata({
|
|
664
|
-
name: archetype_name,
|
|
665
|
-
typeId: typeId,
|
|
666
|
-
target: target,
|
|
667
|
-
functions: functions,
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
return target;
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
const archetypeFieldsSymbol = Symbol.for("bunsane:archetypeFields");
|
|
676
|
-
export function ArcheTypeField<T extends BaseComponent>(
|
|
677
|
-
component: new (...args: any[]) => T,
|
|
678
|
-
options?: ArcheTypeFieldOptions
|
|
679
|
-
) {
|
|
680
|
-
return function (target: any, propertyKey: string) {
|
|
681
|
-
if (!target[archetypeFieldsSymbol]) {
|
|
682
|
-
target[archetypeFieldsSymbol] = [];
|
|
683
|
-
}
|
|
684
|
-
target[archetypeFieldsSymbol].push({ propertyKey, component, options });
|
|
685
|
-
};
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
const archetypeUnionFieldsSymbol = Symbol.for("bunsane:archetypeUnionFields");
|
|
689
|
-
export function ArcheTypeUnionField(
|
|
690
|
-
components: (new (...args: any[]) => any)[],
|
|
691
|
-
options?: ArcheTypeFieldOptions
|
|
692
|
-
) {
|
|
693
|
-
return function (target: any, propertyKey: string) {
|
|
694
|
-
if (!target[archetypeUnionFieldsSymbol]) {
|
|
695
|
-
target[archetypeUnionFieldsSymbol] = [];
|
|
696
|
-
}
|
|
697
|
-
target[archetypeUnionFieldsSymbol].push({
|
|
698
|
-
propertyKey,
|
|
699
|
-
components,
|
|
700
|
-
options,
|
|
701
|
-
});
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
const archetypeRelationsSymbol = Symbol.for("bunsane:archetypeRelations");
|
|
706
|
-
|
|
707
|
-
function createRelationDecorator(
|
|
708
|
-
relationType: "hasMany" | "belongsTo" | "hasOne" | "belongsToMany"
|
|
709
|
-
) {
|
|
710
|
-
return function (relatedArcheType: string, options?: RelationOptions) {
|
|
711
|
-
return function (target: any, propertyKey: string) {
|
|
712
|
-
if (!target[archetypeRelationsSymbol]) {
|
|
713
|
-
target[archetypeRelationsSymbol] = [];
|
|
714
|
-
}
|
|
715
|
-
target[archetypeRelationsSymbol].push({
|
|
716
|
-
propertyKey,
|
|
717
|
-
relatedArcheType,
|
|
718
|
-
relationType,
|
|
719
|
-
options,
|
|
720
|
-
});
|
|
721
|
-
};
|
|
722
|
-
};
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
export const HasMany = createRelationDecorator("hasMany");
|
|
726
|
-
export const BelongsTo = createRelationDecorator("belongsTo");
|
|
727
|
-
export const HasOne = createRelationDecorator("hasOne");
|
|
728
|
-
export const BelongsToMany = createRelationDecorator("belongsToMany");
|
|
729
|
-
|
|
730
|
-
// Keep ArcheTypeRelation as alias for backwards compatibility
|
|
731
|
-
export const ArcheTypeRelation = HasMany;
|
|
732
113
|
|
|
733
114
|
export type ArcheTypeResolver = {
|
|
734
115
|
resolver?: string;
|
|
@@ -1299,127 +680,8 @@ export class BaseArcheType {
|
|
|
1299
680
|
* @param entity The entity to populate relations for
|
|
1300
681
|
*/
|
|
1301
682
|
private async populateRelations(entity: Entity): Promise<void> {
|
|
1302
|
-
const {
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
for (const [fieldName, relatedArchetype] of Object.entries(this.relationMap)) {
|
|
1306
|
-
const relationType = this.relationTypes[fieldName];
|
|
1307
|
-
const relationOptions = this.relationOptions[fieldName];
|
|
1308
|
-
|
|
1309
|
-
if (relationType === "belongsTo") {
|
|
1310
|
-
// For belongsTo, load the related entity using foreign key
|
|
1311
|
-
const foreignKey = relationOptions?.foreignKey;
|
|
1312
|
-
if (foreignKey) {
|
|
1313
|
-
let foreignId: string | undefined;
|
|
1314
|
-
|
|
1315
|
-
// Get foreign key value from entity's components
|
|
1316
|
-
if (foreignKey.includes('.')) {
|
|
1317
|
-
const [fieldName, propName] = foreignKey.split('.');
|
|
1318
|
-
const compCtor = this.componentMap[fieldName!];
|
|
1319
|
-
if (compCtor) {
|
|
1320
|
-
const componentInstance = await entity.get(compCtor as any);
|
|
1321
|
-
if (componentInstance && (componentInstance as any)[propName!] !== undefined) {
|
|
1322
|
-
foreignId = (componentInstance as any)[propName!];
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
} else {
|
|
1326
|
-
// OPTIMIZED: Find candidate components first, then load in parallel
|
|
1327
|
-
const candidateComponents: Array<{ compCtor: any }> = [];
|
|
1328
|
-
for (const compCtor of Object.values(this.componentMap)) {
|
|
1329
|
-
const typeId = storage.getComponentId(compCtor.name);
|
|
1330
|
-
const componentProps = storage.getComponentProperties(typeId);
|
|
1331
|
-
const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
|
|
1332
|
-
if (hasForeignKey) {
|
|
1333
|
-
candidateComponents.push({ compCtor });
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
if (candidateComponents.length > 0) {
|
|
1338
|
-
// Load all candidate components in parallel
|
|
1339
|
-
const componentInstances = await Promise.all(
|
|
1340
|
-
candidateComponents.map(({ compCtor }) => entity.get(compCtor as any))
|
|
1341
|
-
);
|
|
1342
|
-
|
|
1343
|
-
// Find the first one with the foreign key value
|
|
1344
|
-
for (const componentInstance of componentInstances) {
|
|
1345
|
-
if (componentInstance && (componentInstance as any)[foreignKey] !== undefined) {
|
|
1346
|
-
foreignId = (componentInstance as any)[foreignKey];
|
|
1347
|
-
break;
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
if (!foreignId && foreignKey === 'id') {
|
|
1354
|
-
foreignId = entity.id;
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
if (foreignId) {
|
|
1358
|
-
// Load related entity
|
|
1359
|
-
let relatedArchetypeInstance: BaseArcheType;
|
|
1360
|
-
if (typeof relatedArchetype === "function") {
|
|
1361
|
-
relatedArchetypeInstance = new (relatedArchetype as any)();
|
|
1362
|
-
} else {
|
|
1363
|
-
// Find archetype by name
|
|
1364
|
-
const relatedArchetypeMetadata = storage.archetypes.find((a) => a.name === relatedArchetype);
|
|
1365
|
-
if (relatedArchetypeMetadata) {
|
|
1366
|
-
relatedArchetypeInstance = new (relatedArchetypeMetadata.target as any)();
|
|
1367
|
-
} else {
|
|
1368
|
-
continue;
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
const relatedEntity = await relatedArchetypeInstance.getEntityWithID(foreignId);
|
|
1373
|
-
if (relatedEntity) {
|
|
1374
|
-
// Attach as computed property (non-persisted)
|
|
1375
|
-
(entity as any)[fieldName] = relatedEntity;
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
} else if (relationType === "hasMany") {
|
|
1380
|
-
// For hasMany, query related entities that reference this entity
|
|
1381
|
-
const foreignKey = relationOptions?.foreignKey;
|
|
1382
|
-
if (foreignKey) {
|
|
1383
|
-
let relatedArchetypeInstance: BaseArcheType;
|
|
1384
|
-
if (typeof relatedArchetype === "function") {
|
|
1385
|
-
relatedArchetypeInstance = new (relatedArchetype as any)();
|
|
1386
|
-
} else {
|
|
1387
|
-
const relatedArchetypeMetadata = storage.archetypes.find((a) => a.name === relatedArchetype);
|
|
1388
|
-
if (relatedArchetypeMetadata) {
|
|
1389
|
-
relatedArchetypeInstance = new (relatedArchetypeMetadata.target as any)();
|
|
1390
|
-
} else {
|
|
1391
|
-
continue;
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
// Find the component in related archetype that has the foreign key
|
|
1396
|
-
let foreignKeyComponent: any = null;
|
|
1397
|
-
for (const compCtor of Object.values(relatedArchetypeInstance.componentMap)) {
|
|
1398
|
-
const typeId = storage.getComponentId(compCtor.name);
|
|
1399
|
-
const componentProps = storage.getComponentProperties(typeId);
|
|
1400
|
-
const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
|
|
1401
|
-
if (hasForeignKey) {
|
|
1402
|
-
foreignKeyComponent = compCtor;
|
|
1403
|
-
break;
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
if (foreignKeyComponent) {
|
|
1408
|
-
// OPTIMIZED: Use Query with filter instead of fetching all + filtering in JS
|
|
1409
|
-
// This pushes the filtering to the database, avoiding N+1 queries
|
|
1410
|
-
const matchingEntities = await new Query()
|
|
1411
|
-
.with(foreignKeyComponent, {
|
|
1412
|
-
filters: [{ field: foreignKey, operator: '=', value: entity.id }]
|
|
1413
|
-
})
|
|
1414
|
-
.exec();
|
|
1415
|
-
|
|
1416
|
-
// Attach as computed property
|
|
1417
|
-
(entity as any)[fieldName] = matchingEntities;
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
|
-
// Note: hasOne and belongsToMany not implemented yet
|
|
1422
|
-
}
|
|
683
|
+
const { populateRelations: doPopulateRelations } = require("./archetype/relationLoader");
|
|
684
|
+
return doPopulateRelations(this, entity);
|
|
1423
685
|
}
|
|
1424
686
|
|
|
1425
687
|
/**
|
|
@@ -1588,713 +850,8 @@ export class BaseArcheType {
|
|
|
1588
850
|
fieldName: string;
|
|
1589
851
|
resolver: (parent: any, args: any, context: any) => any;
|
|
1590
852
|
}> {
|
|
1591
|
-
const
|
|
1592
|
-
|
|
1593
|
-
const archetypeId = storage.getComponentId(this.constructor.name);
|
|
1594
|
-
const archetypeName =
|
|
1595
|
-
storage.archetypes.find((a) => a.typeId === archetypeId)?.name ||
|
|
1596
|
-
this.constructor.name;
|
|
1597
|
-
|
|
1598
|
-
// Generate ID resolver for the main archetype type
|
|
1599
|
-
resolvers.push({
|
|
1600
|
-
typeName: archetypeName,
|
|
1601
|
-
fieldName: "id",
|
|
1602
|
-
resolver: (parent: any) => {
|
|
1603
|
-
return parent.id;
|
|
1604
|
-
},
|
|
1605
|
-
});
|
|
1606
|
-
|
|
1607
|
-
// Generate resolvers for each component field
|
|
1608
|
-
for (const [field, ctor] of Object.entries(this.componentMap)) {
|
|
1609
|
-
const typeId = storage.getComponentId(ctor.name);
|
|
1610
|
-
const typeIdHex = typeId;
|
|
1611
|
-
const componentName = ctor.name;
|
|
1612
|
-
const fieldType = this.fieldTypes[field];
|
|
1613
|
-
|
|
1614
|
-
// Skip components with no properties (like tag components)
|
|
1615
|
-
const componentProps = storage.getComponentProperties(typeId);
|
|
1616
|
-
if (componentProps.length === 0) {
|
|
1617
|
-
continue;
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
// Check if this component should be unwrapped to a scalar
|
|
1621
|
-
const isUnwrapped = shouldUnwrapComponent(
|
|
1622
|
-
componentProps,
|
|
1623
|
-
fieldType
|
|
1624
|
-
);
|
|
1625
|
-
|
|
1626
|
-
if (isUnwrapped) {
|
|
1627
|
-
// For unwrapped components, resolve directly to the 'value' property
|
|
1628
|
-
resolvers.push({
|
|
1629
|
-
typeName: archetypeName,
|
|
1630
|
-
fieldName: field,
|
|
1631
|
-
resolver: async (
|
|
1632
|
-
parent: any,
|
|
1633
|
-
args: any,
|
|
1634
|
-
context: any
|
|
1635
|
-
) => {
|
|
1636
|
-
const entityId = parent?.id;
|
|
1637
|
-
if (!entityId) return (parent as any)[field];
|
|
1638
|
-
|
|
1639
|
-
// Check if parent is an Entity with component state
|
|
1640
|
-
if (parent instanceof Entity) {
|
|
1641
|
-
// If component was explicitly removed, return null immediately
|
|
1642
|
-
if (parent.wasRemoved(ctor)) {
|
|
1643
|
-
return null;
|
|
1644
|
-
}
|
|
1645
|
-
const inMemoryComp = parent.getInMemory(ctor);
|
|
1646
|
-
if (inMemoryComp) {
|
|
1647
|
-
return (inMemoryComp as any)?.value;
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
// Use DataLoader if available
|
|
1652
|
-
if (context?.loaders?.componentsByEntityType) {
|
|
1653
|
-
const componentData =
|
|
1654
|
-
await context.loaders.componentsByEntityType.load(
|
|
1655
|
-
{
|
|
1656
|
-
entityId: entityId,
|
|
1657
|
-
typeId: typeIdHex,
|
|
1658
|
-
}
|
|
1659
|
-
);
|
|
1660
|
-
if (componentData?.data?.value !== undefined) {
|
|
1661
|
-
return componentData.data.value;
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
// Fallback: ensure we have an Entity and query directly
|
|
1666
|
-
const entity = await BaseArcheType.ensureEntity(parent, context);
|
|
1667
|
-
const comp = await entity.get(ctor);
|
|
1668
|
-
return (comp as any)?.value;
|
|
1669
|
-
},
|
|
1670
|
-
});
|
|
1671
|
-
} else {
|
|
1672
|
-
// For complex components, return the full component object
|
|
1673
|
-
resolvers.push({
|
|
1674
|
-
typeName: archetypeName,
|
|
1675
|
-
fieldName: field,
|
|
1676
|
-
resolver: async (
|
|
1677
|
-
parent: any,
|
|
1678
|
-
args: any,
|
|
1679
|
-
context: any
|
|
1680
|
-
) => {
|
|
1681
|
-
const entityId = parent?.id;
|
|
1682
|
-
if (!entityId) return (parent as any)[field];
|
|
1683
|
-
|
|
1684
|
-
// Check if parent is an Entity with the component already loaded in memory
|
|
1685
|
-
// This avoids cache/DataLoader issues for freshly created entities
|
|
1686
|
-
// Use synchronous getInMemory() to avoid triggering unnecessary DB queries
|
|
1687
|
-
if (parent instanceof Entity) {
|
|
1688
|
-
// If component was explicitly removed, return null immediately
|
|
1689
|
-
// This prevents stale DataLoader cache from returning old data
|
|
1690
|
-
if (parent.wasRemoved(ctor)) {
|
|
1691
|
-
return null;
|
|
1692
|
-
}
|
|
1693
|
-
const inMemoryComp = parent.getInMemory(ctor);
|
|
1694
|
-
if (inMemoryComp) {
|
|
1695
|
-
return inMemoryComp;
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
// Use DataLoader if available
|
|
1700
|
-
if (context?.loaders?.componentsByEntityType) {
|
|
1701
|
-
const componentData =
|
|
1702
|
-
await context.loaders.componentsByEntityType.load(
|
|
1703
|
-
{
|
|
1704
|
-
entityId: entityId,
|
|
1705
|
-
typeId: typeIdHex,
|
|
1706
|
-
}
|
|
1707
|
-
);
|
|
1708
|
-
if (componentData?.data) {
|
|
1709
|
-
return componentData.data;
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
// Fallback: ensure we have an Entity and query directly
|
|
1714
|
-
const entity = await BaseArcheType.ensureEntity(parent, context);
|
|
1715
|
-
const comp = await entity.get(ctor);
|
|
1716
|
-
return comp;
|
|
1717
|
-
},
|
|
1718
|
-
});
|
|
1719
|
-
|
|
1720
|
-
// Generate nested field resolvers for component properties
|
|
1721
|
-
const componentTypeName = compNameToFieldName(componentName);
|
|
1722
|
-
|
|
1723
|
-
for (const prop of componentProps) {
|
|
1724
|
-
resolvers.push({
|
|
1725
|
-
typeName: componentTypeName, // Use lowercase component name
|
|
1726
|
-
fieldName: prop.propertyKey,
|
|
1727
|
-
resolver: (parent: any) => parent[prop.propertyKey],
|
|
1728
|
-
});
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
// Generate resolvers for union fields
|
|
1734
|
-
for (const [field, components] of Object.entries(this.unionMap)) {
|
|
1735
|
-
resolvers.push({
|
|
1736
|
-
typeName: archetypeName,
|
|
1737
|
-
fieldName: field,
|
|
1738
|
-
resolver: async (parent: any, args: any, context: any) => {
|
|
1739
|
-
const entityId = parent?.id;
|
|
1740
|
-
if (!entityId) return null;
|
|
1741
|
-
|
|
1742
|
-
// Try to find which component in the union is present on the entity
|
|
1743
|
-
for (const component of components) {
|
|
1744
|
-
const typeId = storage.getComponentId(component.name);
|
|
1745
|
-
|
|
1746
|
-
// Check if parent is an Entity with component state
|
|
1747
|
-
if (parent instanceof Entity) {
|
|
1748
|
-
// If component was explicitly removed, skip it
|
|
1749
|
-
if (parent.wasRemoved(component)) {
|
|
1750
|
-
continue;
|
|
1751
|
-
}
|
|
1752
|
-
const inMemoryComp = parent.getInMemory(component);
|
|
1753
|
-
if (inMemoryComp) {
|
|
1754
|
-
return {
|
|
1755
|
-
__typename: compNameToFieldName(component.name),
|
|
1756
|
-
...(inMemoryComp as any).data?.() ?? inMemoryComp,
|
|
1757
|
-
};
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
if (context?.loaders?.componentsByEntityType) {
|
|
1762
|
-
const componentData =
|
|
1763
|
-
await context.loaders.componentsByEntityType.load(
|
|
1764
|
-
{
|
|
1765
|
-
entityId: entityId,
|
|
1766
|
-
typeId: typeId,
|
|
1767
|
-
}
|
|
1768
|
-
);
|
|
1769
|
-
if (componentData?.data) {
|
|
1770
|
-
// Add __typename for GraphQL union resolution
|
|
1771
|
-
return {
|
|
1772
|
-
__typename: compNameToFieldName(
|
|
1773
|
-
component.name
|
|
1774
|
-
),
|
|
1775
|
-
...componentData.data,
|
|
1776
|
-
};
|
|
1777
|
-
}
|
|
1778
|
-
} else {
|
|
1779
|
-
// Fallback: ensure we have an Entity and query directly
|
|
1780
|
-
const entity = await BaseArcheType.ensureEntity(parent, context);
|
|
1781
|
-
const comp = await entity.get(component);
|
|
1782
|
-
if (comp) {
|
|
1783
|
-
return {
|
|
1784
|
-
__typename: compNameToFieldName(component.name),
|
|
1785
|
-
...(comp as any),
|
|
1786
|
-
};
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
return null;
|
|
1792
|
-
},
|
|
1793
|
-
});
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
// Generate resolvers for relation fields
|
|
1797
|
-
for (const [field, relatedArcheType] of Object.entries(
|
|
1798
|
-
this.relationMap
|
|
1799
|
-
)) {
|
|
1800
|
-
const relationType = this.relationTypes[field];
|
|
1801
|
-
const relationOptions = this.relationOptions[field];
|
|
1802
|
-
const isArray =
|
|
1803
|
-
relationType === "hasMany" || relationType === "belongsToMany";
|
|
1804
|
-
|
|
1805
|
-
// Get the related archetype name
|
|
1806
|
-
let relatedTypeName: string;
|
|
1807
|
-
if (typeof relatedArcheType === "string") {
|
|
1808
|
-
relatedTypeName = relatedArcheType;
|
|
1809
|
-
} else {
|
|
1810
|
-
const relatedArchetypeId = storage.getComponentId(
|
|
1811
|
-
relatedArcheType.name
|
|
1812
|
-
);
|
|
1813
|
-
const relatedArchetypeMetadata = storage.archetypes.find(
|
|
1814
|
-
(a) => a.typeId === relatedArchetypeId
|
|
1815
|
-
);
|
|
1816
|
-
relatedTypeName =
|
|
1817
|
-
relatedArchetypeMetadata?.name ||
|
|
1818
|
-
relatedArcheType.name.replace(/ArcheType$/, "");
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
if (
|
|
1822
|
-
!isArray &&
|
|
1823
|
-
relationType === "belongsTo" &&
|
|
1824
|
-
relationOptions?.foreignKey
|
|
1825
|
-
) {
|
|
1826
|
-
resolvers.push({
|
|
1827
|
-
typeName: archetypeName,
|
|
1828
|
-
fieldName: field,
|
|
1829
|
-
resolver: async (
|
|
1830
|
-
parent: any,
|
|
1831
|
-
args: any,
|
|
1832
|
-
context: any
|
|
1833
|
-
) => {
|
|
1834
|
-
const entityId = parent?.id;
|
|
1835
|
-
if (!entityId) {
|
|
1836
|
-
return null;
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
let foreignId: string | undefined;
|
|
1840
|
-
|
|
1841
|
-
// Attempt to load the component that holds the foreign key via DataLoader
|
|
1842
|
-
if (context?.loaders?.componentsByEntityType) {
|
|
1843
|
-
const foreignKey = relationOptions.foreignKey;
|
|
1844
|
-
if (foreignKey && foreignKey.includes('.')) {
|
|
1845
|
-
// Handle nested foreign key like "field.property"
|
|
1846
|
-
const [fieldName, propName] = foreignKey.split('.');
|
|
1847
|
-
const compCtor = this.componentMap[fieldName!];
|
|
1848
|
-
if (compCtor) {
|
|
1849
|
-
const typeIdForComponent = storage.getComponentId(compCtor.name);
|
|
1850
|
-
const componentData = await context.loaders.componentsByEntityType.load({
|
|
1851
|
-
entityId: entityId,
|
|
1852
|
-
typeId: typeIdForComponent,
|
|
1853
|
-
});
|
|
1854
|
-
if (componentData?.data && componentData.data[propName!] !== undefined) {
|
|
1855
|
-
foreignId = componentData.data[propName!];
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
} else {
|
|
1859
|
-
// Original logic for flat foreign key
|
|
1860
|
-
for (const [componentField, compCtor] of Object.entries(this.componentMap)) {
|
|
1861
|
-
const typeIdForComponent = storage.getComponentId(compCtor.name);
|
|
1862
|
-
const componentProps = storage.getComponentProperties(typeIdForComponent);
|
|
1863
|
-
const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
|
|
1864
|
-
if (!hasForeignKey || !foreignKey) continue;
|
|
1865
|
-
|
|
1866
|
-
const componentData = await context.loaders.componentsByEntityType.load({
|
|
1867
|
-
entityId: entityId,
|
|
1868
|
-
typeId: typeIdForComponent,
|
|
1869
|
-
});
|
|
1870
|
-
|
|
1871
|
-
if (componentData?.data && componentData.data[foreignKey] !== undefined) {
|
|
1872
|
-
foreignId = componentData.data[foreignKey];
|
|
1873
|
-
break;
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
// Fallback: pull the component from the entity directly when DataLoader misses
|
|
1880
|
-
if (!foreignId) {
|
|
1881
|
-
const entity = await BaseArcheType.ensureEntity(parent, context);
|
|
1882
|
-
const foreignKey = relationOptions.foreignKey;
|
|
1883
|
-
if (foreignKey && foreignKey.includes('.')) {
|
|
1884
|
-
// Handle nested foreign key like "field.property"
|
|
1885
|
-
const [fieldName, propName] = foreignKey.split('.');
|
|
1886
|
-
const compCtor = this.componentMap[fieldName!];
|
|
1887
|
-
if (compCtor) {
|
|
1888
|
-
const componentInstance = await entity.get(compCtor as any);
|
|
1889
|
-
if (componentInstance && (componentInstance as any)[propName!] !== undefined) {
|
|
1890
|
-
foreignId = (componentInstance as any)[propName!];
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
} else {
|
|
1894
|
-
// Original logic for flat foreign key
|
|
1895
|
-
for (const compCtor of Object.values(this.componentMap)) {
|
|
1896
|
-
const typeIdForComponent = storage.getComponentId(compCtor.name);
|
|
1897
|
-
const componentProps = storage.getComponentProperties(typeIdForComponent);
|
|
1898
|
-
const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
|
|
1899
|
-
if (!hasForeignKey || !foreignKey) continue;
|
|
1900
|
-
const componentInstance = await entity.get(compCtor as any);
|
|
1901
|
-
if (componentInstance && (componentInstance as any)[foreignKey] !== undefined) {
|
|
1902
|
-
foreignId = (componentInstance as any)[foreignKey];
|
|
1903
|
-
break;
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
if (!foreignId && relationOptions.foreignKey === 'id') {
|
|
1910
|
-
foreignId = entityId;
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
|
-
if (!foreignId) {
|
|
1914
|
-
return null;
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
|
-
// Resolve the related entity using loaders when possible, otherwise hit the database directly
|
|
1918
|
-
if (context.loaders?.entityById) {
|
|
1919
|
-
const relatedEntity =
|
|
1920
|
-
await context.loaders.entityById.load(
|
|
1921
|
-
foreignId
|
|
1922
|
-
);
|
|
1923
|
-
if (relatedEntity) {
|
|
1924
|
-
return relatedEntity;
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
|
|
1928
|
-
return Entity.FindById(foreignId);
|
|
1929
|
-
},
|
|
1930
|
-
});
|
|
1931
|
-
} else if (isArray) {
|
|
1932
|
-
// Array relation resolver
|
|
1933
|
-
resolvers.push({
|
|
1934
|
-
typeName: archetypeName,
|
|
1935
|
-
fieldName: field,
|
|
1936
|
-
resolver: async (
|
|
1937
|
-
parent: any,
|
|
1938
|
-
args: any,
|
|
1939
|
-
context: any
|
|
1940
|
-
) => {
|
|
1941
|
-
const entityId = parent?.id;
|
|
1942
|
-
if (!entityId) return [];
|
|
1943
|
-
|
|
1944
|
-
// If foreignKey is specified, for hasMany, the foreign key is on the related entity
|
|
1945
|
-
if (relationOptions?.foreignKey) {
|
|
1946
|
-
// Find the component that has the foreign key (may be nested like "field.property")
|
|
1947
|
-
let componentCtor: any = null;
|
|
1948
|
-
let foreignKeyField: string = relationOptions.foreignKey;
|
|
1949
|
-
let relatedArchetypeInstance: any = null;
|
|
1950
|
-
|
|
1951
|
-
if (typeof relatedArcheType === "function") {
|
|
1952
|
-
relatedArchetypeInstance = new (relatedArcheType as any)();
|
|
1953
|
-
} else if (typeof relatedArcheType === "string") {
|
|
1954
|
-
// Find the archetype class by name
|
|
1955
|
-
const relatedArchetypeMetadata = storage.archetypes.find((a) => a.name === relatedArcheType);
|
|
1956
|
-
if (relatedArchetypeMetadata) {
|
|
1957
|
-
relatedArchetypeInstance = new (relatedArchetypeMetadata.target as any)();
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
if (relatedArchetypeInstance) {
|
|
1962
|
-
if (relationOptions.foreignKey.includes('.')) {
|
|
1963
|
-
const [fieldName, propName] = relationOptions.foreignKey.split('.');
|
|
1964
|
-
componentCtor = relatedArchetypeInstance.componentMap[fieldName!];
|
|
1965
|
-
foreignKeyField = propName!;
|
|
1966
|
-
} else {
|
|
1967
|
-
// Flat foreign key
|
|
1968
|
-
for (const comp of Object.values(relatedArchetypeInstance.componentMap) as any[]) {
|
|
1969
|
-
const typeId = storage.getComponentId(comp.name);
|
|
1970
|
-
const props = storage.getComponentProperties(typeId);
|
|
1971
|
-
if (props.some(p => p.propertyKey === relationOptions.foreignKey)) {
|
|
1972
|
-
componentCtor = comp;
|
|
1973
|
-
break;
|
|
1974
|
-
}
|
|
1975
|
-
}
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
if (componentCtor) {
|
|
1980
|
-
const query = new Query();
|
|
1981
|
-
query.with(componentCtor, Query.filters(Query.filter(foreignKeyField, Query.filterOp.EQ, entityId)));
|
|
1982
|
-
return await query.exec();
|
|
1983
|
-
} else {
|
|
1984
|
-
console.warn(`No component found with foreign key ${relationOptions.foreignKey} in ${relatedTypeName}`);
|
|
1985
|
-
return [];
|
|
1986
|
-
}
|
|
1987
|
-
} else {
|
|
1988
|
-
// Use DataLoader for relation loading if available
|
|
1989
|
-
if (
|
|
1990
|
-
context?.loaders?.relationsByEntityField
|
|
1991
|
-
) {
|
|
1992
|
-
return context.loaders.relationsByEntityField.load({
|
|
1993
|
-
entityId: entityId,
|
|
1994
|
-
relationField: field,
|
|
1995
|
-
relatedType: relatedTypeName,
|
|
1996
|
-
foreignKey: relationOptions?.foreignKey,
|
|
1997
|
-
});
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
// Fallback: return empty array or implement custom relation query
|
|
2001
|
-
// This should be implemented based on your relation storage strategy
|
|
2002
|
-
console.warn(
|
|
2003
|
-
`No relationsByEntityField loader found for array relation ${field} on ${archetypeName}`
|
|
2004
|
-
);
|
|
2005
|
-
return [];
|
|
2006
|
-
}
|
|
2007
|
-
},
|
|
2008
|
-
});
|
|
2009
|
-
} else {
|
|
2010
|
-
// Single relation resolver
|
|
2011
|
-
resolvers.push({
|
|
2012
|
-
typeName: archetypeName,
|
|
2013
|
-
fieldName: field,
|
|
2014
|
-
resolver: async (
|
|
2015
|
-
parent: any,
|
|
2016
|
-
args: any,
|
|
2017
|
-
context: any
|
|
2018
|
-
) => {
|
|
2019
|
-
const entityId = parent?.id;
|
|
2020
|
-
|
|
2021
|
-
// If foreignKey is specified, treat as belongsTo (foreign key on this entity)
|
|
2022
|
-
if (relationOptions?.foreignKey) {
|
|
2023
|
-
if (!entityId) {
|
|
2024
|
-
return null;
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
let foreignId: string | undefined;
|
|
2028
|
-
|
|
2029
|
-
// Attempt to load the component that holds the foreign key via DataLoader
|
|
2030
|
-
if (context?.loaders?.componentsByEntityType) {
|
|
2031
|
-
const foreignKey = relationOptions.foreignKey;
|
|
2032
|
-
if (foreignKey && foreignKey.includes('.')) {
|
|
2033
|
-
// Handle nested foreign key like "field.property"
|
|
2034
|
-
const [fieldName, propName] = foreignKey.split('.');
|
|
2035
|
-
const compCtor = this.componentMap[fieldName!];
|
|
2036
|
-
if (compCtor) {
|
|
2037
|
-
const typeIdForComponent = storage.getComponentId(compCtor.name);
|
|
2038
|
-
const componentData = await context.loaders.componentsByEntityType.load({
|
|
2039
|
-
entityId: entityId,
|
|
2040
|
-
typeId: typeIdForComponent,
|
|
2041
|
-
});
|
|
2042
|
-
if (componentData?.data && componentData.data[propName!] !== undefined) {
|
|
2043
|
-
foreignId = componentData.data[propName!];
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
} else {
|
|
2047
|
-
// OPTIMIZED: Load all candidate components in parallel via DataLoader
|
|
2048
|
-
const candidateLoads: Array<{ compCtor: any; typeId: string }> = [];
|
|
2049
|
-
for (const [componentField, compCtor] of Object.entries(this.componentMap)) {
|
|
2050
|
-
const typeIdForComponent = storage.getComponentId(compCtor.name);
|
|
2051
|
-
const componentProps = storage.getComponentProperties(typeIdForComponent);
|
|
2052
|
-
const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
|
|
2053
|
-
if (hasForeignKey && foreignKey) {
|
|
2054
|
-
candidateLoads.push({ compCtor, typeId: typeIdForComponent });
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
if (candidateLoads.length > 0) {
|
|
2059
|
-
// Load all candidate components in parallel
|
|
2060
|
-
const componentDataResults = await Promise.all(
|
|
2061
|
-
candidateLoads.map(({ typeId }) =>
|
|
2062
|
-
context.loaders.componentsByEntityType.load({
|
|
2063
|
-
entityId: entityId,
|
|
2064
|
-
typeId: typeId,
|
|
2065
|
-
})
|
|
2066
|
-
)
|
|
2067
|
-
);
|
|
2068
|
-
|
|
2069
|
-
// Find the first one with the foreign key value
|
|
2070
|
-
for (const componentData of componentDataResults) {
|
|
2071
|
-
if (componentData?.data && componentData.data[foreignKey] !== undefined) {
|
|
2072
|
-
foreignId = componentData.data[foreignKey];
|
|
2073
|
-
break;
|
|
2074
|
-
}
|
|
2075
|
-
}
|
|
2076
|
-
}
|
|
2077
|
-
}
|
|
2078
|
-
}
|
|
2079
|
-
|
|
2080
|
-
// Fallback: pull the component from the entity directly when DataLoader misses
|
|
2081
|
-
if (!foreignId) {
|
|
2082
|
-
const entity = await BaseArcheType.ensureEntity(parent, context);
|
|
2083
|
-
const foreignKey = relationOptions.foreignKey;
|
|
2084
|
-
if (foreignKey && foreignKey.includes('.')) {
|
|
2085
|
-
// Handle nested foreign key like "field.property"
|
|
2086
|
-
const [fieldName, propName] = foreignKey.split('.');
|
|
2087
|
-
const compCtor = this.componentMap[fieldName!];
|
|
2088
|
-
if (compCtor) {
|
|
2089
|
-
const componentInstance = await entity.get(compCtor as any);
|
|
2090
|
-
if (componentInstance && (componentInstance as any)[propName!] !== undefined) {
|
|
2091
|
-
foreignId = (componentInstance as any)[propName!];
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
} else {
|
|
2095
|
-
// OPTIMIZED: Find candidates first, then load in parallel
|
|
2096
|
-
const candidateComponents: Array<{ compCtor: any }> = [];
|
|
2097
|
-
for (const compCtor of Object.values(this.componentMap)) {
|
|
2098
|
-
const typeIdForComponent = storage.getComponentId(compCtor.name);
|
|
2099
|
-
const componentProps = storage.getComponentProperties(typeIdForComponent);
|
|
2100
|
-
const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
|
|
2101
|
-
if (hasForeignKey && foreignKey) {
|
|
2102
|
-
candidateComponents.push({ compCtor });
|
|
2103
|
-
}
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
if (candidateComponents.length > 0) {
|
|
2107
|
-
// Load all candidate components in parallel
|
|
2108
|
-
const componentInstances = await Promise.all(
|
|
2109
|
-
candidateComponents.map(({ compCtor }) => entity.get(compCtor as any))
|
|
2110
|
-
);
|
|
2111
|
-
|
|
2112
|
-
// Find the first one with the foreign key value
|
|
2113
|
-
for (const componentInstance of componentInstances) {
|
|
2114
|
-
if (componentInstance && (componentInstance as any)[foreignKey] !== undefined) {
|
|
2115
|
-
foreignId = (componentInstance as any)[foreignKey];
|
|
2116
|
-
break;
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
}
|
|
2120
|
-
}
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
if (!foreignId) {
|
|
2124
|
-
return null;
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
// Resolve the related entity using loaders when possible, otherwise hit the database directly
|
|
2128
|
-
if (context?.loaders?.entityById) {
|
|
2129
|
-
const relatedEntity = await context.loaders.entityById.load(foreignId);
|
|
2130
|
-
if (relatedEntity) {
|
|
2131
|
-
return relatedEntity;
|
|
2132
|
-
}
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
return Entity.FindById(foreignId);
|
|
2136
|
-
} else {
|
|
2137
|
-
// Use DataLoader for relation loading if available
|
|
2138
|
-
if (
|
|
2139
|
-
context?.loaders?.relationsByEntityField
|
|
2140
|
-
) {
|
|
2141
|
-
const results =
|
|
2142
|
-
await context.loaders.relationsByEntityField.load(
|
|
2143
|
-
{
|
|
2144
|
-
entityId: entityId,
|
|
2145
|
-
relationField: field,
|
|
2146
|
-
relatedType: relatedTypeName,
|
|
2147
|
-
foreignKey: relationOptions?.foreignKey,
|
|
2148
|
-
}
|
|
2149
|
-
);
|
|
2150
|
-
if (results.length > 0) {
|
|
2151
|
-
return results[0];
|
|
2152
|
-
}
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
// Fallback: return null or implement custom relation query
|
|
2156
|
-
console.warn(
|
|
2157
|
-
`No relationsByEntityField loader found for single relation ${field} on ${archetypeName}`
|
|
2158
|
-
);
|
|
2159
|
-
return null;
|
|
2160
|
-
}
|
|
2161
|
-
},
|
|
2162
|
-
});
|
|
2163
|
-
}
|
|
2164
|
-
}
|
|
2165
|
-
|
|
2166
|
-
// Generate resolvers for archetype functions
|
|
2167
|
-
for (const { propertyKey, options } of this.functions) {
|
|
2168
|
-
resolvers.push({
|
|
2169
|
-
typeName: archetypeName,
|
|
2170
|
-
fieldName: propertyKey,
|
|
2171
|
-
resolver: async (parent: any, args: any, context: any) => {
|
|
2172
|
-
// Ensure parent is a proper Entity instance
|
|
2173
|
-
// When coming from cache or GraphQL chain, parent might be a plain object
|
|
2174
|
-
let entity: Entity;
|
|
2175
|
-
if (parent instanceof Entity) {
|
|
2176
|
-
entity = parent;
|
|
2177
|
-
} else if (parent && parent.id) {
|
|
2178
|
-
// Parent is a plain object with an ID - load the entity
|
|
2179
|
-
if (context.loaders?.entityById) {
|
|
2180
|
-
const loadedEntity = await context.loaders.entityById.load(parent.id);
|
|
2181
|
-
if (loadedEntity) {
|
|
2182
|
-
entity = loadedEntity;
|
|
2183
|
-
} else {
|
|
2184
|
-
// Create a new Entity instance with the ID
|
|
2185
|
-
entity = new Entity(parent.id);
|
|
2186
|
-
entity.setPersisted(true);
|
|
2187
|
-
}
|
|
2188
|
-
} else {
|
|
2189
|
-
// No DataLoader available - create Entity instance directly
|
|
2190
|
-
entity = new Entity(parent.id);
|
|
2191
|
-
entity.setPersisted(true);
|
|
2192
|
-
}
|
|
2193
|
-
} else {
|
|
2194
|
-
throw new Error(`Invalid parent for ${archetypeName}.${propertyKey}: parent must have an 'id' property`);
|
|
2195
|
-
}
|
|
2196
|
-
|
|
2197
|
-
// If function has arguments, extract and convert them
|
|
2198
|
-
if (options?.args && options.args.length > 0 && args) {
|
|
2199
|
-
const functionArgs: any[] = [];
|
|
2200
|
-
|
|
2201
|
-
for (const argDef of options.args) {
|
|
2202
|
-
const argValue = args[argDef.name];
|
|
2203
|
-
|
|
2204
|
-
if (argValue === undefined || argValue === null) {
|
|
2205
|
-
if (!argDef.nullable) {
|
|
2206
|
-
throw new Error(`Required argument '${argDef.name}' is missing for ${archetypeName}.${propertyKey}`);
|
|
2207
|
-
}
|
|
2208
|
-
functionArgs.push(null);
|
|
2209
|
-
continue;
|
|
2210
|
-
}
|
|
2211
|
-
|
|
2212
|
-
// Convert argument value to the expected type
|
|
2213
|
-
let convertedValue: any = argValue;
|
|
2214
|
-
|
|
2215
|
-
// Check if it's a custom type that needs instantiation
|
|
2216
|
-
if (argDef.type && typeof argDef.type === 'function' && argDef.type !== String && argDef.type !== Number && argDef.type !== Boolean && argDef.type !== Date) {
|
|
2217
|
-
// Check if it's a registered custom type (like ST_Point)
|
|
2218
|
-
const isCustomType = customTypeRegistry.has(argDef.type) ||
|
|
2219
|
-
customTypeNameRegistry.has(argDef.type) ||
|
|
2220
|
-
(argDef.type?.name && registeredCustomTypes.has(argDef.type.name));
|
|
2221
|
-
|
|
2222
|
-
if (isCustomType && typeof argValue === 'object' && !Array.isArray(argValue)) {
|
|
2223
|
-
// Try to instantiate the type if it's a class constructor
|
|
2224
|
-
try {
|
|
2225
|
-
if (argDef.type.prototype && argDef.type.prototype.constructor) {
|
|
2226
|
-
// It's a class, try to instantiate it
|
|
2227
|
-
// First, try object assignment (works for most cases)
|
|
2228
|
-
convertedValue = Object.assign(Object.create(argDef.type.prototype), argValue);
|
|
2229
|
-
|
|
2230
|
-
// Verify the instance was created correctly
|
|
2231
|
-
if (!convertedValue || !(convertedValue instanceof argDef.type)) {
|
|
2232
|
-
// If object assignment didn't work, try constructor with common patterns
|
|
2233
|
-
// This is a fallback for types that require constructor parameters
|
|
2234
|
-
const constructor = argDef.type.prototype.constructor;
|
|
2235
|
-
const paramCount = constructor.length;
|
|
2236
|
-
|
|
2237
|
-
if (paramCount === 2) {
|
|
2238
|
-
// Try common 2-parameter patterns
|
|
2239
|
-
if (argValue.latitude !== undefined && argValue.longitude !== undefined) {
|
|
2240
|
-
convertedValue = new argDef.type(argValue.latitude, argValue.longitude);
|
|
2241
|
-
} else if (argValue.x !== undefined && argValue.y !== undefined) {
|
|
2242
|
-
convertedValue = new argDef.type(argValue.x, argValue.y);
|
|
2243
|
-
} else {
|
|
2244
|
-
// Fallback: use first two object values
|
|
2245
|
-
const values = Object.values(argValue);
|
|
2246
|
-
if (values.length >= 2) {
|
|
2247
|
-
convertedValue = new argDef.type(values[0], values[1]);
|
|
2248
|
-
}
|
|
2249
|
-
}
|
|
2250
|
-
} else if (paramCount === 1) {
|
|
2251
|
-
// Single parameter - try first property value
|
|
2252
|
-
const values = Object.values(argValue);
|
|
2253
|
-
if (values.length >= 1) {
|
|
2254
|
-
convertedValue = new argDef.type(values[0]);
|
|
2255
|
-
}
|
|
2256
|
-
} else if (paramCount === 0) {
|
|
2257
|
-
// No parameters - object assignment should work
|
|
2258
|
-
convertedValue = Object.assign(Object.create(argDef.type.prototype), argValue);
|
|
2259
|
-
}
|
|
2260
|
-
|
|
2261
|
-
// Final fallback
|
|
2262
|
-
if (!convertedValue || !(convertedValue instanceof argDef.type)) {
|
|
2263
|
-
convertedValue = Object.assign(Object.create(argDef.type.prototype), argValue);
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
|
-
} else {
|
|
2267
|
-
// Not a class, use the value as-is
|
|
2268
|
-
convertedValue = argValue;
|
|
2269
|
-
}
|
|
2270
|
-
} catch (e) {
|
|
2271
|
-
// If instantiation fails, try object assignment
|
|
2272
|
-
try {
|
|
2273
|
-
convertedValue = Object.assign(Object.create(argDef.type.prototype || {}), argValue);
|
|
2274
|
-
} catch (e2) {
|
|
2275
|
-
// Fallback to plain object
|
|
2276
|
-
convertedValue = argValue;
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
} else {
|
|
2280
|
-
convertedValue = argValue;
|
|
2281
|
-
}
|
|
2282
|
-
}
|
|
2283
|
-
|
|
2284
|
-
functionArgs.push(convertedValue);
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
// Call function with entity and arguments
|
|
2288
|
-
return await (this as any)[propertyKey](entity, ...functionArgs);
|
|
2289
|
-
} else {
|
|
2290
|
-
// No arguments, call with just entity
|
|
2291
|
-
return await (this as any)[propertyKey](entity);
|
|
2292
|
-
}
|
|
2293
|
-
},
|
|
2294
|
-
});
|
|
2295
|
-
}
|
|
2296
|
-
|
|
2297
|
-
return resolvers;
|
|
853
|
+
const { buildFieldResolvers } = require("./archetype/fieldResolvers");
|
|
854
|
+
return buildFieldResolvers(this);
|
|
2298
855
|
}
|
|
2299
856
|
|
|
2300
857
|
/**
|
|
@@ -2337,597 +894,8 @@ export class BaseArcheType {
|
|
|
2337
894
|
}
|
|
2338
895
|
|
|
2339
896
|
public getZodObjectSchema(options?: { excludeRelations?: boolean; excludeFunctions?: boolean }): ZodObject<any> {
|
|
2340
|
-
const
|
|
2341
|
-
|
|
2342
|
-
const zodShapes: Record<string, any> = {};
|
|
2343
|
-
const storage = getMetadataStorage();
|
|
2344
|
-
const unionSchemas: Array<{
|
|
2345
|
-
fieldName: string;
|
|
2346
|
-
schema: any;
|
|
2347
|
-
components: any[];
|
|
2348
|
-
}> = [];
|
|
2349
|
-
|
|
2350
|
-
for (const [field, ctor] of Object.entries(this.componentMap)) {
|
|
2351
|
-
// Skip union fields - they'll be processed separately
|
|
2352
|
-
if (field.startsWith("union_")) {
|
|
2353
|
-
continue;
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
|
-
const type = this.fieldTypes[field];
|
|
2357
|
-
const typeId = storage.getComponentId(ctor.name);
|
|
2358
|
-
const componentProps = storage.getComponentProperties(typeId);
|
|
2359
|
-
|
|
2360
|
-
// Check if component should be unwrapped based on field type
|
|
2361
|
-
if (shouldUnwrapComponent(componentProps, type)) {
|
|
2362
|
-
// Unwrap to primitive type
|
|
2363
|
-
if (type === String) {
|
|
2364
|
-
zodShapes[field] = z.string();
|
|
2365
|
-
} else if (type === Number) {
|
|
2366
|
-
zodShapes[field] = z.number();
|
|
2367
|
-
} else if (type === Boolean) {
|
|
2368
|
-
zodShapes[field] = z.boolean();
|
|
2369
|
-
} else if (type === Date) {
|
|
2370
|
-
zodShapes[field] = z.date();
|
|
2371
|
-
}
|
|
2372
|
-
} else {
|
|
2373
|
-
// Use component schema for complex types
|
|
2374
|
-
const componentSchema = getOrCreateComponentSchema(
|
|
2375
|
-
ctor,
|
|
2376
|
-
typeId,
|
|
2377
|
-
this.fieldOptions[field]
|
|
2378
|
-
);
|
|
2379
|
-
if (componentSchema) {
|
|
2380
|
-
zodShapes[field] = componentSchema;
|
|
2381
|
-
} else {
|
|
2382
|
-
// Skip components with no properties
|
|
2383
|
-
continue;
|
|
2384
|
-
}
|
|
2385
|
-
}
|
|
2386
|
-
|
|
2387
|
-
if (
|
|
2388
|
-
this.fieldOptions[field]?.nullable &&
|
|
2389
|
-
zodShapes[field] &&
|
|
2390
|
-
!(zodShapes[field] instanceof ZodObject)
|
|
2391
|
-
) {
|
|
2392
|
-
zodShapes[field] = zodShapes[field].nullish();
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
|
-
// Process union fields
|
|
2397
|
-
for (const [fieldName, components] of Object.entries(this.unionMap)) {
|
|
2398
|
-
// Generate schemas for each component in the union
|
|
2399
|
-
const unionComponentSchemas: any[] = [];
|
|
2400
|
-
const unionComponentCtors: any[] = [];
|
|
2401
|
-
|
|
2402
|
-
for (const component of components) {
|
|
2403
|
-
const typeId = storage.getComponentId(component.name);
|
|
2404
|
-
const componentSchema = getOrCreateComponentSchema(
|
|
2405
|
-
component,
|
|
2406
|
-
typeId,
|
|
2407
|
-
this.unionOptions[fieldName]
|
|
2408
|
-
);
|
|
2409
|
-
|
|
2410
|
-
if (componentSchema) {
|
|
2411
|
-
unionComponentSchemas.push(componentSchema);
|
|
2412
|
-
unionComponentCtors.push(component);
|
|
2413
|
-
}
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
// Create union type using Zod with GQLoom support
|
|
2417
|
-
if (unionComponentSchemas.length > 0) {
|
|
2418
|
-
const unionSchema = z
|
|
2419
|
-
.union(unionComponentSchemas)
|
|
2420
|
-
.register(asUnionType, {
|
|
2421
|
-
name:
|
|
2422
|
-
fieldName.charAt(0).toUpperCase() +
|
|
2423
|
-
fieldName.slice(1), // Capitalize field name for type
|
|
2424
|
-
resolveType: (it: any) => {
|
|
2425
|
-
// Determine which type this is based on __typename
|
|
2426
|
-
if (it.__typename) {
|
|
2427
|
-
return it.__typename;
|
|
2428
|
-
}
|
|
2429
|
-
// Fallback: check property presence
|
|
2430
|
-
for (
|
|
2431
|
-
let i = 0;
|
|
2432
|
-
i < unionComponentCtors.length;
|
|
2433
|
-
i++
|
|
2434
|
-
) {
|
|
2435
|
-
const componentProps =
|
|
2436
|
-
storage.getComponentProperties(
|
|
2437
|
-
storage.getComponentId(
|
|
2438
|
-
unionComponentCtors[i].name
|
|
2439
|
-
)
|
|
2440
|
-
);
|
|
2441
|
-
const hasUniqueProps = componentProps.some(
|
|
2442
|
-
(prop) =>
|
|
2443
|
-
it.hasOwnProperty(prop.propertyKey)
|
|
2444
|
-
);
|
|
2445
|
-
if (hasUniqueProps) {
|
|
2446
|
-
return compNameToFieldName(
|
|
2447
|
-
unionComponentCtors[i].name
|
|
2448
|
-
);
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
|
-
return compNameToFieldName(
|
|
2452
|
-
unionComponentCtors[0].name
|
|
2453
|
-
);
|
|
2454
|
-
},
|
|
2455
|
-
});
|
|
2456
|
-
|
|
2457
|
-
zodShapes[fieldName] = unionSchema;
|
|
2458
|
-
unionSchemas.push({
|
|
2459
|
-
fieldName,
|
|
2460
|
-
schema: unionSchema,
|
|
2461
|
-
components: unionComponentSchemas,
|
|
2462
|
-
});
|
|
2463
|
-
|
|
2464
|
-
// Apply nullable option for union fields
|
|
2465
|
-
if (this.unionOptions[fieldName]?.nullable) {
|
|
2466
|
-
zodShapes[fieldName] = zodShapes[fieldName].nullish();
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
// Process relations for GraphQL schema generation (skip if excludeRelations is true)
|
|
2472
|
-
if (!excludeRelations) {
|
|
2473
|
-
for (const [field, relatedArcheType] of Object.entries(
|
|
2474
|
-
this.relationMap
|
|
2475
|
-
)) {
|
|
2476
|
-
const relationType = this.relationTypes[field];
|
|
2477
|
-
const isArray =
|
|
2478
|
-
relationType === "hasMany" || relationType === "belongsToMany";
|
|
2479
|
-
|
|
2480
|
-
// Get the related archetype name
|
|
2481
|
-
let relatedTypeName: string;
|
|
2482
|
-
if (typeof relatedArcheType === "string") {
|
|
2483
|
-
relatedTypeName = relatedArcheType;
|
|
2484
|
-
} else {
|
|
2485
|
-
const relatedArchetypeId = storage.getComponentId(
|
|
2486
|
-
relatedArcheType.name
|
|
2487
|
-
);
|
|
2488
|
-
const relatedArchetypeMetadata = storage.archetypes.find(
|
|
2489
|
-
(a) => a.typeId === relatedArchetypeId
|
|
2490
|
-
);
|
|
2491
|
-
relatedTypeName =
|
|
2492
|
-
relatedArchetypeMetadata?.name ||
|
|
2493
|
-
relatedArcheType.name.replace(/ArcheType$/, "");
|
|
2494
|
-
}
|
|
2495
|
-
|
|
2496
|
-
// For GraphQL relations, we just store the type name as a string reference
|
|
2497
|
-
// The GraphQL schema will use the type name directly, and the full type definition
|
|
2498
|
-
// will be generated when each archetype's getZodObjectSchema() is called
|
|
2499
|
-
|
|
2500
|
-
// For singular relations, add description to the string schema
|
|
2501
|
-
const relatedTypeSchema = z
|
|
2502
|
-
.string()
|
|
2503
|
-
.describe(`Reference to ${relatedTypeName} type`);
|
|
2504
|
-
|
|
2505
|
-
if (isArray) {
|
|
2506
|
-
// HasMany and BelongsToMany should be optional by default (nullable array)
|
|
2507
|
-
// unless explicitly marked as required via nullable: false
|
|
2508
|
-
const shouldBeRequired = this.relationOptions[field]?.nullable === false;
|
|
2509
|
-
// For array relations, the description on the inner string won't show up in GraphQL
|
|
2510
|
-
// We need to store metadata about this being a relation for post-processing
|
|
2511
|
-
zodShapes[field] = shouldBeRequired
|
|
2512
|
-
? z.array(relatedTypeSchema)
|
|
2513
|
-
: z.array(relatedTypeSchema).optional();
|
|
2514
|
-
} else {
|
|
2515
|
-
zodShapes[field] = relatedTypeSchema;
|
|
2516
|
-
|
|
2517
|
-
// For singular relations, apply nullable option
|
|
2518
|
-
if (this.relationOptions[field]?.nullable) {
|
|
2519
|
-
zodShapes[field] = zodShapes[field].nullish();
|
|
2520
|
-
}
|
|
2521
|
-
}
|
|
2522
|
-
}
|
|
2523
|
-
}
|
|
2524
|
-
|
|
2525
|
-
// Process archetype functions
|
|
2526
|
-
// Store function input type names for post-processing
|
|
2527
|
-
const functionInputTypes = new Map<string, string>();
|
|
2528
|
-
|
|
2529
|
-
if (!excludeFunctions) {
|
|
2530
|
-
for (const { propertyKey, options } of this.functions) {
|
|
2531
|
-
let zodType;
|
|
2532
|
-
if (options?.returnType === 'number') {
|
|
2533
|
-
zodType = z.number();
|
|
2534
|
-
} else if (options?.returnType === 'string') {
|
|
2535
|
-
zodType = z.string();
|
|
2536
|
-
} else if (options?.returnType === 'boolean') {
|
|
2537
|
-
zodType = z.boolean();
|
|
2538
|
-
} else if (options?.returnType) {
|
|
2539
|
-
// Assume it's a GraphQL type name, create a string reference
|
|
2540
|
-
zodType = z.string().describe(`Reference to ${options.returnType} type`);
|
|
2541
|
-
} else {
|
|
2542
|
-
const returnType = Reflect.getMetadata("design:returntype", this.constructor.prototype, propertyKey);
|
|
2543
|
-
if (returnType === String) {
|
|
2544
|
-
zodType = z.string();
|
|
2545
|
-
} else if (returnType === Number) {
|
|
2546
|
-
zodType = z.number();
|
|
2547
|
-
} else if (returnType === Boolean) {
|
|
2548
|
-
zodType = z.boolean();
|
|
2549
|
-
} else {
|
|
2550
|
-
zodType = z.any();
|
|
2551
|
-
}
|
|
2552
|
-
}
|
|
2553
|
-
|
|
2554
|
-
// Process function arguments if present
|
|
2555
|
-
if (options?.args && options.args.length > 0) {
|
|
2556
|
-
const archetypeId = storage.getComponentId(this.constructor.name);
|
|
2557
|
-
const archetypeName =
|
|
2558
|
-
storage.archetypes.find((a) => a.typeId === archetypeId)?.name ||
|
|
2559
|
-
this.constructor.name;
|
|
2560
|
-
const inputTypeName = `${archetypeName}_${propertyKey}Args`;
|
|
2561
|
-
|
|
2562
|
-
// Create input type schema for arguments
|
|
2563
|
-
const inputFields: Record<string, any> = {};
|
|
2564
|
-
for (const arg of options.args) {
|
|
2565
|
-
let argZodType: any;
|
|
2566
|
-
|
|
2567
|
-
// Check if it's a registered custom type
|
|
2568
|
-
if (customTypeRegistry.has(arg.type)) {
|
|
2569
|
-
argZodType = customTypeRegistry.get(arg.type)!;
|
|
2570
|
-
} else if (arg.type === String || arg.type === String) {
|
|
2571
|
-
argZodType = z.string();
|
|
2572
|
-
} else if (arg.type === Number) {
|
|
2573
|
-
argZodType = z.number();
|
|
2574
|
-
} else if (arg.type === Boolean) {
|
|
2575
|
-
argZodType = z.boolean();
|
|
2576
|
-
} else if (arg.type === Date) {
|
|
2577
|
-
argZodType = z.date();
|
|
2578
|
-
} else if (registeredCustomTypes.has(arg.type?.name || '')) {
|
|
2579
|
-
// Check if it's registered by name
|
|
2580
|
-
argZodType = registeredCustomTypes.get(arg.type.name);
|
|
2581
|
-
} else {
|
|
2582
|
-
// Try to get from customTypeNameRegistry
|
|
2583
|
-
const typeName = customTypeNameRegistry.get(arg.type);
|
|
2584
|
-
if (typeName && registeredCustomTypes.has(typeName)) {
|
|
2585
|
-
argZodType = registeredCustomTypes.get(typeName);
|
|
2586
|
-
} else {
|
|
2587
|
-
console.warn(`[ArcheType] Unknown argument type for ${archetypeName}.${propertyKey}.${arg.name}: ${arg.type?.name || arg.type}. Falling back to z.any()`);
|
|
2588
|
-
argZodType = z.any();
|
|
2589
|
-
}
|
|
2590
|
-
}
|
|
2591
|
-
|
|
2592
|
-
// Apply nullable if specified
|
|
2593
|
-
if (arg.nullable) {
|
|
2594
|
-
argZodType = argZodType.optional();
|
|
2595
|
-
}
|
|
2596
|
-
|
|
2597
|
-
inputFields[arg.name] = argZodType;
|
|
2598
|
-
}
|
|
2599
|
-
|
|
2600
|
-
// Create and register the input type
|
|
2601
|
-
const inputSchema = z.object(inputFields).register(asObjectType, { name: inputTypeName });
|
|
2602
|
-
registeredCustomTypes.set(inputTypeName, inputSchema);
|
|
2603
|
-
functionInputTypes.set(propertyKey, inputTypeName);
|
|
2604
|
-
}
|
|
2605
|
-
|
|
2606
|
-
zodShapes[propertyKey] = zodType.optional();
|
|
2607
|
-
}
|
|
2608
|
-
}
|
|
2609
|
-
|
|
2610
|
-
const archetypeId = storage.getComponentId(this.constructor.name);
|
|
2611
|
-
const nameFromStorage =
|
|
2612
|
-
storage.archetypes.find((a) => a.typeId === archetypeId)?.name ||
|
|
2613
|
-
this.constructor.name;
|
|
2614
|
-
const shape: Record<string, any> = {
|
|
2615
|
-
__typename: z.literal(nameFromStorage).nullish(),
|
|
2616
|
-
id: z.string().nullish(), // Will be converted to ID in post-processing
|
|
2617
|
-
};
|
|
2618
|
-
for (const [field, zodType] of Object.entries(zodShapes)) {
|
|
2619
|
-
const isNullable =
|
|
2620
|
-
this.fieldOptions[field]?.nullable ||
|
|
2621
|
-
this.unionOptions[field]?.nullable;
|
|
2622
|
-
if (isNullable) {
|
|
2623
|
-
// For nullable fields, make them optional in the GraphQL schema
|
|
2624
|
-
shape[field] = zodType.optional();
|
|
2625
|
-
} else {
|
|
2626
|
-
shape[field] = zodType;
|
|
2627
|
-
}
|
|
2628
|
-
}
|
|
2629
|
-
const r = z.object(shape);
|
|
2630
|
-
|
|
2631
|
-
// Collect all component schemas used by this archetype for weaving
|
|
2632
|
-
const componentSchemasToWeave: any[] = [];
|
|
2633
|
-
for (const [field, zodType] of Object.entries(zodShapes)) {
|
|
2634
|
-
if (zodType instanceof ZodObject) {
|
|
2635
|
-
componentSchemasToWeave.push(zodType);
|
|
2636
|
-
} else if (
|
|
2637
|
-
Array.isArray(zodType) ||
|
|
2638
|
-
(zodType &&
|
|
2639
|
-
typeof zodType === "object" &&
|
|
2640
|
-
zodType._def?.typeName === "ZodUnion")
|
|
2641
|
-
) {
|
|
2642
|
-
// Handle union types
|
|
2643
|
-
if (zodType._def?.typeName === "ZodUnion") {
|
|
2644
|
-
componentSchemasToWeave.push(zodType);
|
|
2645
|
-
}
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
// Weave archetype schema along with its component schemas
|
|
2650
|
-
const schemasToWeave = [r];
|
|
2651
|
-
const schema = weave(ZodWeaver, ...schemasToWeave);
|
|
2652
|
-
let graphqlSchemaString = printSchema(schema);
|
|
2653
|
-
|
|
2654
|
-
// Post-process: Replace 'id: String' with 'id: ID' for all id fields
|
|
2655
|
-
graphqlSchemaString = graphqlSchemaString.replace(
|
|
2656
|
-
/\bid:\s*String\b/g,
|
|
2657
|
-
"id: ID"
|
|
2658
|
-
);
|
|
2659
|
-
|
|
2660
|
-
// Post-process: Replace relation field types with proper GraphQL type references
|
|
2661
|
-
for (const [field, relatedArcheType] of Object.entries(
|
|
2662
|
-
this.relationMap
|
|
2663
|
-
)) {
|
|
2664
|
-
const relationType = this.relationTypes[field];
|
|
2665
|
-
const isArray =
|
|
2666
|
-
relationType === "hasMany" || relationType === "belongsToMany";
|
|
2667
|
-
|
|
2668
|
-
let relatedTypeName: string;
|
|
2669
|
-
if (typeof relatedArcheType === "string") {
|
|
2670
|
-
relatedTypeName = relatedArcheType;
|
|
2671
|
-
} else {
|
|
2672
|
-
const relatedArchetypeId = storage.getComponentId(
|
|
2673
|
-
relatedArcheType.name
|
|
2674
|
-
);
|
|
2675
|
-
const relatedArchetypeMetadata = storage.archetypes.find(
|
|
2676
|
-
(a) => a.typeId === relatedArchetypeId
|
|
2677
|
-
);
|
|
2678
|
-
relatedTypeName =
|
|
2679
|
-
relatedArchetypeMetadata?.name ||
|
|
2680
|
-
relatedArcheType.name.replace(/ArcheType$/, "");
|
|
2681
|
-
}
|
|
2682
|
-
|
|
2683
|
-
// Replace the String field with proper GraphQL type reference
|
|
2684
|
-
if (isArray) {
|
|
2685
|
-
// For arrays: should be required only if explicitly set nullable: false
|
|
2686
|
-
const shouldBeRequired = this.relationOptions[field]?.nullable === false;
|
|
2687
|
-
const suffix = shouldBeRequired ? "!" : "";
|
|
2688
|
-
|
|
2689
|
-
// Step 1: Add description comment if it doesn't exist
|
|
2690
|
-
const descriptionPattern = new RegExp(`"""Reference to ${relatedTypeName} type"""[\\s\\S]*?${field}:`);
|
|
2691
|
-
if (!descriptionPattern.test(graphqlSchemaString)) {
|
|
2692
|
-
// Add description before the field
|
|
2693
|
-
const addDescriptionPattern = new RegExp(
|
|
2694
|
-
`(\\n\\s+)(${field}:\\s*\\[String!?\\]!?)`,
|
|
2695
|
-
"g"
|
|
2696
|
-
);
|
|
2697
|
-
graphqlSchemaString = graphqlSchemaString.replace(
|
|
2698
|
-
addDescriptionPattern,
|
|
2699
|
-
`$1"""Reference to ${relatedTypeName} type"""\n$1$2`
|
|
2700
|
-
);
|
|
2701
|
-
}
|
|
2702
|
-
|
|
2703
|
-
// Step 2: Replace [String!] or [String] with [TypeName!]
|
|
2704
|
-
const replaceTypePattern = new RegExp(
|
|
2705
|
-
`(${field}:\\s*)\\[String!?\\](!?)`,
|
|
2706
|
-
"g"
|
|
2707
|
-
);
|
|
2708
|
-
graphqlSchemaString = graphqlSchemaString.replace(
|
|
2709
|
-
replaceTypePattern,
|
|
2710
|
-
`$1[${relatedTypeName}!]${suffix}`
|
|
2711
|
-
);
|
|
2712
|
-
} else {
|
|
2713
|
-
const isNullable = this.relationOptions[field]?.nullable;
|
|
2714
|
-
const suffix = isNullable ? "" : "!";
|
|
2715
|
-
const pattern = new RegExp(`${field}:\\s*String!?`, "g");
|
|
2716
|
-
graphqlSchemaString = graphqlSchemaString.replace(
|
|
2717
|
-
pattern,
|
|
2718
|
-
`${field}: ${relatedTypeName}${suffix}`
|
|
2719
|
-
);
|
|
2720
|
-
}
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
// Post-process: Add argument definitions to function fields
|
|
2724
|
-
if (!excludeFunctions) {
|
|
2725
|
-
for (const { propertyKey, options } of this.functions) {
|
|
2726
|
-
if (options?.args && options.args.length > 0) {
|
|
2727
|
-
// Build individual argument definitions
|
|
2728
|
-
const argDefs: string[] = [];
|
|
2729
|
-
for (const arg of options.args) {
|
|
2730
|
-
let argTypeName: string;
|
|
2731
|
-
|
|
2732
|
-
// Determine GraphQL type name for the argument
|
|
2733
|
-
// For GraphQL arguments, we prefer input types over object types
|
|
2734
|
-
// First check if there's a registered input type for this type
|
|
2735
|
-
const inputTypeName = inputTypeRegistry.get(arg.type);
|
|
2736
|
-
if (inputTypeName) {
|
|
2737
|
-
argTypeName = inputTypeName;
|
|
2738
|
-
} else {
|
|
2739
|
-
// Fall back to the object type name
|
|
2740
|
-
const registeredTypeName = customTypeNameRegistry.get(arg.type);
|
|
2741
|
-
if (registeredTypeName) {
|
|
2742
|
-
argTypeName = registeredTypeName;
|
|
2743
|
-
} else if (customTypeRegistry.has(arg.type)) {
|
|
2744
|
-
// It's registered but without a name, try to find the name
|
|
2745
|
-
const registeredName = Array.from(registeredCustomTypes.entries())
|
|
2746
|
-
.find(([name, schema]) => schema === customTypeRegistry.get(arg.type))?.[0];
|
|
2747
|
-
argTypeName = registeredName || 'String';
|
|
2748
|
-
} else if (arg.type === String) {
|
|
2749
|
-
argTypeName = 'String';
|
|
2750
|
-
} else if (arg.type === Number) {
|
|
2751
|
-
argTypeName = 'Float';
|
|
2752
|
-
} else if (arg.type === Boolean) {
|
|
2753
|
-
argTypeName = 'Boolean';
|
|
2754
|
-
} else if (arg.type === Date) {
|
|
2755
|
-
argTypeName = 'Date';
|
|
2756
|
-
} else if (arg.type?.name && registeredCustomTypes.has(arg.type.name)) {
|
|
2757
|
-
// Check if the type name is registered
|
|
2758
|
-
argTypeName = arg.type.name;
|
|
2759
|
-
} else if (arg.type?.name) {
|
|
2760
|
-
// Fallback to the type's name if it exists
|
|
2761
|
-
argTypeName = arg.type.name;
|
|
2762
|
-
} else {
|
|
2763
|
-
argTypeName = 'String';
|
|
2764
|
-
}
|
|
2765
|
-
}
|
|
2766
|
-
|
|
2767
|
-
const nullable = arg.nullable ? '' : '!';
|
|
2768
|
-
argDefs.push(`${arg.name}: ${argTypeName}${nullable}`);
|
|
2769
|
-
}
|
|
2770
|
-
|
|
2771
|
-
// Find the function field in the schema and add arguments
|
|
2772
|
-
// The schema format from printSchema is typically:
|
|
2773
|
-
// fieldName: ReturnType
|
|
2774
|
-
// We need to replace it with: fieldName(arg1: Type1, arg2: Type2): ReturnType
|
|
2775
|
-
|
|
2776
|
-
// Escape propertyKey for regex
|
|
2777
|
-
const escapedKey = propertyKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2778
|
-
const escapedTypeName = nameFromStorage.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2779
|
-
|
|
2780
|
-
// Build the replacement string
|
|
2781
|
-
const argsString = argDefs.join(', ');
|
|
2782
|
-
|
|
2783
|
-
// Debug: Log what we're looking for
|
|
2784
|
-
console.log(`[ArcheType] Adding arguments to ${nameFromStorage}.${propertyKey}: ${argsString}`);
|
|
2785
|
-
|
|
2786
|
-
// Try to find and replace the field definition
|
|
2787
|
-
// Look for the field within the type definition
|
|
2788
|
-
// Make it case-insensitive to handle different casing in GraphQL schema
|
|
2789
|
-
const typeStartPattern = new RegExp(`type\\s+${escapedTypeName}\\s*\\{`, 'i');
|
|
2790
|
-
let typeStartMatch = graphqlSchemaString.match(typeStartPattern);
|
|
2791
|
-
|
|
2792
|
-
// If exact match fails, try case-insensitive search for the type name
|
|
2793
|
-
if (!typeStartMatch) {
|
|
2794
|
-
// Try to find the type with any casing
|
|
2795
|
-
const caseInsensitivePattern = new RegExp(`type\\s+([^\\s{]+)\\s*\\{`, 'gi');
|
|
2796
|
-
const allTypes = [...graphqlSchemaString.matchAll(caseInsensitivePattern)];
|
|
2797
|
-
const matchingType = allTypes.find(match =>
|
|
2798
|
-
match[1]!.toLowerCase() === nameFromStorage.toLowerCase()
|
|
2799
|
-
);
|
|
2800
|
-
if (matchingType && matchingType.index !== undefined) {
|
|
2801
|
-
// Create a fake match object
|
|
2802
|
-
typeStartMatch = [matchingType[0], matchingType[1]] as RegExpMatchArray;
|
|
2803
|
-
typeStartMatch.index = matchingType.index;
|
|
2804
|
-
}
|
|
2805
|
-
}
|
|
2806
|
-
|
|
2807
|
-
if (typeStartMatch) {
|
|
2808
|
-
const typeStartIndex = typeStartMatch.index! + typeStartMatch[0].length;
|
|
2809
|
-
// Find the closing brace of this type
|
|
2810
|
-
let braceCount = 1;
|
|
2811
|
-
let typeEndIndex = typeStartIndex;
|
|
2812
|
-
for (let i = typeStartIndex; i < graphqlSchemaString.length && braceCount > 0; i++) {
|
|
2813
|
-
if (graphqlSchemaString[i] === '{') braceCount++;
|
|
2814
|
-
if (graphqlSchemaString[i] === '}') braceCount--;
|
|
2815
|
-
if (braceCount === 0) {
|
|
2816
|
-
typeEndIndex = i;
|
|
2817
|
-
break;
|
|
2818
|
-
}
|
|
2819
|
-
}
|
|
2820
|
-
|
|
2821
|
-
// Extract the type definition
|
|
2822
|
-
const typeDefinition = graphqlSchemaString.substring(typeStartIndex, typeEndIndex);
|
|
2823
|
-
|
|
2824
|
-
// Debug: Log the type definition snippet
|
|
2825
|
-
console.log(`[ArcheType] Type definition for ${nameFromStorage}:`, typeDefinition.substring(0, 200));
|
|
2826
|
-
|
|
2827
|
-
// Find the field within this type definition
|
|
2828
|
-
// Pattern: fieldName: ReturnType or fieldName?: ReturnType
|
|
2829
|
-
const fieldPattern = new RegExp(
|
|
2830
|
-
`(\\n\\s+)(${escapedKey}\\??\\s*:\\s*)([^\\n]+)`,
|
|
2831
|
-
'g'
|
|
2832
|
-
);
|
|
2833
|
-
|
|
2834
|
-
const fieldMatch = fieldPattern.exec(typeDefinition);
|
|
2835
|
-
if (fieldMatch) {
|
|
2836
|
-
const returnType = fieldMatch[3]!.trim();
|
|
2837
|
-
const indent = fieldMatch[1];
|
|
2838
|
-
const replacement = `${indent}${propertyKey}(${argsString}): ${returnType}`;
|
|
2839
|
-
|
|
2840
|
-
console.log(`[ArcheType] Found field match: "${fieldMatch[0]}" -> "${replacement}"`);
|
|
2841
|
-
|
|
2842
|
-
// Replace in the full schema string
|
|
2843
|
-
const fullMatchStart = typeStartIndex + fieldMatch.index!;
|
|
2844
|
-
const fullMatchEnd = fullMatchStart + fieldMatch[0].length;
|
|
2845
|
-
graphqlSchemaString =
|
|
2846
|
-
graphqlSchemaString.substring(0, fullMatchStart) +
|
|
2847
|
-
replacement +
|
|
2848
|
-
graphqlSchemaString.substring(fullMatchEnd);
|
|
2849
|
-
|
|
2850
|
-
console.log(`[ArcheType] Replacement successful for ${nameFromStorage}.${propertyKey}`);
|
|
2851
|
-
} else {
|
|
2852
|
-
console.warn(`[ArcheType] Field pattern not found in type definition. Looking for: ${escapedKey}`);
|
|
2853
|
-
// Fallback: simple replace anywhere
|
|
2854
|
-
const simplePattern = new RegExp(
|
|
2855
|
-
`(${escapedKey}\\??\\s*:\\s*)([^\\n]+)`,
|
|
2856
|
-
'g'
|
|
2857
|
-
);
|
|
2858
|
-
const beforeReplace = graphqlSchemaString;
|
|
2859
|
-
graphqlSchemaString = graphqlSchemaString.replace(
|
|
2860
|
-
simplePattern,
|
|
2861
|
-
(match, fieldDef, returnType) => {
|
|
2862
|
-
console.log(`[ArcheType] Fallback replacement: "${match}" -> "${propertyKey}(${argsString}): ${returnType.trim()}"`);
|
|
2863
|
-
return `${propertyKey}(${argsString}): ${returnType.trim()}`;
|
|
2864
|
-
}
|
|
2865
|
-
);
|
|
2866
|
-
if (beforeReplace === graphqlSchemaString) {
|
|
2867
|
-
console.warn(`[ArcheType] Fallback replacement also failed for ${nameFromStorage}.${propertyKey}`);
|
|
2868
|
-
}
|
|
2869
|
-
}
|
|
2870
|
-
} else {
|
|
2871
|
-
console.warn(`[ArcheType] Type pattern not found for ${nameFromStorage}. Schema snippet:`, graphqlSchemaString.substring(0, 300));
|
|
2872
|
-
// Fallback: simple replace anywhere if type pattern not found
|
|
2873
|
-
const simplePattern = new RegExp(
|
|
2874
|
-
`(${escapedKey}\\??\\s*:\\s*)([^\\n]+)`,
|
|
2875
|
-
'g'
|
|
2876
|
-
);
|
|
2877
|
-
const beforeReplace = graphqlSchemaString;
|
|
2878
|
-
graphqlSchemaString = graphqlSchemaString.replace(
|
|
2879
|
-
simplePattern,
|
|
2880
|
-
(match, fieldDef, returnType) => {
|
|
2881
|
-
console.log(`[ArcheType] Final fallback replacement: "${match}" -> "${propertyKey}(${argsString}): ${returnType.trim()}"`);
|
|
2882
|
-
return `${propertyKey}(${argsString}): ${returnType.trim()}`;
|
|
2883
|
-
}
|
|
2884
|
-
);
|
|
2885
|
-
if (beforeReplace === graphqlSchemaString) {
|
|
2886
|
-
console.warn(`[ArcheType] All replacement attempts failed for ${nameFromStorage}.${propertyKey}`);
|
|
2887
|
-
}
|
|
2888
|
-
}
|
|
2889
|
-
}
|
|
2890
|
-
|
|
2891
|
-
// Replace String return type with actual GraphQL type if specified
|
|
2892
|
-
if (options?.returnType && !['string', 'number', 'boolean'].includes(options.returnType)) {
|
|
2893
|
-
// Find the field in the schema
|
|
2894
|
-
const fieldIndex = graphqlSchemaString.indexOf(` ${propertyKey}`);
|
|
2895
|
-
if (fieldIndex !== -1) {
|
|
2896
|
-
// Extract the line containing this field
|
|
2897
|
-
const lineStart = fieldIndex;
|
|
2898
|
-
const lineEnd = graphqlSchemaString.indexOf('\n', fieldIndex);
|
|
2899
|
-
const fieldLine = graphqlSchemaString.substring(lineStart, lineEnd !== -1 ? lineEnd : graphqlSchemaString.length);
|
|
2900
|
-
|
|
2901
|
-
// Replace String with the actual return type in this line
|
|
2902
|
-
const updatedLine = fieldLine.replace(/:\s*String(\??)(\s*)$/, `: ${options.returnType}$1$2`);
|
|
2903
|
-
|
|
2904
|
-
if (updatedLine !== fieldLine) {
|
|
2905
|
-
// Replace the line in the full schema
|
|
2906
|
-
graphqlSchemaString = graphqlSchemaString.substring(0, lineStart) +
|
|
2907
|
-
updatedLine +
|
|
2908
|
-
graphqlSchemaString.substring(lineEnd !== -1 ? lineEnd : graphqlSchemaString.length);
|
|
2909
|
-
}
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
2912
|
-
}
|
|
2913
|
-
}
|
|
2914
|
-
|
|
2915
|
-
// Debug: Log schema if it contains function arguments
|
|
2916
|
-
if (!excludeFunctions && this.functions.some(f => f.options?.args && f.options.args.length > 0)) {
|
|
2917
|
-
// console.log(`[ArcheType] Final schema for ${nameFromStorage} with function args:`, graphqlSchemaString);
|
|
2918
|
-
}
|
|
2919
|
-
|
|
2920
|
-
// Cache the schema for this archetype
|
|
2921
|
-
const cacheKey = `${nameFromStorage}_${excludeRelations}_${excludeFunctions}`;
|
|
2922
|
-
archetypeSchemaCache.set(cacheKey, {
|
|
2923
|
-
zodSchema: r,
|
|
2924
|
-
graphqlSchema: graphqlSchemaString,
|
|
2925
|
-
});
|
|
2926
|
-
|
|
2927
|
-
// Store for unified weaving
|
|
2928
|
-
allArchetypeZodObjects.set(nameFromStorage, r);
|
|
2929
|
-
|
|
2930
|
-
return r;
|
|
897
|
+
const { buildZodObjectSchema } = require("./archetype/zodSchemaBuilder");
|
|
898
|
+
return buildZodObjectSchema(this, options);
|
|
2931
899
|
}
|
|
2932
900
|
|
|
2933
901
|
/**
|