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.
- package/TODO.md +1 -1
- package/bun.lock +156 -150
- package/core/App.ts +188 -31
- package/core/ArcheType.ts +1044 -26
- package/core/ComponentRegistry.ts +172 -29
- package/core/Components.ts +102 -24
- package/core/Decorators.ts +0 -1
- package/core/Entity.ts +55 -7
- package/core/EntityInterface.ts +4 -0
- package/core/EntityManager.ts +4 -4
- package/core/Query.ts +169 -3
- package/core/RequestLoaders.ts +101 -12
- package/core/SchedulerManager.ts +3 -4
- package/core/metadata/definitions/ArcheType.ts +9 -0
- package/core/metadata/definitions/Component.ts +16 -0
- package/core/metadata/definitions/gqlObject.ts +10 -0
- package/core/metadata/getMetadataStorage.ts +14 -0
- package/core/metadata/index.ts +17 -0
- package/core/metadata/metadata-storage.ts +81 -0
- package/database/DatabaseHelper.ts +22 -20
- package/database/sqlHelpers.ts +0 -2
- package/gql/ArchetypeOperations.ts +281 -0
- package/gql/Generator.ts +252 -62
- package/gql/helpers.ts +5 -5
- package/gql/index.ts +19 -17
- package/gql/types.ts +58 -11
- package/index.ts +93 -82
- package/package.json +39 -37
- package/plugins/index.ts +13 -0
- package/scheduler/index.ts +87 -0
- package/service/Service.ts +4 -0
- package/service/ServiceRegistry.ts +5 -1
- package/service/index.ts +1 -1
- package/swagger/decorators.ts +65 -0
- package/swagger/generator.ts +100 -0
- package/swagger/index.ts +2 -0
- package/tests/bench/insert.bench.ts +1 -0
- package/tests/bench/relations.bench.ts +1 -0
- package/tests/bench/sorting.bench.ts +1 -0
- package/tests/component-hooks-simple.test.ts +117 -0
- package/tests/component-hooks.test.ts +83 -31
- package/tests/component.test.ts +1 -0
- package/tests/hooks.test.ts +1 -0
- package/tests/query.test.ts +46 -4
- package/tests/relations.test.ts +1 -0
- package/types/app.types.ts +0 -0
- package/upload/index.ts +0 -2
- 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
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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;
|