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.
Files changed (224) hide show
  1. package/CHANGELOG.md +445 -318
  2. package/config/cache.config.ts +35 -1
  3. package/core/App.ts +24 -1064
  4. package/core/ArcheType.ts +78 -2110
  5. package/core/BatchLoader.ts +56 -32
  6. package/core/Entity.ts +85 -1043
  7. package/core/EntityHookManager.ts +52 -754
  8. package/core/Logger.ts +10 -0
  9. package/core/RequestContext.ts +64 -6
  10. package/core/RequestLoaders.ts +187 -36
  11. package/core/SchedulerManager.ts +28 -600
  12. package/core/app/bootstrap.ts +133 -0
  13. package/core/app/cors.ts +85 -0
  14. package/core/app/graphqlSetup.ts +56 -0
  15. package/core/app/healthEndpoints.ts +31 -0
  16. package/core/app/metricsCollector.ts +27 -0
  17. package/core/app/preparedStatementWarmup.ts +15 -0
  18. package/core/app/processHandlers.ts +43 -0
  19. package/core/app/requestRouter.ts +310 -0
  20. package/core/app/restRegistry.ts +80 -0
  21. package/core/app/shutdown.ts +97 -0
  22. package/core/app/studioRouter.ts +83 -0
  23. package/core/archetype/customTypes.ts +100 -0
  24. package/core/archetype/decorators.ts +171 -0
  25. package/core/archetype/fieldResolvers.ts +666 -0
  26. package/core/archetype/helpers.ts +29 -0
  27. package/core/archetype/relationLoader.ts +161 -0
  28. package/core/archetype/schemaBuilder.ts +141 -0
  29. package/core/archetype/weaver.ts +218 -0
  30. package/core/archetype/zodSchemaBuilder.ts +527 -0
  31. package/core/cache/CacheManager.ts +173 -267
  32. package/core/cache/CompressionUtils.ts +34 -3
  33. package/core/cache/MemoryCache.ts +40 -37
  34. package/core/cache/RedisCache.ts +4 -4
  35. package/core/cache/health.ts +30 -0
  36. package/core/cache/invalidation.ts +96 -0
  37. package/core/cache/strategies/writeInvalidate.ts +111 -0
  38. package/core/cache/strategies/writeThrough.ts +233 -0
  39. package/core/components/BaseComponent.ts +16 -8
  40. package/core/components/ComponentRegistry.ts +28 -0
  41. package/core/decorators/IndexedField.ts +1 -1
  42. package/core/entity/cacheStrategies.ts +97 -0
  43. package/core/entity/componentAccess.ts +364 -0
  44. package/core/entity/finders.ts +202 -0
  45. package/core/entity/pendingOps.ts +72 -0
  46. package/core/entity/saveEntity.ts +377 -0
  47. package/core/hooks/dispatcher.ts +439 -0
  48. package/core/hooks/guards.ts +155 -0
  49. package/core/hooks/registry.ts +247 -0
  50. package/core/metadata/definitions/Component.ts +1 -1
  51. package/core/metadata/index.ts +15 -4
  52. package/core/middleware/AccessLog.ts +8 -1
  53. package/core/middleware/RateLimit.ts +102 -105
  54. package/core/middleware/RequestId.ts +2 -9
  55. package/core/middleware/SecurityHeaders.ts +2 -11
  56. package/core/middleware/headers.ts +28 -0
  57. package/core/remote/OutboxWorker.ts +213 -183
  58. package/core/remote/RemoteManager.ts +401 -400
  59. package/core/remote/types.ts +153 -151
  60. package/core/requestScope.ts +34 -0
  61. package/core/scheduler/cronEvaluator.ts +174 -0
  62. package/core/scheduler/lifecycleHooks.ts +21 -0
  63. package/core/scheduler/lockCoordinator.ts +27 -0
  64. package/core/scheduler/metrics.ts +14 -0
  65. package/core/scheduler/taskRunner.ts +420 -0
  66. package/database/DatabaseHelper.ts +128 -101
  67. package/database/IndexingStrategy.ts +72 -2
  68. package/database/PreparedStatementCache.ts +20 -5
  69. package/database/cancellable.ts +35 -0
  70. package/database/index.ts +15 -3
  71. package/database/instrumentedDb.ts +141 -0
  72. package/endpoints/archetypes.ts +2 -8
  73. package/endpoints/tables.ts +6 -1
  74. package/gql/index.ts +1 -1
  75. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  76. package/package.json +22 -1
  77. package/query/CTENode.ts +5 -3
  78. package/query/ComponentInclusionNode.ts +240 -13
  79. package/query/OrNode.ts +6 -5
  80. package/query/Query.ts +203 -59
  81. package/query/QueryContext.ts +6 -0
  82. package/query/QueryDAG.ts +7 -2
  83. package/query/membershipSource.ts +66 -0
  84. package/storage/LocalStorageProvider.ts +8 -3
  85. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  86. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  87. package/studio/{index.html → dist/index.html} +3 -2
  88. package/swagger/generator.ts +11 -1
  89. package/upload/UploadManager.ts +8 -6
  90. package/utils/uuid.ts +40 -10
  91. package/.claude/settings.local.json +0 -47
  92. package/.prettierrc +0 -4
  93. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  94. package/.serena/memories/architecture.md +0 -154
  95. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  96. package/.serena/memories/code_style_and_conventions.md +0 -76
  97. package/.serena/memories/project_overview.md +0 -43
  98. package/.serena/memories/schema-dsl-plan.md +0 -107
  99. package/.serena/memories/suggested_commands.md +0 -80
  100. package/.serena/memories/typescript-compilation-status.md +0 -54
  101. package/.serena/project.yml +0 -114
  102. package/BunSane.jpg +0 -0
  103. package/CLAUDE.md +0 -198
  104. package/TODO.md +0 -2
  105. package/bun.lock +0 -302
  106. package/bunfig.toml +0 -10
  107. package/docs/SCALABILITY_PLAN.md +0 -175
  108. package/studio/bun.lock +0 -482
  109. package/studio/package.json +0 -39
  110. package/studio/postcss.config.js +0 -6
  111. package/studio/src/components/DataTable.tsx +0 -211
  112. package/studio/src/components/Layout.tsx +0 -13
  113. package/studio/src/components/PageContainer.tsx +0 -9
  114. package/studio/src/components/PageHeader.tsx +0 -13
  115. package/studio/src/components/SearchBar.tsx +0 -57
  116. package/studio/src/components/Sidebar.tsx +0 -294
  117. package/studio/src/components/ui/button.tsx +0 -56
  118. package/studio/src/components/ui/checkbox.tsx +0 -26
  119. package/studio/src/components/ui/input.tsx +0 -25
  120. package/studio/src/hooks/useDataTable.ts +0 -131
  121. package/studio/src/index.css +0 -36
  122. package/studio/src/lib/api.ts +0 -186
  123. package/studio/src/lib/utils.ts +0 -13
  124. package/studio/src/main.tsx +0 -17
  125. package/studio/src/pages/ArcheType.tsx +0 -239
  126. package/studio/src/pages/Components.tsx +0 -124
  127. package/studio/src/pages/EntityInspector.tsx +0 -302
  128. package/studio/src/pages/QueryRunner.tsx +0 -246
  129. package/studio/src/pages/Table.tsx +0 -94
  130. package/studio/src/pages/Welcome.tsx +0 -241
  131. package/studio/src/routes.tsx +0 -45
  132. package/studio/src/store/archeTypeSettings.ts +0 -30
  133. package/studio/src/store/studio.ts +0 -65
  134. package/studio/src/utils/columnHelpers.tsx +0 -114
  135. package/studio/studio-instructions.md +0 -81
  136. package/studio/tailwind.config.js +0 -77
  137. package/studio/utils.ts +0 -54
  138. package/studio/vite.config.js +0 -19
  139. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  140. package/tests/benchmark/bunfig.toml +0 -9
  141. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  142. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  143. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  144. package/tests/benchmark/fixtures/index.ts +0 -6
  145. package/tests/benchmark/index.ts +0 -22
  146. package/tests/benchmark/noop-preload.ts +0 -3
  147. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  148. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  149. package/tests/benchmark/runners/index.ts +0 -4
  150. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  151. package/tests/benchmark/scripts/generate-db.ts +0 -344
  152. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  153. package/tests/e2e/http.test.ts +0 -130
  154. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  155. package/tests/fixtures/components/TestOrder.ts +0 -23
  156. package/tests/fixtures/components/TestProduct.ts +0 -23
  157. package/tests/fixtures/components/TestUser.ts +0 -20
  158. package/tests/fixtures/components/index.ts +0 -6
  159. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  160. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  161. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  162. package/tests/helpers/MockRedisClient.ts +0 -113
  163. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  164. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  165. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  166. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  167. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  168. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  169. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  170. package/tests/integration/query/Query.exec.test.ts +0 -576
  171. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  172. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  173. package/tests/integration/remote/dlq.test.ts +0 -175
  174. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  175. package/tests/integration/remote/outbox.test.ts +0 -130
  176. package/tests/integration/remote/rpc.test.ts +0 -177
  177. package/tests/pglite-setup.ts +0 -62
  178. package/tests/setup.ts +0 -164
  179. package/tests/stress/BenchmarkRunner.ts +0 -203
  180. package/tests/stress/DataSeeder.ts +0 -190
  181. package/tests/stress/StressTestReporter.ts +0 -229
  182. package/tests/stress/cursor-perf-test.ts +0 -171
  183. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  184. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  185. package/tests/stress/index.ts +0 -7
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  187. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  188. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  189. package/tests/unit/BatchLoader.test.ts +0 -196
  190. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  191. package/tests/unit/cache/CacheManager.test.ts +0 -367
  192. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  193. package/tests/unit/cache/RedisCache.test.ts +0 -411
  194. package/tests/unit/entity/Entity.components.test.ts +0 -317
  195. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  196. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  197. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  198. package/tests/unit/entity/Entity.test.ts +0 -345
  199. package/tests/unit/gql/depthLimit.test.ts +0 -203
  200. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  201. package/tests/unit/health/Health.test.ts +0 -129
  202. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  203. package/tests/unit/middleware/Middleware.test.ts +0 -98
  204. package/tests/unit/middleware/RequestId.test.ts +0 -54
  205. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  206. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  207. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  208. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  209. package/tests/unit/query/Query.test.ts +0 -310
  210. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  211. package/tests/unit/remote/RemoteError.test.ts +0 -55
  212. package/tests/unit/remote/decorators.test.ts +0 -195
  213. package/tests/unit/remote/metrics.test.ts +0 -115
  214. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  215. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  216. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  217. package/tests/unit/schema/schema-integration.test.ts +0 -426
  218. package/tests/unit/schema/schema.test.ts +0 -580
  219. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  220. package/tests/unit/upload/RestUpload.test.ts +0 -267
  221. package/tests/unit/validateEnv.test.ts +0 -82
  222. package/tests/utils/entity-tracker.ts +0 -57
  223. package/tests/utils/index.ts +0 -13
  224. 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
+ }