bunsane 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/scheduled_tasks.lock +1 -0
- package/CHANGELOG.md +104 -0
- package/CLAUDE.md +20 -0
- package/config/cache.config.ts +35 -1
- package/core/App.ts +24 -1060
- package/core/ArcheType.ts +78 -2110
- package/core/Entity.ts +136 -41
- package/core/RequestContext.ts +85 -36
- package/core/RequestLoaders.ts +89 -31
- package/core/SchedulerManager.ts +13 -13
- package/core/app/bootstrap.ts +133 -0
- package/core/app/cors.ts +94 -0
- package/core/app/graphqlSetup.ts +56 -0
- package/core/app/healthEndpoints.ts +31 -0
- package/core/app/metricsCollector.ts +27 -0
- package/core/app/preparedStatementWarmup.ts +55 -0
- package/core/app/processHandlers.ts +43 -0
- package/core/app/requestRouter.ts +309 -0
- package/core/app/restRegistry.ts +72 -0
- package/core/app/shutdown.ts +97 -0
- package/core/app/studioRouter.ts +83 -0
- package/core/archetype/customTypes.ts +100 -0
- package/core/archetype/decorators.ts +171 -0
- package/core/archetype/fieldResolvers.ts +621 -0
- package/core/archetype/helpers.ts +29 -0
- package/core/archetype/relationLoader.ts +118 -0
- package/core/archetype/schemaBuilder.ts +141 -0
- package/core/archetype/weaver.ts +218 -0
- package/core/archetype/zodSchemaBuilder.ts +527 -0
- package/core/cache/CacheManager.ts +144 -9
- package/core/components/BaseComponent.ts +12 -2
- package/core/middleware/AccessLog.ts +8 -1
- package/database/PreparedStatementCache.ts +17 -16
- package/database/cancellable.ts +22 -0
- package/database/instrumentedDb.ts +141 -0
- package/docs/RFC_APP_REFACTOR.md +248 -0
- package/docs/RFC_REFACTOR_TARGETS.md +251 -0
- package/package.json +1 -1
- package/query/ComponentInclusionNode.ts +5 -5
- package/query/Query.ts +65 -48
- package/service/ServiceRegistry.ts +7 -1
- package/service/index.ts +4 -2
- package/tests/integration/loaders/RequestLoaders.abort.test.ts +82 -0
- package/tests/integration/query/Query.abort.test.ts +66 -0
- package/tests/unit/cache/CacheManager.test.ts +152 -1
- package/tests/unit/database/cancellable.test.ts +81 -0
- package/tests/unit/database/instrumentedDb.test.ts +160 -0
- package/tests/unit/entity/Entity.components.test.ts +73 -0
- package/tests/unit/entity/Entity.drainSideEffects.test.ts +51 -0
- package/tests/unit/entity/Entity.reload.test.ts +63 -0
- package/tests/unit/entity/Entity.requireComponents.test.ts +72 -0
- package/tests/unit/query/Query.emptyString.test.ts +69 -0
- package/tests/unit/query/Query.test.ts +6 -4
- package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +95 -0
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
import { Entity } from "../Entity";
|
|
2
|
+
import { getMetadataStorage } from "../metadata";
|
|
3
|
+
import { Query } from "../../query";
|
|
4
|
+
import { compNameToFieldName, shouldUnwrapComponent } from "./helpers";
|
|
5
|
+
import {
|
|
6
|
+
customTypeRegistry,
|
|
7
|
+
customTypeNameRegistry,
|
|
8
|
+
registeredCustomTypes,
|
|
9
|
+
} from "./customTypes";
|
|
10
|
+
|
|
11
|
+
let _ensureEntity: ((parent: any, context: any) => Promise<Entity>) | null = null;
|
|
12
|
+
function ensureEntity(parent: any, context: any): Promise<Entity> {
|
|
13
|
+
if (!_ensureEntity) {
|
|
14
|
+
const { BaseArcheType } = require("../ArcheType");
|
|
15
|
+
_ensureEntity = (BaseArcheType as any).ensureEntity.bind(BaseArcheType);
|
|
16
|
+
}
|
|
17
|
+
return _ensureEntity!(parent, context);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FieldResolverEntry {
|
|
21
|
+
typeName: string;
|
|
22
|
+
fieldName: string;
|
|
23
|
+
resolver: (parent: any, args: any, context: any) => any;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build GraphQL field resolvers for an archetype instance.
|
|
28
|
+
* Extracted from BaseArcheType.generateFieldResolvers().
|
|
29
|
+
*/
|
|
30
|
+
export function buildFieldResolvers(archetype: any): FieldResolverEntry[] {
|
|
31
|
+
const storage = getMetadataStorage();
|
|
32
|
+
const resolvers: FieldResolverEntry[] = [];
|
|
33
|
+
const archetypeId = storage.getComponentId(archetype.constructor.name);
|
|
34
|
+
const archetypeName =
|
|
35
|
+
storage.archetypes.find((a) => a.typeId === archetypeId)?.name ||
|
|
36
|
+
archetype.constructor.name;
|
|
37
|
+
|
|
38
|
+
resolvers.push({
|
|
39
|
+
typeName: archetypeName,
|
|
40
|
+
fieldName: "id",
|
|
41
|
+
resolver: (parent: any) => {
|
|
42
|
+
return parent.id;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
for (const [field, ctor] of Object.entries(archetype.componentMap)) {
|
|
47
|
+
const componentCtor = ctor as any;
|
|
48
|
+
const typeId = storage.getComponentId(componentCtor.name);
|
|
49
|
+
const typeIdHex = typeId;
|
|
50
|
+
const componentName = componentCtor.name;
|
|
51
|
+
const fieldType = archetype.fieldTypes[field];
|
|
52
|
+
|
|
53
|
+
const componentProps = storage.getComponentProperties(typeId);
|
|
54
|
+
if (componentProps.length === 0) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const isUnwrapped = shouldUnwrapComponent(componentProps, fieldType);
|
|
59
|
+
|
|
60
|
+
if (isUnwrapped) {
|
|
61
|
+
resolvers.push({
|
|
62
|
+
typeName: archetypeName,
|
|
63
|
+
fieldName: field,
|
|
64
|
+
resolver: async (parent: any, args: any, context: any) => {
|
|
65
|
+
const entityId = parent?.id;
|
|
66
|
+
if (!entityId) return (parent as any)[field];
|
|
67
|
+
|
|
68
|
+
if (parent instanceof Entity) {
|
|
69
|
+
if (parent.wasRemoved(componentCtor)) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const inMemoryComp = parent.getInMemory(componentCtor);
|
|
73
|
+
if (inMemoryComp) {
|
|
74
|
+
return (inMemoryComp as any)?.value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (context?.loaders?.componentsByEntityType) {
|
|
79
|
+
const componentData =
|
|
80
|
+
await context.loaders.componentsByEntityType.load({
|
|
81
|
+
entityId: entityId,
|
|
82
|
+
typeId: typeIdHex,
|
|
83
|
+
});
|
|
84
|
+
if (componentData?.data?.value !== undefined) {
|
|
85
|
+
return componentData.data.value;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const entity = await ensureEntity(parent, context);
|
|
90
|
+
const comp = await entity.get(componentCtor);
|
|
91
|
+
return (comp as any)?.value;
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
} else {
|
|
95
|
+
resolvers.push({
|
|
96
|
+
typeName: archetypeName,
|
|
97
|
+
fieldName: field,
|
|
98
|
+
resolver: async (parent: any, args: any, context: any) => {
|
|
99
|
+
const entityId = parent?.id;
|
|
100
|
+
if (!entityId) return (parent as any)[field];
|
|
101
|
+
|
|
102
|
+
if (parent instanceof Entity) {
|
|
103
|
+
if (parent.wasRemoved(componentCtor)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const inMemoryComp = parent.getInMemory(componentCtor);
|
|
107
|
+
if (inMemoryComp) {
|
|
108
|
+
return inMemoryComp;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (context?.loaders?.componentsByEntityType) {
|
|
113
|
+
const componentData =
|
|
114
|
+
await context.loaders.componentsByEntityType.load({
|
|
115
|
+
entityId: entityId,
|
|
116
|
+
typeId: typeIdHex,
|
|
117
|
+
});
|
|
118
|
+
if (componentData?.data) {
|
|
119
|
+
return componentData.data;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const entity = await ensureEntity(parent, context);
|
|
124
|
+
const comp = await entity.get(componentCtor);
|
|
125
|
+
return comp;
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const componentTypeName = compNameToFieldName(componentName);
|
|
130
|
+
|
|
131
|
+
for (const prop of componentProps) {
|
|
132
|
+
resolvers.push({
|
|
133
|
+
typeName: componentTypeName,
|
|
134
|
+
fieldName: prop.propertyKey,
|
|
135
|
+
resolver: (parent: any) => parent[prop.propertyKey],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const [field, components] of Object.entries(archetype.unionMap)) {
|
|
142
|
+
const componentList = components as any[];
|
|
143
|
+
resolvers.push({
|
|
144
|
+
typeName: archetypeName,
|
|
145
|
+
fieldName: field,
|
|
146
|
+
resolver: async (parent: any, args: any, context: any) => {
|
|
147
|
+
const entityId = parent?.id;
|
|
148
|
+
if (!entityId) return null;
|
|
149
|
+
|
|
150
|
+
for (const component of componentList) {
|
|
151
|
+
const typeId = storage.getComponentId(component.name);
|
|
152
|
+
|
|
153
|
+
if (parent instanceof Entity) {
|
|
154
|
+
if (parent.wasRemoved(component)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const inMemoryComp = parent.getInMemory(component);
|
|
158
|
+
if (inMemoryComp) {
|
|
159
|
+
return {
|
|
160
|
+
__typename: compNameToFieldName(component.name),
|
|
161
|
+
...(inMemoryComp as any).data?.() ?? inMemoryComp,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (context?.loaders?.componentsByEntityType) {
|
|
167
|
+
const componentData =
|
|
168
|
+
await context.loaders.componentsByEntityType.load({
|
|
169
|
+
entityId: entityId,
|
|
170
|
+
typeId: typeId,
|
|
171
|
+
});
|
|
172
|
+
if (componentData?.data) {
|
|
173
|
+
return {
|
|
174
|
+
__typename: compNameToFieldName(component.name),
|
|
175
|
+
...componentData.data,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
const entity = await ensureEntity(parent, context);
|
|
180
|
+
const comp = await entity.get(component);
|
|
181
|
+
if (comp) {
|
|
182
|
+
return {
|
|
183
|
+
__typename: compNameToFieldName(component.name),
|
|
184
|
+
...(comp as any),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return null;
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const [field, relatedArcheType] of Object.entries(archetype.relationMap)) {
|
|
196
|
+
const relationType = archetype.relationTypes[field];
|
|
197
|
+
const relationOptions = archetype.relationOptions[field];
|
|
198
|
+
const isArray =
|
|
199
|
+
relationType === "hasMany" || relationType === "belongsToMany";
|
|
200
|
+
|
|
201
|
+
let relatedTypeName: string;
|
|
202
|
+
if (typeof relatedArcheType === "string") {
|
|
203
|
+
relatedTypeName = relatedArcheType;
|
|
204
|
+
} else {
|
|
205
|
+
const relatedArchetypeId = storage.getComponentId(
|
|
206
|
+
(relatedArcheType as any).name
|
|
207
|
+
);
|
|
208
|
+
const relatedArchetypeMetadata = storage.archetypes.find(
|
|
209
|
+
(a) => a.typeId === relatedArchetypeId
|
|
210
|
+
);
|
|
211
|
+
relatedTypeName =
|
|
212
|
+
relatedArchetypeMetadata?.name ||
|
|
213
|
+
(relatedArcheType as any).name.replace(/ArcheType$/, "");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (
|
|
217
|
+
!isArray &&
|
|
218
|
+
relationType === "belongsTo" &&
|
|
219
|
+
relationOptions?.foreignKey
|
|
220
|
+
) {
|
|
221
|
+
resolvers.push({
|
|
222
|
+
typeName: archetypeName,
|
|
223
|
+
fieldName: field,
|
|
224
|
+
resolver: async (parent: any, args: any, context: any) => {
|
|
225
|
+
const entityId = parent?.id;
|
|
226
|
+
if (!entityId) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let foreignId: string | undefined;
|
|
231
|
+
|
|
232
|
+
if (context?.loaders?.componentsByEntityType) {
|
|
233
|
+
const foreignKey = relationOptions.foreignKey;
|
|
234
|
+
if (foreignKey && foreignKey.includes('.')) {
|
|
235
|
+
const [fieldName, propName] = foreignKey.split('.');
|
|
236
|
+
const compCtor = archetype.componentMap[fieldName!];
|
|
237
|
+
if (compCtor) {
|
|
238
|
+
const typeIdForComponent = storage.getComponentId(compCtor.name);
|
|
239
|
+
const componentData = await context.loaders.componentsByEntityType.load({
|
|
240
|
+
entityId: entityId,
|
|
241
|
+
typeId: typeIdForComponent,
|
|
242
|
+
});
|
|
243
|
+
if (componentData?.data && componentData.data[propName!] !== undefined) {
|
|
244
|
+
foreignId = componentData.data[propName!];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
for (const [componentField, compCtor] of Object.entries(archetype.componentMap)) {
|
|
249
|
+
const compCtorAny = compCtor as any;
|
|
250
|
+
const typeIdForComponent = storage.getComponentId(compCtorAny.name);
|
|
251
|
+
const componentProps = storage.getComponentProperties(typeIdForComponent);
|
|
252
|
+
const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
|
|
253
|
+
if (!hasForeignKey || !foreignKey) continue;
|
|
254
|
+
|
|
255
|
+
const componentData = await context.loaders.componentsByEntityType.load({
|
|
256
|
+
entityId: entityId,
|
|
257
|
+
typeId: typeIdForComponent,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (componentData?.data && componentData.data[foreignKey] !== undefined) {
|
|
261
|
+
foreignId = componentData.data[foreignKey];
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!foreignId) {
|
|
269
|
+
const entity = await ensureEntity(parent, context);
|
|
270
|
+
const foreignKey = relationOptions.foreignKey;
|
|
271
|
+
if (foreignKey && foreignKey.includes('.')) {
|
|
272
|
+
const [fieldName, propName] = foreignKey.split('.');
|
|
273
|
+
const compCtor = archetype.componentMap[fieldName!];
|
|
274
|
+
if (compCtor) {
|
|
275
|
+
const componentInstance = await entity.get(compCtor as any);
|
|
276
|
+
if (componentInstance && (componentInstance as any)[propName!] !== undefined) {
|
|
277
|
+
foreignId = (componentInstance as any)[propName!];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
for (const compCtor of Object.values(archetype.componentMap)) {
|
|
282
|
+
const compCtorAny = compCtor as any;
|
|
283
|
+
const typeIdForComponent = storage.getComponentId(compCtorAny.name);
|
|
284
|
+
const componentProps = storage.getComponentProperties(typeIdForComponent);
|
|
285
|
+
const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
|
|
286
|
+
if (!hasForeignKey || !foreignKey) continue;
|
|
287
|
+
const componentInstance = await entity.get(compCtorAny);
|
|
288
|
+
if (componentInstance && (componentInstance as any)[foreignKey] !== undefined) {
|
|
289
|
+
foreignId = (componentInstance as any)[foreignKey];
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!foreignId && relationOptions.foreignKey === 'id') {
|
|
297
|
+
foreignId = entityId;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!foreignId) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (context.loaders?.entityById) {
|
|
305
|
+
const relatedEntity =
|
|
306
|
+
await context.loaders.entityById.load(foreignId);
|
|
307
|
+
if (relatedEntity) {
|
|
308
|
+
return relatedEntity;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return Entity.FindById(foreignId);
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
} else if (isArray) {
|
|
316
|
+
resolvers.push({
|
|
317
|
+
typeName: archetypeName,
|
|
318
|
+
fieldName: field,
|
|
319
|
+
resolver: async (parent: any, args: any, context: any) => {
|
|
320
|
+
const entityId = parent?.id;
|
|
321
|
+
if (!entityId) return [];
|
|
322
|
+
|
|
323
|
+
if (relationOptions?.foreignKey) {
|
|
324
|
+
let componentCtor: any = null;
|
|
325
|
+
let foreignKeyField: string = relationOptions.foreignKey;
|
|
326
|
+
let relatedArchetypeInstance: any = null;
|
|
327
|
+
|
|
328
|
+
if (typeof relatedArcheType === "function") {
|
|
329
|
+
relatedArchetypeInstance = new (relatedArcheType as any)();
|
|
330
|
+
} else if (typeof relatedArcheType === "string") {
|
|
331
|
+
const relatedArchetypeMetadata = storage.archetypes.find((a) => a.name === relatedArcheType);
|
|
332
|
+
if (relatedArchetypeMetadata) {
|
|
333
|
+
relatedArchetypeInstance = new (relatedArchetypeMetadata.target as any)();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (relatedArchetypeInstance) {
|
|
338
|
+
if (relationOptions.foreignKey.includes('.')) {
|
|
339
|
+
const [fieldName, propName] = relationOptions.foreignKey.split('.');
|
|
340
|
+
componentCtor = relatedArchetypeInstance.componentMap[fieldName!];
|
|
341
|
+
foreignKeyField = propName!;
|
|
342
|
+
} else {
|
|
343
|
+
for (const comp of Object.values(relatedArchetypeInstance.componentMap) as any[]) {
|
|
344
|
+
const typeId = storage.getComponentId(comp.name);
|
|
345
|
+
const props = storage.getComponentProperties(typeId);
|
|
346
|
+
if (props.some(p => p.propertyKey === relationOptions.foreignKey)) {
|
|
347
|
+
componentCtor = comp;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (componentCtor) {
|
|
355
|
+
const query = new Query();
|
|
356
|
+
query.with(componentCtor, Query.filters(Query.filter(foreignKeyField, Query.filterOp.EQ, entityId)));
|
|
357
|
+
return await query.exec();
|
|
358
|
+
} else {
|
|
359
|
+
console.warn(`No component found with foreign key ${relationOptions.foreignKey} in ${relatedTypeName}`);
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
} else {
|
|
363
|
+
if (context?.loaders?.relationsByEntityField) {
|
|
364
|
+
return context.loaders.relationsByEntityField.load({
|
|
365
|
+
entityId: entityId,
|
|
366
|
+
relationField: field,
|
|
367
|
+
relatedType: relatedTypeName,
|
|
368
|
+
foreignKey: relationOptions?.foreignKey,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
console.warn(
|
|
373
|
+
`No relationsByEntityField loader found for array relation ${field} on ${archetypeName}`
|
|
374
|
+
);
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
} else {
|
|
380
|
+
resolvers.push({
|
|
381
|
+
typeName: archetypeName,
|
|
382
|
+
fieldName: field,
|
|
383
|
+
resolver: async (parent: any, args: any, context: any) => {
|
|
384
|
+
const entityId = parent?.id;
|
|
385
|
+
|
|
386
|
+
if (relationOptions?.foreignKey) {
|
|
387
|
+
if (!entityId) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
let foreignId: string | undefined;
|
|
392
|
+
|
|
393
|
+
if (context?.loaders?.componentsByEntityType) {
|
|
394
|
+
const foreignKey = relationOptions.foreignKey;
|
|
395
|
+
if (foreignKey && foreignKey.includes('.')) {
|
|
396
|
+
const [fieldName, propName] = foreignKey.split('.');
|
|
397
|
+
const compCtor = archetype.componentMap[fieldName!];
|
|
398
|
+
if (compCtor) {
|
|
399
|
+
const typeIdForComponent = storage.getComponentId(compCtor.name);
|
|
400
|
+
const componentData = await context.loaders.componentsByEntityType.load({
|
|
401
|
+
entityId: entityId,
|
|
402
|
+
typeId: typeIdForComponent,
|
|
403
|
+
});
|
|
404
|
+
if (componentData?.data && componentData.data[propName!] !== undefined) {
|
|
405
|
+
foreignId = componentData.data[propName!];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
const candidateLoads: Array<{ compCtor: any; typeId: string }> = [];
|
|
410
|
+
for (const [componentField, compCtor] of Object.entries(archetype.componentMap)) {
|
|
411
|
+
const compCtorAny = compCtor as any;
|
|
412
|
+
const typeIdForComponent = storage.getComponentId(compCtorAny.name);
|
|
413
|
+
const componentProps = storage.getComponentProperties(typeIdForComponent);
|
|
414
|
+
const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
|
|
415
|
+
if (hasForeignKey && foreignKey) {
|
|
416
|
+
candidateLoads.push({ compCtor: compCtorAny, typeId: typeIdForComponent });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (candidateLoads.length > 0) {
|
|
421
|
+
const componentDataResults = await Promise.all(
|
|
422
|
+
candidateLoads.map(({ typeId }) =>
|
|
423
|
+
context.loaders.componentsByEntityType.load({
|
|
424
|
+
entityId: entityId,
|
|
425
|
+
typeId: typeId,
|
|
426
|
+
})
|
|
427
|
+
)
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
for (const componentData of componentDataResults) {
|
|
431
|
+
if (componentData?.data && componentData.data[foreignKey] !== undefined) {
|
|
432
|
+
foreignId = componentData.data[foreignKey];
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (!foreignId) {
|
|
441
|
+
const entity = await ensureEntity(parent, context);
|
|
442
|
+
const foreignKey = relationOptions.foreignKey;
|
|
443
|
+
if (foreignKey && foreignKey.includes('.')) {
|
|
444
|
+
const [fieldName, propName] = foreignKey.split('.');
|
|
445
|
+
const compCtor = archetype.componentMap[fieldName!];
|
|
446
|
+
if (compCtor) {
|
|
447
|
+
const componentInstance = await entity.get(compCtor as any);
|
|
448
|
+
if (componentInstance && (componentInstance as any)[propName!] !== undefined) {
|
|
449
|
+
foreignId = (componentInstance as any)[propName!];
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
const candidateComponents: Array<{ compCtor: any }> = [];
|
|
454
|
+
for (const compCtor of Object.values(archetype.componentMap)) {
|
|
455
|
+
const compCtorAny = compCtor as any;
|
|
456
|
+
const typeIdForComponent = storage.getComponentId(compCtorAny.name);
|
|
457
|
+
const componentProps = storage.getComponentProperties(typeIdForComponent);
|
|
458
|
+
const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
|
|
459
|
+
if (hasForeignKey && foreignKey) {
|
|
460
|
+
candidateComponents.push({ compCtor: compCtorAny });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (candidateComponents.length > 0) {
|
|
465
|
+
const componentInstances = await Promise.all(
|
|
466
|
+
candidateComponents.map(({ compCtor }) => entity.get(compCtor as any))
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
for (const componentInstance of componentInstances) {
|
|
470
|
+
if (componentInstance && (componentInstance as any)[foreignKey] !== undefined) {
|
|
471
|
+
foreignId = (componentInstance as any)[foreignKey];
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (!foreignId) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (context?.loaders?.entityById) {
|
|
484
|
+
const relatedEntity = await context.loaders.entityById.load(foreignId);
|
|
485
|
+
if (relatedEntity) {
|
|
486
|
+
return relatedEntity;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return Entity.FindById(foreignId);
|
|
491
|
+
} else {
|
|
492
|
+
if (context?.loaders?.relationsByEntityField) {
|
|
493
|
+
const results =
|
|
494
|
+
await context.loaders.relationsByEntityField.load({
|
|
495
|
+
entityId: entityId,
|
|
496
|
+
relationField: field,
|
|
497
|
+
relatedType: relatedTypeName,
|
|
498
|
+
foreignKey: relationOptions?.foreignKey,
|
|
499
|
+
});
|
|
500
|
+
if (results.length > 0) {
|
|
501
|
+
return results[0];
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
console.warn(
|
|
506
|
+
`No relationsByEntityField loader found for single relation ${field} on ${archetypeName}`
|
|
507
|
+
);
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
for (const { propertyKey, options } of archetype.functions) {
|
|
516
|
+
resolvers.push({
|
|
517
|
+
typeName: archetypeName,
|
|
518
|
+
fieldName: propertyKey,
|
|
519
|
+
resolver: async (parent: any, args: any, context: any) => {
|
|
520
|
+
let entity: Entity;
|
|
521
|
+
if (parent instanceof Entity) {
|
|
522
|
+
entity = parent;
|
|
523
|
+
} else if (parent && parent.id) {
|
|
524
|
+
if (context.loaders?.entityById) {
|
|
525
|
+
const loadedEntity = await context.loaders.entityById.load(parent.id);
|
|
526
|
+
if (loadedEntity) {
|
|
527
|
+
entity = loadedEntity;
|
|
528
|
+
} else {
|
|
529
|
+
entity = new Entity(parent.id);
|
|
530
|
+
entity.setPersisted(true);
|
|
531
|
+
}
|
|
532
|
+
} else {
|
|
533
|
+
entity = new Entity(parent.id);
|
|
534
|
+
entity.setPersisted(true);
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
throw new Error(`Invalid parent for ${archetypeName}.${propertyKey}: parent must have an 'id' property`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (options?.args && options.args.length > 0 && args) {
|
|
541
|
+
const functionArgs: any[] = [];
|
|
542
|
+
|
|
543
|
+
for (const argDef of options.args) {
|
|
544
|
+
const argValue = args[argDef.name];
|
|
545
|
+
|
|
546
|
+
if (argValue === undefined || argValue === null) {
|
|
547
|
+
if (!argDef.nullable) {
|
|
548
|
+
throw new Error(`Required argument '${argDef.name}' is missing for ${archetypeName}.${propertyKey}`);
|
|
549
|
+
}
|
|
550
|
+
functionArgs.push(null);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
let convertedValue: any = argValue;
|
|
555
|
+
|
|
556
|
+
if (argDef.type && typeof argDef.type === 'function' && argDef.type !== String && argDef.type !== Number && argDef.type !== Boolean && argDef.type !== Date) {
|
|
557
|
+
const isCustomType = customTypeRegistry.has(argDef.type) ||
|
|
558
|
+
customTypeNameRegistry.has(argDef.type) ||
|
|
559
|
+
(argDef.type?.name && registeredCustomTypes.has(argDef.type.name));
|
|
560
|
+
|
|
561
|
+
if (isCustomType && typeof argValue === 'object' && !Array.isArray(argValue)) {
|
|
562
|
+
try {
|
|
563
|
+
if (argDef.type.prototype && argDef.type.prototype.constructor) {
|
|
564
|
+
convertedValue = Object.assign(Object.create(argDef.type.prototype), argValue);
|
|
565
|
+
|
|
566
|
+
if (!convertedValue || !(convertedValue instanceof argDef.type)) {
|
|
567
|
+
const constructor = argDef.type.prototype.constructor;
|
|
568
|
+
const paramCount = constructor.length;
|
|
569
|
+
|
|
570
|
+
if (paramCount === 2) {
|
|
571
|
+
if (argValue.latitude !== undefined && argValue.longitude !== undefined) {
|
|
572
|
+
convertedValue = new argDef.type(argValue.latitude, argValue.longitude);
|
|
573
|
+
} else if (argValue.x !== undefined && argValue.y !== undefined) {
|
|
574
|
+
convertedValue = new argDef.type(argValue.x, argValue.y);
|
|
575
|
+
} else {
|
|
576
|
+
const values = Object.values(argValue);
|
|
577
|
+
if (values.length >= 2) {
|
|
578
|
+
convertedValue = new argDef.type(values[0], values[1]);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} else if (paramCount === 1) {
|
|
582
|
+
const values = Object.values(argValue);
|
|
583
|
+
if (values.length >= 1) {
|
|
584
|
+
convertedValue = new argDef.type(values[0]);
|
|
585
|
+
}
|
|
586
|
+
} else if (paramCount === 0) {
|
|
587
|
+
convertedValue = Object.assign(Object.create(argDef.type.prototype), argValue);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (!convertedValue || !(convertedValue instanceof argDef.type)) {
|
|
591
|
+
convertedValue = Object.assign(Object.create(argDef.type.prototype), argValue);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
} else {
|
|
595
|
+
convertedValue = argValue;
|
|
596
|
+
}
|
|
597
|
+
} catch (e) {
|
|
598
|
+
try {
|
|
599
|
+
convertedValue = Object.assign(Object.create(argDef.type.prototype || {}), argValue);
|
|
600
|
+
} catch (e2) {
|
|
601
|
+
convertedValue = argValue;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
convertedValue = argValue;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
functionArgs.push(convertedValue);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return await archetype[propertyKey](entity, ...functionArgs);
|
|
613
|
+
} else {
|
|
614
|
+
return await archetype[propertyKey](entity);
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return resolvers;
|
|
621
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ComponentPropertyMetadata } from "../metadata/definitions/Component";
|
|
2
|
+
|
|
3
|
+
export const primitiveTypes = [String, Number, Boolean, Date];
|
|
4
|
+
|
|
5
|
+
export function compNameToFieldName(compName: string): string {
|
|
6
|
+
return (
|
|
7
|
+
compName.charAt(0).toLowerCase() +
|
|
8
|
+
compName.slice(1).replace(/Component$/, "Component")
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Helper to determine if a component should be unwrapped to a scalar value.
|
|
14
|
+
* Returns true if the component has a single 'value' property and the field type is primitive.
|
|
15
|
+
*/
|
|
16
|
+
export function shouldUnwrapComponent(
|
|
17
|
+
componentProps: ComponentPropertyMetadata[],
|
|
18
|
+
fieldType: any
|
|
19
|
+
): boolean {
|
|
20
|
+
if (
|
|
21
|
+
fieldType === String ||
|
|
22
|
+
fieldType === Number ||
|
|
23
|
+
fieldType === Boolean ||
|
|
24
|
+
fieldType === Date
|
|
25
|
+
) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|