bunsane 0.1.5 → 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 +869 -122
  19. package/core/ArcheType.ts +2264 -394
  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,57 +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
-
83
- // Collect all unique Zod objects to avoid duplicates
84
- const allZodObjects = new Map<string, any>();
85
-
86
- function collectZodObjects(schema: any) {
87
- if (schema && typeof schema === 'object' && schema._def?.typeName === 'ZodObject') {
88
- const shape = schema._def.shape();
89
- const typename = shape.__typename?._def?.value;
90
- if (typename && !allZodObjects.has(typename)) {
91
- allZodObjects.set(typename, schema);
209
+
210
+ // Combine both archetype and component schemas for weaving
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);
92
296
  }
93
- for (const [key, value] of Object.entries(shape)) {
94
- if (value instanceof ZodObject) {
95
- collectZodObjects(value);
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
+ }
96
380
  }
97
381
  }
98
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;
99
392
  }
100
-
101
- for (const schema of [...archetypeSchemas, ...componentSchemas]) {
102
- collectZodObjects(schema);
103
- }
104
-
105
- const allSchemas = Array.from(allZodObjects.values());
106
-
107
- const schema = weave(ZodWeaver, ...allSchemas);
108
- let schemaString = printSchema(schema);
109
-
110
- // Post-process: Replace 'id: String' with 'id: ID' for all id fields
111
- schemaString = schemaString.replace(/\bid:\s*String\b/g, 'id: ID');
112
-
113
- return schemaString;
114
393
  }
115
394
 
116
395
  // Generate Zod schema for a component and cache it
117
- 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 {
118
401
  // Check cache first
119
402
  if (componentSchemaCache.has(componentId)) {
120
403
  return componentSchemaCache.get(componentId)!;
121
404
  }
122
-
405
+
123
406
  const storage = getMetadataStorage();
124
407
  const props = storage.getComponentProperties(componentId);
125
-
408
+
126
409
  // Return null if no properties - caller should skip this component
127
410
  if (props.length === 0) {
128
411
  return null;
129
412
  }
130
-
413
+
131
414
  const zodFields: Record<string, any> = {
132
- __typename: z.literal(compNameToFieldName(componentCtor.name))
415
+ __typename: z
416
+ .literal(compNameToFieldName(componentCtor.name))
417
+ .nullish(),
133
418
  };
134
419
 
135
420
  for (const prop of props) {
@@ -148,47 +433,120 @@ function getOrCreateComponentSchema(componentCtor: new (...args: any[]) => BaseC
148
433
  zodFields[prop.propertyKey] = z.date();
149
434
  break;
150
435
  default:
151
- 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();
152
442
  }
153
443
  } else if (prop.isEnum && prop.enumValues && prop.enumKeys) {
154
- const enumTypeName = prop.propertyType?.name || `${componentCtor.name}_${prop.propertyKey}_Enum`;
155
- zodFields[prop.propertyKey] = z.enum(prop.enumValues as any).register(asEnumType, {
156
- name: enumTypeName,
157
- valuesConfig: prop.enumKeys.reduce((acc: Record<string, { description: string }>, key, idx) => {
158
- acc[key] = { description: prop.enumValues![idx]! };
159
- return acc;
160
- }, {})
161
- });
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
+ }
162
478
  } else if (customTypeRegistry.has(prop.propertyType)) {
163
- 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
+ }
164
506
  } else {
165
- 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
+ }
166
513
  }
167
-
514
+
168
515
  if (fieldOptions?.nullable) {
169
516
  zodFields[prop.propertyKey] = zodFields[prop.propertyKey].nullish();
170
517
  }
171
518
  }
172
-
519
+
173
520
  const componentSchema = z.object(zodFields);
174
-
521
+
175
522
  // Cache the component schema for reuse
176
523
  componentSchemaCache.set(componentId, componentSchema);
177
-
524
+
178
525
  return componentSchema;
179
526
  }
180
527
 
181
528
  function compNameToFieldName(compName: string): string {
182
- 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
+ );
183
533
  }
184
534
 
185
535
  /**
186
536
  * Helper to determine if a component should be unwrapped to a scalar value.
187
537
  * Returns true if the component has a single 'value' property and the field type is primitive.
188
538
  */
