bunsane 0.3.1 → 0.4.0
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/CHANGELOG.md +445 -318
- package/config/cache.config.ts +35 -1
- package/core/App.ts +24 -1064
- package/core/ArcheType.ts +78 -2110
- package/core/BatchLoader.ts +56 -32
- package/core/Entity.ts +85 -1043
- package/core/EntityHookManager.ts +52 -754
- package/core/Logger.ts +10 -0
- package/core/RequestContext.ts +64 -6
- package/core/RequestLoaders.ts +187 -36
- package/core/SchedulerManager.ts +28 -600
- package/core/app/bootstrap.ts +133 -0
- package/core/app/cors.ts +85 -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 +15 -0
- package/core/app/processHandlers.ts +43 -0
- package/core/app/requestRouter.ts +310 -0
- package/core/app/restRegistry.ts +80 -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 +666 -0
- package/core/archetype/helpers.ts +29 -0
- package/core/archetype/relationLoader.ts +161 -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 +173 -267
- package/core/cache/CompressionUtils.ts +34 -3
- package/core/cache/MemoryCache.ts +40 -37
- package/core/cache/RedisCache.ts +4 -4
- package/core/cache/health.ts +30 -0
- package/core/cache/invalidation.ts +96 -0
- package/core/cache/strategies/writeInvalidate.ts +111 -0
- package/core/cache/strategies/writeThrough.ts +233 -0
- package/core/components/BaseComponent.ts +16 -8
- package/core/components/ComponentRegistry.ts +28 -0
- package/core/decorators/IndexedField.ts +1 -1
- package/core/entity/cacheStrategies.ts +97 -0
- package/core/entity/componentAccess.ts +364 -0
- package/core/entity/finders.ts +202 -0
- package/core/entity/pendingOps.ts +72 -0
- package/core/entity/saveEntity.ts +377 -0
- package/core/hooks/dispatcher.ts +439 -0
- package/core/hooks/guards.ts +155 -0
- package/core/hooks/registry.ts +247 -0
- package/core/metadata/definitions/Component.ts +1 -1
- package/core/metadata/index.ts +15 -4
- package/core/middleware/AccessLog.ts +8 -1
- package/core/middleware/RateLimit.ts +102 -105
- package/core/middleware/RequestId.ts +2 -9
- package/core/middleware/SecurityHeaders.ts +2 -11
- package/core/middleware/headers.ts +28 -0
- package/core/remote/OutboxWorker.ts +213 -183
- package/core/remote/RemoteManager.ts +401 -400
- package/core/remote/types.ts +153 -151
- package/core/requestScope.ts +34 -0
- package/core/scheduler/cronEvaluator.ts +174 -0
- package/core/scheduler/lifecycleHooks.ts +21 -0
- package/core/scheduler/lockCoordinator.ts +27 -0
- package/core/scheduler/metrics.ts +14 -0
- package/core/scheduler/taskRunner.ts +420 -0
- package/database/DatabaseHelper.ts +128 -101
- package/database/IndexingStrategy.ts +72 -2
- package/database/PreparedStatementCache.ts +20 -5
- package/database/cancellable.ts +35 -0
- package/database/index.ts +15 -3
- package/database/instrumentedDb.ts +141 -0
- package/endpoints/archetypes.ts +2 -8
- package/endpoints/tables.ts +6 -1
- package/gql/index.ts +1 -1
- package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
- package/package.json +22 -1
- package/query/CTENode.ts +5 -3
- package/query/ComponentInclusionNode.ts +240 -13
- package/query/OrNode.ts +6 -5
- package/query/Query.ts +203 -59
- package/query/QueryContext.ts +6 -0
- package/query/QueryDAG.ts +7 -2
- package/query/membershipSource.ts +66 -0
- package/storage/LocalStorageProvider.ts +8 -3
- package/studio/dist/assets/index-BMZ67Npg.js +254 -0
- package/studio/dist/assets/index-BpbuYz9g.css +1 -0
- package/studio/{index.html → dist/index.html} +3 -2
- package/swagger/generator.ts +11 -1
- package/upload/UploadManager.ts +8 -6
- package/utils/uuid.ts +40 -10
- package/.claude/settings.local.json +0 -47
- package/.prettierrc +0 -4
- package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
- package/.serena/memories/architecture.md +0 -154
- package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
- package/.serena/memories/code_style_and_conventions.md +0 -76
- package/.serena/memories/project_overview.md +0 -43
- package/.serena/memories/schema-dsl-plan.md +0 -107
- package/.serena/memories/suggested_commands.md +0 -80
- package/.serena/memories/typescript-compilation-status.md +0 -54
- package/.serena/project.yml +0 -114
- package/BunSane.jpg +0 -0
- package/CLAUDE.md +0 -198
- package/TODO.md +0 -2
- package/bun.lock +0 -302
- package/bunfig.toml +0 -10
- package/docs/SCALABILITY_PLAN.md +0 -175
- package/studio/bun.lock +0 -482
- package/studio/package.json +0 -39
- package/studio/postcss.config.js +0 -6
- package/studio/src/components/DataTable.tsx +0 -211
- package/studio/src/components/Layout.tsx +0 -13
- package/studio/src/components/PageContainer.tsx +0 -9
- package/studio/src/components/PageHeader.tsx +0 -13
- package/studio/src/components/SearchBar.tsx +0 -57
- package/studio/src/components/Sidebar.tsx +0 -294
- package/studio/src/components/ui/button.tsx +0 -56
- package/studio/src/components/ui/checkbox.tsx +0 -26
- package/studio/src/components/ui/input.tsx +0 -25
- package/studio/src/hooks/useDataTable.ts +0 -131
- package/studio/src/index.css +0 -36
- package/studio/src/lib/api.ts +0 -186
- package/studio/src/lib/utils.ts +0 -13
- package/studio/src/main.tsx +0 -17
- package/studio/src/pages/ArcheType.tsx +0 -239
- package/studio/src/pages/Components.tsx +0 -124
- package/studio/src/pages/EntityInspector.tsx +0 -302
- package/studio/src/pages/QueryRunner.tsx +0 -246
- package/studio/src/pages/Table.tsx +0 -94
- package/studio/src/pages/Welcome.tsx +0 -241
- package/studio/src/routes.tsx +0 -45
- package/studio/src/store/archeTypeSettings.ts +0 -30
- package/studio/src/store/studio.ts +0 -65
- package/studio/src/utils/columnHelpers.tsx +0 -114
- package/studio/studio-instructions.md +0 -81
- package/studio/tailwind.config.js +0 -77
- package/studio/utils.ts +0 -54
- package/studio/vite.config.js +0 -19
- package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
- package/tests/benchmark/bunfig.toml +0 -9
- package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
- package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
- package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
- package/tests/benchmark/fixtures/index.ts +0 -6
- package/tests/benchmark/index.ts +0 -22
- package/tests/benchmark/noop-preload.ts +0 -3
- package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
- package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
- package/tests/benchmark/runners/index.ts +0 -4
- package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
- package/tests/benchmark/scripts/generate-db.ts +0 -344
- package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
- package/tests/e2e/http.test.ts +0 -130
- package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
- package/tests/fixtures/components/TestOrder.ts +0 -23
- package/tests/fixtures/components/TestProduct.ts +0 -23
- package/tests/fixtures/components/TestUser.ts +0 -20
- package/tests/fixtures/components/index.ts +0 -6
- package/tests/graphql/SchemaGeneration.test.ts +0 -90
- package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
- package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
- package/tests/helpers/MockRedisClient.ts +0 -113
- package/tests/helpers/MockRedisStreamServer.ts +0 -448
- package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
- package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
- package/tests/integration/entity/Entity.persistence.test.ts +0 -333
- package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
- package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
- package/tests/integration/query/Query.edgeCases.test.ts +0 -595
- package/tests/integration/query/Query.exec.test.ts +0 -576
- package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
- package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
- package/tests/integration/remote/dlq.test.ts +0 -175
- package/tests/integration/remote/event-dispatch.test.ts +0 -114
- package/tests/integration/remote/outbox.test.ts +0 -130
- package/tests/integration/remote/rpc.test.ts +0 -177
- package/tests/pglite-setup.ts +0 -62
- package/tests/setup.ts +0 -164
- package/tests/stress/BenchmarkRunner.ts +0 -203
- package/tests/stress/DataSeeder.ts +0 -190
- package/tests/stress/StressTestReporter.ts +0 -229
- package/tests/stress/cursor-perf-test.ts +0 -171
- package/tests/stress/fixtures/RealisticComponents.ts +0 -235
- package/tests/stress/fixtures/StressTestComponents.ts +0 -58
- package/tests/stress/index.ts +0 -7
- package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
- package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
- package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
- package/tests/unit/BatchLoader.test.ts +0 -196
- package/tests/unit/archetype/ArcheType.test.ts +0 -107
- package/tests/unit/cache/CacheManager.test.ts +0 -367
- package/tests/unit/cache/MemoryCache.test.ts +0 -260
- package/tests/unit/cache/RedisCache.test.ts +0 -411
- package/tests/unit/entity/Entity.components.test.ts +0 -317
- package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
- package/tests/unit/entity/Entity.reload.test.ts +0 -63
- package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
- package/tests/unit/entity/Entity.test.ts +0 -345
- package/tests/unit/gql/depthLimit.test.ts +0 -203
- package/tests/unit/gql/operationMiddleware.test.ts +0 -293
- package/tests/unit/health/Health.test.ts +0 -129
- package/tests/unit/middleware/AccessLog.test.ts +0 -37
- package/tests/unit/middleware/Middleware.test.ts +0 -98
- package/tests/unit/middleware/RequestId.test.ts +0 -54
- package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
- package/tests/unit/query/FilterBuilder.test.ts +0 -111
- package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
- package/tests/unit/query/Query.emptyString.test.ts +0 -69
- package/tests/unit/query/Query.test.ts +0 -310
- package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
- package/tests/unit/remote/RemoteError.test.ts +0 -55
- package/tests/unit/remote/decorators.test.ts +0 -195
- package/tests/unit/remote/metrics.test.ts +0 -115
- package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
- package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
- package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
- package/tests/unit/schema/schema-integration.test.ts +0 -426
- package/tests/unit/schema/schema.test.ts +0 -580
- package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
- package/tests/unit/upload/RestUpload.test.ts +0 -267
- package/tests/unit/validateEnv.test.ts +0 -82
- package/tests/utils/entity-tracker.ts +0 -57
- package/tests/utils/index.ts +0 -13
- package/tests/utils/test-context.ts +0 -149
|
@@ -0,0 +1,666 @@
|
|
|
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
|
+
// Detect whether the unwrapped 'value' prop is a Date so we can
|
|
61
|
+
// normalize Date instances to ISO strings before they reach
|
|
62
|
+
// gqloom's GraphQLString coercion (which would call .valueOf() and
|
|
63
|
+
// emit epoch ms instead).
|
|
64
|
+
const unwrappedValueProp = componentProps.find(p => p.propertyKey === 'value');
|
|
65
|
+
const isUnwrappedDate = isUnwrapped && unwrappedValueProp?.propertyType === Date;
|
|
66
|
+
const normalizeDateValue = (v: any) =>
|
|
67
|
+
isUnwrappedDate && v instanceof Date ? v.toISOString() : v;
|
|
68
|
+
|
|
69
|
+
if (isUnwrapped) {
|
|
70
|
+
resolvers.push({
|
|
71
|
+
typeName: archetypeName,
|
|
72
|
+
fieldName: field,
|
|
73
|
+
resolver: async (parent: any, args: any, context: any) => {
|
|
74
|
+
const entityId = parent?.id;
|
|
75
|
+
if (!entityId) return normalizeDateValue((parent as any)[field]);
|
|
76
|
+
|
|
77
|
+
if (parent instanceof Entity) {
|
|
78
|
+
if (parent.wasRemoved(componentCtor)) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const inMemoryComp = parent.getInMemory(componentCtor);
|
|
82
|
+
if (inMemoryComp) {
|
|
83
|
+
return normalizeDateValue((inMemoryComp as any)?.value);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (context?.loaders?.componentsByEntityType) {
|
|
88
|
+
const componentData =
|
|
89
|
+
await context.loaders.componentsByEntityType.load({
|
|
90
|
+
entityId: entityId,
|
|
91
|
+
typeId: typeIdHex,
|
|
92
|
+
});
|
|
93
|
+
if (componentData?.data?.value !== undefined) {
|
|
94
|
+
return normalizeDateValue(componentData.data.value);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const entity = await ensureEntity(parent, context);
|
|
99
|
+
const comp = await entity.get(componentCtor);
|
|
100
|
+
return normalizeDateValue((comp as any)?.value);
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
resolvers.push({
|
|
105
|
+
typeName: archetypeName,
|
|
106
|
+
fieldName: field,
|
|
107
|
+
resolver: async (parent: any, args: any, context: any) => {
|
|
108
|
+
const entityId = parent?.id;
|
|
109
|
+
if (!entityId) return (parent as any)[field];
|
|
110
|
+
|
|
111
|
+
if (parent instanceof Entity) {
|
|
112
|
+
if (parent.wasRemoved(componentCtor)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const inMemoryComp = parent.getInMemory(componentCtor);
|
|
116
|
+
if (inMemoryComp) {
|
|
117
|
+
return inMemoryComp;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (context?.loaders?.componentsByEntityType) {
|
|
122
|
+
const componentData =
|
|
123
|
+
await context.loaders.componentsByEntityType.load({
|
|
124
|
+
entityId: entityId,
|
|
125
|
+
typeId: typeIdHex,
|
|
126
|
+
});
|
|
127
|
+
if (componentData?.data) {
|
|
128
|
+
return componentData.data;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const entity = await ensureEntity(parent, context);
|
|
133
|
+
const comp = await entity.get(componentCtor);
|
|
134
|
+
return comp;
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const componentTypeName = compNameToFieldName(componentName);
|
|
139
|
+
|
|
140
|
+
for (const prop of componentProps) {
|
|
141
|
+
const isDateProp = prop.propertyType === Date;
|
|
142
|
+
resolvers.push({
|
|
143
|
+
typeName: componentTypeName,
|
|
144
|
+
fieldName: prop.propertyKey,
|
|
145
|
+
resolver: (parent: any) => {
|
|
146
|
+
const v = parent[prop.propertyKey];
|
|
147
|
+
if (isDateProp && v instanceof Date) {
|
|
148
|
+
return v.toISOString();
|
|
149
|
+
}
|
|
150
|
+
return v;
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const [field, components] of Object.entries(archetype.unionMap)) {
|
|
158
|
+
const componentList = components as any[];
|
|
159
|
+
resolvers.push({
|
|
160
|
+
typeName: archetypeName,
|
|
161
|
+
fieldName: field,
|
|
162
|
+
resolver: async (parent: any, args: any, context: any) => {
|
|
163
|
+
const entityId = parent?.id;
|
|
164
|
+
if (!entityId) return null;
|
|
165
|
+
|
|
166
|
+
for (const component of componentList) {
|
|
167
|
+
const typeId = storage.getComponentId(component.name);
|
|
168
|
+
|
|
169
|
+
if (parent instanceof Entity) {
|
|
170
|
+
if (parent.wasRemoved(component)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const inMemoryComp = parent.getInMemory(component);
|
|
174
|
+
if (inMemoryComp) {
|
|
175
|
+
return {
|
|
176
|
+
__typename: compNameToFieldName(component.name),
|
|
177
|
+
...(inMemoryComp as any).data?.() ?? inMemoryComp,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (context?.loaders?.componentsByEntityType) {
|
|
183
|
+
const componentData =
|
|
184
|
+
await context.loaders.componentsByEntityType.load({
|
|
185
|
+
entityId: entityId,
|
|
186
|
+
typeId: typeId,
|
|
187
|
+
});
|
|
188
|
+
if (componentData?.data) {
|
|
189
|
+
return {
|
|
190
|
+
__typename: compNameToFieldName(component.name),
|
|
191
|
+
...componentData.data,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
const entity = await ensureEntity(parent, context);
|
|
196
|
+
const comp = await entity.get(component);
|
|
197
|
+
if (comp) {
|
|
198
|
+
return {
|
|
199
|
+
__typename: compNameToFieldName(component.name),
|
|
200
|
+
...(comp as any),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return null;
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const [field, relatedArcheType] of Object.entries(archetype.relationMap)) {
|
|
212
|
+
const relationType = archetype.relationTypes[field];
|
|
213
|
+
const relationOptions = archetype.relationOptions[field];
|
|
214
|
+
const isArray =
|
|
215
|
+
relationType === "hasMany" || relationType === "belongsToMany";
|
|
216
|
+
|
|
217
|
+
let relatedTypeName: string;
|
|
218
|
+
if (typeof relatedArcheType === "string") {
|
|
219
|
+
relatedTypeName = relatedArcheType;
|
|
220
|
+
} else {
|
|
221
|
+
const relatedArchetypeId = storage.getComponentId(
|
|
222
|
+
(relatedArcheType as any).name
|
|
223
|
+
);
|
|
224
|
+
const relatedArchetypeMetadata = storage.archetypes.find(
|
|
225
|
+
(a) => a.typeId === relatedArchetypeId
|
|
226
|
+
);
|
|
227
|
+
relatedTypeName =
|
|
228
|
+
relatedArchetypeMetadata?.name ||
|
|
229
|
+
(relatedArcheType as any).name.replace(/ArcheType$/, "");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (
|
|
233
|
+
!isArray &&
|
|
234
|
+
relationType === "belongsTo" &&
|
|
235
|
+
relationOptions?.foreignKey
|
|
236
|
+
) {
|
|
237
|
+
resolvers.push({
|
|
238
|
+
typeName: archetypeName,
|
|
239
|
+
fieldName: field,
|
|
240
|
+
resolver: async (parent: any, args: any, context: any) => {
|
|
241
|
+
const entityId = parent?.id;
|
|
242
|
+
if (!entityId) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let foreignId: string | undefined;
|
|
247
|
+
|
|
248
|
+
if (context?.loaders?.componentsByEntityType) {
|
|
249
|
+
const foreignKey = relationOptions.foreignKey;
|
|
250
|
+
if (foreignKey && foreignKey.includes('.')) {
|
|
251
|
+
const [fieldName, propName] = foreignKey.split('.');
|
|
252
|
+
const compCtor = archetype.componentMap[fieldName!];
|
|
253
|
+
if (compCtor) {
|
|
254
|
+
const typeIdForComponent = storage.getComponentId(compCtor.name);
|
|
255
|
+
const componentData = await context.loaders.componentsByEntityType.load({
|
|
256
|
+
entityId: entityId,
|
|
257
|
+
typeId: typeIdForComponent,
|
|
258
|
+
});
|
|
259
|
+
if (componentData?.data && componentData.data[propName!] !== undefined) {
|
|
260
|
+
foreignId = componentData.data[propName!];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
for (const [componentField, compCtor] of Object.entries(archetype.componentMap)) {
|
|
265
|
+
const compCtorAny = compCtor as any;
|
|
266
|
+
const typeIdForComponent = storage.getComponentId(compCtorAny.name);
|
|
267
|
+
const componentProps = storage.getComponentProperties(typeIdForComponent);
|
|
268
|
+
const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
|
|
269
|
+
if (!hasForeignKey || !foreignKey) continue;
|
|
270
|
+
|
|
271
|
+
const componentData = await context.loaders.componentsByEntityType.load({
|
|
272
|
+
entityId: entityId,
|
|
273
|
+
typeId: typeIdForComponent,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (componentData?.data && componentData.data[foreignKey] !== undefined) {
|
|
277
|
+
foreignId = componentData.data[foreignKey];
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!foreignId) {
|
|
285
|
+
const entity = await ensureEntity(parent, context);
|
|
286
|
+
const foreignKey = relationOptions.foreignKey;
|
|
287
|
+
if (foreignKey && foreignKey.includes('.')) {
|
|
288
|
+
const [fieldName, propName] = foreignKey.split('.');
|
|
289
|
+
const compCtor = archetype.componentMap[fieldName!];
|
|
290
|
+
if (compCtor) {
|
|
291
|
+
const componentInstance = await entity.get(compCtor as any);
|
|
292
|
+
if (componentInstance && (componentInstance as any)[propName!] !== undefined) {
|
|
293
|
+
foreignId = (componentInstance as any)[propName!];
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
for (const compCtor of Object.values(archetype.componentMap)) {
|
|
298
|
+
const compCtorAny = compCtor as any;
|
|
299
|
+
const typeIdForComponent = storage.getComponentId(compCtorAny.name);
|
|
300
|
+
const componentProps = storage.getComponentProperties(typeIdForComponent);
|
|
301
|
+
const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
|
|
302
|
+
if (!hasForeignKey || !foreignKey) continue;
|
|
303
|
+
const componentInstance = await entity.get(compCtorAny);
|
|
304
|
+
if (componentInstance && (componentInstance as any)[foreignKey] !== undefined) {
|
|
305
|
+
foreignId = (componentInstance as any)[foreignKey];
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!foreignId && relationOptions.foreignKey === 'id') {
|
|
313
|
+
foreignId = entityId;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!foreignId) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (context.loaders?.entityById) {
|
|
321
|
+
const relatedEntity =
|
|
322
|
+
await context.loaders.entityById.load(foreignId);
|
|
323
|
+
if (relatedEntity) {
|
|
324
|
+
return relatedEntity;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return Entity.FindById(foreignId);
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
} else if (isArray) {
|
|
332
|
+
// Resolve the FK-bearing component + field ONCE (lazily, then
|
|
333
|
+
// memoized) rather than re-instantiating the related archetype and
|
|
334
|
+
// walking its component metadata on every parent row. The result is
|
|
335
|
+
// captured in the resolver closure.
|
|
336
|
+
let fkResolution:
|
|
337
|
+
| { componentCtor: any; componentTypeId: string; foreignKeyField: string }
|
|
338
|
+
| null
|
|
339
|
+
| undefined;
|
|
340
|
+
const resolveFk = () => {
|
|
341
|
+
if (fkResolution !== undefined) return fkResolution;
|
|
342
|
+
fkResolution = null;
|
|
343
|
+
if (!relationOptions?.foreignKey) return fkResolution;
|
|
344
|
+
|
|
345
|
+
let relatedArchetypeInstance: any = null;
|
|
346
|
+
if (typeof relatedArcheType === "function") {
|
|
347
|
+
relatedArchetypeInstance = new (relatedArcheType as any)();
|
|
348
|
+
} else if (typeof relatedArcheType === "string") {
|
|
349
|
+
const meta = storage.archetypes.find((a) => a.name === relatedArcheType);
|
|
350
|
+
if (meta) relatedArchetypeInstance = new (meta.target as any)();
|
|
351
|
+
}
|
|
352
|
+
if (!relatedArchetypeInstance) return fkResolution;
|
|
353
|
+
|
|
354
|
+
let componentCtor: any = null;
|
|
355
|
+
let foreignKeyField: string = relationOptions.foreignKey;
|
|
356
|
+
if (relationOptions.foreignKey.includes('.')) {
|
|
357
|
+
const [fieldName, propName] = relationOptions.foreignKey.split('.');
|
|
358
|
+
componentCtor = relatedArchetypeInstance.componentMap[fieldName!];
|
|
359
|
+
foreignKeyField = propName!;
|
|
360
|
+
} else {
|
|
361
|
+
for (const comp of Object.values(relatedArchetypeInstance.componentMap) as any[]) {
|
|
362
|
+
const typeId = storage.getComponentId(comp.name);
|
|
363
|
+
const props = storage.getComponentProperties(typeId);
|
|
364
|
+
if (props.some(p => p.propertyKey === relationOptions.foreignKey)) {
|
|
365
|
+
componentCtor = comp;
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (componentCtor) {
|
|
371
|
+
fkResolution = {
|
|
372
|
+
componentCtor,
|
|
373
|
+
componentTypeId: storage.getComponentId(componentCtor.name),
|
|
374
|
+
foreignKeyField,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
return fkResolution;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
resolvers.push({
|
|
381
|
+
typeName: archetypeName,
|
|
382
|
+
fieldName: field,
|
|
383
|
+
resolver: async (parent: any, args: any, context: any) => {
|
|
384
|
+
const entityId = parent?.id;
|
|
385
|
+
if (!entityId) return [];
|
|
386
|
+
|
|
387
|
+
if (relationOptions?.foreignKey) {
|
|
388
|
+
const r = resolveFk();
|
|
389
|
+
if (!r) {
|
|
390
|
+
console.warn(`No component found with foreign key ${relationOptions.foreignKey} in ${relatedTypeName}`);
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
// Batched path: dedups across sibling parents in the
|
|
394
|
+
// same request via the type-scoped FK loader (was N+1).
|
|
395
|
+
if (context?.loaders?.relationsByComponentFk) {
|
|
396
|
+
return await context.loaders.relationsByComponentFk.load({
|
|
397
|
+
entityId,
|
|
398
|
+
componentTypeId: r.componentTypeId,
|
|
399
|
+
foreignKeyField: r.foreignKeyField,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
// Fallback for non-request contexts (direct service
|
|
403
|
+
// calls with no loaders mounted): single query.
|
|
404
|
+
const query = new Query();
|
|
405
|
+
query.with(r.componentCtor, Query.filters(Query.filter(r.foreignKeyField, Query.filterOp.EQ, entityId)));
|
|
406
|
+
return await query.exec();
|
|
407
|
+
} else {
|
|
408
|
+
if (context?.loaders?.relationsByEntityField) {
|
|
409
|
+
return context.loaders.relationsByEntityField.load({
|
|
410
|
+
entityId: entityId,
|
|
411
|
+
relationField: field,
|
|
412
|
+
relatedType: relatedTypeName,
|
|
413
|
+
foreignKey: relationOptions?.foreignKey,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
console.warn(
|
|
418
|
+
`No relationsByEntityField loader found for array relation ${field} on ${archetypeName}`
|
|
419
|
+
);
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
} else {
|
|
425
|
+
resolvers.push({
|
|
426
|
+
typeName: archetypeName,
|
|
427
|
+
fieldName: field,
|
|
428
|
+
resolver: async (parent: any, args: any, context: any) => {
|
|
429
|
+
const entityId = parent?.id;
|
|
430
|
+
|
|
431
|
+
if (relationOptions?.foreignKey) {
|
|
432
|
+
if (!entityId) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
let foreignId: string | undefined;
|
|
437
|
+
|
|
438
|
+
if (context?.loaders?.componentsByEntityType) {
|
|
439
|
+
const foreignKey = relationOptions.foreignKey;
|
|
440
|
+
if (foreignKey && foreignKey.includes('.')) {
|
|
441
|
+
const [fieldName, propName] = foreignKey.split('.');
|
|
442
|
+
const compCtor = archetype.componentMap[fieldName!];
|
|
443
|
+
if (compCtor) {
|
|
444
|
+
const typeIdForComponent = storage.getComponentId(compCtor.name);
|
|
445
|
+
const componentData = await context.loaders.componentsByEntityType.load({
|
|
446
|
+
entityId: entityId,
|
|
447
|
+
typeId: typeIdForComponent,
|
|
448
|
+
});
|
|
449
|
+
if (componentData?.data && componentData.data[propName!] !== undefined) {
|
|
450
|
+
foreignId = componentData.data[propName!];
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
const candidateLoads: Array<{ compCtor: any; typeId: string }> = [];
|
|
455
|
+
for (const [componentField, compCtor] of Object.entries(archetype.componentMap)) {
|
|
456
|
+
const compCtorAny = compCtor as any;
|
|
457
|
+
const typeIdForComponent = storage.getComponentId(compCtorAny.name);
|
|
458
|
+
const componentProps = storage.getComponentProperties(typeIdForComponent);
|
|
459
|
+
const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
|
|
460
|
+
if (hasForeignKey && foreignKey) {
|
|
461
|
+
candidateLoads.push({ compCtor: compCtorAny, typeId: typeIdForComponent });
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (candidateLoads.length > 0) {
|
|
466
|
+
const componentDataResults = await Promise.all(
|
|
467
|
+
candidateLoads.map(({ typeId }) =>
|
|
468
|
+
context.loaders.componentsByEntityType.load({
|
|
469
|
+
entityId: entityId,
|
|
470
|
+
typeId: typeId,
|
|
471
|
+
})
|
|
472
|
+
)
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
for (const componentData of componentDataResults) {
|
|
476
|
+
if (componentData?.data && componentData.data[foreignKey] !== undefined) {
|
|
477
|
+
foreignId = componentData.data[foreignKey];
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (!foreignId) {
|
|
486
|
+
const entity = await ensureEntity(parent, context);
|
|
487
|
+
const foreignKey = relationOptions.foreignKey;
|
|
488
|
+
if (foreignKey && foreignKey.includes('.')) {
|
|
489
|
+
const [fieldName, propName] = foreignKey.split('.');
|
|
490
|
+
const compCtor = archetype.componentMap[fieldName!];
|
|
491
|
+
if (compCtor) {
|
|
492
|
+
const componentInstance = await entity.get(compCtor as any);
|
|
493
|
+
if (componentInstance && (componentInstance as any)[propName!] !== undefined) {
|
|
494
|
+
foreignId = (componentInstance as any)[propName!];
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
const candidateComponents: Array<{ compCtor: any }> = [];
|
|
499
|
+
for (const compCtor of Object.values(archetype.componentMap)) {
|
|
500
|
+
const compCtorAny = compCtor as any;
|
|
501
|
+
const typeIdForComponent = storage.getComponentId(compCtorAny.name);
|
|
502
|
+
const componentProps = storage.getComponentProperties(typeIdForComponent);
|
|
503
|
+
const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
|
|
504
|
+
if (hasForeignKey && foreignKey) {
|
|
505
|
+
candidateComponents.push({ compCtor: compCtorAny });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (candidateComponents.length > 0) {
|
|
510
|
+
const componentInstances = await Promise.all(
|
|
511
|
+
candidateComponents.map(({ compCtor }) => entity.get(compCtor as any))
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
for (const componentInstance of componentInstances) {
|
|
515
|
+
if (componentInstance && (componentInstance as any)[foreignKey] !== undefined) {
|
|
516
|
+
foreignId = (componentInstance as any)[foreignKey];
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (!foreignId) {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (context?.loaders?.entityById) {
|
|
529
|
+
const relatedEntity = await context.loaders.entityById.load(foreignId);
|
|
530
|
+
if (relatedEntity) {
|
|
531
|
+
return relatedEntity;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return Entity.FindById(foreignId);
|
|
536
|
+
} else {
|
|
537
|
+
if (context?.loaders?.relationsByEntityField) {
|
|
538
|
+
const results =
|
|
539
|
+
await context.loaders.relationsByEntityField.load({
|
|
540
|
+
entityId: entityId,
|
|
541
|
+
relationField: field,
|
|
542
|
+
relatedType: relatedTypeName,
|
|
543
|
+
foreignKey: relationOptions?.foreignKey,
|
|
544
|
+
});
|
|
545
|
+
if (results.length > 0) {
|
|
546
|
+
return results[0];
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
console.warn(
|
|
551
|
+
`No relationsByEntityField loader found for single relation ${field} on ${archetypeName}`
|
|
552
|
+
);
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
for (const { propertyKey, options } of archetype.functions) {
|
|
561
|
+
resolvers.push({
|
|
562
|
+
typeName: archetypeName,
|
|
563
|
+
fieldName: propertyKey,
|
|
564
|
+
resolver: async (parent: any, args: any, context: any) => {
|
|
565
|
+
let entity: Entity;
|
|
566
|
+
if (parent instanceof Entity) {
|
|
567
|
+
entity = parent;
|
|
568
|
+
} else if (parent && parent.id) {
|
|
569
|
+
if (context.loaders?.entityById) {
|
|
570
|
+
const loadedEntity = await context.loaders.entityById.load(parent.id);
|
|
571
|
+
if (loadedEntity) {
|
|
572
|
+
entity = loadedEntity;
|
|
573
|
+
} else {
|
|
574
|
+
entity = new Entity(parent.id);
|
|
575
|
+
entity.setPersisted(true);
|
|
576
|
+
}
|
|
577
|
+
} else {
|
|
578
|
+
entity = new Entity(parent.id);
|
|
579
|
+
entity.setPersisted(true);
|
|
580
|
+
}
|
|
581
|
+
} else {
|
|
582
|
+
throw new Error(`Invalid parent for ${archetypeName}.${propertyKey}: parent must have an 'id' property`);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (options?.args && options.args.length > 0 && args) {
|
|
586
|
+
const functionArgs: any[] = [];
|
|
587
|
+
|
|
588
|
+
for (const argDef of options.args) {
|
|
589
|
+
const argValue = args[argDef.name];
|
|
590
|
+
|
|
591
|
+
if (argValue === undefined || argValue === null) {
|
|
592
|
+
if (!argDef.nullable) {
|
|
593
|
+
throw new Error(`Required argument '${argDef.name}' is missing for ${archetypeName}.${propertyKey}`);
|
|
594
|
+
}
|
|
595
|
+
functionArgs.push(null);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
let convertedValue: any = argValue;
|
|
600
|
+
|
|
601
|
+
if (argDef.type && typeof argDef.type === 'function' && argDef.type !== String && argDef.type !== Number && argDef.type !== Boolean && argDef.type !== Date) {
|
|
602
|
+
const isCustomType = customTypeRegistry.has(argDef.type) ||
|
|
603
|
+
customTypeNameRegistry.has(argDef.type) ||
|
|
604
|
+
(argDef.type?.name && registeredCustomTypes.has(argDef.type.name));
|
|
605
|
+
|
|
606
|
+
if (isCustomType && typeof argValue === 'object' && !Array.isArray(argValue)) {
|
|
607
|
+
try {
|
|
608
|
+
if (argDef.type.prototype && argDef.type.prototype.constructor) {
|
|
609
|
+
convertedValue = Object.assign(Object.create(argDef.type.prototype), argValue);
|
|
610
|
+
|
|
611
|
+
if (!convertedValue || !(convertedValue instanceof argDef.type)) {
|
|
612
|
+
const constructor = argDef.type.prototype.constructor;
|
|
613
|
+
const paramCount = constructor.length;
|
|
614
|
+
|
|
615
|
+
if (paramCount === 2) {
|
|
616
|
+
if (argValue.latitude !== undefined && argValue.longitude !== undefined) {
|
|
617
|
+
convertedValue = new argDef.type(argValue.latitude, argValue.longitude);
|
|
618
|
+
} else if (argValue.x !== undefined && argValue.y !== undefined) {
|
|
619
|
+
convertedValue = new argDef.type(argValue.x, argValue.y);
|
|
620
|
+
} else {
|
|
621
|
+
const values = Object.values(argValue);
|
|
622
|
+
if (values.length >= 2) {
|
|
623
|
+
convertedValue = new argDef.type(values[0], values[1]);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
} else if (paramCount === 1) {
|
|
627
|
+
const values = Object.values(argValue);
|
|
628
|
+
if (values.length >= 1) {
|
|
629
|
+
convertedValue = new argDef.type(values[0]);
|
|
630
|
+
}
|
|
631
|
+
} else if (paramCount === 0) {
|
|
632
|
+
convertedValue = Object.assign(Object.create(argDef.type.prototype), argValue);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (!convertedValue || !(convertedValue instanceof argDef.type)) {
|
|
636
|
+
convertedValue = Object.assign(Object.create(argDef.type.prototype), argValue);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
} else {
|
|
640
|
+
convertedValue = argValue;
|
|
641
|
+
}
|
|
642
|
+
} catch (e) {
|
|
643
|
+
try {
|
|
644
|
+
convertedValue = Object.assign(Object.create(argDef.type.prototype || {}), argValue);
|
|
645
|
+
} catch (e2) {
|
|
646
|
+
convertedValue = argValue;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
} else {
|
|
650
|
+
convertedValue = argValue;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
functionArgs.push(convertedValue);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return await archetype[propertyKey](entity, ...functionArgs);
|
|
658
|
+
} else {
|
|
659
|
+
return await archetype[propertyKey](entity);
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return resolvers;
|
|
666
|
+
}
|