bunsane 0.1.2 → 0.1.3

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 (48) hide show
  1. package/TODO.md +1 -1
  2. package/bun.lock +156 -150
  3. package/core/App.ts +188 -31
  4. package/core/ArcheType.ts +1044 -26
  5. package/core/ComponentRegistry.ts +172 -29
  6. package/core/Components.ts +102 -24
  7. package/core/Decorators.ts +0 -1
  8. package/core/Entity.ts +55 -7
  9. package/core/EntityInterface.ts +4 -0
  10. package/core/EntityManager.ts +4 -4
  11. package/core/Query.ts +169 -3
  12. package/core/RequestLoaders.ts +101 -12
  13. package/core/SchedulerManager.ts +3 -4
  14. package/core/metadata/definitions/ArcheType.ts +9 -0
  15. package/core/metadata/definitions/Component.ts +16 -0
  16. package/core/metadata/definitions/gqlObject.ts +10 -0
  17. package/core/metadata/getMetadataStorage.ts +14 -0
  18. package/core/metadata/index.ts +17 -0
  19. package/core/metadata/metadata-storage.ts +81 -0
  20. package/database/DatabaseHelper.ts +22 -20
  21. package/database/sqlHelpers.ts +0 -2
  22. package/gql/ArchetypeOperations.ts +281 -0
  23. package/gql/Generator.ts +252 -62
  24. package/gql/helpers.ts +5 -5
  25. package/gql/index.ts +19 -17
  26. package/gql/types.ts +58 -11
  27. package/index.ts +93 -82
  28. package/package.json +39 -37
  29. package/plugins/index.ts +13 -0
  30. package/scheduler/index.ts +87 -0
  31. package/service/Service.ts +4 -0
  32. package/service/ServiceRegistry.ts +5 -1
  33. package/service/index.ts +1 -1
  34. package/swagger/decorators.ts +65 -0
  35. package/swagger/generator.ts +100 -0
  36. package/swagger/index.ts +2 -0
  37. package/tests/bench/insert.bench.ts +1 -0
  38. package/tests/bench/relations.bench.ts +1 -0
  39. package/tests/bench/sorting.bench.ts +1 -0
  40. package/tests/component-hooks-simple.test.ts +117 -0
  41. package/tests/component-hooks.test.ts +83 -31
  42. package/tests/component.test.ts +1 -0
  43. package/tests/hooks.test.ts +1 -0
  44. package/tests/query.test.ts +46 -4
  45. package/tests/relations.test.ts +1 -0
  46. package/types/app.types.ts +0 -0
  47. package/upload/index.ts +0 -2
  48. package/core/processors/ImageProcessor.ts +0 -423
package/core/ArcheType.ts CHANGED
@@ -1,35 +1,383 @@
1
1
  import type { BaseComponent, ComponentDataType } from "./Components";
2
+ import type { ComponentPropertyMetadata } from "./metadata/definitions/Component";
3
+ import type { ArcheTypeFieldOptions } from "./metadata/definitions/ArcheType";
2
4
  import { Entity } from "./Entity";
