bunsane 0.3.0 → 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.
Files changed (54) hide show
  1. package/.claude/scheduled_tasks.lock +1 -0
  2. package/CHANGELOG.md +104 -0
  3. package/CLAUDE.md +20 -0
  4. package/config/cache.config.ts +35 -1
  5. package/core/App.ts +24 -1060
  6. package/core/ArcheType.ts +78 -2110
  7. package/core/Entity.ts +136 -41
  8. package/core/RequestContext.ts +85 -36
  9. package/core/RequestLoaders.ts +89 -31
  10. package/core/SchedulerManager.ts +13 -13
  11. package/core/app/bootstrap.ts +133 -0
  12. package/core/app/cors.ts +94 -0
  13. package/core/app/graphqlSetup.ts +56 -0
  14. package/core/app/healthEndpoints.ts +31 -0
  15. package/core/app/metricsCollector.ts +27 -0
  16. package/core/app/preparedStatementWarmup.ts +55 -0
  17. package/core/app/processHandlers.ts +43 -0
  18. package/core/app/requestRouter.ts +309 -0
  19. package/core/app/restRegistry.ts +72 -0
  20. package/core/app/shutdown.ts +97 -0
  21. package/core/app/studioRouter.ts +83 -0
  22. package/core/archetype/customTypes.ts +100 -0
  23. package/core/archetype/decorators.ts +171 -0
  24. package/core/archetype/fieldResolvers.ts +621 -0
  25. package/core/archetype/helpers.ts +29 -0
  26. package/core/archetype/relationLoader.ts +118 -0
  27. package/core/archetype/schemaBuilder.ts +141 -0
  28. package/core/archetype/weaver.ts +218 -0
  29. package/core/archetype/zodSchemaBuilder.ts +527 -0
  30. package/core/cache/CacheManager.ts +144 -9
  31. package/core/components/BaseComponent.ts +12 -2
  32. package/core/middleware/AccessLog.ts +8 -1
  33. package/database/PreparedStatementCache.ts +17 -16
  34. package/database/cancellable.ts +22 -0
  35. package/database/instrumentedDb.ts +141 -0
  36. package/docs/RFC_APP_REFACTOR.md +248 -0
  37. package/docs/RFC_REFACTOR_TARGETS.md +251 -0
  38. package/package.json +1 -1
  39. package/query/ComponentInclusionNode.ts +5 -5
  40. package/query/Query.ts +65 -48
  41. package/service/ServiceRegistry.ts +7 -1
  42. package/service/index.ts +4 -2
  43. package/tests/integration/loaders/RequestLoaders.abort.test.ts +82 -0
  44. package/tests/integration/query/Query.abort.test.ts +66 -0
  45. package/tests/unit/cache/CacheManager.test.ts +152 -1
  46. package/tests/unit/database/cancellable.test.ts +81 -0
  47. package/tests/unit/database/instrumentedDb.test.ts +160 -0
  48. package/tests/unit/entity/Entity.components.test.ts +73 -0
  49. package/tests/unit/entity/Entity.drainSideEffects.test.ts +51 -0
  50. package/tests/unit/entity/Entity.reload.test.ts +63 -0
  51. package/tests/unit/entity/Entity.requireComponents.test.ts +72 -0
  52. package/tests/unit/query/Query.emptyString.test.ts +69 -0
  53. package/tests/unit/query/Query.test.ts +6 -4
  54. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +95 -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
- export {asEnumType, asUnionType, asObjectType};
15
-
16
- const primitiveTypes = [String, Number, Boolean, Date];
17
-
18
- const archetypeFunctionsSymbol = Symbol.for("bunsane:archetypeFunctions");
19
-
20
- export function ArcheTypeFunction(options?: {
21
- returnType?: string;
22
- args?: Array<{
23
- name: string;
24
- type: any;
25
- nullable?: boolean;
26
- }>;
27
- }) {
28
- return function (target: any, propertyKey: string) {
29
- if (!target[archetypeFunctionsSymbol]) {
30
- target[archetypeFunctionsSymbol] = [];
31
- }
32
- target[archetypeFunctionsSymbol].push({ propertyKey, options });
33
- };
34
- }
35
-
36
- const InputFilterSchema = z.object({
37
- field: z.string(),
38
- op: z.string().default("eq"),
39
- value: z.string(),
40
- }).register(asObjectType, { name: "InputFilter" });
41
-
42
- const customTypeRegistry = new Map<any, any>();
43
- const customTypeNameRegistry = new Map<any, string>();
44
- const registeredCustomTypes = new Map<string, any>();
45
- const customTypeSilks = new Map<string, any>(); // Store silk types for unified weaving
46
- const customTypeResolvers: any[] = []; // Store resolvers for custom types
47
- const inputTypeRegistry = new Map<any, string>(); // Map from type to input type name (e.g., ST_Point -> ST_PointInput)
48
-
49
- // Structural signature registry for input type deduplication
50
- // Maps structural signature -> registered input type name
51
- const structuralSignatureRegistry = new Map<string, string>();
52
-
53
- // Import will be done lazily to avoid circular dependencies
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 { Query } = await import("../query");
1303
- const storage = getMetadataStorage();
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 storage = getMetadataStorage();
1592
- const resolvers: Array<any> = [];
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 excludeRelations = options?.excludeRelations ?? false;
2341
- const excludeFunctions = options?.excludeFunctions ?? false;
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
  /**