bunsane 0.1.4 → 0.2.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 (257) hide show
  1. package/.claude/settings.local.json +47 -0
  2. package/.claude/skills/update-memory.md +74 -0
  3. package/.prettierrc +4 -0
  4. package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
  5. package/.serena/memories/architecture.md +154 -0
  6. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
  7. package/.serena/memories/code_style_and_conventions.md +76 -0
  8. package/.serena/memories/project_overview.md +43 -0
  9. package/.serena/memories/schema-dsl-plan.md +107 -0
  10. package/.serena/memories/suggested_commands.md +80 -0
  11. package/.serena/memories/typescript-compilation-status.md +54 -0
  12. package/.serena/project.yml +114 -0
  13. package/TODO.md +1 -7
  14. package/bun.lock +150 -4
  15. package/bunfig.toml +10 -0
  16. package/config/cache.config.ts +77 -0
  17. package/config/upload.config.ts +4 -5
  18. package/core/App.ts +870 -123
  19. package/core/ArcheType.ts +2268 -377
  20. package/core/BatchLoader.ts +181 -71
  21. package/core/Config.ts +153 -0
  22. package/core/Decorators.ts +4 -1
  23. package/core/Entity.ts +621 -92
  24. package/core/EntityHookManager.ts +1 -1
  25. package/core/EntityInterface.ts +3 -1
  26. package/core/EntityManager.ts +1 -13
  27. package/core/ErrorHandler.ts +8 -2
  28. package/core/Logger.ts +9 -0
  29. package/core/Middleware.ts +34 -0
  30. package/core/RequestContext.ts +5 -1
  31. package/core/RequestLoaders.ts +227 -93
  32. package/core/SchedulerManager.ts +193 -52
  33. package/core/cache/CacheAnalytics.ts +399 -0
  34. package/core/cache/CacheFactory.ts +145 -0
  35. package/core/cache/CacheManager.ts +520 -0
  36. package/core/cache/CacheProvider.ts +34 -0
  37. package/core/cache/CacheWarmer.ts +157 -0
  38. package/core/cache/CompressionUtils.ts +110 -0
  39. package/core/cache/MemoryCache.ts +251 -0
  40. package/core/cache/MultiLevelCache.ts +180 -0
  41. package/core/cache/NoOpCache.ts +53 -0
  42. package/core/cache/RedisCache.ts +464 -0
  43. package/core/cache/TTLStrategy.ts +254 -0
  44. package/core/cache/index.ts +6 -0
  45. package/core/components/BaseComponent.ts +120 -0
  46. package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
  47. package/core/components/Decorators.ts +88 -0
  48. package/core/components/Interfaces.ts +7 -0
  49. package/core/components/index.ts +5 -0
  50. package/core/decorators/EntityHooks.ts +0 -3
  51. package/core/decorators/IndexedField.ts +26 -0
  52. package/core/decorators/ScheduledTask.ts +0 -47
  53. package/core/events/EntityLifecycleEvents.ts +1 -1
  54. package/core/health.ts +112 -0
  55. package/core/metadata/definitions/ArcheType.ts +14 -0
  56. package/core/metadata/definitions/Component.ts +9 -0
  57. package/core/metadata/definitions/gqlObject.ts +1 -1
  58. package/core/metadata/index.ts +42 -1
  59. package/core/metadata/metadata-storage.ts +28 -2
  60. package/core/middleware/AccessLog.ts +59 -0
  61. package/core/middleware/RequestId.ts +38 -0
  62. package/core/middleware/SecurityHeaders.ts +62 -0
  63. package/core/middleware/index.ts +3 -0
  64. package/core/scheduler/DistributedLock.ts +266 -0
  65. package/core/scheduler/index.ts +15 -0
  66. package/core/validateEnv.ts +92 -0
  67. package/database/DatabaseHelper.ts +416 -40
  68. package/database/IndexingStrategy.ts +342 -0
  69. package/database/PreparedStatementCache.ts +226 -0
  70. package/database/index.ts +32 -7
  71. package/database/sqlHelpers.ts +14 -2
  72. package/endpoints/archetypes.ts +362 -0
  73. package/endpoints/components.ts +58 -0
  74. package/endpoints/entity.ts +80 -0
  75. package/endpoints/index.ts +27 -0
  76. package/endpoints/query.ts +93 -0
  77. package/endpoints/stats.ts +76 -0
  78. package/endpoints/tables.ts +212 -0
  79. package/endpoints/types.ts +155 -0
  80. package/gql/ArchetypeOperations.ts +32 -86
  81. package/gql/Generator.ts +27 -315
  82. package/gql/GeneratorV2.ts +37 -0
  83. package/gql/builders/InputTypeBuilder.ts +99 -0
  84. package/gql/builders/ResolverBuilder.ts +234 -0
  85. package/gql/builders/TypeDefBuilder.ts +105 -0
  86. package/gql/builders/index.ts +3 -0
  87. package/gql/decorators/Upload.ts +1 -1
  88. package/gql/depthLimit.ts +85 -0
  89. package/gql/graph/GraphNode.ts +224 -0
  90. package/gql/graph/SchemaGraph.ts +278 -0
  91. package/gql/helpers.ts +8 -2
  92. package/gql/index.ts +56 -4
  93. package/gql/middleware.ts +79 -0
  94. package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
  95. package/gql/orchestration/index.ts +1 -0
  96. package/gql/scanner/ServiceScanner.ts +347 -0
  97. package/gql/schema/index.ts +458 -0
  98. package/gql/strategies/TypeGenerationStrategy.ts +329 -0
  99. package/gql/types.ts +1 -0
  100. package/gql/utils/TypeSignature.ts +220 -0
  101. package/gql/utils/index.ts +1 -0
  102. package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
  103. package/gql/visitors/DeduplicationVisitor.ts +82 -0
  104. package/gql/visitors/GraphVisitor.ts +78 -0
  105. package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
  106. package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
  107. package/gql/visitors/TypeCollectorVisitor.ts +79 -0
  108. package/gql/visitors/VisitorComposer.ts +96 -0
  109. package/gql/visitors/index.ts +7 -0
  110. package/package.json +59 -37
  111. package/plugins/index.ts +2 -2
  112. package/query/CTENode.ts +97 -0
  113. package/query/ComponentInclusionNode.ts +689 -0
  114. package/query/FilterBuilder.ts +127 -0
  115. package/query/FilterBuilderRegistry.ts +202 -0
  116. package/query/OrNode.ts +517 -0
  117. package/query/OrQuery.ts +42 -0
  118. package/query/Query.ts +1022 -0
  119. package/query/QueryContext.ts +170 -0
  120. package/query/QueryDAG.ts +122 -0
  121. package/query/QueryNode.ts +65 -0
  122. package/query/SourceNode.ts +53 -0
  123. package/query/builders/FullTextSearchBuilder.ts +236 -0
  124. package/query/index.ts +21 -0
  125. package/scheduler/index.ts +40 -8
  126. package/service/Service.ts +2 -1
  127. package/service/ServiceRegistry.ts +6 -5
  128. package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
  129. package/storage/S3StorageProvider.ts +316 -0
  130. package/{core/storage → storage}/StorageProvider.ts +7 -3
  131. package/studio/bun.lock +482 -0
  132. package/studio/index.html +13 -0
  133. package/studio/package.json +39 -0
  134. package/studio/postcss.config.js +6 -0
  135. package/studio/src/components/DataTable.tsx +211 -0
  136. package/studio/src/components/Layout.tsx +13 -0
  137. package/studio/src/components/PageContainer.tsx +9 -0
  138. package/studio/src/components/PageHeader.tsx +13 -0
  139. package/studio/src/components/SearchBar.tsx +57 -0
  140. package/studio/src/components/Sidebar.tsx +294 -0
  141. package/studio/src/components/ui/button.tsx +56 -0
  142. package/studio/src/components/ui/checkbox.tsx +26 -0
  143. package/studio/src/components/ui/input.tsx +25 -0
  144. package/studio/src/hooks/useDataTable.ts +131 -0
  145. package/studio/src/index.css +36 -0
  146. package/studio/src/lib/api.ts +186 -0
  147. package/studio/src/lib/utils.ts +13 -0
  148. package/studio/src/main.tsx +17 -0
  149. package/studio/src/pages/ArcheType.tsx +239 -0
  150. package/studio/src/pages/Components.tsx +124 -0
  151. package/studio/src/pages/EntityInspector.tsx +302 -0
  152. package/studio/src/pages/QueryRunner.tsx +246 -0
  153. package/studio/src/pages/Table.tsx +94 -0
  154. package/studio/src/pages/Welcome.tsx +241 -0
  155. package/studio/src/routes.tsx +45 -0
  156. package/studio/src/store/archeTypeSettings.ts +30 -0
  157. package/studio/src/store/studio.ts +65 -0
  158. package/studio/src/utils/columnHelpers.tsx +114 -0
  159. package/studio/studio-instructions.md +81 -0
  160. package/studio/tailwind.config.js +77 -0
  161. package/studio/tsconfig.json +24 -0
  162. package/studio/utils.ts +54 -0
  163. package/studio/vite.config.js +19 -0
  164. package/swagger/generator.ts +1 -1
  165. package/tests/e2e/http.test.ts +126 -0
  166. package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
  167. package/tests/fixtures/components/TestOrder.ts +23 -0
  168. package/tests/fixtures/components/TestProduct.ts +23 -0
  169. package/tests/fixtures/components/TestUser.ts +20 -0
  170. package/tests/fixtures/components/index.ts +6 -0
  171. package/tests/graphql/SchemaGeneration.test.ts +90 -0
  172. package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
  173. package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
  174. package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
  175. package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
  176. package/tests/integration/entity/Entity.persistence.test.ts +333 -0
  177. package/tests/integration/query/Query.exec.test.ts +523 -0
  178. package/tests/pglite-setup.ts +61 -0
  179. package/tests/setup.ts +164 -0
  180. package/tests/stress/BenchmarkRunner.ts +203 -0
  181. package/tests/stress/DataSeeder.ts +190 -0
  182. package/tests/stress/StressTestReporter.ts +229 -0
  183. package/tests/stress/cursor-perf-test.ts +171 -0
  184. package/tests/stress/fixtures/StressTestComponents.ts +58 -0
  185. package/tests/stress/index.ts +7 -0
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
  187. package/tests/unit/BatchLoader.test.ts +82 -0
  188. package/tests/unit/archetype/ArcheType.test.ts +107 -0
  189. package/tests/unit/cache/CacheManager.test.ts +347 -0
  190. package/tests/unit/cache/MemoryCache.test.ts +260 -0
  191. package/tests/unit/cache/RedisCache.test.ts +411 -0
  192. package/tests/unit/entity/Entity.components.test.ts +244 -0
  193. package/tests/unit/entity/Entity.test.ts +345 -0
  194. package/tests/unit/gql/depthLimit.test.ts +203 -0
  195. package/tests/unit/gql/operationMiddleware.test.ts +293 -0
  196. package/tests/unit/health/Health.test.ts +129 -0
  197. package/tests/unit/middleware/AccessLog.test.ts +37 -0
  198. package/tests/unit/middleware/Middleware.test.ts +98 -0
  199. package/tests/unit/middleware/RequestId.test.ts +54 -0
  200. package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
  201. package/tests/unit/query/FilterBuilder.test.ts +111 -0
  202. package/tests/unit/query/Query.test.ts +308 -0
  203. package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
  204. package/tests/unit/schema/schema-integration.test.ts +426 -0
  205. package/tests/unit/schema/schema.test.ts +580 -0
  206. package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
  207. package/tests/unit/upload/RestUpload.test.ts +267 -0
  208. package/tests/unit/validateEnv.test.ts +82 -0
  209. package/tests/utils/entity-tracker.ts +57 -0
  210. package/tests/utils/index.ts +13 -0
  211. package/tests/utils/test-context.ts +149 -0
  212. package/tsconfig.json +5 -1
  213. package/types/archetype.types.ts +6 -0
  214. package/types/hooks.types.ts +1 -1
  215. package/types/query.types.ts +110 -0
  216. package/types/scheduler.types.ts +68 -7
  217. package/types/upload.types.ts +1 -0
  218. package/{core → upload}/FileValidator.ts +10 -1
  219. package/upload/RestUpload.ts +130 -0
  220. package/{core/components → upload}/UploadComponent.ts +11 -11
  221. package/{core → upload}/UploadManager.ts +3 -3
  222. package/upload/index.ts +23 -7
  223. package/utils/UploadHelper.ts +27 -6
  224. package/utils/cronParser.ts +16 -6
  225. package/.github/workflows/deploy-docs.yml +0 -57
  226. package/core/Components.ts +0 -202
  227. package/core/EntityCache.ts +0 -15
  228. package/core/Query.ts +0 -880
  229. package/docs/README.md +0 -149
  230. package/docs/_coverpage.md +0 -36
  231. package/docs/_sidebar.md +0 -23
  232. package/docs/api/core.md +0 -568
  233. package/docs/api/hooks.md +0 -554
  234. package/docs/api/index.md +0 -222
  235. package/docs/api/query.md +0 -678
  236. package/docs/api/service.md +0 -744
  237. package/docs/core-concepts/archetypes.md +0 -512
  238. package/docs/core-concepts/components.md +0 -498
  239. package/docs/core-concepts/entity.md +0 -314
  240. package/docs/core-concepts/hooks.md +0 -683
  241. package/docs/core-concepts/query.md +0 -588
  242. package/docs/core-concepts/services.md +0 -647
  243. package/docs/examples/code-examples.md +0 -425
  244. package/docs/getting-started.md +0 -337
  245. package/docs/index.html +0 -97
  246. package/tests/bench/insert.bench.ts +0 -60
  247. package/tests/bench/relations.bench.ts +0 -270
  248. package/tests/bench/sorting.bench.ts +0 -416
  249. package/tests/component-hooks-simple.test.ts +0 -117
  250. package/tests/component-hooks.test.ts +0 -1461
  251. package/tests/component.test.ts +0 -339
  252. package/tests/errorHandling.test.ts +0 -155
  253. package/tests/hooks.test.ts +0 -667
  254. package/tests/query-sorting.test.ts +0 -101
  255. package/tests/query.test.ts +0 -81
  256. package/tests/relations.test.ts +0 -170
  257. package/tests/scheduler.test.ts +0 -724
package/core/ArcheType.ts CHANGED
@@ -1,73 +1,200 @@
1
- import type { BaseComponent, ComponentDataType } from "./Components";
1
+ import type { BaseComponent, ComponentDataType } from "./components";
2
2
  import type { ComponentPropertyMetadata } from "./metadata/definitions/Component";
3
- import type { ArcheTypeFieldOptions } from "./metadata/definitions/ArcheType";
3
+ import type { ArcheTypeFieldOptions, ArcheTypeFunctionMetadata } from "./metadata/definitions/ArcheType";
4
+ import type { GetEntityOptions } from "../types/archetype.types";
4
5
  import { Entity } from "./Entity";
5
6
  import { getMetadataStorage } from "./metadata";
6
7
  import { z, ZodObject } from "zod";
7
- import {weave } from "@gqloom/core";
8
- import { ZodWeaver, asEnumType, asUnionType } from "@gqloom/zod";
8
+ import { weave } from "@gqloom/core";
9
+ import { ZodWeaver, asEnumType, asUnionType, asObjectType } from "@gqloom/zod";
9
10
  import { printSchema } from "graphql";
10
11
  import "reflect-metadata";
12
+ import { Query, type FilterSchema } from "../query";
13
+
14
+ export {asEnumType, asUnionType, asObjectType};
15
+
16
+ const primitiveTypes = [String, Number, Boolean, Date];
17
+
18
+ const archetypeFunctionsSymbol = Symbol.for("bunsane:archetypeFunctions");
19
+
20
+ export function ArcheTypeFunction(options?: {
21
+ returnType?: string;
22
+ args?: Array<{
23
+ name: string;
24
+ type: any;
25
+ nullable?: boolean;
26
+ }>;
27
+ }) {
28
+ return function (target: any, propertyKey: string) {
29
+ if (!target[archetypeFunctionsSymbol]) {
30
+ target[archetypeFunctionsSymbol] = [];
31
+ }
32
+ target[archetypeFunctionsSymbol].push({ propertyKey, options });
33
+ };
34
+ }
35
+
36
+ const InputFilterSchema = z.object({
37
+ field: z.string(),
38
+ op: z.string().default("eq"),
39
+ value: z.string(),
40
+ }).register(asObjectType, { name: "InputFilter" });
11
41
 
12
42
  const customTypeRegistry = new Map<any, any>();
13
43
  const customTypeNameRegistry = new Map<any, string>();
14
44
  const registeredCustomTypes = new Map<string, any>();
15
45
  const customTypeSilks = new Map<string, any>(); // Store silk types for unified weaving
16
46
  const customTypeResolvers: any[] = []; // Store resolvers for custom types
47
+ const inputTypeRegistry = new Map<any, string>(); // Map from type to input type name (e.g., ST_Point -> ST_PointInput)
48
+
49
+ // Structural signature registry for input type deduplication
50
+ // Maps structural signature -> registered input type name
51
+ const structuralSignatureRegistry = new Map<string, string>();
52
+
53
+ // Import will be done lazily to avoid circular dependencies
54
+ let _generateZodStructuralSignature: ((schema: any) => string) | null = null;
55
+
56
+ function getSignatureGenerator(): (schema: any) => string {
57
+ if (!_generateZodStructuralSignature) {
58
+ const { generateZodStructuralSignature } = require('../gql/utils/TypeSignature');
59
+ _generateZodStructuralSignature = generateZodStructuralSignature;
60
+ }
61
+ return _generateZodStructuralSignature!;
62
+ }
17
63
 
18
64
  // Component-level schema cache
19
65
  const componentSchemaCache = new Map<string, ZodObject<any>>(); // componentId -> Zod schema
20
66
 
21
- const archetypeSchemaCache = new Map<string, { zodSchema: ZodObject<any>, graphqlSchema: string }>();
67
+ // Enum schema cache to prevent duplicate registrations
68
+ const enumSchemaCache = new Map<string, any>(); // enumTypeName -> Zod enum schema
69
+
70
+ const archetypeSchemaCache = new Map<
71
+ string,
72
+ { zodSchema: ZodObject<any>; graphqlSchema: string }
73
+ >();
22
74
  const allArchetypeZodObjects = new Map<string, ZodObject<any>>();
23
75
 