189
- function shouldUnwrapComponent(componentProps: ComponentPropertyMetadata[], fieldType: any): boolean {
539
+ function shouldUnwrapComponent(
540
+ componentProps: ComponentPropertyMetadata[],
541
+ fieldType: any
542
+ ): boolean {
190
543
  // If field type is a primitive, unwrap the component to that primitive
191
- if (fieldType === String || fieldType === Number || fieldType === Boolean || fieldType === Date) {
544
+ if (
545
+ fieldType === String ||
546
+ fieldType === Number ||
547
+ fieldType === Boolean ||
548
+ fieldType === Date
549
+ ) {
192
550
  return true;
193
551
  }
194
552
  return false;
@@ -221,20 +579,21 @@ export interface BelongsToManyOptions extends RelationOptions {
221
579
  through: string; // Required for many-to-many
222
580
  }
223
581
 
224
- // TODO: Implement archetype with GraphQL support
225
- export function ArcheType<T extends new () => BaseArcheType>(nameOrOptions?: string | ArcheTypeOptions) {
226
- return function(target: T): T {
582
+ export function ArcheType<T extends new () => BaseArcheType>(
583
+ nameOrOptions?: string | ArcheTypeOptions
584
+ ) {
585
+ return function (target: T): T {
227
586
  const storage = getMetadataStorage();
228
587
  const typeId = storage.getComponentId(target.name);
229
-
588
+
230
589
  let archetype_name = target.name;
231
-
232
- if (typeof nameOrOptions === 'string') {
590
+
591
+ if (typeof nameOrOptions === "string") {
233
592
  archetype_name = nameOrOptions;
234
593
  } else if (nameOrOptions) {
235
594
  archetype_name = nameOrOptions.name || target.name;
236
595
  }
237
-
596
+
238
597
  storage.collectArcheTypeMetadata({
239
598
  name: archetype_name,
240
599
  typeId: typeId,
@@ -244,34 +603,81 @@ export function ArcheType<T extends new () => BaseArcheType>(nameOrOptions?: str
244
603
  const prototype = target.prototype;
245
604
  const fields = prototype[archetypeFieldsSymbol];
246
605
  if (fields) {
247
- for (const {propertyKey, component, options} of fields) {
248
- const type = Reflect.getMetadata('design:type', target.prototype, propertyKey);
249
- 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
+ );
250
619
  }
251
620
  }
252
621
 
253
622
  const unions = prototype[archetypeUnionFieldsSymbol];
254
- if(unions) {
255
- for(const {propertyKey, components, options} of unions) {
256
- 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
+ );
257
632
  }
258
633
  }
259
634
 
260
635
  // Process relations
261
636
  const relations = prototype[archetypeRelationsSymbol];
262
637
  if (relations) {
263
- for (const {propertyKey, relatedArcheType, relationType, options} of relations) {
264
- const type = Reflect.getMetadata('design:type', target.prototype, propertyKey);
265
- 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
+ );
266
657
  }
267
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
+
268
671
  return target;
269
672
  };
270
673
  }
271
674
 
272
- const archetypeFieldsSymbol = Symbol("archetypeFields");
273
- export function ArcheTypeField<T extends BaseComponent>(component: new (...args: any[]) => T, options?: ArcheTypeFieldOptions) {
274
- 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) {
275
681
  if (!target[archetypeFieldsSymbol]) {
276
682
  target[archetypeFieldsSymbol] = [];
277
683
  }
@@ -279,24 +685,30 @@ export function ArcheTypeField<T extends BaseComponent>(component: new (...args:
279
685
  };
280
686
  }
281
687
 
282
- const archetypeUnionFieldsSymbol = Symbol("archetypeUnionFields");
283
- export function ArcheTypeUnionField(components: (new (...args:any[]) => any)[], options?: ArcheTypeFieldOptions) {
284
- return function(target: any, propertyKey: string) {
285
- 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]) {
286
695
  target[archetypeUnionFieldsSymbol] = [];
287
696
  }
288
- target[archetypeUnionFieldsSymbol].push({propertyKey, components, options});
289
- }
697
+ target[archetypeUnionFieldsSymbol].push({
698
+ propertyKey,
699
+ components,
700
+ options,
701
+ });
702
+ };
290
703
  }
291
704
 
292
- const archetypeRelationsSymbol = Symbol("archetypeRelations");
705
+ const archetypeRelationsSymbol = Symbol.for("bunsane:archetypeRelations");
293
706
 
294
- function createRelationDecorator(relationType: 'hasMany' | 'belongsTo' | 'hasOne' | 'belongsToMany') {
295
- return function(
296
- relatedArcheType: string,
297
- options?: RelationOptions
298
- ) {
299
- 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) {
300
712
  if (!target[archetypeRelationsSymbol]) {
301
713
  target[archetypeRelationsSymbol] = [];
302
714
  }
@@ -304,16 +716,16 @@ function createRelationDecorator(relationType: 'hasMany' | 'belongsTo' | 'hasOne
304
716
  propertyKey,
305
717
  relatedArcheType,
306
718
  relationType,
307
- options
719
+ options,
308
720
  });
309
721
  };
310
722
  };
311
723
  }
312
724
 
313
- export const HasMany = createRelationDecorator('hasMany');
314
- export const BelongsTo = createRelationDecorator('belongsTo');
315
- export const HasOne = createRelationDecorator('hasOne');
316
- 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");
317
729
 
318
730
  // Keep ArcheTypeRelation as alias for backwards compatibility
319
731
  export const ArcheTypeRelation = HasMany;
@@ -322,24 +734,213 @@ export type ArcheTypeResolver = {
322
734
  resolver?: string;
323
735
  component?: new (...args: any[]) => BaseComponent;
324
736
  field?: string;
325
- filter?: {[key: string]: any};
326
- }
737
+ filter?: { [key: string]: any };
738
+ };
327
739
 
328
740
  export type ArcheTypeCreateInfo = {
329
741
  name: string;
330
742
  components: Array<new (...args: any[]) => BaseComponent>;
331
743
  };
332
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
+
333
926
  export class BaseArcheType {
334
- protected components: Set<{ ctor: new (...args: any[]) => BaseComponent, data: any }> = new Set();
335
- 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> = {};
336
932
  protected fieldOptions: Record<string, ArcheTypeFieldOptions> = {};
337
933
  protected fieldTypes: Record<string, any> = {};
338
934
  public relationMap: Record<string, typeof BaseArcheType | string> = {};
339
935
  protected relationOptions: Record<string, RelationOptions> = {};
340
- protected relationTypes: Record<string, 'hasMany' | 'belongsTo' | 'hasOne' | 'belongsToMany'> = {};
341
- 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
+ {};
342
942
  protected unionOptions: Record<string, ArcheTypeFieldOptions> = {};
943
+ public functions: Array<{ propertyKey: string; options?: { returnType?: string, args?: [{name: string, type: any, nullable: boolean}] } }> = [];
343
944
 
344
945
  public resolver?: {
345
946
  fields: Record<string, ArcheTypeResolver>;
@@ -348,14 +949,18 @@ export class BaseArcheType {
348
949
  constructor() {
349
950
  const storage = getMetadataStorage();
350
951
  const archetypeId = storage.getComponentId(this.constructor.name);
351
-
952
+
352
953
  // Look up the custom name from metadata (e.g., from @ArcheType("CustomName"))
353
- const archetypeMetadata = storage.archetypes.find(a => a.typeId === archetypeId);
354
- const archetypeName = archetypeMetadata?.name || this.constructor.name.replace(/ArcheType$/, '');
355
-
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
+
356
961
  const fields = storage.archetypes_field_map.get(archetypeName);
357
962
  if (fields) {
358
- for (const {fieldName, component, options, type} of fields) {
963
+ for (const { fieldName, component, options, type } of fields) {
359
964
  this.componentMap[fieldName] = component;
360
965
  if (options) this.fieldOptions[fieldName] = options;
361
966
  if (type) this.fieldTypes[fieldName] = type;
@@ -363,8 +968,8 @@ export class BaseArcheType {
363
968
  }
364
969
 
365
970
  const unions = storage.archetypes_union_map.get(archetypeName);
366
- if(unions) {
367
- for(const {fieldName, components, options, type} of unions) {
971
+ if (unions) {
972
+ for (const { fieldName, components, options, type } of unions) {
368
973
  this.unionMap[fieldName] = components;
369
974
  if (options) this.unionOptions[fieldName] = options;
370
975
  }
@@ -373,12 +978,21 @@ export class BaseArcheType {
373
978
  // Process relations
374
979
  const relations = storage.archetypes_relations_map.get(archetypeName);
375
980
  if (relations) {
376
- 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) {
377
988
  this.relationMap[fieldName] = relatedArcheType as any;
378
989
  this.relationTypes[fieldName] = relationType;
379
990
  if (options) this.relationOptions[fieldName] = options;
380
991
  }
381
992
  }
993
+
994
+ // Collect archetype functions
995
+ this.functions = this.constructor.prototype[archetypeFunctionsSymbol] || [];
382
996
  }
383
997
 
384
998
  // constructor(components: Array<new (...args: any[]) => BaseComponent>) {
@@ -387,11 +1001,13 @@ export class BaseArcheType {
387
1001
  // }
388
1002
  // }
389
1003
 
390
- 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 {
391
1008
  return { component, field: field as string };
392
1009
  }
393
1010
 
394
-
395
1011
  static Create(info: ArcheTypeCreateInfo): BaseArcheType {
396
1012
  const archetype = new BaseArcheType();
397
1013
  archetype.components = new Set();
@@ -400,26 +1016,28 @@ export class BaseArcheType {
400
1016
  }
401
1017
  return archetype;
402
1018
  }
403
-
404
-
405
- 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
+ ) {
406
1024
  this.componentMap[compNameToFieldName(ctor.name)] = ctor;
407
1025
  this.components.add({ ctor, data });
408
1026
  }
409
1027
 
410
-
411
1028
  // TODO: Can we make this type-safe?
412
1029
  public fill(input: object, strict: boolean = false): this {
413
1030
  const storage = getMetadataStorage();
414
-
1031
+
415
1032
  for (const [key, value] of Object.entries(input)) {
416
1033
  if (value !== undefined) {
417
1034
  const compCtor = this.componentMap[key];
418
1035
  if (compCtor) {
419
1036
  const fieldType = this.fieldTypes[key];
420
1037
  const typeId = storage.getComponentId(compCtor.name);
421
- const componentProps = storage.getComponentProperties(typeId);
422
-
1038
+ const componentProps =
1039
+ storage.getComponentProperties(typeId);
1040
+
423
1041
  // Check if this is a primitive field that should be unwrapped
424
1042
  if (shouldUnwrapComponent(componentProps, fieldType)) {
425
1043
  // For primitive types, wrap in { value }
@@ -431,12 +1049,18 @@ export class BaseArcheType {
431
1049
  } else if (this.unionMap[key]) {
432
1050
  // Handle union fields
433
1051
  const unionComponents = this.unionMap[key];
434
- const selectedComponent = this.determineUnionComponent(value, unionComponents, storage);
435
-
1052
+ const selectedComponent = this.determineUnionComponent(
1053
+ value,
1054
+ unionComponents,
1055
+ storage
1056
+ );
1057
+
436
1058
  if (selectedComponent) {
437
1059
  this.addComponent(selectedComponent, value as any);
438
1060
  } else if (strict) {
439
- 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
+ );
440
1064
  }
441
1065
  } else {
442
1066
  // direct property
@@ -445,12 +1069,14 @@ export class BaseArcheType {
445
1069
  }
446
1070
  }
447
1071
  for (const [field, ctor] of Object.entries(this.componentMap)) {
448
- 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
+ );
449
1075
  if (!alreadyAdded) {
450
1076
  this.addComponent(ctor, {} as any);
451
1077
  }
452
1078
  }
453
-
1079
+
454
1080
  return this;
455
1081
  }
456
1082
 
@@ -461,9 +1087,13 @@ export class BaseArcheType {
461
1087
  * @param storage Metadata storage
462
1088
  * @returns The selected component constructor, or null if none match
463
1089
  */
464
- 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 {
465
1095
  // If value has __typename, use it to determine the component
466
- if (value && typeof value === 'object' && value.__typename) {
1096
+ if (value && typeof value === "object" && value.__typename) {
467
1097
  const expectedTypeName = value.__typename;
468
1098
  for (const component of unionComponents) {
469
1099
  const componentTypeName = compNameToFieldName(component.name);
@@ -472,39 +1102,42 @@ export class BaseArcheType {
472
1102
  }
473
1103
  }
474
1104
  }
475
-
1105
+
476
1106
  // Fallback: Try to infer based on property presence
477
- if (value && typeof value === 'object') {
1107
+ if (value && typeof value === "object") {
478
1108
  for (const component of unionComponents) {
479
1109
  const typeId = storage.getComponentId(component.name);
480
1110
  const componentProps = storage.getComponentProperties(typeId);
481
-
1111
+
482
1112
  // Check if any properties of this component are present in the value
483
- const hasMatchingProps = componentProps.some((prop: ComponentPropertyMetadata) =>
484
- value.hasOwnProperty(prop.propertyKey)
1113
+ const hasMatchingProps = componentProps.some(
1114
+ (prop: ComponentPropertyMetadata) =>
1115
+ value.hasOwnProperty(prop.propertyKey)
485
1116
  );
486
-
1117
+
487
1118
  if (hasMatchingProps) {
488
1119
  return component;
489
1120
  }
490
1121
  }
491
1122
  }
492
-
1123
+
493
1124
  // If no component matches, return the first one as default
494
1125
  return unionComponents[0] || null;
495
- } async updateEntity<T>(entity: Entity, updates: Partial<T>) {
1126
+ }
1127
+ async updateEntity<T>(entity: Entity, updates: Partial<T>) {
496
1128
  const storage = getMetadataStorage();
497
-
1129
+
498
1130
  for (const key of Object.keys(updates)) {
499
- if(key === 'id' || key === '_id') continue;
1131
+ if (key === "id" || key === "_id") continue;
500
1132
  const value = updates[key as keyof T];
501
1133
  if (value !== undefined) {
502
1134
  const compCtor = this.componentMap[key];
503
1135
  if (compCtor) {
504
1136
  const fieldType = this.fieldTypes[key];
505
1137
  const typeId = storage.getComponentId(compCtor.name);
506
- const componentProps = storage.getComponentProperties(typeId);
507
-
1138
+ const componentProps =
1139
+ storage.getComponentProperties(typeId);
1140
+
508
1141
  // Check if this is a primitive field that should be unwrapped
509
1142
  if (shouldUnwrapComponent(componentProps, fieldType)) {
510
1143
  // For primitive types, wrap in { value }
@@ -516,8 +1149,12 @@ export class BaseArcheType {
516
1149
  } else if (this.unionMap[key]) {
517
1150
  // Handle union fields
518
1151
  const unionComponents = this.unionMap[key];
519
- const selectedComponent = this.determineUnionComponent(value, unionComponents, storage);
520
-
1152
+ const selectedComponent = this.determineUnionComponent(
1153
+ value,
1154
+ unionComponents,
1155
+ storage
1156
+ );
1157
+
521
1158
  if (selectedComponent) {
522
1159
  await entity.set(selectedComponent, value as any);
523
1160
  }
@@ -527,6 +1164,7 @@ export class BaseArcheType {
527
1164
  }
528
1165
  }
529
1166
  }
1167
+ return entity;
530
1168
  }
531
1169
 
532
1170
  /**
@@ -552,14 +1190,296 @@ export class BaseArcheType {
552
1190
  }
553
1191
 
554
1192
  /**
555
- * Unwraps an entity into a plain object containing the component data.
556
- * @param entity The entity to unwrap
557
- * @param exclude An optional array of field names to exclude from the result (e.g., sensitive data like passwords)
558
- * @returns A promise that resolves to an object with component data
559
- */
560
- public async Unwrap(entity: Entity, exclude: string[] = []): Promise<Record<string, any>> {
561
- const result: any = { id: entity.id };
562
-
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
+ }
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
+
563
1483
  // Handle regular components
564
1484
  for (const [field, ctor] of Object.entries(this.componentMap)) {
565
1485
  if (exclude.includes(field)) continue;
@@ -568,7 +1488,7 @@ export class BaseArcheType {
568
1488
  result[field] = (comp as any).value;
569
1489
  }
570
1490
  }
571
-
1491
+
572
1492
  // Handle union fields
573
1493
  for (const [field, components] of Object.entries(this.unionMap)) {
574
1494
  if (exclude.includes(field)) continue;
@@ -577,13 +1497,13 @@ export class BaseArcheType {
577
1497
  if (comp) {
578
1498
  result[field] = {
579
1499
  __typename: compNameToFieldName(component.name),
580
- ...(comp as any)
1500
+ ...(comp as any),
581
1501
  };
582
1502
  break; // Only take the first matching component
583
1503
  }
584
1504
  }
585
1505
  }
586
-
1506
+
587
1507
  // for direct fields
588
1508
  for (const field of Object.keys(this.fieldTypes)) {
589
1509
  if (exclude.includes(field)) continue;
@@ -598,16 +1518,19 @@ export class BaseArcheType {
598
1518
  * Gets the property metadata for all components in this archetype.
599
1519
  * @returns A record mapping field names to their component property metadata arrays
600
1520
  */
601
- public getComponentProperties(): Record<string, ComponentPropertyMetadata[]> {
1521
+ public getComponentProperties(): Record<
1522
+ string,
1523
+ ComponentPropertyMetadata[]
1524
+ > {
602
1525
  const storage = getMetadataStorage();
603
1526
  const result: Record<string, ComponentPropertyMetadata[]> = {};
604
-
1527
+
605
1528
  // Regular components
606
1529
  for (const [field, ctor] of Object.entries(this.componentMap)) {
607
1530
  const typeId = storage.getComponentId(ctor.name);
608
1531
  result[field] = storage.getComponentProperties(typeId);
609
1532
  }
610
-
1533
+
611
1534
  // Union components (for each union field, include properties of all components)
612
1535
  for (const [field, components] of Object.entries(this.unionMap)) {
613
1536
  const allProps: ComponentPropertyMetadata[] = [];
@@ -617,16 +1540,38 @@ export class BaseArcheType {
617
1540
  }
618
1541
  result[field] = allProps;
619
1542
  }
620
-
1543
+
621
1544
  return result;
622
1545
  }
623
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
+
624
1569
  /**
625
1570
  * Generates GraphQL field resolver functions for this archetype.
626
1571
  * These resolvers handle both simple fields and component-based fields with DataLoader support.
627
- *
1572
+ *
628
1573
  * @returns An array of resolver metadata that can be registered with GraphQL
629
- *
1574
+ *
630
1575
  * @example
631
1576
  * const resolvers = serviceAreaArcheType.generateFieldResolvers();
632
1577
  * // Returns array of: { typeName, fieldName, resolver }
@@ -639,15 +1584,17 @@ export class BaseArcheType {
639
1584
  const storage = getMetadataStorage();
640
1585
  const resolvers: Array<any> = [];
641
1586
  const archetypeId = storage.getComponentId(this.constructor.name);
642
- 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;
643
1590
 
644
1591
  // Generate ID resolver for the main archetype type
645
1592
  resolvers.push({
646
1593
  typeName: archetypeName,
647
- fieldName: 'id',
648
- resolver: (parent: Entity) => {
1594
+ fieldName: "id",
1595
+ resolver: (parent: any) => {
649
1596
  return parent.id;
650
- }
1597
+ },
651
1598
  });
652
1599
 
653
1600
  // Generate resolvers for each component field
@@ -656,74 +1603,121 @@ export class BaseArcheType {
656
1603
  const typeIdHex = typeId;
657
1604
  const componentName = ctor.name;
658
1605
  const fieldType = this.fieldTypes[field];
659
-
1606
+
660
1607
  // Skip components with no properties (like tag components)
661
1608
  const componentProps = storage.getComponentProperties(typeId);
662
1609
  if (componentProps.length === 0) {
663
1610
  continue;
664
1611
  }
665
-
1612
+
666
1613
  // Check if this component should be unwrapped to a scalar
667
- const isUnwrapped = shouldUnwrapComponent(componentProps, fieldType);
668
-
1614
+ const isUnwrapped = shouldUnwrapComponent(
1615
+ componentProps,
1616
+ fieldType
1617
+ );
1618
+
669
1619
  if (isUnwrapped) {
670
1620
  // For unwrapped components, resolve directly to the 'value' property
671
1621
  resolvers.push({
672
1622
  typeName: archetypeName,
673
1623
  fieldName: field,
674
- resolver: async (parent: Entity, args: any, context: any) => {
675
- const entity = parent;
676
-
677
- // Use DataLoader if available, but fall back when no row exists
678
- if (context.loaders) {
679
- const componentData = await context.loaders.componentsByEntityType.load({
680
- entityId: entity.id,
681
- typeId: typeIdHex // Pass hex string directly
682
- });
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
+ );
683
1653
  if (componentData?.data?.value !== undefined) {
684
1654
  return componentData.data.value;
685
1655
  }
686
1656
  }
687
1657
 
688
- // 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);
689
1660
  const comp = await entity.get(ctor);
690
1661
  return (comp as any)?.value;
691
- }
1662
+ },
692
1663
  });
693
1664
  } else {
694
1665
  // For complex components, return the full component object
695
1666
  resolvers.push({
696
1667
  typeName: archetypeName,
697
1668
  fieldName: field,
698
- resolver: async (parent: Entity, args: any, context: any) => {
699
- const entity = parent;
700
- if (!entity || !entity.id) return (parent as any)[field];
701
-
702
- // Use DataLoader if available, but fall back when no row exists
703
- if (context.loaders) {
704
- const componentData = await context.loaders.componentsByEntityType.load({
705
- entityId: entity.id,
706
- typeId: typeIdHex // Pass hex string directly
707
- });
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
+ );
708
1701
  if (componentData?.data) {
709
1702
  return componentData.data;
710
1703
  }
711
1704
  }
712
1705
 
713
- // 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);
714
1708
  const comp = await entity.get(ctor);
715
1709
  return comp;
716
- }
1710
+ },
717
1711
  });
718
1712
 
719
1713
  // Generate nested field resolvers for component properties
720
1714
  const componentTypeName = compNameToFieldName(componentName);
721
-
1715
+
722
1716
  for (const prop of componentProps) {
723
1717
  resolvers.push({
724
- typeName: componentTypeName, // Use lowercase component name
1718
+ typeName: componentTypeName, // Use lowercase component name
725
1719
  fieldName: prop.propertyKey,
726
- resolver: (parent: any) => parent[prop.propertyKey]
1720
+ resolver: (parent: any) => parent[prop.propertyKey],
727
1721
  });
728
1722
  }
729
1723
  }
@@ -734,171 +1728,537 @@ export class BaseArcheType {
734
1728
  resolvers.push({
735
1729
  typeName: archetypeName,
736
1730
  fieldName: field,
737
- resolver: async (parent: Entity, args: any, context: any) => {
738
- const entity = parent;
739
-
1731
+ resolver: async (parent: any, args: any, context: any) => {
1732
+ const entityId = parent?.id;
1733
+ if (!entityId) return null;
1734
+
740
1735
  // Try to find which component in the union is present on the entity
741
1736
  for (const component of components) {
742
1737
  const typeId = storage.getComponentId(component.name);
743
-
744
- if (context.loaders) {
745
- const componentData = await context.loaders.componentsByEntityType.load({
746
- entityId: entity.id,
747
- typeId: typeId
748
- });
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
+ );
749
1762
  if (componentData?.data) {
750
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) {
751
1776
  return {
752
1777
  __typename: compNameToFieldName(component.name),
753
- ...componentData.data
1778
+ ...(comp as any),
754
1779
  };
755
1780
  }
756
1781
  }
757
-
758
- // Fallback
759
- const comp = await entity.get(component);
760
- if (comp) {
761
- return {
762
- __typename: compNameToFieldName(component.name),
763
- ...(comp as any)
764
- };
765
- }
766
1782
  }
767
-
1783
+
768
1784
  return null;
769
- }
1785
+ },
770
1786
  });
771
1787
  }
772
1788
 
773
1789
  // Generate resolvers for relation fields
774
- for (const [field, relatedArcheType] of Object.entries(this.relationMap)) {
1790
+ for (const [field, relatedArcheType] of Object.entries(
1791
+ this.relationMap
1792
+ )) {
775
1793
  const relationType = this.relationTypes[field];
776
1794
  const relationOptions = this.relationOptions[field];
777
- const isArray = relationType === 'hasMany' || relationType === 'belongsToMany';
778
-
1795
+ const isArray =
1796
+ relationType === "hasMany" || relationType === "belongsToMany";
1797
+
779
1798
  // Get the related archetype name
780
1799
  let relatedTypeName: string;
781
- if (typeof relatedArcheType === 'string') {
1800
+ if (typeof relatedArcheType === "string") {
782
1801
  relatedTypeName = relatedArcheType;
783
1802
  } else {
784
- const relatedArchetypeId = storage.getComponentId(relatedArcheType.name);
785
- const relatedArchetypeMetadata = storage.archetypes.find(a => a.typeId === relatedArchetypeId);
786
- 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$/, "");
787
1812
  }
788
-
789
- if (!isArray && relationType === 'belongsTo' && relationOptions?.foreignKey) {
1813
+
1814
+ if (
1815
+ !isArray &&
1816
+ relationType === "belongsTo" &&
1817
+ relationOptions?.foreignKey
1818
+ ) {
790
1819
  resolvers.push({
791
1820
  typeName: archetypeName,
792
1821
  fieldName: field,
793
- resolver: async (parent: Entity, args: any, context: any) => {
794
- const entity = parent;
795
- 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) {
796
1829
  return null;
797
1830
  }
798
1831
 
799
1832
  let foreignId: string | undefined;
800
1833
 
801
1834
  // Attempt to load the component that holds the foreign key via DataLoader
802
- if (context.loaders) {
803
- for (const [componentField, compCtor] of Object.entries(this.componentMap)) {
804
- const typeIdForComponent = storage.getComponentId(compCtor.name);
805
- const componentProps = storage.getComponentProperties(typeIdForComponent);
806
- const hasForeignKey = componentProps.some(prop => prop.propertyKey === relationOptions.foreignKey);
807
- if (!hasForeignKey || !relationOptions.foreignKey) continue;
808
-
809
- const componentData = await context.loaders.componentsByEntityType.load({
810
- entityId: entity.id,
811
- typeId: typeIdForComponent
812
- });
813
-
814
- if (componentData?.data && componentData.data[relationOptions.foreignKey] !== undefined) {
815
- foreignId = componentData.data[relationOptions.foreignKey];
816
- 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
+ }
817
1868
  }
818
1869
  }
819
1870
  }
820
1871
 
821
1872
  // Fallback: pull the component from the entity directly when DataLoader misses
822
1873
  if (!foreignId) {
823
- for (const compCtor of Object.values(this.componentMap)) {
824
- const typeIdForComponent = storage.getComponentId(compCtor.name);
825
- const componentProps = storage.getComponentProperties(typeIdForComponent);
826
- const hasForeignKey = componentProps.some(prop => prop.propertyKey === relationOptions.foreignKey);
827
- if (!hasForeignKey || !relationOptions.foreignKey) continue;
828
- const componentInstance = await entity.get(compCtor as any);
829
- if (componentInstance && (componentInstance as any)[relationOptions.foreignKey] !== undefined) {
830
- foreignId = (componentInstance as any)[relationOptions.foreignKey];
831
- 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
+ }
832
1898
  }
833
1899
  }
834
1900
  }
835
1901
 
1902
+ if (!foreignId && relationOptions.foreignKey === 'id') {
1903
+ foreignId = entityId;
1904
+ }
1905
+
836
1906
  if (!foreignId) {
837
1907
  return null;
838
1908
  }
839
1909
 
840
1910
  // Resolve the related entity using loaders when possible, otherwise hit the database directly
841
1911
  if (context.loaders?.entityById) {
842
- const relatedEntity = await context.loaders.entityById.load(foreignId);
1912
+ const relatedEntity =
1913
+ await context.loaders.entityById.load(
1914
+ foreignId
1915
+ );
843
1916
  if (relatedEntity) {
844
1917
  return relatedEntity;
845
1918
  }
846
1919
  }
847
1920
 
848
1921
  return Entity.FindById(foreignId);
849
- }
1922
+ },
850
1923
  });
851
1924
  } else if (isArray) {
852
1925
  // Array relation resolver
853
1926
  resolvers.push({
854
1927
  typeName: archetypeName,
855
1928
  fieldName: field,
856
- resolver: async (parent: Entity, args: any, context: any) => {
857
- const entity = parent;
858
-
859
- // Use DataLoader for relation loading if available
860
- if (context.loaders && context.loaders.relationsByEntityField) {
861
- return context.loaders.relationsByEntityField.load({
862
- entityId: entity.id,
863
- relationField: field,
864
- relatedType: relatedTypeName,
865
- foreignKey: relationOptions?.foreignKey
866
- });
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 [];
867
1999
  }
868
-
869
- // Fallback: return empty array or implement custom relation query
870
- // This should be implemented based on your relation storage strategy
871
- console.warn(`No relationsByEntityField loader found for array relation ${field} on ${archetypeName}`);
872
- return [];
873
- }
2000
+ },
874
2001
  });
875
2002
  } else {
876
2003
  // Single relation resolver
877
2004
  resolvers.push({
878
2005
  typeName: archetypeName,
879
2006
  fieldName: field,
880
- resolver: async (parent: Entity, args: any, context: any) => {
881
- 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[] = [];
882
2167
 
883
- // Use DataLoader for relation loading if available
884
- if (context.loaders && context.loaders.relationsByEntityField) {
885
- const results = await context.loaders.relationsByEntityField.load({
886
- entityId: entity.id,
887
- relationField: field,
888
- relatedType: relatedTypeName,
889
- foreignKey: relationOptions?.foreignKey
890
- });
891
- if (results.length > 0) {
892
- 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
+ }
893
2249
  }
2250
+
2251
+ functionArgs.push(convertedValue);
894
2252
  }
895
2253
 
896
- // Fallback: return null or implement custom relation query
897
- console.warn(`No relationsByEntityField loader found for single relation ${field} on ${archetypeName}`);
898
- 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);
899
2259
  }
900
- });
901
- }
2260
+ },
2261
+ });
902
2262
  }
903
2263
 
904
2264
  return resolvers;
@@ -907,9 +2267,9 @@ export class BaseArcheType {
907
2267
  /**
908
2268
  * Registers all auto-generated field resolvers for this archetype with a service.
909
2269
  * This eliminates the need to manually write @GraphQLField decorators.
910
- *
2270
+ *
911
2271
  * @param service The service instance to attach resolvers to
912
- *
2272
+ *
913
2273
  * @example
914
2274
  * class AreaService extends BaseService {
915
2275
  * constructor(app: App) {
@@ -920,44 +2280,50 @@ export class BaseArcheType {
920
2280
  * }
921
2281
  */
922
2282
  public registerFieldResolvers(service: any): void {
2283
+ this.getZodObjectSchema(); // Ensure schema is generated
923
2284
  const resolvers = this.generateFieldResolvers();
924
-
2285
+
925
2286
  if (!service.__graphqlFields) {
926
2287
  service.__graphqlFields = [];
927
2288
  }
928
-
2289
+
929
2290
  for (const { typeName, fieldName, resolver } of resolvers) {
930
2291
  // Create a unique method name
931
2292
  const methodName = `_autoResolver_${typeName}_${fieldName}`;
932
-
2293
+
933
2294
  // Attach resolver as a method
934
2295
  service[methodName] = resolver;
935
-
2296
+
936
2297
  // Register with GraphQL metadata
937
2298
  service.__graphqlFields.push({
938
2299
  type: typeName,
939
2300
  field: fieldName,
940
- propertyKey: methodName
2301
+ propertyKey: methodName,
941
2302
  });
942
2303
  }
943
2304
  }
944
2305
 
945
- // TODO: Here
946
- 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;
947
2309
  const zodShapes: Record<string, any> = {};
948
2310
  const storage = getMetadataStorage();
949
- const unionSchemas: Array<{ fieldName: string; schema: any; components: any[] }> = [];
2311
+ const unionSchemas: Array<{
2312
+ fieldName: string;
2313
+ schema: any;
2314
+ components: any[];
2315
+ }> = [];
950
2316
 
951
2317
  for (const [field, ctor] of Object.entries(this.componentMap)) {
952
2318
  // Skip union fields - they'll be processed separately
953
- if (field.startsWith('union_')) {
2319
+ if (field.startsWith("union_")) {
954
2320
  continue;
955
2321
  }
956
2322
 
957
2323
  const type = this.fieldTypes[field];
958
2324
  const typeId = storage.getComponentId(ctor.name);
959
2325
  const componentProps = storage.getComponentProperties(typeId);
960
-
2326
+
961
2327
  // Check if component should be unwrapped based on field type
962
2328
  if (shouldUnwrapComponent(componentProps, type)) {
963
2329
  // Unwrap to primitive type
@@ -972,7 +2338,11 @@ export class BaseArcheType {
972
2338
  }
973
2339
  } else {
974
2340
  // Use component schema for complex types
975
- const componentSchema = getOrCreateComponentSchema(ctor, typeId, this.fieldOptions[field]);
2341
+ const componentSchema = getOrCreateComponentSchema(
2342
+ ctor,
2343
+ typeId,
2344
+ this.fieldOptions[field]
2345
+ );
976
2346
  if (componentSchema) {
977
2347
  zodShapes[field] = componentSchema;
978
2348
  } else {
@@ -980,8 +2350,12 @@ export class BaseArcheType {
980
2350
  continue;
981
2351
  }
982
2352
  }
983
-
984
- 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
+ ) {
985
2359
  zodShapes[field] = zodShapes[field].nullish();
986
2360
  }
987
2361
  }
@@ -991,49 +2365,69 @@ export class BaseArcheType {
991
2365
  // Generate schemas for each component in the union
992
2366
  const unionComponentSchemas: any[] = [];
993
2367
  const unionComponentCtors: any[] = [];
994
-
2368
+
995
2369
  for (const component of components) {
996
2370
  const typeId = storage.getComponentId(component.name);
997
- const componentSchema = getOrCreateComponentSchema(component, typeId, this.unionOptions[fieldName]);
998
-
2371
+ const componentSchema = getOrCreateComponentSchema(
2372
+ component,
2373
+ typeId,
2374
+ this.unionOptions[fieldName]
2375
+ );
2376
+
999
2377
  if (componentSchema) {
1000
2378
  unionComponentSchemas.push(componentSchema);
1001
2379
  unionComponentCtors.push(component);
1002
2380
  }
1003
2381
  }
1004
-
2382
+
1005
2383
  // Create union type using Zod with GQLoom support
1006
2384
  if (unionComponentSchemas.length > 0) {
1007
- const unionSchema = z.union(unionComponentSchemas).register(asUnionType, {
1008
- name: fieldName.charAt(0).toUpperCase() + fieldName.slice(1), // Capitalize field name for type
1009
- resolveType: (it: any) => {
1010
- // Determine which type this is based on __typename
1011
- if (it.__typename) {
1012
- return it.__typename;
1013
- }
1014
- // Fallback: check property presence
1015
- for (let i = 0; i < unionComponentCtors.length; i++) {
1016
- const componentProps = storage.getComponentProperties(
1017
- storage.getComponentId(unionComponentCtors[i].name)
1018
- );
1019
- const hasUniqueProps = componentProps.some(prop =>
1020
- it.hasOwnProperty(prop.propertyKey)
1021
- );
1022
- if (hasUniqueProps) {
1023
- 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;
1024
2395
  }
1025
- }
1026
- return compNameToFieldName(unionComponentCtors[0].name);
1027
- }
1028
- });
1029
-
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
+
1030
2424
  zodShapes[fieldName] = unionSchema;
1031
2425
  unionSchemas.push({
1032
2426
  fieldName,
1033
2427
  schema: unionSchema,
1034
- components: unionComponentSchemas
2428
+ components: unionComponentSchemas,
1035
2429
  });
1036
-
2430
+
1037
2431
  // Apply nullable option for union fields
1038
2432
  if (this.unionOptions[fieldName]?.nullable) {
1039
2433
  zodShapes[fieldName] = zodShapes[fieldName].nullish();
@@ -1041,45 +2435,157 @@ export class BaseArcheType {
1041
2435
  }
1042
2436
  }
1043
2437
 
1044
- // Process relations for GraphQL schema generation
1045
- for (const [field, relatedArcheType] of Object.entries(this.relationMap)) {
1046
- const relationType = this.relationTypes[field];
1047
- const isArray = relationType === 'hasMany' || relationType === 'belongsToMany';
1048
-
1049
- // Get the related archetype name
1050
- let relatedTypeName: string;
1051
- if (typeof relatedArcheType === 'string') {
1052
- relatedTypeName = relatedArcheType;
1053
- } else {
1054
- const relatedArchetypeId = storage.getComponentId(relatedArcheType.name);
1055
- const relatedArchetypeMetadata = storage.archetypes.find(a => a.typeId === relatedArchetypeId);
1056
- relatedTypeName = relatedArchetypeMetadata?.name || relatedArcheType.name.replace(/ArcheType$/, '');
1057
- }
1058
-
1059
- // For GraphQL relations, we just store the type name as a string reference
1060
- // The GraphQL schema will use the type name directly, and the full type definition
1061
- // will be generated when each archetype's getZodObjectSchema() is called
1062
- const relatedTypeSchema = z.string().describe(`Reference to ${relatedTypeName} type`);
1063
-
1064
- if (isArray) {
1065
- zodShapes[field] = z.array(relatedTypeSchema);
1066
- } else {
1067
- 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
+ }
1068
2489
  }
1069
-
1070
- if (this.relationOptions[field]?.nullable) {
1071
- 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();
1072
2574
  }
1073
2575
  }
1074
2576
 
1075
2577
  const archetypeId = storage.getComponentId(this.constructor.name);
1076
- 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;
1077
2581
  const shape: Record<string, any> = {
1078
2582
  __typename: z.literal(nameFromStorage).nullish(),
1079
- 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
1080
2584
  };
1081
2585
  for (const [field, zodType] of Object.entries(zodShapes)) {
1082
- const isNullable = this.fieldOptions[field]?.nullable || this.unionOptions[field]?.nullable;
2586
+ const isNullable =
2587
+ this.fieldOptions[field]?.nullable ||
2588
+ this.unionOptions[field]?.nullable;
1083
2589
  if (isNullable) {
1084
2590
  // For nullable fields, make them optional in the GraphQL schema
1085
2591
  shape[field] = zodType.optional();
@@ -1088,74 +2594,438 @@ export class BaseArcheType {
1088
2594
  }
1089
2595
  }
1090
2596
  const r = z.object(shape);
1091
-
2597
+
1092
2598
  // Collect all component schemas used by this archetype for weaving
1093
2599
  const componentSchemasToWeave: any[] = [];
1094
2600
  for (const [field, zodType] of Object.entries(zodShapes)) {
1095
2601
  if (zodType instanceof ZodObject) {
1096
2602
  componentSchemasToWeave.push(zodType);
1097
- } 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
+ ) {
1098
2609
  // Handle union types
1099
- if (zodType._def?.typeName === 'ZodUnion') {
2610
+ if (zodType._def?.typeName === "ZodUnion") {
1100
2611
  componentSchemasToWeave.push(zodType);
1101
2612
  }
1102
2613
  }
1103
2614
  }
1104
-
2615
+
1105
2616
  // Weave archetype schema along with its component schemas
1106
2617
  const schemasToWeave = [r];
1107
2618
  const schema = weave(ZodWeaver, ...schemasToWeave);
1108
2619
  let graphqlSchemaString = printSchema(schema);
1109
-
2620
+
1110
2621
  // Post-process: Replace 'id: String' with 'id: ID' for all id fields
1111
- graphqlSchemaString = graphqlSchemaString.replace(/\bid:\s*String\b/g, 'id: ID');
1112
-
2622
+ graphqlSchemaString = graphqlSchemaString.replace(
2623
+ /\bid:\s*String\b/g,
2624
+ "id: ID"
2625
+ );
2626
+
1113
2627
  // Post-process: Replace relation field types with proper GraphQL type references
1114
- for (const [field, relatedArcheType] of Object.entries(this.relationMap)) {
2628
+ for (const [field, relatedArcheType] of Object.entries(
2629
+ this.relationMap
2630
+ )) {
1115
2631
  const relationType = this.relationTypes[field];
1116
- const isArray = relationType === 'hasMany' || relationType === 'belongsToMany';
1117
-
2632
+ const isArray =
2633
+ relationType === "hasMany" || relationType === "belongsToMany";
2634
+
1118
2635
  let relatedTypeName: string;
1119
- if (typeof relatedArcheType === 'string') {
2636
+ if (typeof relatedArcheType === "string") {
1120
2637
  relatedTypeName = relatedArcheType;
1121
2638
  } else {
1122
- const relatedArchetypeId = storage.getComponentId(relatedArcheType.name);
1123
- const relatedArchetypeMetadata = storage.archetypes.find(a => a.typeId === relatedArchetypeId);
1124
- 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$/, "");
1125
2648
  }
1126
-
2649
+
1127
2650
  // Replace the String field with proper GraphQL type reference
1128
2651
  if (isArray) {
1129
- const pattern = new RegExp(`${field}:\\s*\\[String!?\\]!?`, 'g');
1130
- 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
+ );
1131
2679
  } else {
1132
- const pattern = new RegExp(`${field}:\\s*String!?`, 'g');
1133
- 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
+ );
1134
2687
  }
1135
2688
  }
1136
-
1137
- // console.log("WeavedSchema:", graphqlSchemaString);
1138
-
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
+
1139
2887
  // Cache the schema for this archetype
1140
- archetypeSchemaCache.set(nameFromStorage, {
2888
+ const cacheKey = `${nameFromStorage}_${excludeRelations}_${excludeFunctions}`;
2889
+ archetypeSchemaCache.set(cacheKey, {
1141
2890
  zodSchema: r,
1142
- graphqlSchema: graphqlSchemaString
2891
+ graphqlSchema: graphqlSchemaString,
1143
2892
  });
1144
-
2893
+
1145
2894
  // Store for unified weaving
1146
2895
  allArchetypeZodObjects.set(nameFromStorage, r);
1147
-
2896
+
1148
2897
  return r;
1149
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
+ }
1150
3025
  }
1151
3026
 
1152
- export type InferArcheType<T extends BaseArcheType> = {
1153
- [K in keyof T['componentMap']]: T['componentMap'][K] extends new (...args: any[]) => infer C ? C : never
1154
- };
1155
3027
 
1156
- // Alternative: Infer from the actual instance properties (recommended)
1157
- export type InferArcheTypeFromInstance<T extends BaseArcheType> = {
1158
- [K in keyof T as T[K] extends BaseComponent ? K : never]: T[K]
1159
- };
1160
3028
 
1161
- export default BaseArcheType;
3029
+
3030
+
3031
+ export default BaseArcheType;