5
+ import { getMetadataStorage } from "./metadata";
6
+ import { z, ZodObject } from "zod";
7
+ import {weave } from "@gqloom/core";
8
+ import { ZodWeaver, asEnumType, asUnionType } from "@gqloom/zod";
9
+ import { printSchema } from "graphql";
10
+ import "reflect-metadata";
11
+
12
+ const customTypeRegistry = new Map<any, any>();
13
+ const customTypeNameRegistry = new Map<any, string>();
14
+ const registeredCustomTypes = new Map<string, any>();
15
+ const customTypeSilks = new Map<string, any>(); // Store silk types for unified weaving
16
+ const customTypeResolvers: any[] = []; // Store resolvers for custom types
17
+
18
+ // Component-level schema cache
19
+ const componentSchemaCache = new Map<string, ZodObject<any>>(); // componentId -> Zod schema
20
+
21
+ const archetypeSchemaCache = new Map<string, { zodSchema: ZodObject<any>, graphqlSchema: string }>();
22
+ const allArchetypeZodObjects = new Map<string, ZodObject<any>>();
23
+
24
+ export function registerCustomZodType(type: any, schema: any, typeName?: string) {
25
+ // If a type name is provided and it's a ZodObject, add __typename to control GraphQL naming
26
+ if (typeName && schema instanceof ZodObject) {
27
+ // Extend the schema with __typename literal to control the GraphQL type name
28
+ const shape = schema.shape;
29
+ const namedSchema = z.object({
30
+ __typename: z.literal(typeName).nullish(),
31
+ ...shape
32
+ });
33
+ customTypeRegistry.set(type, namedSchema);
34
+ if (typeName) {
35
+ customTypeNameRegistry.set(type, typeName);
36
+ registeredCustomTypes.set(typeName, namedSchema);
37
+ }
38
+ } else {
39
+ customTypeRegistry.set(type, schema);
40
+ if (typeName) {
41
+ customTypeNameRegistry.set(type, typeName);
42
+ registeredCustomTypes.set(typeName, schema);
43
+ }
44
+ }
45
+ }
46
+
47
+ export function getArchetypeSchema(archetypeName: string) {
48
+ return archetypeSchemaCache.get(archetypeName);
49
+ }
50
+
51
+ export function getAllArchetypeSchemas() {
52
+ return Array.from(archetypeSchemaCache.values());
53
+ }
54
+
55
+ export function getRegisteredCustomTypes() {
56
+ return registeredCustomTypes;
57
+ }
58
+
59
+ export function weaveAllArchetypes() {
60
+ // First, ensure all archetype schemas are generated
61
+ const storage = getMetadataStorage();
62
+ for (const archetypeMetadata of storage.archetypes) {
63
+ const archetypeName = archetypeMetadata.name;
64
+ if (!archetypeSchemaCache.has(archetypeName)) {
65
+ try {
66
+ const ArchetypeClass = archetypeMetadata.target as any;
67
+ const instance = new ArchetypeClass();
68
+ instance.getZodObjectSchema(); // Generate and cache the schema
69
+ } catch (error) {
70
+ console.warn(`Could not generate schema for archetype ${archetypeName}:`, error);
71
+ }
72
+ }
73
+ }
74
+
75
+ if (allArchetypeZodObjects.size === 0) {
76
+ return null;
77
+ }
78
+ // Weave all archetype schemas together along with all component schemas
79
+ // This ensures that nested component types are also included in the unified schema
80
+ const archetypeSchemas = Array.from(allArchetypeZodObjects.values());
81
+ const componentSchemas = Array.from(componentSchemaCache.values());
82
+
83
+ // Combine both archetype and component schemas for weaving
84
+ const allSchemas = [...archetypeSchemas, ...componentSchemas];
85
+
86
+ const schema = weave(ZodWeaver, ...allSchemas);
87
+ let schemaString = printSchema(schema);
88
+
89
+ // Post-process: Replace 'id: String' with 'id: ID' for all id fields
90
+ schemaString = schemaString.replace(/\bid:\s*String\b/g, 'id: ID');
91
+
92
+ return schemaString;
93
+ }
94
+
95
+ // Generate Zod schema for a component and cache it
96
+ function getOrCreateComponentSchema(componentCtor: new (...args: any[]) => BaseComponent, componentId: string, fieldOptions?: ArcheTypeFieldOptions): any | null {
97
+ // Check cache first
98
+ if (componentSchemaCache.has(componentId)) {
99
+ return componentSchemaCache.get(componentId)!;
100
+ }
101
+
102
+ const storage = getMetadataStorage();
103
+ const props = storage.getComponentProperties(componentId);
104
+
105
+ // Return null if no properties - caller should skip this component
106
+ if (props.length === 0) {
107
+ return null;
108
+ }
109
+
110
+ const zodFields: Record<string, any> = {
111
+ __typename: z.literal(compNameToFieldName(componentCtor.name))
112
+ };
113
+
114
+ for (const prop of props) {
115
+ if (prop.isPrimitive) {
116
+ switch (prop.propertyType) {
117
+ case String:
118
+ zodFields[prop.propertyKey] = z.string();
119
+ break;
120
+ case Number:
121
+ zodFields[prop.propertyKey] = z.number();
122
+ break;
123
+ case Boolean:
124
+ zodFields[prop.propertyKey] = z.boolean();
125
+ break;
126
+ case Date:
127
+ zodFields[prop.propertyKey] = z.date();
128
+ break;
129
+ default:
130
+ zodFields[prop.propertyKey] = z.any();
131
+ }
132
+ } else if (prop.isEnum && prop.enumValues && prop.enumKeys) {
133
+ const enumTypeName = prop.propertyType?.name || `${componentCtor.name}_${prop.propertyKey}_Enum`;
134
+ zodFields[prop.propertyKey] = z.enum(prop.enumValues as any).register(asEnumType, {
135
+ name: enumTypeName,
136
+ valuesConfig: prop.enumKeys.reduce((acc: Record<string, { description: string }>, key, idx) => {
137
+ acc[key] = { description: prop.enumValues![idx]! };
138
+ return acc;
139
+ }, {})
140
+ });
141
+ } else if (customTypeRegistry.has(prop.propertyType)) {
142
+ zodFields[prop.propertyKey] = customTypeRegistry.get(prop.propertyType)!;
143
+ } else {
144
+ zodFields[prop.propertyKey] = z.any();
145
+ }
146
+
147
+ if (fieldOptions?.nullable) {
148
+ zodFields[prop.propertyKey] = zodFields[prop.propertyKey].nullish();
149
+ }
150
+ }
151
+
152
+ const componentSchema = z.object(zodFields);
153
+
154
+ // Cache the component schema for reuse
155
+ componentSchemaCache.set(componentId, componentSchema);
156
+
157
+ return componentSchema;
158
+ }
3
159
 
4
160
  function compNameToFieldName(compName: string): string {
5
161
  return compName.charAt(0).toLowerCase() + compName.slice(1).replace(/Component$/, '');
6
162
  }
7
163
 
8
164
  /**
9
- * ArcheType provides a layer of abstraction for creating entities with predefined sets of components.
10
- * This makes entity creation more elegant and reduces code repetition.
11
- *
12
- * Example usage:
13
- * ```typescript
14
- * const UserArcheType = new ArcheType([NameComponent, EmailComponent, PasswordComponent]);
15
- *
16
- *
17
- * // FROM Request or other source
18
- * const userInput = { name: "John Doe", email: "john@example.com", password: "securepassword" };
19
- * const entity = UserArcheType.fill(userInput).createEntity();
20
- * await entity.save();
21
- * ```
165
+ * Helper to determine if a component should be unwrapped to a scalar value.
166
+ * Returns true if the component has a single 'value' property and the field type is primitive.
22
167
  */
168
+ function shouldUnwrapComponent(componentProps: ComponentPropertyMetadata[], fieldType: any): boolean {
169
+ // If field type is a primitive, unwrap the component to that primitive
170
+ if (fieldType === String || fieldType === Number || fieldType === Boolean || fieldType === Date) {
171
+ return true;
172
+ }
173
+ return false;
174
+ }
175
+
176
+ export type ArcheTypeOptions = {
177
+ name?: string;
178
+ };
23
179
 