24
- export function registerCustomZodType(type: any, schema: any, typeName?: string) {
76
+ export function registerCustomZodType(
77
+ type: any,
78
+ schema: any,
79
+ typeName?: string,
80
+ inputTypeName?: string
81
+ ) {
25
82
  // If a type name is provided and it's a ZodObject, add __typename to control GraphQL naming
26
83
  if (typeName && schema instanceof ZodObject) {
27
84
  // Extend the schema with __typename literal to control the GraphQL type name
28
85
  const shape = schema.shape;
29
86
  const namedSchema = z.object({
30
87
  __typename: z.literal(typeName).nullish(),
31
- ...shape
88
+ ...shape,
32
89
  });
33
90
  customTypeRegistry.set(type, namedSchema);
34
91
  if (typeName) {
35
92
  customTypeNameRegistry.set(type, typeName);
36
93
  registeredCustomTypes.set(typeName, namedSchema);
37
94
  }
95
+
96
+ // Register input type if provided (for use in GraphQL arguments)
97
+ if (inputTypeName) {
98
+ // Create input type schema (without __typename, as input types don't have it)
99
+ const inputSchema = z.object(shape).register(asObjectType, { name: inputTypeName });
100
+ registeredCustomTypes.set(inputTypeName, inputSchema);
101
+ inputTypeRegistry.set(type, inputTypeName);
102
+
103
+ // Register structural signature for input type deduplication
104
+ try {
105
+ const generateSignature = getSignatureGenerator();
106
+ const signature = generateSignature(z.object(shape));
107
+ structuralSignatureRegistry.set(signature, inputTypeName);
108
+ } catch (e) {
109
+ // Signature registration is optional, don't fail if it errors
110
+ }
111
+ }
38
112
  } else {
39
113
  customTypeRegistry.set(type, schema);
40
114
  if (typeName) {
41
115
  customTypeNameRegistry.set(type, typeName);
42
116
  registeredCustomTypes.set(typeName, schema);
43
117
  }
118
+
119
+ // Register input type if provided
120
+ if (inputTypeName && schema instanceof ZodObject) {
121
+ const inputSchema = schema.register(asObjectType, { name: inputTypeName });
122
+ registeredCustomTypes.set(inputTypeName, inputSchema);
123
+ inputTypeRegistry.set(type, inputTypeName);
124
+
125
+ // Register structural signature for input type deduplication
126
+ try {
127
+ const generateSignature = getSignatureGenerator();
128
+ const signature = generateSignature(schema);
129
+ structuralSignatureRegistry.set(signature, inputTypeName);
130
+ } catch (e) {
131
+ // Signature registration is optional, don't fail if it errors
132
+ }
133
+ }
44
134
  }
45
135
  }
46
136
 
47
- export function getArchetypeSchema(archetypeName: string) {
48
- return archetypeSchemaCache.get(archetypeName);
137
+ export function getArchetypeSchema(archetypeName: string, excludeRelations = false, excludeFunctions = false) {
138
+ const cacheKey = `${archetypeName}_${excludeRelations}_${excludeFunctions}`;
139
+ return archetypeSchemaCache.get(cacheKey);
49
140
  }
50
141
 
51
142
  export function getAllArchetypeSchemas() {
52
- return Array.from(archetypeSchemaCache.values());
143
+ return Array.from(archetypeSchemaCache.entries())
144
+ .filter(([key]) => key.endsWith('_false_false'))
145
+ .map(([, value]) => value);
53
146
  }
54
147
 
55
148
  export function getRegisteredCustomTypes() {
56
149
  return registeredCustomTypes;
57
150
  }
58
151
 
152
+ /**
153
+ * Find a matching registered input type for a given Zod schema based on structural equivalence.
154
+ * This enables deduplication of input types that have the same structure but were created
155
+ * through different transformations (.omit(), .extend(), etc.)
156
+ *
157
+ * @param schema - The Zod schema to find a match for
158
+ * @returns The registered input type name if found, null otherwise
159
+ */
160
+ export function findMatchingInputType(schema: any): string | null {
161
+ if (!schema) return null;
162
+
163
+ try {
164
+ const generateSignature = getSignatureGenerator();
165
+ const signature = generateSignature(schema);
166
+ return structuralSignatureRegistry.get(signature) || null;
167
+ } catch (e) {
168
+ return null;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Get the structural signature registry (for debugging/testing purposes)
174
+ */
175
+ export function getStructuralSignatureRegistry(): Map<string, string> {
176
+ return structuralSignatureRegistry;
177
+ }
178
+
59
179
  export function weaveAllArchetypes() {
60
180
  // First, ensure all archetype schemas are generated
61
181
  const storage = getMetadataStorage();
182
+ const archetypeNames: string[] = [];
183
+
62
184
  for (const archetypeMetadata of storage.archetypes) {
63
185
  const archetypeName = archetypeMetadata.name;
64
- if (!archetypeSchemaCache.has(archetypeName)) {
186
+ archetypeNames.push(archetypeName);
187
+ const fullSchemaCacheKey = `${archetypeName}_false_false`;
188
+ if (!archetypeSchemaCache.has(fullSchemaCacheKey)) {
65
189
  try {
66
190
  const ArchetypeClass = archetypeMetadata.target as any;
67
191
  const instance = new ArchetypeClass();
68
192
  instance.getZodObjectSchema(); // Generate and cache the schema
69
193
  } catch (error) {
70
- console.warn(`Could not generate schema for archetype ${archetypeName}:`, error);
194
+ console.warn(
195
+ `Could not generate schema for archetype ${archetypeName}:`,
196
+ error
197
+ );
71
198
  }
72
199
  }
73
200
  }
@@ -79,36 +206,215 @@ export function weaveAllArchetypes() {
79
206
  // This ensures that nested component types are also included in the unified schema
80
207
  const archetypeSchemas = Array.from(allArchetypeZodObjects.values());
81
208
  const componentSchemas = Array.from(componentSchemaCache.values());
82
-
209
+
83
210
  // Combine both archetype and component schemas for weaving
84
- const allSchemas = [...archetypeSchemas, ...componentSchemas];
85
-
86
- const schema = weave(ZodWeaver, ...allSchemas);
87
- let schemaString = printSchema(schema);
88
-
89
- // Post-process: Replace 'id: String' with 'id: ID' for all id fields
90
- schemaString = schemaString.replace(/\bid:\s*String\b/g, 'id: ID');
91
-
92
- return schemaString;
211
+ const allSchemas = archetypeSchemas;
212
+
213
+ try {
214
+ const schema = weave(ZodWeaver, ...allSchemas);
215
+ let schemaString = printSchema(schema);
216
+
217
+ // Add Date scalar if not present
218
+ if (!schemaString.includes('scalar Date')) {
219
+ schemaString = 'scalar Date\n\n' + schemaString;
220
+ }
221
+
222
+ // Post-process: Replace 'id: String' with 'id: ID' for all id fields
223
+ schemaString = schemaString.replace(/\bid:\s*String\b/g, "id: ID");
224
+
225
+ // Post-process: Replace date fields (start_at, end_at, created_at, updated_at, etc.) with Date scalar
226
+ // Match common date field patterns
227
+ schemaString = schemaString.replace(/\b(\w*_at|\w*_date|\w*Date|date\w*):\s*String(!?)/gi, (match, fieldName, nullable) => {
228
+ return `${fieldName}: Date${nullable}`;
229
+ });
230
+
231
+ // Post-process: Replace relation String fields with proper GraphQL type references
232
+ // Collect all relation metadata from all archetypes
233
+ for (const archetypeMetadata of storage.archetypes) {
234
+ const archetypeName = archetypeMetadata.name;
235
+ try {
236
+ const ArchetypeClass = archetypeMetadata.target as any;
237
+ const instance = new ArchetypeClass();
238
+
239
+ // Process each relation field
240
+ for (const [field, relatedArcheType] of Object.entries(instance.relationMap)) {
241
+ const relationType = instance.relationTypes[field];
242
+ const isArray = relationType === "hasMany" || relationType === "belongsToMany";
243
+
244
+ let relatedTypeName: string;
245
+ if (typeof relatedArcheType === "string") {
246
+ relatedTypeName = relatedArcheType;
247
+ } else {
248
+ const relatedArchetypeId = storage.getComponentId((relatedArcheType as any).name);
249
+ const relatedArchetypeMetadata = storage.archetypes.find(
250
+ (a) => a.typeId === relatedArchetypeId
251
+ );
252
+ relatedTypeName = relatedArchetypeMetadata?.name || (relatedArcheType as any).name.replace(/ArcheType$/, "");
253
+ }
254
+
255
+ if (isArray) {
256
+ // Step 1: Add description if it doesn't exist
257
+ const hasDescription = new RegExp(`"""Reference to ${relatedTypeName} type"""[\\s\\S]{0,50}${field}:`).test(schemaString);
258
+ if (!hasDescription) {
259
+ const addDescPattern = new RegExp(
260
+ `(type ${archetypeName} \\{[\\s\\S]*?)(\\n\\s+)(${field}:\\s*\\[String!?\\]!?)`,
261
+ "g"
262
+ );
263
+ schemaString = schemaString.replace(
264
+ addDescPattern,
265
+ `$1$2"""Reference to ${relatedTypeName} type"""$2$3`
266
+ );
267
+ }
268
+
269
+ // Step 2: Replace [String!] with [TypeName!]
270
+ const shouldBeRequired = instance.relationOptions[field]?.nullable === false;
271
+ const suffix = shouldBeRequired ? "!" : "";
272
+ const replacePattern = new RegExp(
273
+ `(type ${archetypeName} \\{[\\s\\S]*?${field}:\\s*)\\[String!?\\](!?)`,
274
+ "g"
275
+ );
276
+ schemaString = schemaString.replace(
277
+ replacePattern,
278
+ `$1[${relatedTypeName}!]${suffix}`
279
+ );
280
+ } else {
281
+ // Singular relations already have descriptions from Zod, just replace type
282
+ const pattern = new RegExp(
283
+ `(type ${archetypeName} \\{[\\s\\S]*?${field}:\\s*)String(!?)`,
284
+ "g"
285
+ );
286
+ const isNullable = instance.relationOptions[field]?.nullable;
287
+ const suffix = isNullable ? "" : "!";
288
+ schemaString = schemaString.replace(
289
+ pattern,
290
+ `$1${relatedTypeName}${suffix}`
291
+ );
292
+ }
293
+ }
294
+ } catch (error) {
295
+ console.warn(`Could not process relations for archetype ${archetypeMetadata.name}:`, error);
296
+ }
297
+
298
+ // Process each function field
299
+ if (archetypeMetadata.functions) {
300
+ for (const { propertyKey, options } of archetypeMetadata.functions) {
301
+
302
+ // Add arguments if present
303
+ if (options?.args && options.args.length > 0) {
304
+ const argDefs: string[] = [];
305
+ for (const arg of options.args) {
306
+ let argTypeName: string;
307
+
308
+ const inputTypeName = inputTypeRegistry.get(arg.type);
309
+ if (inputTypeName) {
310
+ argTypeName = inputTypeName;
311
+ } else {
312
+ const registeredTypeName = customTypeNameRegistry.get(arg.type);
313
+ if (registeredTypeName) {
314
+ argTypeName = registeredTypeName;
315
+ } else if (arg.type === String) {
316
+ argTypeName = 'String';
317
+ } else if (arg.type === Number) {
318
+ argTypeName = 'Float';
319
+ } else if (arg.type === Boolean) {
320
+ argTypeName = 'Boolean';
321
+ } else if (arg.type === Date) {
322
+ argTypeName = 'Date';
323
+ } else if (arg.type?.name) {
324
+ argTypeName = arg.type.name;
325
+ } else {
326
+ argTypeName = 'String';
327
+ }
328
+ }
329
+
330
+ const nullable = arg.nullable ? '' : '!';
331
+ argDefs.push(`${arg.name}: ${argTypeName}${nullable}`);
332
+ }
333
+
334
+ const argsString = argDefs.join(', ');
335
+ const escapedKey = propertyKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
336
+
337
+ // Pattern to add arguments: fieldName: Type -> fieldName(args): Type
338
+ // Capture leading whitespace separately to preserve it
339
+ const argPattern = new RegExp(
340
+ `(\\s+)(${escapedKey}\\??\\s*:\\s*)([^\\n]+)`,
341
+ 'g'
342
+ );
343
+
344
+ schemaString = schemaString.replace(
345
+ argPattern,
346
+ (match, leadingSpace, fieldDef, returnType) => {
347
+ return `${leadingSpace}${fieldDef.trim().replace(':', '')}(${argsString}): ${returnType.trim()}`;
348
+ }
349
+ );
350
+ }
351
+
352
+ if (options?.returnType && !['string', 'number', 'boolean'].includes(options.returnType)) {
353
+ // Find the archetype type definition first
354
+ const typePattern = new RegExp(`type ${archetypeName}\\s*\\{([\\s\\S]*?)\\n\\}`, 'g');
355
+ const typeMatch = typePattern.exec(schemaString);
356
+
357
+ if (typeMatch) {
358
+ const typeBody = typeMatch[1]!;
359
+
360
+ // Find the field line in the type body
361
+ const fieldIndex = typeBody.indexOf(` ${propertyKey}`);
362
+ if (fieldIndex !== -1) {
363
+ const lineStart = fieldIndex;
364
+ const lineEnd = typeBody.indexOf('\n', fieldIndex);
365
+ const fieldLine = typeBody.substring(lineStart, lineEnd !== -1 ? lineEnd : typeBody.length);
366
+
367
+ // Replace String with the actual return type in this line
368
+ const updatedLine = fieldLine.replace(/:\s*String(\??)(\s*)$/, `: ${options.returnType}$1$2`);
369
+
370
+ if (updatedLine !== fieldLine) {
371
+ // Replace in the full schema
372
+ const fullFieldIndex = schemaString.indexOf(typeMatch[0]) + typeMatch[0].indexOf(fieldLine);
373
+ schemaString = schemaString.substring(0, fullFieldIndex) +
374
+ updatedLine +
375
+ schemaString.substring(fullFieldIndex + fieldLine.length);
376
+ }
377
+ }
378
+ }
379
+ }
380
+ }
381
+ }
382
+ }
383
+
384
+ return schemaString;
385
+ } catch (error) {
386
+ console.warn(
387
+ `Failed to weave all archetypes due to duplicate types.\n` +
388
+ `Archetypes being processed: ${archetypeNames.join(', ')}\n` +
389
+ `Error: ${error}`
390
+ );
391
+ return null;
392
+ }
93
393
  }
94
394
 
95
395
  // Generate Zod schema for a component and cache it
96
- function getOrCreateComponentSchema(componentCtor: new (...args: any[]) => BaseComponent, componentId: string, fieldOptions?: ArcheTypeFieldOptions): any | null {
396
+ function getOrCreateComponentSchema(
397
+ componentCtor: new (...args: any[]) => BaseComponent,
398
+ componentId: string,
399
+ fieldOptions?: ArcheTypeFieldOptions
400
+ ): any | null {
97
401
  // Check cache first
98
402
  if (componentSchemaCache.has(componentId)) {
99
403
  return componentSchemaCache.get(componentId)!;
100
404
  }
101
-
405
+
102
406
  const storage = getMetadataStorage();
103
407
  const props = storage.getComponentProperties(componentId);
104
-
408
+
105
409
  // Return null if no properties - caller should skip this component
106
410
  if (props.length === 0) {
107
411
  return null;
108
412
  }
109
-
413
+
110
414
  const zodFields: Record<string, any> = {
111
- __typename: z.literal(compNameToFieldName(componentCtor.name))
415
+ __typename: z
416
+ .literal(compNameToFieldName(componentCtor.name))
417
+ .nullish(),
112
418
  };
113
419
 
114
420
  for (const prop of props) {
@@ -127,47 +433,120 @@ function getOrCreateComponentSchema(componentCtor: new (...args: any[]) => BaseC
127
433
  zodFields[prop.propertyKey] = z.date();
128
434
  break;
129
435
  default:
130
- zodFields[prop.propertyKey] = z.any();
436
+ console.warn(`[ArcheType] Unknown primitive type for ${componentCtor.name}.${prop.propertyKey}: ${prop.propertyType?.name}. Falling back to z.string()`);
437
+ zodFields[prop.propertyKey] = z.string();
438
+ }
439
+ if (prop.isOptional) {
440
+ zodFields[prop.propertyKey] =
441
+ zodFields[prop.propertyKey].optional();
131
442
  }
132
443
  } else if (prop.isEnum && prop.enumValues && prop.enumKeys) {
133
- const enumTypeName = prop.propertyType?.name || `${componentCtor.name}_${prop.propertyKey}_Enum`;
134
- zodFields[prop.propertyKey] = z.enum(prop.enumValues as any).register(asEnumType, {
135
- name: enumTypeName,
136
- valuesConfig: prop.enumKeys.reduce((acc: Record<string, { description: string }>, key, idx) => {
137
- acc[key] = { description: prop.enumValues![idx]! };
138
- return acc;
139
- }, {})
140
- });
444
+ const enumTypeName =
445
+ prop.propertyType?.name ||
446
+ `${componentCtor.name}_${prop.propertyKey}_Enum`;
447
+
448
+ // Check if this enum has already been registered
449
+ let enumSchema = enumSchemaCache.get(enumTypeName);
450
+
451
+ if (!enumSchema) {
452
+ // Register the enum for the first time
453
+ enumSchema = z
454
+ .enum(prop.enumValues as any)
455
+ .register(asEnumType, {
456
+ name: enumTypeName,
457
+ valuesConfig: prop.enumKeys.reduce(
458
+ (
459
+ acc: Record<string, { description: string }>,
460
+ key,
461
+ idx
462
+ ) => {
463
+ acc[key] = { description: prop.enumValues![idx]! };
464
+ return acc;
465
+ },
466
+ {}
467
+ ),
468
+ });
469
+ // Cache it for reuse
470
+ enumSchemaCache.set(enumTypeName, enumSchema);
471
+ }
472
+
473
+ zodFields[prop.propertyKey] = enumSchema;
474
+ if (prop.isOptional) {
475
+ zodFields[prop.propertyKey] =
476
+ zodFields[prop.propertyKey].optional();
477
+ }
141
478
  } else if (customTypeRegistry.has(prop.propertyType)) {
142
- zodFields[prop.propertyKey] = customTypeRegistry.get(prop.propertyType)!;
479
+ zodFields[prop.propertyKey] = customTypeRegistry.get(
480
+ prop.propertyType
481
+ )!;
482
+ if (prop.isOptional) {
483
+ zodFields[prop.propertyKey] =
484
+ zodFields[prop.propertyKey].optional();
485
+ }
486
+ } else if (prop.arrayOf) {
487
+ if (customTypeRegistry.has(prop.arrayOf)) {
488
+ zodFields[prop.propertyKey] = z.array(customTypeRegistry.get(prop.arrayOf)!);
489
+ } else if (primitiveTypes.includes(prop.arrayOf)) {
490
+ if (prop.arrayOf === String) {
491
+ zodFields[prop.propertyKey] = z.array(z.string());
492
+ } else if (prop.arrayOf === Number) {
493
+ zodFields[prop.propertyKey] = z.array(z.number());
494
+ } else if (prop.arrayOf === Boolean) {
495
+ zodFields[prop.propertyKey] = z.array(z.boolean());
496
+ } else if (prop.arrayOf === Date) {
497
+ zodFields[prop.propertyKey] = z.array(z.date());
498
+ }
499
+ } else {
500
+ console.warn(`[ArcheType] Unknown array element type for ${componentCtor.name}.${prop.propertyKey}: ${prop.arrayOf?.name}. Falling back to z.array(z.string())`);
501
+ zodFields[prop.propertyKey] = z.array(z.string());
502
+ }
503
+ if (prop.isOptional) {
504
+ zodFields[prop.propertyKey] = zodFields[prop.propertyKey].optional();
505
+ }
143
506
  } else {
144
- zodFields[prop.propertyKey] = z.any();
507
+ console.warn(`[ArcheType] Unknown type for ${componentCtor.name}.${prop.propertyKey}: ${prop.propertyType?.name}. Falling back to z.string()`);
508
+ zodFields[prop.propertyKey] = z.string();
509
+ if (prop.isOptional) {
510
+ zodFields[prop.propertyKey] =
511
+ zodFields[prop.propertyKey].optional();
512
+ }
145
513
  }
146
-
514
+
147
515
  if (fieldOptions?.nullable) {
148
516
  zodFields[prop.propertyKey] = zodFields[prop.propertyKey].nullish();
149
517
  }
150
518
  }
151
-
519
+
152
520
  const componentSchema = z.object(zodFields);
153
-
521
+
154
522
  // Cache the component schema for reuse
155
523
  componentSchemaCache.set(componentId, componentSchema);
156
-
524
+
157
525
  return componentSchema;
158
526
  }
159
527
 
160
528
  function compNameToFieldName(compName: string): string {
161
- return compName.charAt(0).toLowerCase() + compName.slice(1).replace(/Component$/, '');
529
+ return (
530
+ compName.charAt(0).toLowerCase() +
531
+ compName.slice(1).replace(/Component$/, "Component")
532
+ );
162
533
  }
163
534
 
164
535
  /**
165
536
  * Helper to determine if a component should be unwrapped to a scalar value.
166
537
  * Returns true if the component has a single 'value' property and the field type is primitive.
167
538
  */
168
- function shouldUnwrapComponent(componentProps: ComponentPropertyMetadata[], fieldType: any): boolean {
539
+ function shouldUnwrapComponent(
540
+ componentProps: ComponentPropertyMetadata[],
541
+ fieldType: any
542
+ ): boolean {
169
543
  // If field type is a primitive, unwrap the component to that primitive
170
- if (fieldType === String || fieldType === Number || fieldType === Boolean || fieldType === Date) {
544
+ if (
545
+ fieldType === String ||
546
+ fieldType === Number ||
547
+ fieldType === Boolean ||
548
+ fieldType === Date
549
+ ) {
171
550
  return true;
172
551
  }
173
552
  return false;
@@ -200,20 +579,21 @@ export interface BelongsToManyOptions extends RelationOptions {
200
579
  through: string; // Required for many-to-many
201
580
  }
202
581
 
203
- // TODO: Implement archetype with GraphQL support
204
- export function ArcheType<T extends new () => BaseArcheType>(nameOrOptions?: string | ArcheTypeOptions) {
205
- return function(target: T): T {
582
+ export function ArcheType<T extends new () => BaseArcheType>(
583
+ nameOrOptions?: string | ArcheTypeOptions
584
+ ) {
585
+ return function (target: T): T {
206
586
  const storage = getMetadataStorage();
207
587
  const typeId = storage.getComponentId(target.name);
208
-
588
+
209
589
  let archetype_name = target.name;
210
-
211
- if (typeof nameOrOptions === 'string') {
590
+
591
+ if (typeof nameOrOptions === "string") {
212
592
  archetype_name = nameOrOptions;
213
593
  } else if (nameOrOptions) {
214
594
  archetype_name = nameOrOptions.name || target.name;
215
595
  }
216
-
596
+
217
597
  storage.collectArcheTypeMetadata({
218
598
  name: archetype_name,
219
599
  typeId: typeId,
@@ -223,34 +603,81 @@ export function ArcheType<T extends new () => BaseArcheType>(nameOrOptions?: str
223
603
  const prototype = target.prototype;
224
604
  const fields = prototype[archetypeFieldsSymbol];
225
605
  if (fields) {
226
- for (const {propertyKey, component, options} of fields) {
227
- const type = Reflect.getMetadata('design:type', target.prototype, propertyKey);
228
- storage.collectArchetypeField(archetype_name, propertyKey, component, options, type);
606
+ for (const { propertyKey, component, options } of fields) {
607
+ const type = Reflect.getMetadata(
608
+ "design:type",
609
+ target.prototype,
610
+ propertyKey
611
+ );
612
+ storage.collectArchetypeField(
613
+ archetype_name,
614
+ propertyKey,
615
+ component,
616
+ options,
617
+ type
618
+ );
229
619
  }
230
620
  }
231
621
 
232
622
  const unions = prototype[archetypeUnionFieldsSymbol];
233
- if(unions) {
234
- for(const {propertyKey, components, options} of unions) {
235
- storage.collectArchetypeUnion(archetype_name, propertyKey, components, options, 'union');
623
+ if (unions) {
624
+ for (const { propertyKey, components, options } of unions) {
625
+ storage.collectArchetypeUnion(
626
+ archetype_name,
627
+ propertyKey,
628
+ components,
629
+ options,
630
+ "union"
631
+ );
236
632
  }
237
633
  }
238
634
 
239
635
  // Process relations
240
636
  const relations = prototype[archetypeRelationsSymbol];
241
637
  if (relations) {
242
- for (const {propertyKey, relatedArcheType, relationType, options} of relations) {
243
- const type = Reflect.getMetadata('design:type', target.prototype, propertyKey);
244
- storage.collectArchetypeRelation(archetype_name, propertyKey, relatedArcheType, relationType, options, type);
638
+ for (const {
639
+ propertyKey,
640
+ relatedArcheType,
641
+ relationType,
642
+ options,
643
+ } of relations) {
644
+ const type = Reflect.getMetadata(
645
+ "design:type",
646
+ target.prototype,
647
+ propertyKey
648
+ );
649
+ storage.collectArchetypeRelation(
650
+ archetype_name,
651
+ propertyKey,
652
+ relatedArcheType,
653
+ relationType,
654
+ options,
655
+ type
656
+ );
245
657
  }
246
658
  }
659
+
660
+ // Process functions
661
+ const functions = prototype[archetypeFunctionsSymbol];
662
+ if (functions) {
663
+ storage.collectArcheTypeMetadata({
664
+ name: archetype_name,
665
+ typeId: typeId,
666
+ target: target,
667
+ functions: functions,
668
+ });
669
+ }
670
+
247
671
  return target;
248
672
  };
249
673
  }
250
674
 
251
- const archetypeFieldsSymbol = Symbol("archetypeFields");
252
- export function ArcheTypeField<T extends BaseComponent>(component: new (...args: any[]) => T, options?: ArcheTypeFieldOptions) {
253
- return function(target: any, propertyKey: string) {
675
+ const archetypeFieldsSymbol = Symbol.for("bunsane:archetypeFields");
676
+ export function ArcheTypeField<T extends BaseComponent>(
677
+ component: new (...args: any[]) => T,
678
+ options?: ArcheTypeFieldOptions
679
+ ) {
680
+ return function (target: any, propertyKey: string) {
254
681
  if (!target[archetypeFieldsSymbol]) {
255
682
  target[archetypeFieldsSymbol] = [];
256
683
  }
@@ -258,24 +685,30 @@ export function ArcheTypeField<T extends BaseComponent>(component: new (...args:
258
685
  };
259
686
  }
260
687
 
261
- const archetypeUnionFieldsSymbol = Symbol("archetypeUnionFields");
262
- export function ArcheTypeUnionField(components: (new (...args:any[]) => any)[], options?: ArcheTypeFieldOptions) {
263
- return function(target: any, propertyKey: string) {
264
- if(!target[archetypeUnionFieldsSymbol]) {
688
+ const archetypeUnionFieldsSymbol = Symbol.for("bunsane:archetypeUnionFields");
689
+ export function ArcheTypeUnionField(
690
+ components: (new (...args: any[]) => any)[],
691
+ options?: ArcheTypeFieldOptions
692
+ ) {
693
+ return function (target: any, propertyKey: string) {
694
+ if (!target[archetypeUnionFieldsSymbol]) {
265
695
  target[archetypeUnionFieldsSymbol] = [];
266
696
  }
267
- target[archetypeUnionFieldsSymbol].push({propertyKey, components, options});
268
- }
697
+ target[archetypeUnionFieldsSymbol].push({
698
+ propertyKey,
699
+ components,
700
+ options,
701
+ });
702
+ };
269
703
  }
270
704
 
271
- const archetypeRelationsSymbol = Symbol("archetypeRelations");
705
+ const archetypeRelationsSymbol = Symbol.for("bunsane:archetypeRelations");
272
706
 
273
- function createRelationDecorator(relationType: 'hasMany' | 'belongsTo' | 'hasOne' | 'belongsToMany') {
274
- return function(
275
- relatedArcheType: string,
276
- options?: RelationOptions
277
- ) {
278
- return function(target: any, propertyKey: string) {
707
+ function createRelationDecorator(
708
+ relationType: "hasMany" | "belongsTo" | "hasOne" | "belongsToMany"
709
+ ) {
710
+ return function (relatedArcheType: string, options?: RelationOptions) {
711
+ return function (target: any, propertyKey: string) {
279
712
  if (!target[archetypeRelationsSymbol]) {
280
713
  target[archetypeRelationsSymbol] = [];
281
714
  }
@@ -283,16 +716,16 @@ function createRelationDecorator(relationType: 'hasMany' | 'belongsTo' | 'hasOne
283
716
  propertyKey,
284
717
  relatedArcheType,
285
718
  relationType,
286
- options
719
+ options,
287
720
  });
288
721
  };
289
722
  };
290
723
  }
291
724
 
292
- export const HasMany = createRelationDecorator('hasMany');
293
- export const BelongsTo = createRelationDecorator('belongsTo');
294
- export const HasOne = createRelationDecorator('hasOne');
295
- export const BelongsToMany = createRelationDecorator('belongsToMany');
725
+ export const HasMany = createRelationDecorator("hasMany");
726
+ export const BelongsTo = createRelationDecorator("belongsTo");
727
+ export const HasOne = createRelationDecorator("hasOne");
728
+ export const BelongsToMany = createRelationDecorator("belongsToMany");
296
729
 
297
730
  // Keep ArcheTypeRelation as alias for backwards compatibility
298
731
  export const ArcheTypeRelation = HasMany;
@@ -301,24 +734,213 @@ export type ArcheTypeResolver = {
301
734
  resolver?: string;
302
735
  component?: new (...args: any[]) => BaseComponent;
303
736
  field?: string;
304
- filter?: {[key: string]: any};
305
- }
737
+ filter?: { [key: string]: any };
738
+ };
306
739
 
307
740
  export type ArcheTypeCreateInfo = {
308
741
  name: string;
309
742
  components: Array<new (...args: any[]) => BaseComponent>;
310
743
  };
311
744
 
745
+ export type ArcheTypeOwnProperties<T extends BaseArcheType> = Omit<T, keyof BaseArcheType>;
746
+
747
+ /**
748
+ * Result type that provides direct typed access to archetype fields.
749
+ * Wraps an entity with its archetype's component data exposed as properties.
750
+ */
751
+ export type ArcheTypeResult<T extends BaseArcheType> = {
752
+ /** The underlying entity */
753
+ entity: Entity;
754
+ /** Entity ID shorthand */
755
+ id: string;
756
+ /** Save changes to the entity */
757
+ save(): Promise<void>;
758
+ } & {
759
+ [K in keyof T as T[K] extends BaseComponent ? K : never]:
760
+ T[K] extends BaseComponent ? ComponentDataType<T[K]> : never;
761
+ };
762
+
763
+ /**
764
+ * Query builder for ArcheTypes that returns fully-typed results.
765
+ * Auto-includes all archetype components and provides typed filter methods.
766
+ *
767
+ * @example
768
+ * ```typescript
769
+ * const players = await Player.query()
770
+ * .filter('health', 'gt', { current: 50 })
771
+ * .exec();
772
+ *
773
+ * for (const player of players) {
774
+ * console.log(player.position.x, player.health.current);
775
+ * }
776
+ * ```
777
+ */
778
+ export class ArcheTypeQuery<T extends BaseArcheType> {
779
+ private innerQuery: Query<any>;
780
+ private archetypeInstance: T;
781
+ private archetypeCtor: new () => T;
782
+
783
+ constructor(archetypeCtor: new () => T) {
784
+ this.archetypeCtor = archetypeCtor;
785
+ this.archetypeInstance = new archetypeCtor();
786
+ this.innerQuery = new Query();
787
+
788
+ // Auto-add all archetype components to the query
789
+ for (const [_, componentCtor] of Object.entries(this.archetypeInstance.componentMap)) {
790
+ this.innerQuery = this.innerQuery.with(componentCtor as any);
791
+ }
792
+ }
793
+
794
+ /**
795
+ * Add a filter on an archetype field.
796
+ * @param field The archetype field name (maps to a component)
797
+ * @param operator Filter operator: eq, neq, gt, gte, lt, lte, in, like
798
+ * @param value The value to filter by (partial component data)
799
+ */
800
+ public filter<K extends keyof ArcheTypeOwnProperties<T>>(
801
+ field: K,
802
+ operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'notIn' | 'like',
803
+ value: Partial<T[K] extends BaseComponent ? ComponentDataType<T[K]> : never>
804
+ ): this {
805
+ const componentCtor = this.archetypeInstance.componentMap[field as string];
806
+ if (!componentCtor) {
807
+ throw new Error(`Field '${String(field)}' is not a component field on this archetype`);
808
+ }
809
+
810
+ // Map operator to FilterOp
811
+ const opMap: Record<string, string> = {
812
+ 'eq': '=', 'neq': '!=', 'gt': '>', 'gte': '>=',
813
+ 'lt': '<', 'lte': '<=', 'in': 'IN', 'notIn': 'NOT IN', 'like': 'LIKE', 'ilike': 'ILIKE'
814
+ };
815
+ const filterOp = opMap[operator] || '=';
816
+
817
+ // Build filters from the partial value
818
+ const filters = Object.entries(value as object).map(([propKey, propValue]) => ({
819
+ field: propKey,
820
+ operator: filterOp,
821
+ value: propValue
822
+ }));
823
+
824
+ // Re-add the component with filters
825
+ this.innerQuery = this.innerQuery.with(componentCtor as any, { filters });
826
+
827
+ return this;
828
+ }
829
+
830
+ /**
831
+ * Limit the number of results
832
+ */
833
+ public take(limit: number): this {
834
+ this.innerQuery = this.innerQuery.take(limit);
835
+ return this;
836
+ }
837
+
838
+ /**
839
+ * Skip a number of results (for pagination)
840
+ */
841
+ public offset(offset: number): this {
842
+ this.innerQuery = this.innerQuery.offset(offset);
843
+ return this;
844
+ }
845
+
846
+ /**
847
+ * Sort results by a component field
848
+ */
849
+ public sortBy<K extends keyof ArcheTypeOwnProperties<T>>(
850
+ field: K,
851
+ property: T[K] extends BaseComponent ? keyof ComponentDataType<T[K]> : never,
852
+ direction: 'ASC' | 'DESC' = 'ASC'
853
+ ): this {
854
+ const componentCtor = this.archetypeInstance.componentMap[field as string];
855
+ if (!componentCtor) {
856
+ throw new Error(`Field '${String(field)}' is not a component field on this archetype`);
857
+ }
858
+ // Cast needed because innerQuery has dynamic component types
859
+ (this.innerQuery as any).sortBy(componentCtor, property, direction);
860
+ return this;
861
+ }
862
+
863
+ /**
864
+ * Enable populate mode to load all component data
865
+ */
866
+ public populate(): this {
867
+ this.innerQuery = this.innerQuery.populate();
868
+ return this;
869
+ }
870
+
871
+ /**
872
+ * Bypass cache for this query
873
+ */
874
+ public noCache(): this {
875
+ this.innerQuery = this.innerQuery.noCache();
876
+ return this;
877
+ }
878
+
879
+ /**
880
+ * Execute the query and return typed archetype results
881
+ */
882
+ public async exec(): Promise<ArcheTypeResult<T>[]> {
883
+ const entities = await this.innerQuery.populate().exec();
884
+ return entities.map(entity => this.wrapAsArchetype(entity as Entity));
885
+ }
886
+
887
+ /**
888
+ * Execute the query and return the first result (or null)
889
+ */
890
+ public async first(): Promise<ArcheTypeResult<T> | null> {
891
+ const results = await this.innerQuery.take(1).populate().exec();
892
+ return results[0] ? this.wrapAsArchetype(results[0] as Entity) : null;
893
+ }
894
+
895
+ /**
896
+ * Get the count of matching entities
897
+ */
898
+ public count(): Promise<number> {
899
+ return this.innerQuery.count();
900
+ }
901
+
902
+ /**
903
+ * Wrap an entity as an ArcheTypeResult with direct property access
904
+ */
905
+ private wrapAsArchetype(entity: Entity): ArcheTypeResult<T> {
906
+ const result: any = {
907
+ entity,
908
+ id: entity.id,
909
+ save: async () => {
910
+ await entity.save();
911
+ }
912
+ };
913
+
914
+ // Add component data as direct properties
915
+ for (const [fieldName, componentCtor] of Object.entries(this.archetypeInstance.componentMap)) {
916
+ const comp = entity.getInMemory(componentCtor as any);
917
+ if (comp) {
918
+ result[fieldName] = (comp as any).data();
919
+ }
920
+ }
921
+
922
+ return result as ArcheTypeResult<T>;
923
+ }
924
+ }
925
+
312
926
  export class BaseArcheType {
313
- protected components: Set<{ ctor: new (...args: any[]) => BaseComponent, data: any }> = new Set();
314
- public componentMap: Record<string, typeof BaseComponent> = {};
927
+ protected components: Set<{
928
+ ctor: new (...args: any[]) => BaseComponent;
929
+ data: any;
930
+ }> = new Set();
931
+ public componentMap: Record<string, typeof BaseComponent> = {};
315
932
  protected fieldOptions: Record<string, ArcheTypeFieldOptions> = {};
316
933
  protected fieldTypes: Record<string, any> = {};
317
934
  public relationMap: Record<string, typeof BaseArcheType | string> = {};
318
935
  protected relationOptions: Record<string, RelationOptions> = {};
319
- protected relationTypes: Record<string, 'hasMany' | 'belongsTo' | 'hasOne' | 'belongsToMany'> = {};
320
- public unionMap: Record<string, (new (...args: any[]) => BaseComponent)[]> = {};
936
+ protected relationTypes: Record<
937
+ string,
938
+ "hasMany" | "belongsTo" | "hasOne" | "belongsToMany"
939
+ > = {};
940
+ public unionMap: Record<string, (new (...args: any[]) => BaseComponent)[]> =
941
+ {};
321
942
  protected unionOptions: Record<string, ArcheTypeFieldOptions> = {};
943
+ public functions: Array<{ propertyKey: string; options?: { returnType?: string, args?: [{name: string, type: any, nullable: boolean}] } }> = [];
322
944
 
323
945
  public resolver?: {
324
946
  fields: Record<string, ArcheTypeResolver>;
@@ -327,14 +949,18 @@ export class BaseArcheType {
327
949
  constructor() {
328
950
  const storage = getMetadataStorage();
329
951
  const archetypeId = storage.getComponentId(this.constructor.name);
330
-
952
+
331
953
  // Look up the custom name from metadata (e.g., from @ArcheType("CustomName"))
332
- const archetypeMetadata = storage.archetypes.find(a => a.typeId === archetypeId);
333
- const archetypeName = archetypeMetadata?.name || this.constructor.name.replace(/ArcheType$/, '');
334
-
954
+ const archetypeMetadata = storage.archetypes.find(
955
+ (a) => a.typeId === archetypeId
956
+ );
957
+ const archetypeName =
958
+ archetypeMetadata?.name ||
959
+ this.constructor.name.replace(/ArcheType$/, "");
960
+
335
961
  const fields = storage.archetypes_field_map.get(archetypeName);
336
962
  if (fields) {
337
- for (const {fieldName, component, options, type} of fields) {
963
+ for (const { fieldName, component, options, type } of fields) {
338
964
  this.componentMap[fieldName] = component;
339
965
  if (options) this.fieldOptions[fieldName] = options;
340
966
  if (type) this.fieldTypes[fieldName] = type;
@@ -342,8 +968,8 @@ export class BaseArcheType {
342
968
  }
343
969
 
344
970
  const unions = storage.archetypes_union_map.get(archetypeName);
345
- if(unions) {
346
- for(const {fieldName, components, options, type} of unions) {
971
+ if (unions) {
972
+ for (const { fieldName, components, options, type } of unions) {
347
973
  this.unionMap[fieldName] = components;
348
974
  if (options) this.unionOptions[fieldName] = options;
349
975
  }
@@ -352,12 +978,21 @@ export class BaseArcheType {
352
978
  // Process relations
353
979
  const relations = storage.archetypes_relations_map.get(archetypeName);
354
980
  if (relations) {
355
- for (const {fieldName, relatedArcheType, relationType, options, type} of relations) {
981
+ for (const {
982
+ fieldName,
983
+ relatedArcheType,
984
+ relationType,
985
+ options,
986
+ type,
987
+ } of relations) {
356
988
  this.relationMap[fieldName] = relatedArcheType as any;
357
989
  this.relationTypes[fieldName] = relationType;
358
990
  if (options) this.relationOptions[fieldName] = options;
359
991
  }
360
992
  }
993
+
994
+ // Collect archetype functions
995
+ this.functions = this.constructor.prototype[archetypeFunctionsSymbol] || [];
361
996
  }
362
997
 
363
998
  // constructor(components: Array<new (...args: any[]) => BaseComponent>) {
@@ -366,11 +1001,13 @@ export class BaseArcheType {
366
1001
  // }
367
1002
  // }
368
1003
 
369
- static ResolveField<T extends BaseComponent>(component: new (...args: any[]) => T, field: keyof T): ArcheTypeResolver {
1004
+ static ResolveField<T extends BaseComponent>(
1005
+ component: new (...args: any[]) => T,
1006
+ field: keyof T
1007
+ ): ArcheTypeResolver {
370
1008
  return { component, field: field as string };
371
1009
  }
372
1010
 
373
-
374
1011
  static Create(info: ArcheTypeCreateInfo): BaseArcheType {
375
1012
  const archetype = new BaseArcheType();
376
1013
  archetype.components = new Set();
@@ -379,26 +1016,28 @@ export class BaseArcheType {
379
1016
  }
380
1017
  return archetype;
381
1018
  }
382
-
383
-
384
- private addComponent<T extends BaseComponent>(ctor: new (...args: any[]) => T, data: ComponentDataType<T>) {
1019
+
1020
+ private addComponent<T extends BaseComponent>(
1021
+ ctor: new (...args: any[]) => T,
1022
+ data: ComponentDataType<T>
1023
+ ) {
385
1024
  this.componentMap[compNameToFieldName(ctor.name)] = ctor;
386
1025
  this.components.add({ ctor, data });
387
1026
  }
388
1027
 
389
-
390
1028
  // TODO: Can we make this type-safe?
391
1029
  public fill(input: object, strict: boolean = false): this {
392
1030
  const storage = getMetadataStorage();
393
-
1031
+
394
1032
  for (const [key, value] of Object.entries(input)) {
395
1033
  if (value !== undefined) {
396
1034
  const compCtor = this.componentMap[key];
397
1035
  if (compCtor) {
398
1036
  const fieldType = this.fieldTypes[key];
399
1037
  const typeId = storage.getComponentId(compCtor.name);
400
- const componentProps = storage.getComponentProperties(typeId);
401
-
1038
+ const componentProps =
1039
+ storage.getComponentProperties(typeId);
1040
+
402
1041
  // Check if this is a primitive field that should be unwrapped
403
1042
  if (shouldUnwrapComponent(componentProps, fieldType)) {
404
1043
  // For primitive types, wrap in { value }
@@ -410,12 +1049,18 @@ export class BaseArcheType {
410
1049
  } else if (this.unionMap[key]) {
411
1050
  // Handle union fields
412
1051
  const unionComponents = this.unionMap[key];
413
- const selectedComponent = this.determineUnionComponent(value, unionComponents, storage);
414
-
1052
+ const selectedComponent = this.determineUnionComponent(
1053
+ value,
1054
+ unionComponents,
1055
+ storage
1056
+ );
1057
+
415
1058
  if (selectedComponent) {
416
1059
  this.addComponent(selectedComponent, value as any);
417
1060
  } else if (strict) {
418
- throw new Error(`Could not determine component type for union field '${key}'`);
1061
+ throw new Error(
1062
+ `Could not determine component type for union field '${key}'`
1063
+ );
419
1064
  }
420
1065
  } else {
421
1066
  // direct property
@@ -424,12 +1069,14 @@ export class BaseArcheType {
424
1069
  }
425
1070
  }
426
1071
  for (const [field, ctor] of Object.entries(this.componentMap)) {
427
- const alreadyAdded = Array.from(this.components).some(c => c.ctor === ctor);
1072
+ const alreadyAdded = Array.from(this.components).some(
1073
+ (c) => c.ctor === ctor
1074
+ );
428
1075
  if (!alreadyAdded) {
429
1076
  this.addComponent(ctor, {} as any);
430
1077
  }
431
1078
  }
432
-
1079
+
433
1080
  return this;
434
1081
  }
435
1082
 
@@ -440,9 +1087,13 @@ export class BaseArcheType {
440
1087
  * @param storage Metadata storage
441
1088
  * @returns The selected component constructor, or null if none match
442
1089
  */
443
- private determineUnionComponent(value: any, unionComponents: (new (...args: any[]) => BaseComponent)[], storage: any): (new (...args: any[]) => BaseComponent) | null {
1090
+ private determineUnionComponent(
1091
+ value: any,
1092
+ unionComponents: (new (...args: any[]) => BaseComponent)[],
1093
+ storage: any
1094
+ ): (new (...args: any[]) => BaseComponent) | null {
444
1095
  // If value has __typename, use it to determine the component
445
- if (value && typeof value === 'object' && value.__typename) {
1096
+ if (value && typeof value === "object" && value.__typename) {
446
1097
  const expectedTypeName = value.__typename;
447
1098
  for (const component of unionComponents) {
448
1099
  const componentTypeName = compNameToFieldName(component.name);
@@ -451,39 +1102,42 @@ export class BaseArcheType {
451
1102
  }
452
1103
  }
453
1104
  }
454
-
1105
+
455
1106
  // Fallback: Try to infer based on property presence
456
- if (value && typeof value === 'object') {
1107
+ if (value && typeof value === "object") {
457
1108
  for (const component of unionComponents) {
458
1109
  const typeId = storage.getComponentId(component.name);
459
1110
  const componentProps = storage.getComponentProperties(typeId);
460
-
1111
+
461
1112
  // Check if any properties of this component are present in the value
462
- const hasMatchingProps = componentProps.some((prop: ComponentPropertyMetadata) =>
463
- value.hasOwnProperty(prop.propertyKey)
1113
+ const hasMatchingProps = componentProps.some(
1114
+ (prop: ComponentPropertyMetadata) =>
1115
+ value.hasOwnProperty(prop.propertyKey)
464
1116
  );
465
-
1117
+
466
1118
  if (hasMatchingProps) {
467
1119
  return component;
468
1120
  }
469
1121
  }
470
1122
  }
471
-
1123
+
472
1124
  // If no component matches, return the first one as default
473
1125
  return unionComponents[0] || null;
474
- } async updateEntity<T>(entity: Entity, updates: Partial<T>) {
1126
+ }
1127
+ async updateEntity<T>(entity: Entity, updates: Partial<T>) {
475
1128
  const storage = getMetadataStorage();
476
-
1129
+
477
1130
  for (const key of Object.keys(updates)) {
478
- if(key === 'id' || key === '_id') continue;
1131
+ if (key === "id" || key === "_id") continue;
479
1132
  const value = updates[key as keyof T];
480
1133
  if (value !== undefined) {
481
1134
  const compCtor = this.componentMap[key];
482
1135
  if (compCtor) {
483
1136
  const fieldType = this.fieldTypes[key];
484
1137
  const typeId = storage.getComponentId(compCtor.name);
485
- const componentProps = storage.getComponentProperties(typeId);
486
-
1138
+ const componentProps =
1139
+ storage.getComponentProperties(typeId);
1140
+
487
1141
  // Check if this is a primitive field that should be unwrapped
488
1142
  if (shouldUnwrapComponent(componentProps, fieldType)) {
489
1143
  // For primitive types, wrap in { value }
@@ -495,8 +1149,12 @@ export class BaseArcheType {
495
1149
  } else if (this.unionMap[key]) {
496
1150
  // Handle union fields
497
1151
  const unionComponents = this.unionMap[key];
498
- const selectedComponent = this.determineUnionComponent(value, unionComponents, storage);
499
-
1152
+ const selectedComponent = this.determineUnionComponent(
1153
+ value,
1154
+ unionComponents,
1155
+ storage
1156
+ );
1157
+
500
1158
  if (selectedComponent) {
501
1159
  await entity.set(selectedComponent, value as any);
502
1160
  }
@@ -506,6 +1164,7 @@ export class BaseArcheType {
506
1164
  }
507
1165
  }
508
1166
  }
1167
+ return entity;
509
1168
  }
510
1169
 
511
1170
  /**
@@ -531,14 +1190,296 @@ export class BaseArcheType {
531
1190
  }
532
1191
 
533
1192
  /**
534
- * Unwraps an entity into a plain object containing the component data.
535
- * @param entity The entity to unwrap
536
- * @param exclude An optional array of field names to exclude from the result (e.g., sensitive data like passwords)
537
- * @returns A promise that resolves to an object with component data
538
- */
539
- public async Unwrap(entity: Entity, exclude: string[] = []): Promise<Record<string, any>> {
540
- const result: any = { id: entity.id };
1193
+ * Retrieves an entity by ID and populates it with all components defined in this archetype.
1194
+ *
1195
+ * @param id The entity ID to retrieve
1196
+ * @param options Optional configuration for component loading and behavior
1197
+ * @returns A promise that resolves to the populated Entity or null if not found
1198
+ *
1199
+ * @example
1200
+ * // Basic usage
1201
+ * const serviceArea = await serviceAreaArcheType.getEntityWithID('uuid-123');
1202
+ *
1203
+ * @example
1204
+ * // With options
1205
+ * const serviceArea = await serviceAreaArcheType.getEntityWithID('uuid-123', {
1206
+ * includeComponents: ['info', 'label'],
1207
+ * populateRelations: true,
1208
+ * throwOnNotFound: true
1209
+ * });
1210
+ */
1211
+ public async getEntityWithID(id: string, options?: GetEntityOptions): Promise<Entity | null> {
1212
+ // Validate ID to prevent PostgreSQL UUID parsing errors
1213
+ if (!id || typeof id !== 'string' || id.trim() === '') {
1214
+ if (options?.throwOnNotFound) {
1215
+ throw new Error(`Invalid entity ID provided: "${id}"`);
1216
+ }
1217
+ return null;
1218
+ }
1219
+
1220
+ const { Query } = await import("../query");
1221
+
1222
+ // Build query with selected components for batch loading
1223
+ // Use `any` since components are dynamically added in loop
1224
+ let query: any = new Query().findById(id);
1225
+
1226
+ // Determine which components to load
1227
+ const componentsToLoad = this.getComponentsToLoad(options);
1228
+
1229
+ for (const componentCtor of componentsToLoad) {
1230
+ query = query.with(componentCtor as any);
1231
+ }
541
1232
 
1233
+ const entities = await query.exec();
1234
+ if (entities.length === 0) {
1235
+ if (options?.throwOnNotFound) {
1236
+ throw new Error(`Entity with ID ${id} not found`);
1237
+ }
1238
+ return null;
1239
+ }
1240
+
1241
+ const entity = entities[0]!;
1242
+
1243
+ // Populate relations if requested
1244
+ if (options?.populateRelations) {
1245
+ await this.populateRelations(entity);
1246
+ }
1247
+
1248
+ return entity;
1249
+ }
1250
+
1251
+ /**
1252
+ * Determines which components should be loaded based on the options.
1253
+ * @param options The options specifying component inclusion/exclusion
1254
+ * @returns Array of component constructors to load
1255
+ */
1256
+ private getComponentsToLoad(options?: GetEntityOptions): (new (...args: any[]) => BaseComponent)[] {
1257
+ let componentsToLoad: (new (...args: any[]) => BaseComponent)[] = [];
1258
+
1259
+ // Start with all regular components
1260
+ componentsToLoad.push(...Object.values(this.componentMap));
1261
+
1262
+ // Add union components
1263
+ for (const componentCtors of Object.values(this.unionMap)) {
1264
+ componentsToLoad.push(...componentCtors);
1265
+ }
1266
+
1267
+ // Apply include filter
1268
+ if (options?.includeComponents) {
1269
+ const includeSet = new Set(options.includeComponents);
1270
+ componentsToLoad = componentsToLoad.filter(ctor => {
1271
+ const fieldName = compNameToFieldName(ctor.name);
1272
+ return includeSet.has(fieldName);
1273
+ });
1274
+ }
1275
+
1276
+ // Apply exclude filter
1277
+ if (options?.excludeComponents) {
1278
+ const excludeSet = new Set(options.excludeComponents);
1279
+ componentsToLoad = componentsToLoad.filter(ctor => {
1280
+ const fieldName = compNameToFieldName(ctor.name);
1281
+ return !excludeSet.has(fieldName);
1282
+ });
1283
+ }
1284
+
1285
+ // Respect nullable options (skip nullable components by default unless explicitly included)
1286
+ if (!options?.includeComponents) {
1287
+ componentsToLoad = componentsToLoad.filter(ctor => {
1288
+ const fieldName = compNameToFieldName(ctor.name);
1289
+ const isNullable = this.fieldOptions[fieldName]?.nullable === true;
1290
+ return !isNullable;
1291
+ });
1292
+ }
1293
+
1294
+ return componentsToLoad;
1295
+ }
1296
+
1297
+ /**
1298
+ * Populates relations for the given entity.
1299
+ * @param entity The entity to populate relations for
1300
+ */
1301
+ private async populateRelations(entity: Entity): Promise<void> {
1302
+ const { Query } = await import("../query");
1303
+ const storage = getMetadataStorage();
1304
+
1305
+ for (const [fieldName, relatedArchetype] of Object.entries(this.relationMap)) {
1306
+ const relationType = this.relationTypes[fieldName];
1307
+ const relationOptions = this.relationOptions[fieldName];
1308
+
1309
+ if (relationType === "belongsTo") {
1310
+ // For belongsTo, load the related entity using foreign key
1311
+ const foreignKey = relationOptions?.foreignKey;
1312
+ if (foreignKey) {
1313
+ let foreignId: string | undefined;
1314
+
1315
+ // Get foreign key value from entity's components
1316
+ if (foreignKey.includes('.')) {
1317
+ const [fieldName, propName] = foreignKey.split('.');
1318
+ const compCtor = this.componentMap[fieldName!];
1319
+ if (compCtor) {
1320
+ const componentInstance = await entity.get(compCtor as any);
1321
+ if (componentInstance && (componentInstance as any)[propName!] !== undefined) {
1322
+ foreignId = (componentInstance as any)[propName!];
1323
+ }
1324
+ }
1325
+ } else {
1326
+ for (const compCtor of Object.values(this.componentMap)) {
1327
+ const typeId = storage.getComponentId(compCtor.name);
1328
+ const componentProps = storage.getComponentProperties(typeId);
1329
+ const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
1330
+ if (!hasForeignKey) continue;
1331
+
1332
+ const componentInstance = await entity.get(compCtor as any);
1333
+ if (componentInstance && (componentInstance as any)[foreignKey] !== undefined) {
1334
+ foreignId = (componentInstance as any)[foreignKey];
1335
+ break;
1336
+ }
1337
+ }
1338
+ }
1339
+
1340
+ if (!foreignId && foreignKey === 'id') {
1341
+ foreignId = entity.id;
1342
+ }
1343
+
1344
+ if (foreignId) {
1345
+ // Load related entity
1346
+ let relatedArchetypeInstance: BaseArcheType;
1347
+ if (typeof relatedArchetype === "function") {
1348
+ relatedArchetypeInstance = new (relatedArchetype as any)();
1349
+ } else {
1350
+ // Find archetype by name
1351
+ const relatedArchetypeMetadata = storage.archetypes.find((a) => a.name === relatedArchetype);
1352
+ if (relatedArchetypeMetadata) {
1353
+ relatedArchetypeInstance = new (relatedArchetypeMetadata.target as any)();
1354
+ } else {
1355
+ continue;
1356
+ }
1357
+ }
1358
+
1359
+ const relatedEntity = await relatedArchetypeInstance.getEntityWithID(foreignId);
1360
+ if (relatedEntity) {
1361
+ // Attach as computed property (non-persisted)
1362
+ (entity as any)[fieldName] = relatedEntity;
1363
+ }
1364
+ }
1365
+ }
1366
+ } else if (relationType === "hasMany") {
1367
+ // For hasMany, query related entities that reference this entity
1368
+ const foreignKey = relationOptions?.foreignKey;
1369
+ if (foreignKey) {
1370
+ let relatedArchetypeInstance: BaseArcheType;
1371
+ if (typeof relatedArchetype === "function") {
1372
+ relatedArchetypeInstance = new (relatedArchetype as any)();
1373
+ } else {
1374
+ const relatedArchetypeMetadata = storage.archetypes.find((a) => a.name === relatedArchetype);
1375
+ if (relatedArchetypeMetadata) {
1376
+ relatedArchetypeInstance = new (relatedArchetypeMetadata.target as any)();
1377
+ } else {
1378
+ continue;
1379
+ }
1380
+ }
1381
+
1382
+ // Find the component in related archetype that has the foreign key
1383
+ let foreignKeyComponent: any = null;
1384
+ for (const compCtor of Object.values(relatedArchetypeInstance.componentMap)) {
1385
+ const typeId = storage.getComponentId(compCtor.name);
1386
+ const componentProps = storage.getComponentProperties(typeId);
1387
+ const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
1388
+ if (hasForeignKey) {
1389
+ foreignKeyComponent = compCtor;
1390
+ break;
1391
+ }
1392
+ }
1393
+
1394
+ if (foreignKeyComponent) {
1395
+ // Query related entities
1396
+ const relatedEntities = await new Query()
1397
+ .with(foreignKeyComponent)
1398
+ .exec();
1399
+
1400
+ // Filter entities that reference this entity
1401
+ const matchingEntities: Entity[] = [];
1402
+ for (const relatedEntity of relatedEntities) {
1403
+ const componentInstance = await relatedEntity.get(foreignKeyComponent);
1404
+ if (componentInstance && (componentInstance as any)[foreignKey] === entity.id) {
1405
+ matchingEntities.push(relatedEntity);
1406
+ }
1407
+ }
1408
+
1409
+ // Attach as computed property
1410
+ (entity as any)[fieldName] = matchingEntities;
1411
+ }
1412
+ }
1413
+ }
1414
+ // Note: hasOne and belongsToMany not implemented yet
1415
+ }
1416
+ }
1417
+
1418
+ /**
1419
+ * Static convenience method to get an entity with ID using an archetype class.
1420
+ *
1421
+ * @param archetypeClass The archetype class to use for loading
1422
+ * @param id The entity ID to retrieve
1423
+ * @param options Optional configuration for component loading and behavior
1424
+ * @returns A promise that resolves to the populated Entity or null if not found
1425
+ *
1426
+ * @example
1427
+ * // Using static method
1428
+ * const serviceArea = await BaseArcheType.getEntityWithID(ServiceAreaArcheTypeClass, 'uuid-123');
1429
+ */
1430
+ static async getEntityWithID<T extends BaseArcheType>(
1431
+ archetypeClass: new () => T,
1432
+ id: string,
1433
+ options?: GetEntityOptions
1434
+ ): Promise<Entity | null> {
1435
+ const instance = new archetypeClass();
1436
+ return instance.getEntityWithID(id, options);
1437
+ }
1438
+
1439
+ /**
1440
+ * Create a typed query builder for this archetype.
1441
+ * Auto-includes all archetype components and returns typed results.
1442
+ *
1443
+ * @example
1444
+ * ```typescript
1445
+ * // Subclass usage (most common)
1446
+ * class Player extends BaseArcheType {
1447
+ * @ArcheTypeField(Position) position!: Position;
1448
+ * @ArcheTypeField(Health) health!: Health;
1449
+ * }
1450
+ *
1451
+ * const players = await Player.query()
1452
+ * .filter('health', 'gt', { current: 50 })
1453
+ * .sortBy('position', 'x', 'ASC')
1454
+ * .take(10)
1455
+ * .exec();
1456
+ *
1457
+ * for (const player of players) {
1458
+ * // Direct typed access - no async, no null checks
1459
+ * console.log(player.position.x, player.health.current);
1460
+ * // Access underlying entity when needed
1461
+ * await player.save();
1462
+ * }
1463
+ * ```
1464
+ */
1465
+ static query<T extends BaseArcheType>(
1466
+ this: new () => T
1467
+ ): ArcheTypeQuery<T> {
1468
+ return new ArcheTypeQuery<T>(this);
1469
+ }
1470
+
1471
+ /**
1472
+ * Unwraps an entity into a plain object containing the component data.
1473
+ * @param entity The entity to unwrap
1474
+ * @param exclude An optional array of field names to exclude from the result (e.g., sensitive data like passwords)
1475
+ * @returns A promise that resolves to an object with component data
1476
+ */
1477
+ public async Unwrap(
1478
+ entity: Entity,
1479
+ exclude: string[] = []
1480
+ ): Promise<Record<string, any>> {
1481
+ const result: any = { id: entity.id };
1482
+
542
1483
  // Handle regular components
543
1484
  for (const [field, ctor] of Object.entries(this.componentMap)) {
544
1485
  if (exclude.includes(field)) continue;
@@ -547,7 +1488,7 @@ export class BaseArcheType {
547
1488
  result[field] = (comp as any).value;
548
1489
  }
549
1490
  }
550
-
1491
+
551
1492
  // Handle union fields
552
1493
  for (const [field, components] of Object.entries(this.unionMap)) {
553
1494
  if (exclude.includes(field)) continue;
@@ -556,13 +1497,13 @@ export class BaseArcheType {
556
1497
  if (comp) {
557
1498
  result[field] = {
558
1499
  __typename: compNameToFieldName(component.name),
559
- ...(comp as any)
1500
+ ...(comp as any),
560
1501
  };
561
1502
  break; // Only take the first matching component
562
1503
  }
563
1504
  }
564
1505
  }
565
-
1506
+
566
1507
  // for direct fields
567
1508
  for (const field of Object.keys(this.fieldTypes)) {
568
1509
  if (exclude.includes(field)) continue;
@@ -577,16 +1518,19 @@ export class BaseArcheType {
577
1518
  * Gets the property metadata for all components in this archetype.
578
1519
  * @returns A record mapping field names to their component property metadata arrays
579
1520
  */
580
- public getComponentProperties(): Record<string, ComponentPropertyMetadata[]> {
1521
+ public getComponentProperties(): Record<
1522
+ string,
1523
+ ComponentPropertyMetadata[]
1524
+ > {
581
1525
  const storage = getMetadataStorage();
582
1526
  const result: Record<string, ComponentPropertyMetadata[]> = {};
583
-
1527
+
584
1528
  // Regular components
585
1529
  for (const [field, ctor] of Object.entries(this.componentMap)) {
586
1530
  const typeId = storage.getComponentId(ctor.name);
587
1531
  result[field] = storage.getComponentProperties(typeId);
588
1532
  }
589
-
1533
+
590
1534
  // Union components (for each union field, include properties of all components)
591
1535
  for (const [field, components] of Object.entries(this.unionMap)) {
592
1536
  const allProps: ComponentPropertyMetadata[] = [];
@@ -596,16 +1540,38 @@ export class BaseArcheType {
596
1540
  }
597
1541
  result[field] = allProps;
598
1542
  }
599
-
1543
+
600
1544
  return result;
601
1545
  }
602
1546
 
1547
+ /**
1548
+ * Helper to ensure we have a proper Entity instance for resolvers.
1549
+ * Handles cases where parent comes from GraphQL chain as a plain object.
1550
+ */
1551
+ private static async ensureEntity(parent: any, context: any): Promise<Entity> {
1552
+ if (parent instanceof Entity) {
1553
+ return parent;
1554
+ }
1555
+ if (parent && parent.id) {
1556
+ // Try to load via DataLoader first
1557
+ if (context?.loaders?.entityById) {
1558
+ const loaded = await context.loaders.entityById.load(parent.id);
1559
+ if (loaded) return loaded;
1560
+ }
1561
+ // Fallback: create Entity instance
1562
+ const entity = new Entity(parent.id);
1563
+ entity.setPersisted(true);
1564
+ return entity;
1565
+ }
1566
+ throw new Error('Invalid parent object: missing id property');
1567
+ }
1568
+
603
1569
  /**
604
1570
  * Generates GraphQL field resolver functions for this archetype.
605
1571
  * These resolvers handle both simple fields and component-based fields with DataLoader support.
606
- *
1572
+ *
607
1573
  * @returns An array of resolver metadata that can be registered with GraphQL
608
- *
1574
+ *
609
1575
  * @example
610
1576
  * const resolvers = serviceAreaArcheType.generateFieldResolvers();
611
1577
  * // Returns array of: { typeName, fieldName, resolver }
@@ -618,15 +1584,17 @@ export class BaseArcheType {
618
1584
  const storage = getMetadataStorage();
619
1585
  const resolvers: Array<any> = [];
620
1586
  const archetypeId = storage.getComponentId(this.constructor.name);
621
- const archetypeName = storage.archetypes.find(a => a.typeId === archetypeId)?.name || this.constructor.name;
1587
+ const archetypeName =
1588
+ storage.archetypes.find((a) => a.typeId === archetypeId)?.name ||
1589
+ this.constructor.name;
622
1590
 
623
1591
  // Generate ID resolver for the main archetype type
624
1592
  resolvers.push({
625
1593
  typeName: archetypeName,
626
- fieldName: 'id',
627
- resolver: (parent: Entity) => {
1594
+ fieldName: "id",
1595
+ resolver: (parent: any) => {
628
1596
  return parent.id;
629
- }
1597
+ },
630
1598
  });
631
1599
 
632
1600
  // Generate resolvers for each component field
@@ -635,74 +1603,121 @@ export class BaseArcheType {
635
1603
  const typeIdHex = typeId;
636
1604
  const componentName = ctor.name;
637
1605
  const fieldType = this.fieldTypes[field];
638
-
1606
+
639
1607
  // Skip components with no properties (like tag components)
640
1608
  const componentProps = storage.getComponentProperties(typeId);
641
1609
  if (componentProps.length === 0) {
642
1610
  continue;
643
1611
  }
644
-
1612
+
645
1613
  // Check if this component should be unwrapped to a scalar
646
- const isUnwrapped = shouldUnwrapComponent(componentProps, fieldType);
647
-
1614
+ const isUnwrapped = shouldUnwrapComponent(
1615
+ componentProps,
1616
+ fieldType
1617
+ );
1618
+
648
1619
  if (isUnwrapped) {
649
1620
  // For unwrapped components, resolve directly to the 'value' property
650
1621
  resolvers.push({
651
1622
  typeName: archetypeName,
652
1623
  fieldName: field,
653
- resolver: async (parent: Entity, args: any, context: any) => {
654
- const entity = parent;
655
-
656
- // Use DataLoader if available, but fall back when no row exists
657
- if (context.loaders) {
658
- const componentData = await context.loaders.componentsByEntityType.load({
659
- entityId: entity.id,
660
- typeId: typeIdHex // Pass hex string directly
661
- });
1624
+ resolver: async (
1625
+ parent: any,
1626
+ args: any,
1627
+ context: any
1628
+ ) => {
1629
+ const entityId = parent?.id;
1630
+ if (!entityId) return (parent as any)[field];
1631
+
1632
+ // Check if parent is an Entity with component state
1633
+ if (parent instanceof Entity) {
1634
+ // If component was explicitly removed, return null immediately
1635
+ if (parent.wasRemoved(ctor)) {
1636
+ return null;
1637
+ }
1638
+ const inMemoryComp = parent.getInMemory(ctor);
1639
+ if (inMemoryComp) {
1640
+ return (inMemoryComp as any)?.value;
1641
+ }
1642
+ }
1643
+
1644
+ // Use DataLoader if available
1645
+ if (context?.loaders?.componentsByEntityType) {
1646
+ const componentData =
1647
+ await context.loaders.componentsByEntityType.load(
1648
+ {
1649
+ entityId: entityId,
1650
+ typeId: typeIdHex,
1651
+ }
1652
+ );
662
1653
  if (componentData?.data?.value !== undefined) {
663
1654
  return componentData.data.value;
664
1655
  }
665
1656
  }
666
1657
 
667
- // Fallback: direct query ensures component data is returned
1658
+ // Fallback: ensure we have an Entity and query directly
1659
+ const entity = await BaseArcheType.ensureEntity(parent, context);
668
1660
  const comp = await entity.get(ctor);
669
1661
  return (comp as any)?.value;
670
- }
1662
+ },
671
1663
  });
672
1664
  } else {
673
1665
  // For complex components, return the full component object
674
1666
  resolvers.push({
675
1667
  typeName: archetypeName,
676
1668
  fieldName: field,
677
- resolver: async (parent: Entity, args: any, context: any) => {
678
- const entity = parent;
679
- if (!entity || !entity.id) return (parent as any)[field];
680
-
681
- // Use DataLoader if available, but fall back when no row exists
682
- if (context.loaders) {
683
- const componentData = await context.loaders.componentsByEntityType.load({
684
- entityId: entity.id,
685
- typeId: typeIdHex // Pass hex string directly
686
- });
1669
+ resolver: async (
1670
+ parent: any,
1671
+ args: any,
1672
+ context: any
1673
+ ) => {
1674
+ const entityId = parent?.id;
1675
+ if (!entityId) return (parent as any)[field];
1676
+
1677
+ // Check if parent is an Entity with the component already loaded in memory
1678
+ // This avoids cache/DataLoader issues for freshly created entities
1679
+ // Use synchronous getInMemory() to avoid triggering unnecessary DB queries
1680
+ if (parent instanceof Entity) {
1681
+ // If component was explicitly removed, return null immediately
1682
+ // This prevents stale DataLoader cache from returning old data
1683
+ if (parent.wasRemoved(ctor)) {
1684
+ return null;
1685
+ }
1686
+ const inMemoryComp = parent.getInMemory(ctor);
1687
+ if (inMemoryComp) {
1688
+ return inMemoryComp;
1689
+ }
1690
+ }
1691
+
1692
+ // Use DataLoader if available
1693
+ if (context?.loaders?.componentsByEntityType) {
1694
+ const componentData =
1695
+ await context.loaders.componentsByEntityType.load(
1696
+ {
1697
+ entityId: entityId,
1698
+ typeId: typeIdHex,
1699
+ }
1700
+ );
687
1701
  if (componentData?.data) {
688
1702
  return componentData.data;
689
1703
  }
690
1704
  }
691
1705
 
692
- // Fallback: direct query ensures component data is returned
1706
+ // Fallback: ensure we have an Entity and query directly
1707
+ const entity = await BaseArcheType.ensureEntity(parent, context);
693
1708
  const comp = await entity.get(ctor);
694
1709
  return comp;
695
- }
1710
+ },
696
1711
  });
697
1712
 
698
1713
  // Generate nested field resolvers for component properties
699
1714
  const componentTypeName = compNameToFieldName(componentName);
700
-
1715
+
701
1716
  for (const prop of componentProps) {
702
1717
  resolvers.push({
703
- typeName: componentTypeName, // Use lowercase component name
1718
+ typeName: componentTypeName, // Use lowercase component name
704
1719
  fieldName: prop.propertyKey,
705
- resolver: (parent: any) => parent[prop.propertyKey]
1720
+ resolver: (parent: any) => parent[prop.propertyKey],
706
1721
  });
707
1722
  }
708
1723
  }
@@ -713,171 +1728,537 @@ export class BaseArcheType {
713
1728
  resolvers.push({
714
1729
  typeName: archetypeName,
715
1730
  fieldName: field,
716
- resolver: async (parent: Entity, args: any, context: any) => {
717
- const entity = parent;
718
-
1731
+ resolver: async (parent: any, args: any, context: any) => {
1732
+ const entityId = parent?.id;
1733
+ if (!entityId) return null;
1734
+
719
1735
  // Try to find which component in the union is present on the entity
720
1736
  for (const component of components) {
721
1737
  const typeId = storage.getComponentId(component.name);
722
-
723
- if (context.loaders) {
724
- const componentData = await context.loaders.componentsByEntityType.load({
725
- entityId: entity.id,
726
- typeId: typeId
727
- });
1738
+
1739
+ // Check if parent is an Entity with component state
1740
+ if (parent instanceof Entity) {
1741
+ // If component was explicitly removed, skip it
1742
+ if (parent.wasRemoved(component)) {
1743
+ continue;
1744
+ }
1745
+ const inMemoryComp = parent.getInMemory(component);
1746
+ if (inMemoryComp) {
1747
+ return {
1748
+ __typename: compNameToFieldName(component.name),
1749
+ ...(inMemoryComp as any).data?.() ?? inMemoryComp,
1750
+ };
1751
+ }
1752
+ }
1753
+
1754
+ if (context?.loaders?.componentsByEntityType) {
1755
+ const componentData =
1756
+ await context.loaders.componentsByEntityType.load(
1757
+ {
1758
+ entityId: entityId,
1759
+ typeId: typeId,
1760
+ }
1761
+ );
728
1762
  if (componentData?.data) {
729
1763
  // Add __typename for GraphQL union resolution
1764
+ return {
1765
+ __typename: compNameToFieldName(
1766
+ component.name
1767
+ ),
1768
+ ...componentData.data,
1769
+ };
1770
+ }
1771
+ } else {
1772
+ // Fallback: ensure we have an Entity and query directly
1773
+ const entity = await BaseArcheType.ensureEntity(parent, context);
1774
+ const comp = await entity.get(component);
1775
+ if (comp) {
730
1776
  return {
731
1777
  __typename: compNameToFieldName(component.name),
732
- ...componentData.data
1778
+ ...(comp as any),
733
1779
  };
734
1780
  }
735
1781
  }
736
-
737
- // Fallback
738
- const comp = await entity.get(component);
739
- if (comp) {
740
- return {
741
- __typename: compNameToFieldName(component.name),
742
- ...(comp as any)
743
- };
744
- }
745
1782
  }
746
-
1783
+
747
1784
  return null;
748
- }
1785
+ },
749
1786
  });
750
1787
  }
751
1788
 
752
1789
  // Generate resolvers for relation fields
753
- for (const [field, relatedArcheType] of Object.entries(this.relationMap)) {
1790
+ for (const [field, relatedArcheType] of Object.entries(
1791
+ this.relationMap
1792
+ )) {
754
1793
  const relationType = this.relationTypes[field];
755
1794
  const relationOptions = this.relationOptions[field];
756
- const isArray = relationType === 'hasMany' || relationType === 'belongsToMany';
757
-
1795
+ const isArray =
1796
+ relationType === "hasMany" || relationType === "belongsToMany";
1797
+
758
1798
  // Get the related archetype name
759
1799
  let relatedTypeName: string;
760
- if (typeof relatedArcheType === 'string') {
1800
+ if (typeof relatedArcheType === "string") {
761
1801
  relatedTypeName = relatedArcheType;
762
1802
  } else {
763
- const relatedArchetypeId = storage.getComponentId(relatedArcheType.name);
764
- const relatedArchetypeMetadata = storage.archetypes.find(a => a.typeId === relatedArchetypeId);
765
- relatedTypeName = relatedArchetypeMetadata?.name || relatedArcheType.name.replace(/ArcheType$/, '');
1803
+ const relatedArchetypeId = storage.getComponentId(
1804
+ relatedArcheType.name
1805
+ );
1806
+ const relatedArchetypeMetadata = storage.archetypes.find(
1807
+ (a) => a.typeId === relatedArchetypeId
1808
+ );
1809
+ relatedTypeName =
1810
+ relatedArchetypeMetadata?.name ||
1811
+ relatedArcheType.name.replace(/ArcheType$/, "");
766
1812
  }
767
-
768
- if (!isArray && relationType === 'belongsTo' && relationOptions?.foreignKey) {
1813
+
1814
+ if (
1815
+ !isArray &&
1816
+ relationType === "belongsTo" &&
1817
+ relationOptions?.foreignKey
1818
+ ) {
769
1819
  resolvers.push({
770
1820
  typeName: archetypeName,
771
1821
  fieldName: field,
772
- resolver: async (parent: Entity, args: any, context: any) => {
773
- const entity = parent;
774
- if (!entity || !entity.id) {
1822
+ resolver: async (
1823
+ parent: any,
1824
+ args: any,
1825
+ context: any
1826
+ ) => {
1827
+ const entityId = parent?.id;
1828
+ if (!entityId) {
775
1829
  return null;
776
1830
  }
777
1831
 
778
1832
  let foreignId: string | undefined;
779
1833
 
780
1834
  // Attempt to load the component that holds the foreign key via DataLoader
781
- if (context.loaders) {
782
- for (const [componentField, compCtor] of Object.entries(this.componentMap)) {
783
- const typeIdForComponent = storage.getComponentId(compCtor.name);
784
- const componentProps = storage.getComponentProperties(typeIdForComponent);
785
- const hasForeignKey = componentProps.some(prop => prop.propertyKey === relationOptions.foreignKey);
786
- if (!hasForeignKey || !relationOptions.foreignKey) continue;
787
-
788
- const componentData = await context.loaders.componentsByEntityType.load({
789
- entityId: entity.id,
790
- typeId: typeIdForComponent
791
- });
792
-
793
- if (componentData?.data && componentData.data[relationOptions.foreignKey] !== undefined) {
794
- foreignId = componentData.data[relationOptions.foreignKey];
795
- break;
1835
+ if (context?.loaders?.componentsByEntityType) {
1836
+ const foreignKey = relationOptions.foreignKey;
1837
+ if (foreignKey && foreignKey.includes('.')) {
1838
+ // Handle nested foreign key like "field.property"
1839
+ const [fieldName, propName] = foreignKey.split('.');
1840
+ const compCtor = this.componentMap[fieldName!];
1841
+ if (compCtor) {
1842
+ const typeIdForComponent = storage.getComponentId(compCtor.name);
1843
+ const componentData = await context.loaders.componentsByEntityType.load({
1844
+ entityId: entityId,
1845
+ typeId: typeIdForComponent,
1846
+ });
1847
+ if (componentData?.data && componentData.data[propName!] !== undefined) {
1848
+ foreignId = componentData.data[propName!];
1849
+ }
1850
+ }
1851
+ } else {
1852
+ // Original logic for flat foreign key
1853
+ for (const [componentField, compCtor] of Object.entries(this.componentMap)) {
1854
+ const typeIdForComponent = storage.getComponentId(compCtor.name);
1855
+ const componentProps = storage.getComponentProperties(typeIdForComponent);
1856
+ const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
1857
+ if (!hasForeignKey || !foreignKey) continue;
1858
+
1859
+ const componentData = await context.loaders.componentsByEntityType.load({
1860
+ entityId: entityId,
1861
+ typeId: typeIdForComponent,
1862
+ });
1863
+
1864
+ if (componentData?.data && componentData.data[foreignKey] !== undefined) {
1865
+ foreignId = componentData.data[foreignKey];
1866
+ break;
1867
+ }
796
1868
  }
797
1869
  }
798
1870
  }
799
1871
 
800
1872
  // Fallback: pull the component from the entity directly when DataLoader misses
801
1873
  if (!foreignId) {
802
- for (const compCtor of Object.values(this.componentMap)) {
803
- const typeIdForComponent = storage.getComponentId(compCtor.name);
804
- const componentProps = storage.getComponentProperties(typeIdForComponent);
805
- const hasForeignKey = componentProps.some(prop => prop.propertyKey === relationOptions.foreignKey);
806
- if (!hasForeignKey || !relationOptions.foreignKey) continue;
807
- const componentInstance = await entity.get(compCtor as any);
808
- if (componentInstance && (componentInstance as any)[relationOptions.foreignKey] !== undefined) {
809
- foreignId = (componentInstance as any)[relationOptions.foreignKey];
810
- break;
1874
+ const entity = await BaseArcheType.ensureEntity(parent, context);
1875
+ const foreignKey = relationOptions.foreignKey;
1876
+ if (foreignKey && foreignKey.includes('.')) {
1877
+ // Handle nested foreign key like "field.property"
1878
+ const [fieldName, propName] = foreignKey.split('.');
1879
+ const compCtor = this.componentMap[fieldName!];
1880
+ if (compCtor) {
1881
+ const componentInstance = await entity.get(compCtor as any);
1882
+ if (componentInstance && (componentInstance as any)[propName!] !== undefined) {
1883
+ foreignId = (componentInstance as any)[propName!];
1884
+ }
1885
+ }
1886
+ } else {
1887
+ // Original logic for flat foreign key
1888
+ for (const compCtor of Object.values(this.componentMap)) {
1889
+ const typeIdForComponent = storage.getComponentId(compCtor.name);
1890
+ const componentProps = storage.getComponentProperties(typeIdForComponent);
1891
+ const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
1892
+ if (!hasForeignKey || !foreignKey) continue;
1893
+ const componentInstance = await entity.get(compCtor as any);
1894
+ if (componentInstance && (componentInstance as any)[foreignKey] !== undefined) {
1895
+ foreignId = (componentInstance as any)[foreignKey];
1896
+ break;
1897
+ }
811
1898
  }
812
1899
  }
813
1900
  }
814
1901
 
1902
+ if (!foreignId && relationOptions.foreignKey === 'id') {
1903
+ foreignId = entityId;
1904
+ }
1905
+
815
1906
  if (!foreignId) {
816
1907
  return null;
817
1908
  }
818
1909
 
819
1910
  // Resolve the related entity using loaders when possible, otherwise hit the database directly
820
1911
  if (context.loaders?.entityById) {
821
- const relatedEntity = await context.loaders.entityById.load(foreignId);
1912
+ const relatedEntity =
1913
+ await context.loaders.entityById.load(
1914
+ foreignId
1915
+ );
822
1916
  if (relatedEntity) {
823
1917
  return relatedEntity;
824
1918
  }
825
1919
  }
826
1920
 
827
1921
  return Entity.FindById(foreignId);
828
- }
1922
+ },
829
1923
  });
830
1924
  } else if (isArray) {
831
1925
  // Array relation resolver
832
1926
  resolvers.push({
833
1927
  typeName: archetypeName,
834
1928
  fieldName: field,
835
- resolver: async (parent: Entity, args: any, context: any) => {
836
- const entity = parent;
837
-
838
- // Use DataLoader for relation loading if available
839
- if (context.loaders && context.loaders.relationsByEntityField) {
840
- return context.loaders.relationsByEntityField.load({
841
- entityId: entity.id,
842
- relationField: field,
843
- relatedType: relatedTypeName,
844
- foreignKey: relationOptions?.foreignKey
845
- });
1929
+ resolver: async (
1930
+ parent: any,
1931
+ args: any,
1932
+ context: any
1933
+ ) => {
1934
+ const entityId = parent?.id;
1935
+ if (!entityId) return [];
1936
+
1937
+ // If foreignKey is specified, for hasMany, the foreign key is on the related entity
1938
+ if (relationOptions?.foreignKey) {
1939
+ // Find the component that has the foreign key (may be nested like "field.property")
1940
+ let componentCtor: any = null;
1941
+ let foreignKeyField: string = relationOptions.foreignKey;
1942
+ let relatedArchetypeInstance: any = null;
1943
+
1944
+ if (typeof relatedArcheType === "function") {
1945
+ relatedArchetypeInstance = new (relatedArcheType as any)();
1946
+ } else if (typeof relatedArcheType === "string") {
1947
+ // Find the archetype class by name
1948
+ const relatedArchetypeMetadata = storage.archetypes.find((a) => a.name === relatedArcheType);
1949
+ if (relatedArchetypeMetadata) {
1950
+ relatedArchetypeInstance = new (relatedArchetypeMetadata.target as any)();
1951
+ }
1952
+ }
1953
+
1954
+ if (relatedArchetypeInstance) {
1955
+ if (relationOptions.foreignKey.includes('.')) {
1956
+ const [fieldName, propName] = relationOptions.foreignKey.split('.');
1957
+ componentCtor = relatedArchetypeInstance.componentMap[fieldName!];
1958
+ foreignKeyField = propName!;
1959
+ } else {
1960
+ // Flat foreign key
1961
+ for (const comp of Object.values(relatedArchetypeInstance.componentMap) as any[]) {
1962
+ const typeId = storage.getComponentId(comp.name);
1963
+ const props = storage.getComponentProperties(typeId);
1964
+ if (props.some(p => p.propertyKey === relationOptions.foreignKey)) {
1965
+ componentCtor = comp;
1966
+ break;
1967
+ }
1968
+ }
1969
+ }
1970
+ }
1971
+
1972
+ if (componentCtor) {
1973
+ const query = new Query();
1974
+ query.with(componentCtor, Query.filters(Query.filter(foreignKeyField, Query.filterOp.EQ, entityId)));
1975
+ return await query.exec();
1976
+ } else {
1977
+ console.warn(`No component found with foreign key ${relationOptions.foreignKey} in ${relatedTypeName}`);
1978
+ return [];
1979
+ }
1980
+ } else {
1981
+ // Use DataLoader for relation loading if available
1982
+ if (
1983
+ context?.loaders?.relationsByEntityField
1984
+ ) {
1985
+ return context.loaders.relationsByEntityField.load({
1986
+ entityId: entityId,
1987
+ relationField: field,
1988
+ relatedType: relatedTypeName,
1989
+ foreignKey: relationOptions?.foreignKey,
1990
+ });
1991
+ }
1992
+
1993
+ // Fallback: return empty array or implement custom relation query
1994
+ // This should be implemented based on your relation storage strategy
1995
+ console.warn(
1996
+ `No relationsByEntityField loader found for array relation ${field} on ${archetypeName}`
1997
+ );
1998
+ return [];
846
1999
  }
847
-
848
- // Fallback: return empty array or implement custom relation query
849
- // This should be implemented based on your relation storage strategy
850
- console.warn(`No relationsByEntityField loader found for array relation ${field} on ${archetypeName}`);
851
- return [];
852
- }
2000
+ },
853
2001
  });
854
2002
  } else {
855
2003
  // Single relation resolver
856
2004
  resolvers.push({
857
2005
  typeName: archetypeName,
858
2006
  fieldName: field,
859
- resolver: async (parent: Entity, args: any, context: any) => {
860
- const entity = parent;
2007
+ resolver: async (
2008
+ parent: any,
2009
+ args: any,
2010
+ context: any
2011
+ ) => {
2012
+ const entityId = parent?.id;
2013
+
2014
+ // If foreignKey is specified, treat as belongsTo (foreign key on this entity)
2015
+ if (relationOptions?.foreignKey) {
2016
+ if (!entityId) {
2017
+ return null;
2018
+ }
2019
+
2020
+ let foreignId: string | undefined;
2021
+
2022
+ // Attempt to load the component that holds the foreign key via DataLoader
2023
+ if (context?.loaders?.componentsByEntityType) {
2024
+ const foreignKey = relationOptions.foreignKey;
2025
+ if (foreignKey && foreignKey.includes('.')) {
2026
+ // Handle nested foreign key like "field.property"
2027
+ const [fieldName, propName] = foreignKey.split('.');
2028
+ const compCtor = this.componentMap[fieldName!];
2029
+ if (compCtor) {
2030
+ const typeIdForComponent = storage.getComponentId(compCtor.name);
2031
+ const componentData = await context.loaders.componentsByEntityType.load({
2032
+ entityId: entityId,
2033
+ typeId: typeIdForComponent,
2034
+ });
2035
+ if (componentData?.data && componentData.data[propName!] !== undefined) {
2036
+ foreignId = componentData.data[propName!];
2037
+ }
2038
+ }
2039
+ } else {
2040
+ // Original logic for flat foreign key
2041
+ for (const [componentField, compCtor] of Object.entries(this.componentMap)) {
2042
+ const typeIdForComponent = storage.getComponentId(compCtor.name);
2043
+ const componentProps = storage.getComponentProperties(typeIdForComponent);
2044
+ const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
2045
+ if (!hasForeignKey || !foreignKey) continue;
2046
+
2047
+ const componentData = await context.loaders.componentsByEntityType.load({
2048
+ entityId: entityId,
2049
+ typeId: typeIdForComponent,
2050
+ });
2051
+
2052
+ if (componentData?.data && componentData.data[foreignKey] !== undefined) {
2053
+ foreignId = componentData.data[foreignKey];
2054
+ break;
2055
+ }
2056
+ }
2057
+ }
2058
+ }
2059
+
2060
+ // Fallback: pull the component from the entity directly when DataLoader misses
2061
+ if (!foreignId) {
2062
+ const entity = await BaseArcheType.ensureEntity(parent, context);
2063
+ const foreignKey = relationOptions.foreignKey;
2064
+ if (foreignKey && foreignKey.includes('.')) {
2065
+ // Handle nested foreign key like "field.property"
2066
+ const [fieldName, propName] = foreignKey.split('.');
2067
+ const compCtor = this.componentMap[fieldName!];
2068
+ if (compCtor) {
2069
+ const componentInstance = await entity.get(compCtor as any);
2070
+ if (componentInstance && (componentInstance as any)[propName!] !== undefined) {
2071
+ foreignId = (componentInstance as any)[propName!];
2072
+ }
2073
+ }
2074
+ } else {
2075
+ // Original logic for flat foreign key
2076
+ for (const compCtor of Object.values(this.componentMap)) {
2077
+ const typeIdForComponent = storage.getComponentId(compCtor.name);
2078
+ const componentProps = storage.getComponentProperties(typeIdForComponent);
2079
+ const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
2080
+ if (!hasForeignKey || !foreignKey) continue;
2081
+ const componentInstance = await entity.get(compCtor as any);
2082
+ if (componentInstance && (componentInstance as any)[foreignKey] !== undefined) {
2083
+ foreignId = (componentInstance as any)[foreignKey];
2084
+ break;
2085
+ }
2086
+ }
2087
+ }
2088
+ }
2089
+
2090
+ if (!foreignId) {
2091
+ return null;
2092
+ }
2093
+
2094
+ // Resolve the related entity using loaders when possible, otherwise hit the database directly
2095
+ if (context?.loaders?.entityById) {
2096
+ const relatedEntity = await context.loaders.entityById.load(foreignId);
2097
+ if (relatedEntity) {
2098
+ return relatedEntity;
2099
+ }
2100
+ }
2101
+
2102
+ return Entity.FindById(foreignId);
2103
+ } else {
2104
+ // Use DataLoader for relation loading if available
2105
+ if (
2106
+ context?.loaders?.relationsByEntityField
2107
+ ) {
2108
+ const results =
2109
+ await context.loaders.relationsByEntityField.load(
2110
+ {
2111
+ entityId: entityId,
2112
+ relationField: field,
2113
+ relatedType: relatedTypeName,
2114
+ foreignKey: relationOptions?.foreignKey,
2115
+ }
2116
+ );
2117
+ if (results.length > 0) {
2118
+ return results[0];
2119
+ }
2120
+ }
2121
+
2122
+ // Fallback: return null or implement custom relation query
2123
+ console.warn(
2124
+ `No relationsByEntityField loader found for single relation ${field} on ${archetypeName}`
2125
+ );
2126
+ return null;
2127
+ }
2128
+ },
2129
+ });
2130
+ }
2131
+ }
2132
+
2133
+ // Generate resolvers for archetype functions
2134
+ for (const { propertyKey, options } of this.functions) {
2135
+ resolvers.push({
2136
+ typeName: archetypeName,
2137
+ fieldName: propertyKey,
2138
+ resolver: async (parent: any, args: any, context: any) => {
2139
+ // Ensure parent is a proper Entity instance
2140
+ // When coming from cache or GraphQL chain, parent might be a plain object
2141
+ let entity: Entity;
2142
+ if (parent instanceof Entity) {
2143
+ entity = parent;
2144
+ } else if (parent && parent.id) {
2145
+ // Parent is a plain object with an ID - load the entity
2146
+ if (context.loaders?.entityById) {
2147
+ const loadedEntity = await context.loaders.entityById.load(parent.id);
2148
+ if (loadedEntity) {
2149
+ entity = loadedEntity;
2150
+ } else {
2151
+ // Create a new Entity instance with the ID
2152
+ entity = new Entity(parent.id);
2153
+ entity.setPersisted(true);
2154
+ }
2155
+ } else {
2156
+ // No DataLoader available - create Entity instance directly
2157
+ entity = new Entity(parent.id);
2158
+ entity.setPersisted(true);
2159
+ }
2160
+ } else {
2161
+ throw new Error(`Invalid parent for ${archetypeName}.${propertyKey}: parent must have an 'id' property`);
2162
+ }
2163
+
2164
+ // If function has arguments, extract and convert them
2165
+ if (options?.args && options.args.length > 0 && args) {
2166
+ const functionArgs: any[] = [];
861
2167
 
862
- // Use DataLoader for relation loading if available
863
- if (context.loaders && context.loaders.relationsByEntityField) {
864
- const results = await context.loaders.relationsByEntityField.load({
865
- entityId: entity.id,
866
- relationField: field,
867
- relatedType: relatedTypeName,
868
- foreignKey: relationOptions?.foreignKey
869
- });
870
- if (results.length > 0) {
871
- return results[0];
2168
+ for (const argDef of options.args) {
2169
+ const argValue = args[argDef.name];
2170
+
2171
+ if (argValue === undefined || argValue === null) {
2172
+ if (!argDef.nullable) {
2173
+ throw new Error(`Required argument '${argDef.name}' is missing for ${archetypeName}.${propertyKey}`);
2174
+ }
2175
+ functionArgs.push(null);
2176
+ continue;
2177
+ }
2178
+
2179
+ // Convert argument value to the expected type
2180
+ let convertedValue: any = argValue;
2181
+
2182
+ // Check if it's a custom type that needs instantiation
2183
+ if (argDef.type && typeof argDef.type === 'function' && argDef.type !== String && argDef.type !== Number && argDef.type !== Boolean && argDef.type !== Date) {
2184
+ // Check if it's a registered custom type (like ST_Point)
2185
+ const isCustomType = customTypeRegistry.has(argDef.type) ||
2186
+ customTypeNameRegistry.has(argDef.type) ||
2187
+ (argDef.type?.name && registeredCustomTypes.has(argDef.type.name));
2188
+
2189
+ if (isCustomType && typeof argValue === 'object' && !Array.isArray(argValue)) {
2190
+ // Try to instantiate the type if it's a class constructor
2191
+ try {
2192
+ if (argDef.type.prototype && argDef.type.prototype.constructor) {
2193
+ // It's a class, try to instantiate it
2194
+ // First, try object assignment (works for most cases)
2195
+ convertedValue = Object.assign(Object.create(argDef.type.prototype), argValue);
2196
+
2197
+ // Verify the instance was created correctly
2198
+ if (!convertedValue || !(convertedValue instanceof argDef.type)) {
2199
+ // If object assignment didn't work, try constructor with common patterns
2200
+ // This is a fallback for types that require constructor parameters
2201
+ const constructor = argDef.type.prototype.constructor;
2202
+ const paramCount = constructor.length;
2203
+
2204
+ if (paramCount === 2) {
2205
+ // Try common 2-parameter patterns
2206
+ if (argValue.latitude !== undefined && argValue.longitude !== undefined) {
2207
+ convertedValue = new argDef.type(argValue.latitude, argValue.longitude);
2208
+ } else if (argValue.x !== undefined && argValue.y !== undefined) {
2209
+ convertedValue = new argDef.type(argValue.x, argValue.y);
2210
+ } else {
2211
+ // Fallback: use first two object values
2212
+ const values = Object.values(argValue);
2213
+ if (values.length >= 2) {
2214
+ convertedValue = new argDef.type(values[0], values[1]);
2215
+ }
2216
+ }
2217
+ } else if (paramCount === 1) {
2218
+ // Single parameter - try first property value
2219
+ const values = Object.values(argValue);
2220
+ if (values.length >= 1) {
2221
+ convertedValue = new argDef.type(values[0]);
2222
+ }
2223
+ } else if (paramCount === 0) {
2224
+ // No parameters - object assignment should work
2225
+ convertedValue = Object.assign(Object.create(argDef.type.prototype), argValue);
2226
+ }
2227
+
2228
+ // Final fallback
2229
+ if (!convertedValue || !(convertedValue instanceof argDef.type)) {
2230
+ convertedValue = Object.assign(Object.create(argDef.type.prototype), argValue);
2231
+ }
2232
+ }
2233
+ } else {
2234
+ // Not a class, use the value as-is
2235
+ convertedValue = argValue;
2236
+ }
2237
+ } catch (e) {
2238
+ // If instantiation fails, try object assignment
2239
+ try {
2240
+ convertedValue = Object.assign(Object.create(argDef.type.prototype || {}), argValue);
2241
+ } catch (e2) {
2242
+ // Fallback to plain object
2243
+ convertedValue = argValue;
2244
+ }
2245
+ }
2246
+ } else {
2247
+ convertedValue = argValue;
2248
+ }
872
2249
  }
2250
+
2251
+ functionArgs.push(convertedValue);
873
2252
  }
874
2253
 
875
- // Fallback: return null or implement custom relation query
876
- console.warn(`No relationsByEntityField loader found for single relation ${field} on ${archetypeName}`);
877
- return null;
2254
+ // Call function with entity and arguments
2255
+ return await (this as any)[propertyKey](entity, ...functionArgs);
2256
+ } else {
2257
+ // No arguments, call with just entity
2258
+ return await (this as any)[propertyKey](entity);
878
2259
  }
879
- });
880
- }
2260
+ },
2261
+ });
881
2262
  }
882
2263
 
883
2264
  return resolvers;
@@ -886,9 +2267,9 @@ export class BaseArcheType {
886
2267
  /**
887
2268
  * Registers all auto-generated field resolvers for this archetype with a service.
888
2269
  * This eliminates the need to manually write @GraphQLField decorators.
889
- *
2270
+ *
890
2271
  * @param service The service instance to attach resolvers to
891
- *
2272
+ *
892
2273
  * @example
893
2274
  * class AreaService extends BaseService {
894
2275
  * constructor(app: App) {
@@ -899,44 +2280,50 @@ export class BaseArcheType {
899
2280
  * }
900
2281
  */
901
2282
  public registerFieldResolvers(service: any): void {
2283
+ this.getZodObjectSchema(); // Ensure schema is generated
902
2284
  const resolvers = this.generateFieldResolvers();
903
-
2285
+
904
2286
  if (!service.__graphqlFields) {
905
2287
  service.__graphqlFields = [];
906
2288
  }
907
-
2289
+
908
2290
  for (const { typeName, fieldName, resolver } of resolvers) {
909
2291
  // Create a unique method name
910
2292
  const methodName = `_autoResolver_${typeName}_${fieldName}`;
911
-
2293
+
912
2294
  // Attach resolver as a method
913
2295
  service[methodName] = resolver;
914
-
2296
+
915
2297
  // Register with GraphQL metadata
916
2298
  service.__graphqlFields.push({
917
2299
  type: typeName,
918
2300
  field: fieldName,
919
- propertyKey: methodName
2301
+ propertyKey: methodName,
920
2302
  });
921
2303
  }
922
2304
  }
923
2305
 
924
- // TODO: Here
925
- public getZodObjectSchema(): ZodObject<any> {
2306
+ public getZodObjectSchema(options?: { excludeRelations?: boolean; excludeFunctions?: boolean }): ZodObject<any> {
2307
+ const excludeRelations = options?.excludeRelations ?? false;
2308
+ const excludeFunctions = options?.excludeFunctions ?? false;
926
2309
  const zodShapes: Record<string, any> = {};
927
2310
  const storage = getMetadataStorage();
928
- const unionSchemas: Array<{ fieldName: string; schema: any; components: any[] }> = [];
2311
+ const unionSchemas: Array<{
2312
+ fieldName: string;
2313
+ schema: any;
2314
+ components: any[];
2315
+ }> = [];
929
2316
 
930
2317
  for (const [field, ctor] of Object.entries(this.componentMap)) {
931
2318
  // Skip union fields - they'll be processed separately
932
- if (field.startsWith('union_')) {
2319
+ if (field.startsWith("union_")) {
933
2320
  continue;
934
2321
  }
935
2322
 
936
2323
  const type = this.fieldTypes[field];
937
2324
  const typeId = storage.getComponentId(ctor.name);
938
2325
  const componentProps = storage.getComponentProperties(typeId);
939
-
2326
+
940
2327
  // Check if component should be unwrapped based on field type
941
2328
  if (shouldUnwrapComponent(componentProps, type)) {
942
2329
  // Unwrap to primitive type
@@ -951,7 +2338,11 @@ export class BaseArcheType {
951
2338
  }
952
2339
  } else {
953
2340
  // Use component schema for complex types
954
- const componentSchema = getOrCreateComponentSchema(ctor, typeId, this.fieldOptions[field]);
2341
+ const componentSchema = getOrCreateComponentSchema(
2342
+ ctor,
2343
+ typeId,
2344
+ this.fieldOptions[field]
2345
+ );
955
2346
  if (componentSchema) {
956
2347
  zodShapes[field] = componentSchema;
957
2348
  } else {
@@ -959,8 +2350,12 @@ export class BaseArcheType {
959
2350
  continue;
960
2351
  }
961
2352
  }
962
-
963
- if (this.fieldOptions[field]?.nullable && zodShapes[field] && !(zodShapes[field] instanceof ZodObject)) {
2353
+
2354
+ if (
2355
+ this.fieldOptions[field]?.nullable &&
2356
+ zodShapes[field] &&
2357
+ !(zodShapes[field] instanceof ZodObject)
2358
+ ) {
964
2359
  zodShapes[field] = zodShapes[field].nullish();
965
2360
  }
966
2361
  }
@@ -970,49 +2365,69 @@ export class BaseArcheType {
970
2365
  // Generate schemas for each component in the union
971
2366
  const unionComponentSchemas: any[] = [];
972
2367
  const unionComponentCtors: any[] = [];
973
-
2368
+
974
2369
  for (const component of components) {
975
2370
  const typeId = storage.getComponentId(component.name);
976
- const componentSchema = getOrCreateComponentSchema(component, typeId, this.unionOptions[fieldName]);
977
-
2371
+ const componentSchema = getOrCreateComponentSchema(
2372
+ component,
2373
+ typeId,
2374
+ this.unionOptions[fieldName]
2375
+ );
2376
+
978
2377
  if (componentSchema) {
979
2378
  unionComponentSchemas.push(componentSchema);
980
2379
  unionComponentCtors.push(component);
981
2380
  }
982
2381
  }
983
-
2382
+
984
2383
  // Create union type using Zod with GQLoom support
985
2384
  if (unionComponentSchemas.length > 0) {
986
- const unionSchema = z.union(unionComponentSchemas).register(asUnionType, {
987
- name: fieldName.charAt(0).toUpperCase() + fieldName.slice(1), // Capitalize field name for type
988
- resolveType: (it: any) => {
989
- // Determine which type this is based on __typename
990
- if (it.__typename) {
991
- return it.__typename;
992
- }
993
- // Fallback: check property presence
994
- for (let i = 0; i < unionComponentCtors.length; i++) {
995
- const componentProps = storage.getComponentProperties(
996
- storage.getComponentId(unionComponentCtors[i].name)
997
- );
998
- const hasUniqueProps = componentProps.some(prop =>
999
- it.hasOwnProperty(prop.propertyKey)
1000
- );
1001
- if (hasUniqueProps) {
1002
- return compNameToFieldName(unionComponentCtors[i].name);
2385
+ const unionSchema = z
2386
+ .union(unionComponentSchemas)
2387
+ .register(asUnionType, {
2388
+ name:
2389
+ fieldName.charAt(0).toUpperCase() +
2390
+ fieldName.slice(1), // Capitalize field name for type
2391
+ resolveType: (it: any) => {
2392
+ // Determine which type this is based on __typename
2393
+ if (it.__typename) {
2394
+ return it.__typename;
1003
2395
  }
1004
- }
1005
- return compNameToFieldName(unionComponentCtors[0].name);
1006
- }
1007
- });
1008
-
2396
+ // Fallback: check property presence
2397
+ for (
2398
+ let i = 0;
2399
+ i < unionComponentCtors.length;
2400
+ i++
2401
+ ) {
2402
+ const componentProps =
2403
+ storage.getComponentProperties(
2404
+ storage.getComponentId(
2405
+ unionComponentCtors[i].name
2406
+ )
2407
+ );
2408
+ const hasUniqueProps = componentProps.some(
2409
+ (prop) =>
2410
+ it.hasOwnProperty(prop.propertyKey)
2411
+ );
2412
+ if (hasUniqueProps) {
2413
+ return compNameToFieldName(
2414
+ unionComponentCtors[i].name
2415
+ );
2416
+ }
2417
+ }
2418
+ return compNameToFieldName(
2419
+ unionComponentCtors[0].name
2420
+ );
2421
+ },
2422
+ });
2423
+
1009
2424
  zodShapes[fieldName] = unionSchema;
1010
2425
  unionSchemas.push({
1011
2426
  fieldName,
1012
2427
  schema: unionSchema,
1013
- components: unionComponentSchemas
2428
+ components: unionComponentSchemas,
1014
2429
  });
1015
-
2430
+
1016
2431
  // Apply nullable option for union fields
1017
2432
  if (this.unionOptions[fieldName]?.nullable) {
1018
2433
  zodShapes[fieldName] = zodShapes[fieldName].nullish();
@@ -1020,45 +2435,157 @@ export class BaseArcheType {
1020
2435
  }
1021
2436
  }
1022
2437
 
1023
- // Process relations for GraphQL schema generation
1024
- for (const [field, relatedArcheType] of Object.entries(this.relationMap)) {
1025
- const relationType = this.relationTypes[field];
1026
- const isArray = relationType === 'hasMany' || relationType === 'belongsToMany';
1027
-
1028
- // Get the related archetype name
1029
- let relatedTypeName: string;
1030
- if (typeof relatedArcheType === 'string') {
1031
- relatedTypeName = relatedArcheType;
1032
- } else {
1033
- const relatedArchetypeId = storage.getComponentId(relatedArcheType.name);
1034
- const relatedArchetypeMetadata = storage.archetypes.find(a => a.typeId === relatedArchetypeId);
1035
- relatedTypeName = relatedArchetypeMetadata?.name || relatedArcheType.name.replace(/ArcheType$/, '');
1036
- }
1037
-
1038
- // For GraphQL relations, we just store the type name as a string reference
1039
- // The GraphQL schema will use the type name directly, and the full type definition
1040
- // will be generated when each archetype's getZodObjectSchema() is called
1041
- const relatedTypeSchema = z.string().describe(`Reference to ${relatedTypeName} type`);
1042
-
1043
- if (isArray) {
1044
- zodShapes[field] = z.array(relatedTypeSchema);
1045
- } else {
1046
- zodShapes[field] = relatedTypeSchema;
2438
+ // Process relations for GraphQL schema generation (skip if excludeRelations is true)
2439
+ if (!excludeRelations) {
2440
+ for (const [field, relatedArcheType] of Object.entries(
2441
+ this.relationMap
2442
+ )) {
2443
+ const relationType = this.relationTypes[field];
2444
+ const isArray =
2445
+ relationType === "hasMany" || relationType === "belongsToMany";
2446
+
2447
+ // Get the related archetype name
2448
+ let relatedTypeName: string;
2449
+ if (typeof relatedArcheType === "string") {
2450
+ relatedTypeName = relatedArcheType;
2451
+ } else {
2452
+ const relatedArchetypeId = storage.getComponentId(
2453
+ relatedArcheType.name
2454
+ );
2455
+ const relatedArchetypeMetadata = storage.archetypes.find(
2456
+ (a) => a.typeId === relatedArchetypeId
2457
+ );
2458
+ relatedTypeName =
2459
+ relatedArchetypeMetadata?.name ||
2460
+ relatedArcheType.name.replace(/ArcheType$/, "");
2461
+ }
2462
+
2463
+ // For GraphQL relations, we just store the type name as a string reference
2464
+ // The GraphQL schema will use the type name directly, and the full type definition
2465
+ // will be generated when each archetype's getZodObjectSchema() is called
2466
+
2467
+ // For singular relations, add description to the string schema
2468
+ const relatedTypeSchema = z
2469
+ .string()
2470
+ .describe(`Reference to ${relatedTypeName} type`);
2471
+
2472
+ if (isArray) {
2473
+ // HasMany and BelongsToMany should be optional by default (nullable array)
2474
+ // unless explicitly marked as required via nullable: false
2475
+ const shouldBeRequired = this.relationOptions[field]?.nullable === false;
2476
+ // For array relations, the description on the inner string won't show up in GraphQL
2477
+ // We need to store metadata about this being a relation for post-processing
2478
+ zodShapes[field] = shouldBeRequired
2479
+ ? z.array(relatedTypeSchema)
2480
+ : z.array(relatedTypeSchema).optional();
2481
+ } else {
2482
+ zodShapes[field] = relatedTypeSchema;
2483
+
2484
+ // For singular relations, apply nullable option
2485
+ if (this.relationOptions[field]?.nullable) {
2486
+ zodShapes[field] = zodShapes[field].nullish();
2487
+ }
2488
+ }
1047
2489
  }
1048
-
1049
- if (this.relationOptions[field]?.nullable) {
1050
- zodShapes[field] = zodShapes[field].nullish();
2490
+ }
2491
+
2492
+ // Process archetype functions
2493
+ // Store function input type names for post-processing
2494
+ const functionInputTypes = new Map<string, string>();
2495
+
2496
+ if (!excludeFunctions) {
2497
+ for (const { propertyKey, options } of this.functions) {
2498
+ let zodType;
2499
+ if (options?.returnType === 'number') {
2500
+ zodType = z.number();
2501
+ } else if (options?.returnType === 'string') {
2502
+ zodType = z.string();
2503
+ } else if (options?.returnType === 'boolean') {
2504
+ zodType = z.boolean();
2505
+ } else if (options?.returnType) {
2506
+ // Assume it's a GraphQL type name, create a string reference
2507
+ zodType = z.string().describe(`Reference to ${options.returnType} type`);
2508
+ } else {
2509
+ const returnType = Reflect.getMetadata("design:returntype", this.constructor.prototype, propertyKey);
2510
+ if (returnType === String) {
2511
+ zodType = z.string();
2512
+ } else if (returnType === Number) {
2513
+ zodType = z.number();
2514
+ } else if (returnType === Boolean) {
2515
+ zodType = z.boolean();
2516
+ } else {
2517
+ zodType = z.any();
2518
+ }
2519
+ }
2520
+
2521
+ // Process function arguments if present
2522
+ if (options?.args && options.args.length > 0) {
2523
+ const archetypeId = storage.getComponentId(this.constructor.name);
2524
+ const archetypeName =
2525
+ storage.archetypes.find((a) => a.typeId === archetypeId)?.name ||
2526
+ this.constructor.name;
2527
+ const inputTypeName = `${archetypeName}_${propertyKey}Args`;
2528
+
2529
+ // Create input type schema for arguments
2530
+ const inputFields: Record<string, any> = {};
2531
+ for (const arg of options.args) {
2532
+ let argZodType: any;
2533
+
2534
+ // Check if it's a registered custom type
2535
+ if (customTypeRegistry.has(arg.type)) {
2536
+ argZodType = customTypeRegistry.get(arg.type)!;
2537
+ } else if (arg.type === String || arg.type === String) {
2538
+ argZodType = z.string();
2539
+ } else if (arg.type === Number) {
2540
+ argZodType = z.number();
2541
+ } else if (arg.type === Boolean) {
2542
+ argZodType = z.boolean();
2543
+ } else if (arg.type === Date) {
2544
+ argZodType = z.date();
2545
+ } else if (registeredCustomTypes.has(arg.type?.name || '')) {
2546
+ // Check if it's registered by name
2547
+ argZodType = registeredCustomTypes.get(arg.type.name);
2548
+ } else {
2549
+ // Try to get from customTypeNameRegistry
2550
+ const typeName = customTypeNameRegistry.get(arg.type);
2551
+ if (typeName && registeredCustomTypes.has(typeName)) {
2552
+ argZodType = registeredCustomTypes.get(typeName);
2553
+ } else {
2554
+ console.warn(`[ArcheType] Unknown argument type for ${archetypeName}.${propertyKey}.${arg.name}: ${arg.type?.name || arg.type}. Falling back to z.any()`);
2555
+ argZodType = z.any();
2556
+ }
2557
+ }
2558
+
2559
+ // Apply nullable if specified
2560
+ if (arg.nullable) {
2561
+ argZodType = argZodType.optional();
2562
+ }
2563
+
2564
+ inputFields[arg.name] = argZodType;
2565
+ }
2566
+
2567
+ // Create and register the input type
2568
+ const inputSchema = z.object(inputFields).register(asObjectType, { name: inputTypeName });
2569
+ registeredCustomTypes.set(inputTypeName, inputSchema);
2570
+ functionInputTypes.set(propertyKey, inputTypeName);
2571
+ }
2572
+
2573
+ zodShapes[propertyKey] = zodType.optional();
1051
2574
  }
1052
2575
  }
1053
2576
 
1054
2577
  const archetypeId = storage.getComponentId(this.constructor.name);
1055
- const nameFromStorage = storage.archetypes.find(a => a.typeId === archetypeId)?.name || this.constructor.name;
2578
+ const nameFromStorage =
2579
+ storage.archetypes.find((a) => a.typeId === archetypeId)?.name ||
2580
+ this.constructor.name;
1056
2581
  const shape: Record<string, any> = {
1057
2582
  __typename: z.literal(nameFromStorage).nullish(),
1058
- id: z.string().nullish(), // Will be converted to ID in post-processing
2583
+ id: z.string().nullish(), // Will be converted to ID in post-processing
1059
2584
  };
1060
2585
  for (const [field, zodType] of Object.entries(zodShapes)) {
1061
- const isNullable = this.fieldOptions[field]?.nullable || this.unionOptions[field]?.nullable;
2586
+ const isNullable =
2587
+ this.fieldOptions[field]?.nullable ||
2588
+ this.unionOptions[field]?.nullable;
1062
2589
  if (isNullable) {
1063
2590
  // For nullable fields, make them optional in the GraphQL schema
1064
2591
  shape[field] = zodType.optional();
@@ -1067,74 +2594,438 @@ export class BaseArcheType {
1067
2594
  }
1068
2595
  }
1069
2596
  const r = z.object(shape);
1070
-
2597
+
1071
2598
  // Collect all component schemas used by this archetype for weaving
1072
2599
  const componentSchemasToWeave: any[] = [];
1073
2600
  for (const [field, zodType] of Object.entries(zodShapes)) {
1074
2601
  if (zodType instanceof ZodObject) {
1075
2602
  componentSchemasToWeave.push(zodType);
1076
- } else if (Array.isArray(zodType) || (zodType && typeof zodType === 'object' && zodType._def?.typeName === 'ZodUnion')) {
2603
+ } else if (
2604
+ Array.isArray(zodType) ||
2605
+ (zodType &&
2606
+ typeof zodType === "object" &&
2607
+ zodType._def?.typeName === "ZodUnion")
2608
+ ) {
1077
2609
  // Handle union types
1078
- if (zodType._def?.typeName === 'ZodUnion') {
2610
+ if (zodType._def?.typeName === "ZodUnion") {
1079
2611
  componentSchemasToWeave.push(zodType);
1080
2612
  }
1081
2613
  }
1082
2614
  }
1083
-
2615
+
1084
2616
  // Weave archetype schema along with its component schemas
1085
- const schemasToWeave = [r, ...componentSchemasToWeave];
2617
+ const schemasToWeave = [r];
1086
2618
  const schema = weave(ZodWeaver, ...schemasToWeave);
1087
2619
  let graphqlSchemaString = printSchema(schema);
1088
-
2620
+
1089
2621
  // Post-process: Replace 'id: String' with 'id: ID' for all id fields
1090
- graphqlSchemaString = graphqlSchemaString.replace(/\bid:\s*String\b/g, 'id: ID');
1091
-
2622
+ graphqlSchemaString = graphqlSchemaString.replace(
2623
+ /\bid:\s*String\b/g,
2624
+ "id: ID"
2625
+ );
2626
+
1092
2627
  // Post-process: Replace relation field types with proper GraphQL type references
1093
- for (const [field, relatedArcheType] of Object.entries(this.relationMap)) {
2628
+ for (const [field, relatedArcheType] of Object.entries(
2629
+ this.relationMap
2630
+ )) {
1094
2631
  const relationType = this.relationTypes[field];
1095
- const isArray = relationType === 'hasMany' || relationType === 'belongsToMany';
1096
-
2632
+ const isArray =
2633
+ relationType === "hasMany" || relationType === "belongsToMany";
2634
+
1097
2635
  let relatedTypeName: string;
1098
- if (typeof relatedArcheType === 'string') {
2636
+ if (typeof relatedArcheType === "string") {
1099
2637
  relatedTypeName = relatedArcheType;
1100
2638
  } else {
1101
- const relatedArchetypeId = storage.getComponentId(relatedArcheType.name);
1102
- const relatedArchetypeMetadata = storage.archetypes.find(a => a.typeId === relatedArchetypeId);
1103
- relatedTypeName = relatedArchetypeMetadata?.name || relatedArcheType.name.replace(/ArcheType$/, '');
2639
+ const relatedArchetypeId = storage.getComponentId(
2640
+ relatedArcheType.name
2641
+ );
2642
+ const relatedArchetypeMetadata = storage.archetypes.find(
2643
+ (a) => a.typeId === relatedArchetypeId
2644
+ );
2645
+ relatedTypeName =
2646
+ relatedArchetypeMetadata?.name ||
2647
+ relatedArcheType.name.replace(/ArcheType$/, "");
1104
2648
  }
1105
-
2649
+
1106
2650
  // Replace the String field with proper GraphQL type reference
1107
2651
  if (isArray) {
1108
- const pattern = new RegExp(`${field}:\\s*\\[String!?\\]!?`, 'g');
1109
- graphqlSchemaString = graphqlSchemaString.replace(pattern, `${field}: [${relatedTypeName}!]!`);
2652
+ // For arrays: should be required only if explicitly set nullable: false
2653
+ const shouldBeRequired = this.relationOptions[field]?.nullable === false;
2654
+ const suffix = shouldBeRequired ? "!" : "";
2655
+
2656
+ // Step 1: Add description comment if it doesn't exist
2657
+ const descriptionPattern = new RegExp(`"""Reference to ${relatedTypeName} type"""[\\s\\S]*?${field}:`);
2658
+ if (!descriptionPattern.test(graphqlSchemaString)) {
2659
+ // Add description before the field
2660
+ const addDescriptionPattern = new RegExp(
2661
+ `(\\n\\s+)(${field}:\\s*\\[String!?\\]!?)`,
2662
+ "g"
2663
+ );
2664
+ graphqlSchemaString = graphqlSchemaString.replace(
2665
+ addDescriptionPattern,
2666
+ `$1"""Reference to ${relatedTypeName} type"""\n$1$2`
2667
+ );
2668
+ }
2669
+
2670
+ // Step 2: Replace [String!] or [String] with [TypeName!]
2671
+ const replaceTypePattern = new RegExp(
2672
+ `(${field}:\\s*)\\[String!?\\](!?)`,
2673
+ "g"
2674
+ );
2675
+ graphqlSchemaString = graphqlSchemaString.replace(
2676
+ replaceTypePattern,
2677
+ `$1[${relatedTypeName}!]${suffix}`
2678
+ );
1110
2679
  } else {
1111
- const pattern = new RegExp(`${field}:\\s*String!?`, 'g');
1112
- graphqlSchemaString = graphqlSchemaString.replace(pattern, `${field}: ${relatedTypeName}!`);
2680
+ const isNullable = this.relationOptions[field]?.nullable;
2681
+ const suffix = isNullable ? "" : "!";
2682
+ const pattern = new RegExp(`${field}:\\s*String!?`, "g");
2683
+ graphqlSchemaString = graphqlSchemaString.replace(
2684
+ pattern,
2685
+ `${field}: ${relatedTypeName}${suffix}`
2686
+ );
1113
2687
  }
1114
2688
  }
1115
-
1116
- // console.log("WeavedSchema:", graphqlSchemaString);
1117
-
2689
+
2690
+ // Post-process: Add argument definitions to function fields
2691
+ if (!excludeFunctions) {
2692
+ for (const { propertyKey, options } of this.functions) {
2693
+ if (options?.args && options.args.length > 0) {
2694
+ // Build individual argument definitions
2695
+ const argDefs: string[] = [];
2696
+ for (const arg of options.args) {
2697
+ let argTypeName: string;
2698
+
2699
+ // Determine GraphQL type name for the argument
2700
+ // For GraphQL arguments, we prefer input types over object types
2701
+ // First check if there's a registered input type for this type
2702
+ const inputTypeName = inputTypeRegistry.get(arg.type);
2703
+ if (inputTypeName) {
2704
+ argTypeName = inputTypeName;
2705
+ } else {
2706
+ // Fall back to the object type name
2707
+ const registeredTypeName = customTypeNameRegistry.get(arg.type);
2708
+ if (registeredTypeName) {
2709
+ argTypeName = registeredTypeName;
2710
+ } else if (customTypeRegistry.has(arg.type)) {
2711
+ // It's registered but without a name, try to find the name
2712
+ const registeredName = Array.from(registeredCustomTypes.entries())
2713
+ .find(([name, schema]) => schema === customTypeRegistry.get(arg.type))?.[0];
2714
+ argTypeName = registeredName || 'String';
2715
+ } else if (arg.type === String) {
2716
+ argTypeName = 'String';
2717
+ } else if (arg.type === Number) {
2718
+ argTypeName = 'Float';
2719
+ } else if (arg.type === Boolean) {
2720
+ argTypeName = 'Boolean';
2721
+ } else if (arg.type === Date) {
2722
+ argTypeName = 'Date';
2723
+ } else if (arg.type?.name && registeredCustomTypes.has(arg.type.name)) {
2724
+ // Check if the type name is registered
2725
+ argTypeName = arg.type.name;
2726
+ } else if (arg.type?.name) {
2727
+ // Fallback to the type's name if it exists
2728
+ argTypeName = arg.type.name;
2729
+ } else {
2730
+ argTypeName = 'String';
2731
+ }
2732
+ }
2733
+
2734
+ const nullable = arg.nullable ? '' : '!';
2735
+ argDefs.push(`${arg.name}: ${argTypeName}${nullable}`);
2736
+ }
2737
+
2738
+ // Find the function field in the schema and add arguments
2739
+ // The schema format from printSchema is typically:
2740
+ // fieldName: ReturnType
2741
+ // We need to replace it with: fieldName(arg1: Type1, arg2: Type2): ReturnType
2742
+
2743
+ // Escape propertyKey for regex
2744
+ const escapedKey = propertyKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2745
+ const escapedTypeName = nameFromStorage.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2746
+
2747
+ // Build the replacement string
2748
+ const argsString = argDefs.join(', ');
2749
+
2750
+ // Debug: Log what we're looking for
2751
+ console.log(`[ArcheType] Adding arguments to ${nameFromStorage}.${propertyKey}: ${argsString}`);
2752
+
2753
+ // Try to find and replace the field definition
2754
+ // Look for the field within the type definition
2755
+ // Make it case-insensitive to handle different casing in GraphQL schema
2756
+ const typeStartPattern = new RegExp(`type\\s+${escapedTypeName}\\s*\\{`, 'i');
2757
+ let typeStartMatch = graphqlSchemaString.match(typeStartPattern);
2758
+
2759
+ // If exact match fails, try case-insensitive search for the type name
2760
+ if (!typeStartMatch) {
2761
+ // Try to find the type with any casing
2762
+ const caseInsensitivePattern = new RegExp(`type\\s+([^\\s{]+)\\s*\\{`, 'gi');
2763
+ const allTypes = [...graphqlSchemaString.matchAll(caseInsensitivePattern)];
2764
+ const matchingType = allTypes.find(match =>
2765
+ match[1]!.toLowerCase() === nameFromStorage.toLowerCase()
2766
+ );
2767
+ if (matchingType && matchingType.index !== undefined) {
2768
+ // Create a fake match object
2769
+ typeStartMatch = [matchingType[0], matchingType[1]] as RegExpMatchArray;
2770
+ typeStartMatch.index = matchingType.index;
2771
+ }
2772
+ }
2773
+
2774
+ if (typeStartMatch) {
2775
+ const typeStartIndex = typeStartMatch.index! + typeStartMatch[0].length;
2776
+ // Find the closing brace of this type
2777
+ let braceCount = 1;
2778
+ let typeEndIndex = typeStartIndex;
2779
+ for (let i = typeStartIndex; i < graphqlSchemaString.length && braceCount > 0; i++) {
2780
+ if (graphqlSchemaString[i] === '{') braceCount++;
2781
+ if (graphqlSchemaString[i] === '}') braceCount--;
2782
+ if (braceCount === 0) {
2783
+ typeEndIndex = i;
2784
+ break;
2785
+ }
2786
+ }
2787
+
2788
+ // Extract the type definition
2789
+ const typeDefinition = graphqlSchemaString.substring(typeStartIndex, typeEndIndex);
2790
+
2791
+ // Debug: Log the type definition snippet
2792
+ console.log(`[ArcheType] Type definition for ${nameFromStorage}:`, typeDefinition.substring(0, 200));
2793
+
2794
+ // Find the field within this type definition
2795
+ // Pattern: fieldName: ReturnType or fieldName?: ReturnType
2796
+ const fieldPattern = new RegExp(
2797
+ `(\\n\\s+)(${escapedKey}\\??\\s*:\\s*)([^\\n]+)`,
2798
+ 'g'
2799
+ );
2800
+
2801
+ const fieldMatch = fieldPattern.exec(typeDefinition);
2802
+ if (fieldMatch) {
2803
+ const returnType = fieldMatch[3]!.trim();
2804
+ const indent = fieldMatch[1];
2805
+ const replacement = `${indent}${propertyKey}(${argsString}): ${returnType}`;
2806
+
2807
+ console.log(`[ArcheType] Found field match: "${fieldMatch[0]}" -> "${replacement}"`);
2808
+
2809
+ // Replace in the full schema string
2810
+ const fullMatchStart = typeStartIndex + fieldMatch.index!;
2811
+ const fullMatchEnd = fullMatchStart + fieldMatch[0].length;
2812
+ graphqlSchemaString =
2813
+ graphqlSchemaString.substring(0, fullMatchStart) +
2814
+ replacement +
2815
+ graphqlSchemaString.substring(fullMatchEnd);
2816
+
2817
+ console.log(`[ArcheType] Replacement successful for ${nameFromStorage}.${propertyKey}`);
2818
+ } else {
2819
+ console.warn(`[ArcheType] Field pattern not found in type definition. Looking for: ${escapedKey}`);
2820
+ // Fallback: simple replace anywhere
2821
+ const simplePattern = new RegExp(
2822
+ `(${escapedKey}\\??\\s*:\\s*)([^\\n]+)`,
2823
+ 'g'
2824
+ );
2825
+ const beforeReplace = graphqlSchemaString;
2826
+ graphqlSchemaString = graphqlSchemaString.replace(
2827
+ simplePattern,
2828
+ (match, fieldDef, returnType) => {
2829
+ console.log(`[ArcheType] Fallback replacement: "${match}" -> "${propertyKey}(${argsString}): ${returnType.trim()}"`);
2830
+ return `${propertyKey}(${argsString}): ${returnType.trim()}`;
2831
+ }
2832
+ );
2833
+ if (beforeReplace === graphqlSchemaString) {
2834
+ console.warn(`[ArcheType] Fallback replacement also failed for ${nameFromStorage}.${propertyKey}`);
2835
+ }
2836
+ }
2837
+ } else {
2838
+ console.warn(`[ArcheType] Type pattern not found for ${nameFromStorage}. Schema snippet:`, graphqlSchemaString.substring(0, 300));
2839
+ // Fallback: simple replace anywhere if type pattern not found
2840
+ const simplePattern = new RegExp(
2841
+ `(${escapedKey}\\??\\s*:\\s*)([^\\n]+)`,
2842
+ 'g'
2843
+ );
2844
+ const beforeReplace = graphqlSchemaString;
2845
+ graphqlSchemaString = graphqlSchemaString.replace(
2846
+ simplePattern,
2847
+ (match, fieldDef, returnType) => {
2848
+ console.log(`[ArcheType] Final fallback replacement: "${match}" -> "${propertyKey}(${argsString}): ${returnType.trim()}"`);
2849
+ return `${propertyKey}(${argsString}): ${returnType.trim()}`;
2850
+ }
2851
+ );
2852
+ if (beforeReplace === graphqlSchemaString) {
2853
+ console.warn(`[ArcheType] All replacement attempts failed for ${nameFromStorage}.${propertyKey}`);
2854
+ }
2855
+ }
2856
+ }
2857
+
2858
+ // Replace String return type with actual GraphQL type if specified
2859
+ if (options?.returnType && !['string', 'number', 'boolean'].includes(options.returnType)) {
2860
+ // Find the field in the schema
2861
+ const fieldIndex = graphqlSchemaString.indexOf(` ${propertyKey}`);
2862
+ if (fieldIndex !== -1) {
2863
+ // Extract the line containing this field
2864
+ const lineStart = fieldIndex;
2865
+ const lineEnd = graphqlSchemaString.indexOf('\n', fieldIndex);
2866
+ const fieldLine = graphqlSchemaString.substring(lineStart, lineEnd !== -1 ? lineEnd : graphqlSchemaString.length);
2867
+
2868
+ // Replace String with the actual return type in this line
2869
+ const updatedLine = fieldLine.replace(/:\s*String(\??)(\s*)$/, `: ${options.returnType}$1$2`);
2870
+
2871
+ if (updatedLine !== fieldLine) {
2872
+ // Replace the line in the full schema
2873
+ graphqlSchemaString = graphqlSchemaString.substring(0, lineStart) +
2874
+ updatedLine +
2875
+ graphqlSchemaString.substring(lineEnd !== -1 ? lineEnd : graphqlSchemaString.length);
2876
+ }
2877
+ }
2878
+ }
2879
+ }
2880
+ }
2881
+
2882
+ // Debug: Log schema if it contains function arguments
2883
+ if (!excludeFunctions && this.functions.some(f => f.options?.args && f.options.args.length > 0)) {
2884
+ // console.log(`[ArcheType] Final schema for ${nameFromStorage} with function args:`, graphqlSchemaString);
2885
+ }
2886
+
1118
2887
  // Cache the schema for this archetype
1119
- archetypeSchemaCache.set(nameFromStorage, {
2888
+ const cacheKey = `${nameFromStorage}_${excludeRelations}_${excludeFunctions}`;
2889
+ archetypeSchemaCache.set(cacheKey, {
1120
2890
  zodSchema: r,
1121
- graphqlSchema: graphqlSchemaString
2891
+ graphqlSchema: graphqlSchemaString,
1122
2892
  });
1123
-
2893
+
1124
2894
  // Store for unified weaving
1125
2895
  allArchetypeZodObjects.set(nameFromStorage, r);
1126
-
2896
+
1127
2897
  return r;
1128
2898
  }
2899
+
2900
+ /**
2901
+ * Get a Zod schema suitable for GraphQL input types (excludes relations and functions)
2902
+ */
2903
+ public getInputSchema(): ZodObject<any> {
2904
+ return this.getZodObjectSchema({ excludeRelations: true, excludeFunctions: true });
2905
+ }
2906
+
2907
+ /**
2908
+ * Apply validations to specific fields in the input schema
2909
+ * @param validations - Object mapping field paths to Zod schemas or refinement functions
2910
+ * @returns Modified Zod schema with validations applied
2911
+ *
2912
+ * @example
2913
+ * archetype.withValidation({
2914
+ * name: z.string().min(3),
2915
+ * 'info.label': z.string().min(3)
2916
+ * })
2917
+ */
2918
+ public withValidation(validations: Record<string, any>): ZodObject<any> {
2919
+ const baseSchema = this.getInputSchema();
2920
+ const shape = { ...baseSchema.shape };
2921
+
2922
+ for (const [path, validation] of Object.entries(validations)) {
2923
+ if (path.includes('.')) {
2924
+ // Handle nested fields like 'info.label'
2925
+ const [field, ...nestedPath] = path.split('.');
2926
+
2927
+ if (shape[field!]) {
2928
+ const currentField = shape[field!];
2929
+
2930
+ // Check if it's an optional field and unwrap it
2931
+ const isOptional = currentField._def?.typeName === 'ZodOptional';
2932
+ const innerSchema = isOptional ? currentField.unwrap() : currentField;
2933
+
2934
+ // Check if it's a ZodObject - handle both typeName and type property
2935
+ const isZodObject = innerSchema._def?.typeName === 'ZodObject' ||
2936
+ innerSchema._def?.type === 'object' ||
2937
+ innerSchema.type === 'object';
2938
+
2939
+ if (isZodObject && innerSchema.shape) {
2940
+ // Deep clone the nested shape to avoid mutations
2941
+ const nestedShape = { ...innerSchema.shape };
2942
+
2943
+ // Apply validation to the nested field
2944
+ if (nestedPath.length === 1 && nestedShape[nestedPath[0]!]) {
2945
+ nestedShape[nestedPath[0]!] = validation;
2946
+ } else if (nestedPath.length > 1) {
2947
+ // Handle deeper nesting (e.g., 'info.data.label')
2948
+ let current = nestedShape;
2949
+ for (let i = 0; i < nestedPath.length - 1; i++) {
2950
+ const key = nestedPath[i]!;
2951
+ if (current[key] && current[key]._def?.typeName === 'ZodObject') {
2952
+ current[key] = { ...current[key].shape };
2953
+ current = current[key];
2954
+ }
2955
+ }
2956
+ const lastKey = nestedPath[nestedPath.length - 1]!;
2957
+ if (current[lastKey]) {
2958
+ current[lastKey] = validation;
2959
+ }
2960
+ }
2961
+
2962
+ const newNestedSchema = z.object(nestedShape);
2963
+ // Preserve the optionality of the parent object
2964
+ shape[field!] = isOptional ? newNestedSchema.optional() : newNestedSchema;
2965
+ }
2966
+ }
2967
+ } else {
2968
+ // Handle top-level fields - directly replace with the validation
2969
+ shape[path] = validation;
2970
+ }
2971
+ }
2972
+
2973
+ return z.object(shape);
2974
+ }
2975
+
2976
+ public getFilterSchema(): ZodObject<any> {
2977
+ const baseSchema = this.getZodObjectSchema({ excludeRelations: true, excludeFunctions: true });
2978
+ const filterShape: Record<string, any> = {};
2979
+ for (const key of Object.keys(baseSchema.shape)) {
2980
+ // Only include fields that are explicitly set to filterable: true
2981
+ const isFilterable = this.fieldOptions[key]?.filterable === true || this.unionOptions[key]?.filterable === true;
2982
+ if (isFilterable) {
2983
+ filterShape[key] = InputFilterSchema.optional();
2984
+ }
2985
+ }
2986
+ const filterSchema = z.object(filterShape);
2987
+ return filterSchema;
2988
+ }
2989
+
2990
+ public buildFilterBranches(filter?: FilterSchema<any>): any[] {
2991
+ if (!filter) return [];
2992
+ const branches = [];
2993
+
2994
+ for (const [fieldName, componentCtor] of Object.entries(this.componentMap)) {
2995
+ const fieldOption = this.fieldOptions[fieldName];
2996
+ if (fieldOption?.filterable && filter[fieldName]?.value) {
2997
+ const filterPart = filter[fieldName];
2998
+ const defaultField = this.getDefaultFilterField(componentCtor);
2999
+ const operator = filterPart.op ? Query.filterOp[(filterPart.op.toUpperCase() as keyof typeof Query.filterOp)] : Query.filterOp.LIKE;
3000
+
3001
+ branches.push({
3002
+ component: componentCtor,
3003
+ filters: [
3004
+ {
3005
+ field: filterPart.field || defaultField,
3006
+ operator,
3007
+ value: operator === Query.filterOp.LIKE ? `%${filterPart.value}%` : filterPart.value,
3008
+ },
3009
+ ],
3010
+ });
3011
+ }
3012
+ }
3013
+
3014
+ return branches;
3015
+ }
3016
+
3017
+ private getDefaultFilterField(componentCtor: any): string {
3018
+ const storage = getMetadataStorage();
3019
+ const typeId = storage.getComponentId(componentCtor.name);
3020
+ const props = storage.getComponentProperties(typeId);
3021
+ const hasValue = props.some(p => p.propertyKey === 'value');
3022
+ const hasLabel = props.some(p => p.propertyKey === 'label');
3023
+ return hasValue ? 'value' : hasLabel ? 'label' : props[0]?.propertyKey || 'value';
3024
+ }
1129
3025
  }
1130
3026
 
1131
- export type InferArcheType<T extends BaseArcheType> = {
1132
- [K in keyof T['componentMap']]: T['componentMap'][K] extends new (...args: any[]) => infer C ? C : never
1133
- };
1134
3027
 
1135
- // Alternative: Infer from the actual instance properties (recommended)
1136
- export type InferArcheTypeFromInstance<T extends BaseArcheType> = {
1137
- [K in keyof T as T[K] extends BaseComponent ? K : never]: T[K]
1138
- };
1139
3028
 
1140
- export default BaseArcheType;
3029
+
3030
+
3031
+ export default BaseArcheType;