180
+ export interface RelationOptions {
181
+ nullable?: boolean;
182
+ foreignKey?: string;
183
+ through?: string;
184
+ cascade?: boolean;
185
+ }
186
+
187
+ export interface HasManyOptions extends RelationOptions {
188
+ // Additional HasMany specific options
189
+ }
190
+
191
+ export interface BelongsToOptions extends RelationOptions {
192
+ // Additional BelongsTo specific options
193
+ }
194
+
195
+ export interface HasOneOptions extends RelationOptions {
196
+ // Additional HasOne specific options
197
+ }
198
+
199
+ export interface BelongsToManyOptions extends RelationOptions {
200
+ through: string; // Required for many-to-many
201
+ }
202
+
203
+ // TODO: Implement archetype with GraphQL support
204
+ export function ArcheType<T extends new () => BaseArcheType>(nameOrOptions?: string | ArcheTypeOptions) {
205
+ return function(target: T): T {
206
+ const storage = getMetadataStorage();
207
+ const typeId = storage.getComponentId(target.name);
208
+
209
+ let archetype_name = target.name;
210
+
211
+ if (typeof nameOrOptions === 'string') {
212
+ archetype_name = nameOrOptions;
213
+ } else if (nameOrOptions) {
214
+ archetype_name = nameOrOptions.name || target.name;
215
+ }
216
+
217
+ storage.collectArcheTypeMetadata({
218
+ name: archetype_name,
219
+ typeId: typeId,
220
+ target: target,
221
+ });
24
222
 
25
- class ArcheType {
223
+ const prototype = target.prototype;
224
+ const fields = prototype[archetypeFieldsSymbol];
225
+ if (fields) {
226
+ for (const {propertyKey, component, options} of fields) {
227
+ const type = Reflect.getMetadata('design:type', target.prototype, propertyKey);
228
+ storage.collectArchetypeField(archetype_name, propertyKey, component, options, type);
229
+ }
230
+ }
231
+
232
+ const unions = prototype[archetypeUnionFieldsSymbol];
233
+ if(unions) {
234
+ for(const {propertyKey, components, options} of unions) {
235
+ storage.collectArchetypeUnion(archetype_name, propertyKey, components, options, 'union');
236
+ }
237
+ }
238
+
239
+ // Process relations
240
+ const relations = prototype[archetypeRelationsSymbol];
241
+ if (relations) {
242
+ for (const {propertyKey, relatedArcheType, relationType, options} of relations) {
243
+ const type = Reflect.getMetadata('design:type', target.prototype, propertyKey);
244
+ storage.collectArchetypeRelation(archetype_name, propertyKey, relatedArcheType, relationType, options, type);
245
+ }
246
+ }
247
+ return target;
248
+ };
249
+ }
250
+
251
+ const archetypeFieldsSymbol = Symbol("archetypeFields");
252
+ export function ArcheTypeField<T extends BaseComponent>(component: new (...args: any[]) => T, options?: ArcheTypeFieldOptions) {
253
+ return function(target: any, propertyKey: string) {
254
+ if (!target[archetypeFieldsSymbol]) {
255
+ target[archetypeFieldsSymbol] = [];
256
+ }
257
+ target[archetypeFieldsSymbol].push({ propertyKey, component, options });
258
+ };
259
+ }
260
+
261
+ const archetypeUnionFieldsSymbol = Symbol("archetypeUnionFields");
262
+ export function ArcheTypeUnionField(components: (new (...args:any[]) => any)[], options?: ArcheTypeFieldOptions) {
263
+ return function(target: any, propertyKey: string) {
264
+ if(!target[archetypeUnionFieldsSymbol]) {
265
+ target[archetypeUnionFieldsSymbol] = [];
266
+ }
267
+ target[archetypeUnionFieldsSymbol].push({propertyKey, components, options});
268
+ }
269
+ }
270
+
271
+ const archetypeRelationsSymbol = Symbol("archetypeRelations");
272
+
273
+ function createRelationDecorator(relationType: 'hasMany' | 'belongsTo' | 'hasOne' | 'belongsToMany') {
274
+ return function(
275
+ relatedArcheType: string,
276
+ options?: RelationOptions
277
+ ) {
278
+ return function(target: any, propertyKey: string) {
279
+ if (!target[archetypeRelationsSymbol]) {
280
+ target[archetypeRelationsSymbol] = [];
281
+ }
282
+ target[archetypeRelationsSymbol].push({
283
+ propertyKey,
284
+ relatedArcheType,
285
+ relationType,
286
+ options
287
+ });
288
+ };
289
+ };
290
+ }
291
+
292
+ export const HasMany = createRelationDecorator('hasMany');
293
+ export const BelongsTo = createRelationDecorator('belongsTo');
294
+ export const HasOne = createRelationDecorator('hasOne');
295
+ export const BelongsToMany = createRelationDecorator('belongsToMany');
296
+
297
+ // Keep ArcheTypeRelation as alias for backwards compatibility
298
+ export const ArcheTypeRelation = HasMany;
299
+
300
+ export type ArcheTypeResolver = {
301
+ resolver?: string;
302
+ component?: new (...args: any[]) => BaseComponent;
303
+ field?: string;
304
+ filter?: {[key: string]: any};
305
+ }
306
+
307
+ export type ArcheTypeCreateInfo = {
308
+ name: string;
309
+ components: Array<new (...args: any[]) => BaseComponent>;
310
+ };
311
+
312
+ export class BaseArcheType {
26
313
  protected components: Set<{ ctor: new (...args: any[]) => BaseComponent, data: any }> = new Set();
27
- protected componentMap: Record<string, typeof BaseComponent> = {};
314
+ public componentMap: Record<string, typeof BaseComponent> = {};
315
+ protected fieldOptions: Record<string, ArcheTypeFieldOptions> = {};
316
+ protected fieldTypes: Record<string, any> = {};
317
+ public relationMap: Record<string, typeof BaseArcheType | string> = {};
318
+ protected relationOptions: Record<string, RelationOptions> = {};
319
+ protected relationTypes: Record<string, 'hasMany' | 'belongsTo' | 'hasOne' | 'belongsToMany'> = {};
320
+ public unionMap: Record<string, (new (...args: any[]) => BaseComponent)[]> = {};
321
+ protected unionOptions: Record<string, ArcheTypeFieldOptions> = {};
28
322
 
29
- constructor(components: Array<new (...args: any[]) => BaseComponent>) {
30
- for (const ctor of components) {
31
- this.componentMap[compNameToFieldName(ctor.name)] = ctor;
323
+ public resolver?: {
324
+ fields: Record<string, ArcheTypeResolver>;
325
+ };
326
+
327
+ constructor() {
328
+ const storage = getMetadataStorage();
329
+ const archetypeId = storage.getComponentId(this.constructor.name);
330
+
331
+ // Look up the custom name from metadata (e.g., from @ArcheType("CustomName"))
332
+ const archetypeMetadata = storage.archetypes.find(a => a.typeId === archetypeId);
333
+ const archetypeName = archetypeMetadata?.name || this.constructor.name.replace(/ArcheType$/, '');
334
+
335
+ const fields = storage.archetypes_field_map.get(archetypeName);
336
+ if (fields) {
337
+ for (const {fieldName, component, options, type} of fields) {
338
+ this.componentMap[fieldName] = component;
339
+ if (options) this.fieldOptions[fieldName] = options;
340
+ if (type) this.fieldTypes[fieldName] = type;
341
+ }
342
+ }
343
+
344
+ const unions = storage.archetypes_union_map.get(archetypeName);
345
+ if(unions) {
346
+ for(const {fieldName, components, options, type} of unions) {
347
+ this.unionMap[fieldName] = components;
348
+ if (options) this.unionOptions[fieldName] = options;
349
+ }
350
+ }
351
+
352
+ // Process relations
353
+ const relations = storage.archetypes_relations_map.get(archetypeName);
354
+ if (relations) {
355
+ for (const {fieldName, relatedArcheType, relationType, options, type} of relations) {
356
+ this.relationMap[fieldName] = relatedArcheType as any;
357
+ this.relationTypes[fieldName] = relationType;
358
+ if (options) this.relationOptions[fieldName] = options;
359
+ }
360
+ }
361
+ }
362
+
363
+ // constructor(components: Array<new (...args: any[]) => BaseComponent>) {
364
+ // for (const ctor of components) {
365
+ // this.componentMap[compNameToFieldName(ctor.name)] = ctor;
366
+ // }
367
+ // }
368
+
369
+ static ResolveField<T extends BaseComponent>(component: new (...args: any[]) => T, field: keyof T): ArcheTypeResolver {
370
+ return { component, field: field as string };
371
+ }
372
+
373
+
374
+ static Create(info: ArcheTypeCreateInfo): BaseArcheType {
375
+ const archetype = new BaseArcheType();
376
+ archetype.components = new Set();
377
+ for (const ctor of info.components) {
378
+ archetype.componentMap[compNameToFieldName(ctor.name)] = ctor;
32
379
  }
380
+ return archetype;
33
381
  }
34
382
 
35
383
 
@@ -41,15 +389,37 @@ class ArcheType {
41
389
 
42
390
  // TODO: Can we make this type-safe?
43
391
  public fill(input: object, strict: boolean = false): this {
392
+ const storage = getMetadataStorage();
393
+
44
394
  for (const [key, value] of Object.entries(input)) {
45
395
  if (value !== undefined) {
46
396
  const compCtor = this.componentMap[key];
47
397
  if (compCtor) {
48
- this.addComponent(compCtor, { value });
49
- } else {
50
- if (strict) {
51
- throw new Error(`Component for field '${key}' not found in archetype.`);
398
+ const fieldType = this.fieldTypes[key];
399
+ const typeId = storage.getComponentId(compCtor.name);
400
+ const componentProps = storage.getComponentProperties(typeId);
401
+
402
+ // Check if this is a primitive field that should be unwrapped
403
+ if (shouldUnwrapComponent(componentProps, fieldType)) {
404
+ // For primitive types, wrap in { value }
405
+ this.addComponent(compCtor, { value } as any);
406
+ } else {
407
+ // For complex types, pass data directly
408
+ this.addComponent(compCtor, value as any);
52
409
  }
410
+ } else if (this.unionMap[key]) {
411
+ // Handle union fields
412
+ const unionComponents = this.unionMap[key];
413
+ const selectedComponent = this.determineUnionComponent(value, unionComponents, storage);
414
+
415
+ if (selectedComponent) {
416
+ this.addComponent(selectedComponent, value as any);
417
+ } else if (strict) {
418
+ throw new Error(`Could not determine component type for union field '${key}'`);
419
+ }
420
+ } else {
421
+ // direct property
422
+ (this as any)[key] = value;
53
423
  }
54
424
  }
55
425
  }
@@ -63,16 +433,76 @@ class ArcheType {
63
433
  return this;
64
434
  }
65
435
 
66
- async updateEntity<T>(entity: Entity, updates: Partial<T>) {
436
+ /**
437
+ * Determines which component in a union should be used based on the input data.
438
+ * @param value The input data for the union field
439
+ * @param unionComponents Array of possible component constructors
440
+ * @param storage Metadata storage
441
+ * @returns The selected component constructor, or null if none match
442
+ */
443
+ private determineUnionComponent(value: any, unionComponents: (new (...args: any[]) => BaseComponent)[], storage: any): (new (...args: any[]) => BaseComponent) | null {
444
+ // If value has __typename, use it to determine the component
445
+ if (value && typeof value === 'object' && value.__typename) {
446
+ const expectedTypeName = value.__typename;
447
+ for (const component of unionComponents) {
448
+ const componentTypeName = compNameToFieldName(component.name);
449
+ if (componentTypeName === expectedTypeName) {
450
+ return component;
451
+ }
452
+ }
453
+ }
454
+
455
+ // Fallback: Try to infer based on property presence
456
+ if (value && typeof value === 'object') {
457
+ for (const component of unionComponents) {
458
+ const typeId = storage.getComponentId(component.name);
459
+ const componentProps = storage.getComponentProperties(typeId);
460
+
461
+ // Check if any properties of this component are present in the value
462
+ const hasMatchingProps = componentProps.some((prop: ComponentPropertyMetadata) =>
463
+ value.hasOwnProperty(prop.propertyKey)
464
+ );
465
+
466
+ if (hasMatchingProps) {
467
+ return component;
468
+ }
469
+ }
470
+ }
471
+
472
+ // If no component matches, return the first one as default
473
+ return unionComponents[0] || null;
474
+ } async updateEntity<T>(entity: Entity, updates: Partial<T>) {
475
+ const storage = getMetadataStorage();
476
+
67
477
  for (const key of Object.keys(updates)) {
68
478
  if(key === 'id' || key === '_id') continue;
69
479
  const value = updates[key as keyof T];
70
480
  if (value !== undefined) {
71
481
  const compCtor = this.componentMap[key];
72
482
  if (compCtor) {
73
- await entity.set(compCtor, { value });
483
+ const fieldType = this.fieldTypes[key];
484
+ const typeId = storage.getComponentId(compCtor.name);
485
+ const componentProps = storage.getComponentProperties(typeId);
486
+
487
+ // Check if this is a primitive field that should be unwrapped
488
+ if (shouldUnwrapComponent(componentProps, fieldType)) {
489
+ // For primitive types, wrap in { value }
490
+ await entity.set(compCtor, { value });
491
+ } else {
492
+ // For complex types, pass data directly
493
+ await entity.set(compCtor, value as any);
494
+ }
495
+ } else if (this.unionMap[key]) {
496
+ // Handle union fields
497
+ const unionComponents = this.unionMap[key];
498
+ const selectedComponent = this.determineUnionComponent(value, unionComponents, storage);
499
+
500
+ if (selectedComponent) {
501
+ await entity.set(selectedComponent, value as any);
502
+ }
74
503
  } else {
75
- throw new Error(`Component for field '${key}' not found in archetype.`);
504
+ // direct, set on archetype
505
+ (this as any)[key] = value;
76
506
  }
77
507
  }
78
508
  }
@@ -108,6 +538,8 @@ class ArcheType {
108
538
  */
109
539
  public async Unwrap(entity: Entity, exclude: string[] = []): Promise<Record<string, any>> {
110
540
  const result: any = { id: entity.id };
541
+
542
+ // Handle regular components
111
543
  for (const [field, ctor] of Object.entries(this.componentMap)) {
112
544
  if (exclude.includes(field)) continue;
113
545
  const comp = await entity.get(ctor as any);
@@ -115,8 +547,594 @@ class ArcheType {
115
547
  result[field] = (comp as any).value;
116
548
  }
117
549
  }
550
+
551
+ // Handle union fields
552
+ for (const [field, components] of Object.entries(this.unionMap)) {
553
+ if (exclude.includes(field)) continue;
554
+ for (const component of components) {
555
+ const comp = await entity.get(component);
556
+ if (comp) {
557
+ result[field] = {
558
+ __typename: compNameToFieldName(component.name),
559
+ ...(comp as any)
560
+ };
561
+ break; // Only take the first matching component
562
+ }
563
+ }
564
+ }
565
+
566
+ // for direct fields
567
+ for (const field of Object.keys(this.fieldTypes)) {
568
+ if (exclude.includes(field)) continue;
569
+ if (!this.componentMap[field] && !this.unionMap[field]) {
570
+ result[field] = (this as any)[field];
571
+ }
572
+ }
118
573
  return result;
119
574
  }
575
+
576
+ /**
577
+ * Gets the property metadata for all components in this archetype.
578
+ * @returns A record mapping field names to their component property metadata arrays
579
+ */
580
+ public getComponentProperties(): Record<string, ComponentPropertyMetadata[]> {
581
+ const storage = getMetadataStorage();
582
+ const result: Record<string, ComponentPropertyMetadata[]> = {};
583
+
584
+ // Regular components
585
+ for (const [field, ctor] of Object.entries(this.componentMap)) {
586
+ const typeId = storage.getComponentId(ctor.name);
587
+ result[field] = storage.getComponentProperties(typeId);
588
+ }
589
+
590
+ // Union components (for each union field, include properties of all components)
591
+ for (const [field, components] of Object.entries(this.unionMap)) {
592
+ const allProps: ComponentPropertyMetadata[] = [];
593
+ for (const component of components) {
594
+ const typeId = storage.getComponentId(component.name);
595
+ allProps.push(...storage.getComponentProperties(typeId));
596
+ }
597
+ result[field] = allProps;
598
+ }
599
+
600
+ return result;
601
+ }
602
+
603
+ /**
604
+ * Generates GraphQL field resolver functions for this archetype.
605
+ * These resolvers handle both simple fields and component-based fields with DataLoader support.
606
+ *
607
+ * @returns An array of resolver metadata that can be registered with GraphQL
608
+ *
609
+ * @example
610
+ * const resolvers = serviceAreaArcheType.generateFieldResolvers();
611
+ * // Returns array of: { typeName, fieldName, resolver }
612
+ */
613
+ public generateFieldResolvers(): Array<{
614
+ typeName: string;
615
+ fieldName: string;
616
+ resolver: (parent: any, args: any, context: any) => any;
617
+ }> {
618
+ const storage = getMetadataStorage();
619
+ const resolvers: Array<any> = [];
620
+ const archetypeId = storage.getComponentId(this.constructor.name);
621
+ const archetypeName = storage.archetypes.find(a => a.typeId === archetypeId)?.name || this.constructor.name;
622
+
623
+ // Generate ID resolver for the main archetype type
624
+ resolvers.push({
625
+ typeName: archetypeName,
626
+ fieldName: 'id',
627
+ resolver: (parent: Entity) => {
628
+ return parent.id;
629
+ }
630
+ });
631
+
632
+ // Generate resolvers for each component field
633
+ for (const [field, ctor] of Object.entries(this.componentMap)) {
634
+ const typeId = storage.getComponentId(ctor.name);
635
+ const typeIdHex = typeId;
636
+ const componentName = ctor.name;
637
+ const fieldType = this.fieldTypes[field];
638
+
639
+ // Skip components with no properties (like tag components)
640
+ const componentProps = storage.getComponentProperties(typeId);
641
+ if (componentProps.length === 0) {
642
+ continue;
643
+ }
644
+
645
+ // Check if this component should be unwrapped to a scalar
646
+ const isUnwrapped = shouldUnwrapComponent(componentProps, fieldType);
647
+
648
+ if (isUnwrapped) {
649
+ // For unwrapped components, resolve directly to the 'value' property
650
+ resolvers.push({
651
+ typeName: archetypeName,
652
+ fieldName: field,
653
+ resolver: async (parent: Entity, args: any, context: any) => {
654
+ const entity = parent;
655
+
656
+ // Use DataLoader if available, but fall back when no row exists
657
+ if (context.loaders) {
658
+ const componentData = await context.loaders.componentsByEntityType.load({
659
+ entityId: entity.id,
660
+ typeId: typeIdHex // Pass hex string directly
661
+ });
662
+ if (componentData?.data?.value !== undefined) {
663
+ return componentData.data.value;
664
+ }
665
+ }
666
+
667
+ // Fallback: direct query ensures component data is returned
668
+ const comp = await entity.get(ctor);
669
+ return (comp as any)?.value;
670
+ }
671
+ });
672
+ } else {
673
+ // For complex components, return the full component object
674
+ resolvers.push({
675
+ typeName: archetypeName,
676
+ fieldName: field,
677
+ resolver: async (parent: Entity, args: any, context: any) => {
678
+ const entity = parent;
679
+ if (!entity || !entity.id) return (parent as any)[field];
680
+
681
+ // Use DataLoader if available, but fall back when no row exists
682
+ if (context.loaders) {
683
+ const componentData = await context.loaders.componentsByEntityType.load({
684
+ entityId: entity.id,
685
+ typeId: typeIdHex // Pass hex string directly
686
+ });
687
+ if (componentData?.data) {
688
+ return componentData.data;
689
+ }
690
+ }
691
+
692
+ // Fallback: direct query ensures component data is returned
693
+ const comp = await entity.get(ctor);
694
+ return comp;
695
+ }
696
+ });
697
+
698
+ // Generate nested field resolvers for component properties
699
+ const componentTypeName = compNameToFieldName(componentName);
700
+
701
+ for (const prop of componentProps) {
702
+ resolvers.push({
703
+ typeName: componentTypeName, // Use lowercase component name
704
+ fieldName: prop.propertyKey,
705
+ resolver: (parent: any) => parent[prop.propertyKey]
706
+ });
707
+ }
708
+ }
709
+ }
710
+
711
+ // Generate resolvers for union fields
712
+ for (const [field, components] of Object.entries(this.unionMap)) {
713
+ resolvers.push({
714
+ typeName: archetypeName,
715
+ fieldName: field,
716
+ resolver: async (parent: Entity, args: any, context: any) => {
717
+ const entity = parent;
718
+
719
+ // Try to find which component in the union is present on the entity
720
+ for (const component of components) {
721
+ const typeId = storage.getComponentId(component.name);
722
+
723
+ if (context.loaders) {
724
+ const componentData = await context.loaders.componentsByEntityType.load({
725
+ entityId: entity.id,
726
+ typeId: typeId
727
+ });
728
+ if (componentData?.data) {
729
+ // Add __typename for GraphQL union resolution
730
+ return {
731
+ __typename: compNameToFieldName(component.name),
732
+ ...componentData.data
733
+ };
734
+ }
735
+ }
736
+
737
+ // Fallback
738
+ const comp = await entity.get(component);
739
+ if (comp) {
740
+ return {
741
+ __typename: compNameToFieldName(component.name),
742
+ ...(comp as any)
743
+ };
744
+ }
745
+ }
746
+
747
+ return null;
748
+ }
749
+ });
750
+ }
751
+
752
+ // Generate resolvers for relation fields
753
+ for (const [field, relatedArcheType] of Object.entries(this.relationMap)) {
754
+ const relationType = this.relationTypes[field];
755
+ const relationOptions = this.relationOptions[field];
756
+ const isArray = relationType === 'hasMany' || relationType === 'belongsToMany';
757
+
758
+ // Get the related archetype name
759
+ let relatedTypeName: string;
760
+ if (typeof relatedArcheType === 'string') {
761
+ relatedTypeName = relatedArcheType;
762
+ } else {
763
+ const relatedArchetypeId = storage.getComponentId(relatedArcheType.name);
764
+ const relatedArchetypeMetadata = storage.archetypes.find(a => a.typeId === relatedArchetypeId);
765
+ relatedTypeName = relatedArchetypeMetadata?.name || relatedArcheType.name.replace(/ArcheType$/, '');
766
+ }
767
+
768
+ if (!isArray && relationType === 'belongsTo' && relationOptions?.foreignKey) {
769
+ resolvers.push({
770
+ typeName: archetypeName,
771
+ fieldName: field,
772
+ resolver: async (parent: Entity, args: any, context: any) => {
773
+ const entity = parent;
774
+ if (!entity || !entity.id) {
775
+ return null;
776
+ }
777
+
778
+ let foreignId: string | undefined;
779
+
780
+ // Attempt to load the component that holds the foreign key via DataLoader
781
+ if (context.loaders) {
782
+ for (const [componentField, compCtor] of Object.entries(this.componentMap)) {
783
+ const typeIdForComponent = storage.getComponentId(compCtor.name);
784
+ const componentProps = storage.getComponentProperties(typeIdForComponent);
785
+ const hasForeignKey = componentProps.some(prop => prop.propertyKey === relationOptions.foreignKey);
786
+ if (!hasForeignKey || !relationOptions.foreignKey) continue;
787
+
788
+ const componentData = await context.loaders.componentsByEntityType.load({
789
+ entityId: entity.id,
790
+ typeId: typeIdForComponent
791
+ });
792
+
793
+ if (componentData?.data && componentData.data[relationOptions.foreignKey] !== undefined) {
794
+ foreignId = componentData.data[relationOptions.foreignKey];
795
+ break;
796
+ }
797
+ }
798
+ }
799
+
800
+ // Fallback: pull the component from the entity directly when DataLoader misses
801
+ if (!foreignId) {
802
+ for (const compCtor of Object.values(this.componentMap)) {
803
+ const typeIdForComponent = storage.getComponentId(compCtor.name);
804
+ const componentProps = storage.getComponentProperties(typeIdForComponent);
805
+ const hasForeignKey = componentProps.some(prop => prop.propertyKey === relationOptions.foreignKey);
806
+ if (!hasForeignKey || !relationOptions.foreignKey) continue;
807
+ const componentInstance = await entity.get(compCtor as any);
808
+ if (componentInstance && (componentInstance as any)[relationOptions.foreignKey] !== undefined) {
809
+ foreignId = (componentInstance as any)[relationOptions.foreignKey];
810
+ break;
811
+ }
812
+ }
813
+ }
814
+
815
+ if (!foreignId) {
816
+ return null;
817
+ }
818
+
819
+ // Resolve the related entity using loaders when possible, otherwise hit the database directly
820
+ if (context.loaders?.entityById) {
821
+ const relatedEntity = await context.loaders.entityById.load(foreignId);
822
+ if (relatedEntity) {
823
+ return relatedEntity;
824
+ }
825
+ }
826
+
827
+ return Entity.FindById(foreignId);
828
+ }
829
+ });
830
+ } else if (isArray) {
831
+ // Array relation resolver
832
+ resolvers.push({
833
+ typeName: archetypeName,
834
+ fieldName: field,
835
+ resolver: async (parent: Entity, args: any, context: any) => {
836
+ const entity = parent;
837
+
838
+ // Use DataLoader for relation loading if available
839
+ if (context.loaders && context.loaders.relationsByEntityField) {
840
+ return context.loaders.relationsByEntityField.load({
841
+ entityId: entity.id,
842
+ relationField: field,
843
+ relatedType: relatedTypeName,
844
+ foreignKey: relationOptions?.foreignKey
845
+ });
846
+ }
847
+
848
+ // Fallback: return empty array or implement custom relation query
849
+ // This should be implemented based on your relation storage strategy
850
+ console.warn(`No relationsByEntityField loader found for array relation ${field} on ${archetypeName}`);
851
+ return [];
852
+ }
853
+ });
854
+ } else {
855
+ // Single relation resolver
856
+ resolvers.push({
857
+ typeName: archetypeName,
858
+ fieldName: field,
859
+ resolver: async (parent: Entity, args: any, context: any) => {
860
+ const entity = parent;
861
+
862
+ // Use DataLoader for relation loading if available
863
+ if (context.loaders && context.loaders.relationsByEntityField) {
864
+ const results = await context.loaders.relationsByEntityField.load({
865
+ entityId: entity.id,
866
+ relationField: field,
867
+ relatedType: relatedTypeName,
868
+ foreignKey: relationOptions?.foreignKey
869
+ });
870
+ if (results.length > 0) {
871
+ return results[0];
872
+ }
873
+ }
874
+
875
+ // Fallback: return null or implement custom relation query
876
+ console.warn(`No relationsByEntityField loader found for single relation ${field} on ${archetypeName}`);
877
+ return null;
878
+ }
879
+ });
880
+ }
881
+ }
882
+
883
+ return resolvers;
884
+ }
885
+
886
+ /**
887
+ * Registers all auto-generated field resolvers for this archetype with a service.
888
+ * This eliminates the need to manually write @GraphQLField decorators.
889
+ *
890
+ * @param service The service instance to attach resolvers to
891
+ *
892
+ * @example
893
+ * class AreaService extends BaseService {
894
+ * constructor(app: App) {
895
+ * super();
896
+ * // Auto-register all field resolvers!
897
+ * serviceAreaArcheType.registerFieldResolvers(this);
898
+ * }
899
+ * }
900
+ */
901
+ public registerFieldResolvers(service: any): void {
902
+ const resolvers = this.generateFieldResolvers();
903
+
904
+ if (!service.__graphqlFields) {
905
+ service.__graphqlFields = [];
906
+ }
907
+
908
+ for (const { typeName, fieldName, resolver } of resolvers) {
909
+ // Create a unique method name
910
+ const methodName = `_autoResolver_${typeName}_${fieldName}`;
911
+
912
+ // Attach resolver as a method
913
+ service[methodName] = resolver;
914
+
915
+ // Register with GraphQL metadata
916
+ service.__graphqlFields.push({
917
+ type: typeName,
918
+ field: fieldName,
919
+ propertyKey: methodName
920
+ });
921
+ }
922
+ }
923
+
924
+ // TODO: Here
925
+ public getZodObjectSchema(): ZodObject<any> {
926
+ const zodShapes: Record<string, any> = {};
927
+ const storage = getMetadataStorage();
928
+ const unionSchemas: Array<{ fieldName: string; schema: any; components: any[] }> = [];
929
+
930
+ for (const [field, ctor] of Object.entries(this.componentMap)) {
931
+ // Skip union fields - they'll be processed separately
932
+ if (field.startsWith('union_')) {
933
+ continue;
934
+ }
935
+
936
+ const type = this.fieldTypes[field];
937
+ const typeId = storage.getComponentId(ctor.name);
938
+ const componentProps = storage.getComponentProperties(typeId);
939
+
940
+ // Check if component should be unwrapped based on field type
941
+ if (shouldUnwrapComponent(componentProps, type)) {
942
+ // Unwrap to primitive type
943
+ if (type === String) {
944
+ zodShapes[field] = z.string();
945
+ } else if (type === Number) {
946
+ zodShapes[field] = z.number();
947
+ } else if (type === Boolean) {
948
+ zodShapes[field] = z.boolean();
949
+ } else if (type === Date) {
950
+ zodShapes[field] = z.date();
951
+ }
952
+ } else {
953
+ // Use component schema for complex types
954
+ const componentSchema = getOrCreateComponentSchema(ctor, typeId, this.fieldOptions[field]);
955
+ if (componentSchema) {
956
+ zodShapes[field] = componentSchema;
957
+ } else {
958
+ // Skip components with no properties
959
+ continue;
960
+ }
961
+ }
962
+
963
+ if (this.fieldOptions[field]?.nullable && zodShapes[field] && !(zodShapes[field] instanceof ZodObject)) {
964
+ zodShapes[field] = zodShapes[field].nullish();
965
+ }
966
+ }
967
+
968
+ // Process union fields
969
+ for (const [fieldName, components] of Object.entries(this.unionMap)) {
970
+ // Generate schemas for each component in the union
971
+ const unionComponentSchemas: any[] = [];
972
+ const unionComponentCtors: any[] = [];
973
+
974
+ for (const component of components) {
975
+ const typeId = storage.getComponentId(component.name);
976
+ const componentSchema = getOrCreateComponentSchema(component, typeId, this.unionOptions[fieldName]);
977
+
978
+ if (componentSchema) {
979
+ unionComponentSchemas.push(componentSchema);
980
+ unionComponentCtors.push(component);
981
+ }
982
+ }
983
+
984
+ // Create union type using Zod with GQLoom support
985
+ if (unionComponentSchemas.length > 0) {
986
+ const unionSchema = z.union(unionComponentSchemas).register(asUnionType, {
987
+ name: fieldName.charAt(0).toUpperCase() + fieldName.slice(1), // Capitalize field name for type
988
+ resolveType: (it: any) => {
989
+ // Determine which type this is based on __typename
990
+ if (it.__typename) {
991
+ return it.__typename;
992
+ }
993
+ // Fallback: check property presence
994
+ for (let i = 0; i < unionComponentCtors.length; i++) {
995
+ const componentProps = storage.getComponentProperties(
996
+ storage.getComponentId(unionComponentCtors[i].name)
997
+ );
998
+ const hasUniqueProps = componentProps.some(prop =>
999
+ it.hasOwnProperty(prop.propertyKey)
1000
+ );
1001
+ if (hasUniqueProps) {
1002
+ return compNameToFieldName(unionComponentCtors[i].name);
1003
+ }
1004
+ }
1005
+ return compNameToFieldName(unionComponentCtors[0].name);
1006
+ }
1007
+ });
1008
+
1009
+ zodShapes[fieldName] = unionSchema;
1010
+ unionSchemas.push({
1011
+ fieldName,
1012
+ schema: unionSchema,
1013
+ components: unionComponentSchemas
1014
+ });
1015
+
1016
+ // Apply nullable option for union fields
1017
+ if (this.unionOptions[fieldName]?.nullable) {
1018
+ zodShapes[fieldName] = zodShapes[fieldName].nullish();
1019
+ }
1020
+ }
1021
+ }
1022
+
1023
+ // Process relations for GraphQL schema generation
1024
+ for (const [field, relatedArcheType] of Object.entries(this.relationMap)) {
1025
+ const relationType = this.relationTypes[field];
1026
+ const isArray = relationType === 'hasMany' || relationType === 'belongsToMany';
1027
+
1028
+ // Get the related archetype name
1029
+ let relatedTypeName: string;
1030
+ if (typeof relatedArcheType === 'string') {
1031
+ relatedTypeName = relatedArcheType;
1032
+ } else {
1033
+ const relatedArchetypeId = storage.getComponentId(relatedArcheType.name);
1034
+ const relatedArchetypeMetadata = storage.archetypes.find(a => a.typeId === relatedArchetypeId);
1035
+ relatedTypeName = relatedArchetypeMetadata?.name || relatedArcheType.name.replace(/ArcheType$/, '');
1036
+ }
1037
+
1038
+ // For GraphQL relations, we just store the type name as a string reference
1039
+ // The GraphQL schema will use the type name directly, and the full type definition
1040
+ // will be generated when each archetype's getZodObjectSchema() is called
1041
+ const relatedTypeSchema = z.string().describe(`Reference to ${relatedTypeName} type`);
1042
+
1043
+ if (isArray) {
1044
+ zodShapes[field] = z.array(relatedTypeSchema);
1045
+ } else {
1046
+ zodShapes[field] = relatedTypeSchema;
1047
+ }
1048
+
1049
+ if (this.relationOptions[field]?.nullable) {
1050
+ zodShapes[field] = zodShapes[field].nullish();
1051
+ }
1052
+ }
1053
+
1054
+ const archetypeId = storage.getComponentId(this.constructor.name);
1055
+ const nameFromStorage = storage.archetypes.find(a => a.typeId === archetypeId)?.name || this.constructor.name;
1056
+ const shape: Record<string, any> = {
1057
+ __typename: z.literal(nameFromStorage).nullish(),
1058
+ id: z.string().nullish(), // Will be converted to ID in post-processing
1059
+ };
1060
+ for (const [field, zodType] of Object.entries(zodShapes)) {
1061
+ const isNullable = this.fieldOptions[field]?.nullable || this.unionOptions[field]?.nullable;
1062
+ if (isNullable) {
1063
+ // For nullable fields, make them optional in the GraphQL schema
1064
+ shape[field] = zodType.optional();
1065
+ } else {
1066
+ shape[field] = zodType;
1067
+ }
1068
+ }
1069
+ const r = z.object(shape);
1070
+
1071
+ // Collect all component schemas used by this archetype for weaving
1072
+ const componentSchemasToWeave: any[] = [];
1073
+ for (const [field, zodType] of Object.entries(zodShapes)) {
1074
+ if (zodType instanceof ZodObject) {
1075
+ componentSchemasToWeave.push(zodType);
1076
+ } else if (Array.isArray(zodType) || (zodType && typeof zodType === 'object' && zodType._def?.typeName === 'ZodUnion')) {
1077
+ // Handle union types
1078
+ if (zodType._def?.typeName === 'ZodUnion') {
1079
+ componentSchemasToWeave.push(zodType);
1080
+ }
1081
+ }
1082
+ }
1083
+
1084
+ // Weave archetype schema along with its component schemas
1085
+ const schemasToWeave = [r, ...componentSchemasToWeave];
1086
+ const schema = weave(ZodWeaver, ...schemasToWeave);
1087
+ let graphqlSchemaString = printSchema(schema);
1088
+
1089
+ // Post-process: Replace 'id: String' with 'id: ID' for all id fields
1090
+ graphqlSchemaString = graphqlSchemaString.replace(/\bid:\s*String\b/g, 'id: ID');
1091
+
1092
+ // Post-process: Replace relation field types with proper GraphQL type references
1093
+ for (const [field, relatedArcheType] of Object.entries(this.relationMap)) {
1094
+ const relationType = this.relationTypes[field];
1095
+ const isArray = relationType === 'hasMany' || relationType === 'belongsToMany';
1096
+
1097
+ let relatedTypeName: string;
1098
+ if (typeof relatedArcheType === 'string') {
1099
+ relatedTypeName = relatedArcheType;
1100
+ } else {
1101
+ const relatedArchetypeId = storage.getComponentId(relatedArcheType.name);
1102
+ const relatedArchetypeMetadata = storage.archetypes.find(a => a.typeId === relatedArchetypeId);
1103
+ relatedTypeName = relatedArchetypeMetadata?.name || relatedArcheType.name.replace(/ArcheType$/, '');
1104
+ }
1105
+
1106
+ // Replace the String field with proper GraphQL type reference
1107
+ if (isArray) {
1108
+ const pattern = new RegExp(`${field}:\\s*\\[String!?\\]!?`, 'g');
1109
+ graphqlSchemaString = graphqlSchemaString.replace(pattern, `${field}: [${relatedTypeName}!]!`);
1110
+ } else {
1111
+ const pattern = new RegExp(`${field}:\\s*String!?`, 'g');
1112
+ graphqlSchemaString = graphqlSchemaString.replace(pattern, `${field}: ${relatedTypeName}!`);
1113
+ }
1114
+ }
1115
+
1116
+ // console.log("WeavedSchema:", graphqlSchemaString);
1117
+
1118
+ // Cache the schema for this archetype
1119
+ archetypeSchemaCache.set(nameFromStorage, {
1120
+ zodSchema: r,
1121
+ graphqlSchema: graphqlSchemaString
1122
+ });
1123
+
1124
+ // Store for unified weaving
1125
+ allArchetypeZodObjects.set(nameFromStorage, r);
1126
+
1127
+ return r;
1128
+ }
120
1129
  }
121
1130
 
122
- export default ArcheType;
1131
+ export type InferArcheType<T extends BaseArcheType> = {
1132
+ [K in keyof T['componentMap']]: T['componentMap'][K] extends new (...args: any[]) => infer C ? C : never
1133
+ };
1134
+
1135
+ // Alternative: Infer from the actual instance properties (recommended)
1136
+ export type InferArcheTypeFromInstance<T extends BaseArcheType> = {
1137
+ [K in keyof T as T[K] extends BaseComponent ? K : never]: T[K]
1138
+ };
1139
+
1140
+ export default BaseArcheType;