bunsane 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/CHANGELOG.md +445 -318
  2. package/config/cache.config.ts +35 -1
  3. package/core/App.ts +24 -1064
  4. package/core/ArcheType.ts +78 -2110
  5. package/core/BatchLoader.ts +56 -32
  6. package/core/Entity.ts +85 -1043
  7. package/core/EntityHookManager.ts +52 -754
  8. package/core/Logger.ts +10 -0
  9. package/core/RequestContext.ts +64 -6
  10. package/core/RequestLoaders.ts +187 -36
  11. package/core/SchedulerManager.ts +28 -600
  12. package/core/app/bootstrap.ts +133 -0
  13. package/core/app/cors.ts +85 -0
  14. package/core/app/graphqlSetup.ts +56 -0
  15. package/core/app/healthEndpoints.ts +31 -0
  16. package/core/app/metricsCollector.ts +27 -0
  17. package/core/app/preparedStatementWarmup.ts +15 -0
  18. package/core/app/processHandlers.ts +43 -0
  19. package/core/app/requestRouter.ts +310 -0
  20. package/core/app/restRegistry.ts +80 -0
  21. package/core/app/shutdown.ts +97 -0
  22. package/core/app/studioRouter.ts +83 -0
  23. package/core/archetype/customTypes.ts +100 -0
  24. package/core/archetype/decorators.ts +171 -0
  25. package/core/archetype/fieldResolvers.ts +666 -0
  26. package/core/archetype/helpers.ts +29 -0
  27. package/core/archetype/relationLoader.ts +161 -0
  28. package/core/archetype/schemaBuilder.ts +141 -0
  29. package/core/archetype/weaver.ts +218 -0
  30. package/core/archetype/zodSchemaBuilder.ts +527 -0
  31. package/core/cache/CacheManager.ts +173 -267
  32. package/core/cache/CompressionUtils.ts +34 -3
  33. package/core/cache/MemoryCache.ts +40 -37
  34. package/core/cache/RedisCache.ts +4 -4
  35. package/core/cache/health.ts +30 -0
  36. package/core/cache/invalidation.ts +96 -0
  37. package/core/cache/strategies/writeInvalidate.ts +111 -0
  38. package/core/cache/strategies/writeThrough.ts +233 -0
  39. package/core/components/BaseComponent.ts +16 -8
  40. package/core/components/ComponentRegistry.ts +28 -0
  41. package/core/decorators/IndexedField.ts +1 -1
  42. package/core/entity/cacheStrategies.ts +97 -0
  43. package/core/entity/componentAccess.ts +364 -0
  44. package/core/entity/finders.ts +202 -0
  45. package/core/entity/pendingOps.ts +72 -0
  46. package/core/entity/saveEntity.ts +377 -0
  47. package/core/hooks/dispatcher.ts +439 -0
  48. package/core/hooks/guards.ts +155 -0
  49. package/core/hooks/registry.ts +247 -0
  50. package/core/metadata/definitions/Component.ts +1 -1
  51. package/core/metadata/index.ts +15 -4
  52. package/core/middleware/AccessLog.ts +8 -1
  53. package/core/middleware/RateLimit.ts +102 -105
  54. package/core/middleware/RequestId.ts +2 -9
  55. package/core/middleware/SecurityHeaders.ts +2 -11
  56. package/core/middleware/headers.ts +28 -0
  57. package/core/remote/OutboxWorker.ts +213 -183
  58. package/core/remote/RemoteManager.ts +401 -400
  59. package/core/remote/types.ts +153 -151
  60. package/core/requestScope.ts +34 -0
  61. package/core/scheduler/cronEvaluator.ts +174 -0
  62. package/core/scheduler/lifecycleHooks.ts +21 -0
  63. package/core/scheduler/lockCoordinator.ts +27 -0
  64. package/core/scheduler/metrics.ts +14 -0
  65. package/core/scheduler/taskRunner.ts +420 -0
  66. package/database/DatabaseHelper.ts +128 -101
  67. package/database/IndexingStrategy.ts +72 -2
  68. package/database/PreparedStatementCache.ts +20 -5
  69. package/database/cancellable.ts +35 -0
  70. package/database/index.ts +15 -3
  71. package/database/instrumentedDb.ts +141 -0
  72. package/endpoints/archetypes.ts +2 -8
  73. package/endpoints/tables.ts +6 -1
  74. package/gql/index.ts +1 -1
  75. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  76. package/package.json +22 -1
  77. package/query/CTENode.ts +5 -3
  78. package/query/ComponentInclusionNode.ts +240 -13
  79. package/query/OrNode.ts +6 -5
  80. package/query/Query.ts +203 -59
  81. package/query/QueryContext.ts +6 -0
  82. package/query/QueryDAG.ts +7 -2
  83. package/query/membershipSource.ts +66 -0
  84. package/storage/LocalStorageProvider.ts +8 -3
  85. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  86. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  87. package/studio/{index.html → dist/index.html} +3 -2
  88. package/swagger/generator.ts +11 -1
  89. package/upload/UploadManager.ts +8 -6
  90. package/utils/uuid.ts +40 -10
  91. package/.claude/settings.local.json +0 -47
  92. package/.prettierrc +0 -4
  93. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  94. package/.serena/memories/architecture.md +0 -154
  95. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  96. package/.serena/memories/code_style_and_conventions.md +0 -76
  97. package/.serena/memories/project_overview.md +0 -43
  98. package/.serena/memories/schema-dsl-plan.md +0 -107
  99. package/.serena/memories/suggested_commands.md +0 -80
  100. package/.serena/memories/typescript-compilation-status.md +0 -54
  101. package/.serena/project.yml +0 -114
  102. package/BunSane.jpg +0 -0
  103. package/CLAUDE.md +0 -198
  104. package/TODO.md +0 -2
  105. package/bun.lock +0 -302
  106. package/bunfig.toml +0 -10
  107. package/docs/SCALABILITY_PLAN.md +0 -175
  108. package/studio/bun.lock +0 -482
  109. package/studio/package.json +0 -39
  110. package/studio/postcss.config.js +0 -6
  111. package/studio/src/components/DataTable.tsx +0 -211
  112. package/studio/src/components/Layout.tsx +0 -13
  113. package/studio/src/components/PageContainer.tsx +0 -9
  114. package/studio/src/components/PageHeader.tsx +0 -13
  115. package/studio/src/components/SearchBar.tsx +0 -57
  116. package/studio/src/components/Sidebar.tsx +0 -294
  117. package/studio/src/components/ui/button.tsx +0 -56
  118. package/studio/src/components/ui/checkbox.tsx +0 -26
  119. package/studio/src/components/ui/input.tsx +0 -25
  120. package/studio/src/hooks/useDataTable.ts +0 -131
  121. package/studio/src/index.css +0 -36
  122. package/studio/src/lib/api.ts +0 -186
  123. package/studio/src/lib/utils.ts +0 -13
  124. package/studio/src/main.tsx +0 -17
  125. package/studio/src/pages/ArcheType.tsx +0 -239
  126. package/studio/src/pages/Components.tsx +0 -124
  127. package/studio/src/pages/EntityInspector.tsx +0 -302
  128. package/studio/src/pages/QueryRunner.tsx +0 -246
  129. package/studio/src/pages/Table.tsx +0 -94
  130. package/studio/src/pages/Welcome.tsx +0 -241
  131. package/studio/src/routes.tsx +0 -45
  132. package/studio/src/store/archeTypeSettings.ts +0 -30
  133. package/studio/src/store/studio.ts +0 -65
  134. package/studio/src/utils/columnHelpers.tsx +0 -114
  135. package/studio/studio-instructions.md +0 -81
  136. package/studio/tailwind.config.js +0 -77
  137. package/studio/utils.ts +0 -54
  138. package/studio/vite.config.js +0 -19
  139. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  140. package/tests/benchmark/bunfig.toml +0 -9
  141. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  142. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  143. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  144. package/tests/benchmark/fixtures/index.ts +0 -6
  145. package/tests/benchmark/index.ts +0 -22
  146. package/tests/benchmark/noop-preload.ts +0 -3
  147. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  148. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  149. package/tests/benchmark/runners/index.ts +0 -4
  150. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  151. package/tests/benchmark/scripts/generate-db.ts +0 -344
  152. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  153. package/tests/e2e/http.test.ts +0 -130
  154. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  155. package/tests/fixtures/components/TestOrder.ts +0 -23
  156. package/tests/fixtures/components/TestProduct.ts +0 -23
  157. package/tests/fixtures/components/TestUser.ts +0 -20
  158. package/tests/fixtures/components/index.ts +0 -6
  159. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  160. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  161. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  162. package/tests/helpers/MockRedisClient.ts +0 -113
  163. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  164. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  165. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  166. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  167. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  168. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  169. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  170. package/tests/integration/query/Query.exec.test.ts +0 -576
  171. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  172. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  173. package/tests/integration/remote/dlq.test.ts +0 -175
  174. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  175. package/tests/integration/remote/outbox.test.ts +0 -130
  176. package/tests/integration/remote/rpc.test.ts +0 -177
  177. package/tests/pglite-setup.ts +0 -62
  178. package/tests/setup.ts +0 -164
  179. package/tests/stress/BenchmarkRunner.ts +0 -203
  180. package/tests/stress/DataSeeder.ts +0 -190
  181. package/tests/stress/StressTestReporter.ts +0 -229
  182. package/tests/stress/cursor-perf-test.ts +0 -171
  183. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  184. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  185. package/tests/stress/index.ts +0 -7
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  187. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  188. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  189. package/tests/unit/BatchLoader.test.ts +0 -196
  190. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  191. package/tests/unit/cache/CacheManager.test.ts +0 -367
  192. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  193. package/tests/unit/cache/RedisCache.test.ts +0 -411
  194. package/tests/unit/entity/Entity.components.test.ts +0 -317
  195. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  196. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  197. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  198. package/tests/unit/entity/Entity.test.ts +0 -345
  199. package/tests/unit/gql/depthLimit.test.ts +0 -203
  200. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  201. package/tests/unit/health/Health.test.ts +0 -129
  202. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  203. package/tests/unit/middleware/Middleware.test.ts +0 -98
  204. package/tests/unit/middleware/RequestId.test.ts +0 -54
  205. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  206. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  207. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  208. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  209. package/tests/unit/query/Query.test.ts +0 -310
  210. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  211. package/tests/unit/remote/RemoteError.test.ts +0 -55
  212. package/tests/unit/remote/decorators.test.ts +0 -195
  213. package/tests/unit/remote/metrics.test.ts +0 -115
  214. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  215. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  216. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  217. package/tests/unit/schema/schema-integration.test.ts +0 -426
  218. package/tests/unit/schema/schema.test.ts +0 -580
  219. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  220. package/tests/unit/upload/RestUpload.test.ts +0 -267
  221. package/tests/unit/validateEnv.test.ts +0 -82
  222. package/tests/utils/entity-tracker.ts +0 -57
  223. package/tests/utils/index.ts +0 -13
  224. package/tests/utils/test-context.ts +0 -149
@@ -0,0 +1,29 @@
1
+ import type { ComponentPropertyMetadata } from "../metadata/definitions/Component";
2
+
3
+ export const primitiveTypes = [String, Number, Boolean, Date];
4
+
5
+ export function compNameToFieldName(compName: string): string {
6
+ return (
7
+ compName.charAt(0).toLowerCase() +
8
+ compName.slice(1).replace(/Component$/, "Component")
9
+ );
10
+ }
11
+
12
+ /**
13
+ * Helper to determine if a component should be unwrapped to a scalar value.
14
+ * Returns true if the component has a single 'value' property and the field type is primitive.
15
+ */
16
+ export function shouldUnwrapComponent(
17
+ componentProps: ComponentPropertyMetadata[],
18
+ fieldType: any
19
+ ): boolean {
20
+ if (
21
+ fieldType === String ||
22
+ fieldType === Number ||
23
+ fieldType === Boolean ||
24
+ fieldType === Date
25
+ ) {
26
+ return true;
27
+ }
28
+ return false;
29
+ }
@@ -0,0 +1,161 @@
1
+ import { Entity } from "../Entity";
2
+ import { getMetadataStorage } from "../metadata";
3
+ import { Query } from "../../query";
4
+ import { getRequestScope } from "../requestScope";
5
+
6
+ /**
7
+ * Populate relation fields on an entity according to the archetype's relationMap.
8
+ * Extracted from BaseArcheType.populateRelations().
9
+ *
10
+ * When called inside a request scope (GraphQL execution), relation loads go
11
+ * through the request's DataLoaders so sibling entities resolved in the same
12
+ * tick batch into single queries (previously: one `new Query()` per relation
13
+ * per entity — a hard N+1). Relation fields of one entity are resolved
14
+ * concurrently for the same reason.
15
+ */
16
+ export async function populateRelations(archetype: any, entity: Entity): Promise<void> {
17
+ const storage = getMetadataStorage();
18
+
19
+ const fieldPromises: Promise<void>[] = [];
20
+ for (const [fieldName, relatedArchetype] of Object.entries(archetype.relationMap)) {
21
+ const relationType = archetype.relationTypes[fieldName];
22
+ const relationOptions = archetype.relationOptions[fieldName];
23
+
24
+ if (relationType === "belongsTo") {
25
+ fieldPromises.push(populateBelongsTo(archetype, entity, fieldName, relatedArchetype, relationOptions, storage));
26
+ } else if (relationType === "hasMany") {
27
+ fieldPromises.push(populateHasMany(entity, fieldName, relatedArchetype, relationOptions, storage));
28
+ }
29
+ }
30
+ await Promise.all(fieldPromises);
31
+ }
32
+
33
+ function resolveRelatedArchetypeInstance(relatedArchetype: any, storage: any): any | null {
34
+ if (typeof relatedArchetype === "function") {
35
+ return new (relatedArchetype as any)();
36
+ }
37
+ const meta = storage.archetypes.find((a: any) => a.name === relatedArchetype);
38
+ return meta ? new (meta.target as any)() : null;
39
+ }
40
+
41
+ async function populateBelongsTo(
42
+ archetype: any,
43
+ entity: Entity,
44
+ fieldName: string,
45
+ relatedArchetype: any,
46
+ relationOptions: any,
47
+ storage: any,
48
+ ): Promise<void> {
49
+ const foreignKey = relationOptions?.foreignKey;
50
+ if (!foreignKey) return;
51
+
52
+ let foreignId: string | undefined;
53
+
54
+ if (foreignKey.includes('.')) {
55
+ const [innerField, propName] = foreignKey.split('.');
56
+ const compCtor = archetype.componentMap[innerField!];
57
+ if (compCtor) {
58
+ // entity.get batches via the ambient request scope when present
59
+ const componentInstance = await entity.get(compCtor as any);
60
+ if (componentInstance && (componentInstance as any)[propName!] !== undefined) {
61
+ foreignId = (componentInstance as any)[propName!];
62
+ }
63
+ }
64
+ } else {
65
+ const candidateComponents: Array<{ compCtor: any }> = [];
66
+ for (const compCtor of Object.values(archetype.componentMap)) {
67
+ const compCtorAny = compCtor as any;
68
+ const typeId = storage.getComponentId(compCtorAny.name);
69
+ const componentProps = storage.getComponentProperties(typeId);
70
+ const hasForeignKey = componentProps.some((prop: any) => prop.propertyKey === foreignKey);
71
+ if (hasForeignKey) {
72
+ candidateComponents.push({ compCtor: compCtorAny });
73
+ }
74
+ }
75
+
76
+ if (candidateComponents.length > 0) {
77
+ const componentInstances = await Promise.all(
78
+ candidateComponents.map(({ compCtor }) => entity.get(compCtor as any))
79
+ );
80
+
81
+ for (const componentInstance of componentInstances) {
82
+ if (componentInstance && (componentInstance as any)[foreignKey] !== undefined) {
83
+ foreignId = (componentInstance as any)[foreignKey];
84
+ break;
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ if (!foreignId && foreignKey === 'id') {
91
+ foreignId = entity.id;
92
+ }
93
+ if (!foreignId) return;
94
+
95
+ // Batched path: the request-scoped entityById loader dedups/batches
96
+ // sibling lookups. The returned shell entity lazy-loads components
97
+ // through the same scope's component loader.
98
+ const scope = getRequestScope();
99
+ if (scope?.loaders?.entityById) {
100
+ const relatedEntity = await scope.loaders.entityById.load(foreignId);
101
+ if (relatedEntity) {
102
+ (entity as any)[fieldName] = relatedEntity;
103
+ }
104
+ return;
105
+ }
106
+
107
+ const relatedArchetypeInstance = resolveRelatedArchetypeInstance(relatedArchetype, storage);
108
+ if (!relatedArchetypeInstance) return;
109
+ const relatedEntity = await relatedArchetypeInstance.getEntityWithID(foreignId);
110
+ if (relatedEntity) {
111
+ (entity as any)[fieldName] = relatedEntity;
112
+ }
113
+ }
114
+
115
+ async function populateHasMany(
116
+ entity: Entity,
117
+ fieldName: string,
118
+ relatedArchetype: any,
119
+ relationOptions: any,
120
+ storage: any,
121
+ ): Promise<void> {
122
+ const foreignKey = relationOptions?.foreignKey;
123
+ if (!foreignKey) return;
124
+
125
+ const relatedArchetypeInstance = resolveRelatedArchetypeInstance(relatedArchetype, storage);
126
+ if (!relatedArchetypeInstance) return;
127
+
128
+ let foreignKeyComponent: any = null;
129
+ for (const compCtor of Object.values(relatedArchetypeInstance.componentMap)) {
130
+ const compCtorAny = compCtor as any;
131
+ const typeId = storage.getComponentId(compCtorAny.name);
132
+ const componentProps = storage.getComponentProperties(typeId);
133
+ const hasForeignKey = componentProps.some((prop: any) => prop.propertyKey === foreignKey);
134
+ if (hasForeignKey) {
135
+ foreignKeyComponent = compCtorAny;
136
+ break;
137
+ }
138
+ }
139
+ if (!foreignKeyComponent) return;
140
+
141
+ // Batched path: type-scoped FK loader collapses sibling parents sharing
142
+ // the same (componentType, fkField) into one query.
143
+ const scope = getRequestScope();
144
+ if (scope?.loaders?.relationsByComponentFk) {
145
+ const componentTypeId = storage.getComponentId(foreignKeyComponent.name);
146
+ (entity as any)[fieldName] = await scope.loaders.relationsByComponentFk.load({
147
+ entityId: entity.id,
148
+ componentTypeId,
149
+ foreignKeyField: foreignKey,
150
+ });
151
+ return;
152
+ }
153
+
154
+ const matchingEntities = await new Query()
155
+ .with(foreignKeyComponent, {
156
+ filters: [{ field: foreignKey, operator: '=', value: entity.id }]
157
+ })
158
+ .exec();
159
+
160
+ (entity as any)[fieldName] = matchingEntities;
161
+ }
@@ -0,0 +1,141 @@
1
+ import type { BaseComponent } from "../components";
2
+ import type { ArcheTypeFieldOptions } from "../metadata/definitions/ArcheType";
3
+ import { z, ZodObject } from "zod";
4
+ import { asEnumType } from "@gqloom/zod";
5
+ import { getMetadataStorage } from "../metadata";
6
+ import { compNameToFieldName, primitiveTypes } from "./helpers";
7
+ import { customTypeRegistry } from "./customTypes";
8
+
9
+ // Component-level schema cache
10
+ export const componentSchemaCache = new Map<string, ZodObject<any>>();
11
+
12
+ // Enum schema cache to prevent duplicate registrations
13
+ export const enumSchemaCache = new Map<string, any>();
14
+
15
+ /**
16
+ * Generate Zod schema for a component and cache it.
17
+ */
18
+ export function getOrCreateComponentSchema(
19
+ componentCtor: new (...args: any[]) => BaseComponent,
20
+ componentId: string,
21
+ fieldOptions?: ArcheTypeFieldOptions
22
+ ): any | null {
23
+ if (componentSchemaCache.has(componentId)) {
24
+ return componentSchemaCache.get(componentId)!;
25
+ }
26
+
27
+ const storage = getMetadataStorage();
28
+ const props = storage.getComponentProperties(componentId);
29
+
30
+ if (props.length === 0) {
31
+ return null;
32
+ }
33
+
34
+ const zodFields: Record<string, any> = {
35
+ __typename: z
36
+ .literal(compNameToFieldName(componentCtor.name))
37
+ .nullish(),
38
+ };
39
+
40
+ for (const prop of props) {
41
+ if (prop.isPrimitive) {
42
+ switch (prop.propertyType) {
43
+ case String:
44
+ zodFields[prop.propertyKey] = z.string();
45
+ break;
46
+ case Number:
47
+ zodFields[prop.propertyKey] = z.number();
48
+ break;
49
+ case Boolean:
50
+ zodFields[prop.propertyKey] = z.boolean();
51
+ break;
52
+ case Date:
53
+ zodFields[prop.propertyKey] = z.date();
54
+ break;
55
+ default:
56
+ console.warn(`[ArcheType] Unknown primitive type for ${componentCtor.name}.${prop.propertyKey}: ${prop.propertyType?.name}. Falling back to z.string()`);
57
+ zodFields[prop.propertyKey] = z.string();
58
+ }
59
+ if (prop.isOptional) {
60
+ zodFields[prop.propertyKey] =
61
+ zodFields[prop.propertyKey].optional();
62
+ }
63
+ } else if (prop.isEnum && prop.enumValues && prop.enumKeys) {
64
+ const enumTypeName =
65
+ prop.propertyType?.name ||
66
+ `${componentCtor.name}_${prop.propertyKey}_Enum`;
67
+
68
+ let enumSchema = enumSchemaCache.get(enumTypeName);
69
+
70
+ if (!enumSchema) {
71
+ enumSchema = z
72
+ .enum(prop.enumValues as any)
73
+ .register(asEnumType, {
74
+ name: enumTypeName,
75
+ valuesConfig: prop.enumKeys.reduce(
76
+ (
77
+ acc: Record<string, { description: string }>,
78
+ key,
79
+ idx
80
+ ) => {
81
+ acc[key] = { description: prop.enumValues![idx]! };
82
+ return acc;
83
+ },
84
+ {}
85
+ ),
86
+ });
87
+ enumSchemaCache.set(enumTypeName, enumSchema);
88
+ }
89
+
90
+ zodFields[prop.propertyKey] = enumSchema;
91
+ if (prop.isOptional) {
92
+ zodFields[prop.propertyKey] =
93
+ zodFields[prop.propertyKey].optional();
94
+ }
95
+ } else if (customTypeRegistry.has(prop.propertyType)) {
96
+ zodFields[prop.propertyKey] = customTypeRegistry.get(
97
+ prop.propertyType
98
+ )!;
99
+ if (prop.isOptional) {
100
+ zodFields[prop.propertyKey] =
101
+ zodFields[prop.propertyKey].optional();
102
+ }
103
+ } else if (prop.arrayOf) {
104
+ if (customTypeRegistry.has(prop.arrayOf)) {
105
+ zodFields[prop.propertyKey] = z.array(customTypeRegistry.get(prop.arrayOf)!);
106
+ } else if (primitiveTypes.includes(prop.arrayOf)) {
107
+ if (prop.arrayOf === String) {
108
+ zodFields[prop.propertyKey] = z.array(z.string());
109
+ } else if (prop.arrayOf === Number) {
110
+ zodFields[prop.propertyKey] = z.array(z.number());
111
+ } else if (prop.arrayOf === Boolean) {
112
+ zodFields[prop.propertyKey] = z.array(z.boolean());
113
+ } else if (prop.arrayOf === Date) {
114
+ zodFields[prop.propertyKey] = z.array(z.date());
115
+ }
116
+ } else {
117
+ console.warn(`[ArcheType] Unknown array element type for ${componentCtor.name}.${prop.propertyKey}: ${prop.arrayOf?.name}. Falling back to z.array(z.string())`);
118
+ zodFields[prop.propertyKey] = z.array(z.string());
119
+ }
120
+ if (prop.isOptional) {
121
+ zodFields[prop.propertyKey] = zodFields[prop.propertyKey].optional();
122
+ }
123
+ } else {
124
+ console.warn(`[ArcheType] Unknown type for ${componentCtor.name}.${prop.propertyKey}: ${prop.propertyType?.name}. Falling back to z.string()`);
125
+ zodFields[prop.propertyKey] = z.string();
126
+ if (prop.isOptional) {
127
+ zodFields[prop.propertyKey] =
128
+ zodFields[prop.propertyKey].optional();
129
+ }
130
+ }
131
+
132
+ if (fieldOptions?.nullable) {
133
+ zodFields[prop.propertyKey] = zodFields[prop.propertyKey].nullish();
134
+ }
135
+ }
136
+
137
+ const componentSchema = z.object(zodFields);
138
+ componentSchemaCache.set(componentId, componentSchema);
139
+
140
+ return componentSchema;
141
+ }
@@ -0,0 +1,218 @@
1
+ import { ZodObject } from "zod";
2
+ import { weave } from "@gqloom/core";
3
+ import { ZodWeaver } from "@gqloom/zod";
4
+ import { printSchema } from "graphql";
5
+ import { getMetadataStorage } from "../metadata";
6
+ import { componentSchemaCache } from "./schemaBuilder";
7
+ import { inputTypeRegistry, customTypeNameRegistry } from "./customTypes";
8
+
9
+ export const archetypeSchemaCache = new Map<
10
+ string,
11
+ { zodSchema: ZodObject<any>; graphqlSchema: string }
12
+ >();
13
+ export const allArchetypeZodObjects = new Map<string, ZodObject<any>>();
14
+
15
+ export function getArchetypeSchema(archetypeName: string, excludeRelations = false, excludeFunctions = false) {
16
+ const cacheKey = `${archetypeName}_${excludeRelations}_${excludeFunctions}`;
17
+ return archetypeSchemaCache.get(cacheKey);
18
+ }
19
+
20
+ export function getAllArchetypeSchemas() {
21
+ return Array.from(archetypeSchemaCache.entries())
22
+ .filter(([key]) => key.endsWith('_false_false'))
23
+ .map(([, value]) => value);
24
+ }
25
+
26
+ export function weaveAllArchetypes() {
27
+ const storage = getMetadataStorage();
28
+ const archetypeNames: string[] = [];
29
+
30
+ for (const archetypeMetadata of storage.archetypes) {
31
+ const archetypeName = archetypeMetadata.name;
32
+ archetypeNames.push(archetypeName);
33
+ const fullSchemaCacheKey = `${archetypeName}_false_false`;
34
+ if (!archetypeSchemaCache.has(fullSchemaCacheKey)) {
35
+ try {
36
+ const ArchetypeClass = archetypeMetadata.target as any;
37
+ const instance = new ArchetypeClass();
38
+ instance.getZodObjectSchema();
39
+ } catch (error) {
40
+ console.warn(
41
+ `Could not generate schema for archetype ${archetypeName}:`,
42
+ error
43
+ );
44
+ }
45
+ }
46
+ }
47
+
48
+ if (allArchetypeZodObjects.size === 0) {
49
+ return null;
50
+ }
51
+ const archetypeSchemas = Array.from(allArchetypeZodObjects.values());
52
+ const componentSchemas = Array.from(componentSchemaCache.values());
53
+
54
+ const allSchemas = archetypeSchemas;
55
+
56
+ try {
57
+ const schema = weave(ZodWeaver, ...allSchemas);
58
+ let schemaString = printSchema(schema);
59
+
60
+ if (!schemaString.includes('scalar Date')) {
61
+ schemaString = 'scalar Date\n\n' + schemaString;
62
+ }
63
+
64
+ schemaString = schemaString.replace(/\bid:\s*String\b/g, "id: ID");
65
+
66
+ schemaString = schemaString.replace(/\b(\w*_at|\w*_date|\w*Date|date\w*):\s*String(!?)/gi, (match, fieldName, nullable) => {
67
+ return `${fieldName}: Date${nullable}`;
68
+ });
69
+
70
+ for (const archetypeMetadata of storage.archetypes) {
71
+ const archetypeName = archetypeMetadata.name;
72
+ try {
73
+ const ArchetypeClass = archetypeMetadata.target as any;
74
+ const instance = new ArchetypeClass();
75
+
76
+ for (const [field, relatedArcheType] of Object.entries(instance.relationMap)) {
77
+ const relationType = instance.relationTypes[field];
78
+ const isArray = relationType === "hasMany" || relationType === "belongsToMany";
79
+
80
+ let relatedTypeName: string;
81
+ if (typeof relatedArcheType === "string") {
82
+ relatedTypeName = relatedArcheType;
83
+ } else {
84
+ const relatedArchetypeId = storage.getComponentId((relatedArcheType as any).name);
85
+ const relatedArchetypeMetadata = storage.archetypes.find(
86
+ (a) => a.typeId === relatedArchetypeId
87
+ );
88
+ relatedTypeName = relatedArchetypeMetadata?.name || (relatedArcheType as any).name.replace(/ArcheType$/, "");
89
+ }
90
+
91
+ if (isArray) {
92
+ const hasDescription = new RegExp(`"""Reference to ${relatedTypeName} type"""[\\s\\S]{0,50}${field}:`).test(schemaString);
93
+ if (!hasDescription) {
94
+ const addDescPattern = new RegExp(
95
+ `(type ${archetypeName} \\{[\\s\\S]*?)(\\n\\s+)(${field}:\\s*\\[String!?\\]!?)`,
96
+ "g"
97
+ );
98
+ schemaString = schemaString.replace(
99
+ addDescPattern,
100
+ `$1$2"""Reference to ${relatedTypeName} type"""$2$3`
101
+ );
102
+ }
103
+
104
+ const shouldBeRequired = instance.relationOptions[field]?.nullable === false;
105
+ const suffix = shouldBeRequired ? "!" : "";
106
+ const replacePattern = new RegExp(
107
+ `(type ${archetypeName} \\{[\\s\\S]*?${field}:\\s*)\\[String!?\\](!?)`,
108
+ "g"
109
+ );
110
+ schemaString = schemaString.replace(
111
+ replacePattern,
112
+ `$1[${relatedTypeName}!]${suffix}`
113
+ );
114
+ } else {
115
+ const pattern = new RegExp(
116
+ `(type ${archetypeName} \\{[\\s\\S]*?${field}:\\s*)String(!?)`,
117
+ "g"
118
+ );
119
+ const isNullable = instance.relationOptions[field]?.nullable;
120
+ const suffix = isNullable ? "" : "!";
121
+ schemaString = schemaString.replace(
122
+ pattern,
123
+ `$1${relatedTypeName}${suffix}`
124
+ );
125
+ }
126
+ }
127
+ } catch (error) {
128
+ console.warn(`Could not process relations for archetype ${archetypeMetadata.name}:`, error);
129
+ }
130
+
131
+ if (archetypeMetadata.functions) {
132
+ for (const { propertyKey, options } of archetypeMetadata.functions) {
133
+
134
+ if (options?.args && options.args.length > 0) {
135
+ const argDefs: string[] = [];
136
+ for (const arg of options.args) {
137
+ let argTypeName: string;
138
+
139
+ const inputTypeName = inputTypeRegistry.get(arg.type);
140
+ if (inputTypeName) {
141
+ argTypeName = inputTypeName;
142
+ } else {
143
+ const registeredTypeName = customTypeNameRegistry.get(arg.type);
144
+ if (registeredTypeName) {
145
+ argTypeName = registeredTypeName;
146
+ } else if (arg.type === String) {
147
+ argTypeName = 'String';
148
+ } else if (arg.type === Number) {
149
+ argTypeName = 'Float';
150
+ } else if (arg.type === Boolean) {
151
+ argTypeName = 'Boolean';
152
+ } else if (arg.type === Date) {
153
+ argTypeName = 'Date';
154
+ } else if (arg.type?.name) {
155
+ argTypeName = arg.type.name;
156
+ } else {
157
+ argTypeName = 'String';
158
+ }
159
+ }
160
+
161
+ const nullable = arg.nullable ? '' : '!';
162
+ argDefs.push(`${arg.name}: ${argTypeName}${nullable}`);
163
+ }
164
+
165
+ const argsString = argDefs.join(', ');
166
+ const escapedKey = propertyKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
167
+
168
+ const argPattern = new RegExp(
169
+ `(\\s+)(${escapedKey}\\??\\s*:\\s*)([^\\n]+)`,
170
+ 'g'
171
+ );
172
+
173
+ schemaString = schemaString.replace(
174
+ argPattern,
175
+ (match, leadingSpace, fieldDef, returnType) => {
176
+ return `${leadingSpace}${fieldDef.trim().replace(':', '')}(${argsString}): ${returnType.trim()}`;
177
+ }
178
+ );
179
+ }
180
+
181
+ if (options?.returnType && !['string', 'number', 'boolean'].includes(options.returnType)) {
182
+ const typePattern = new RegExp(`type ${archetypeName}\\s*\\{([\\s\\S]*?)\\n\\}`, 'g');
183
+ const typeMatch = typePattern.exec(schemaString);
184
+
185
+ if (typeMatch) {
186
+ const typeBody = typeMatch[1]!;
187
+
188
+ const fieldIndex = typeBody.indexOf(` ${propertyKey}`);
189
+ if (fieldIndex !== -1) {
190
+ const lineStart = fieldIndex;
191
+ const lineEnd = typeBody.indexOf('\n', fieldIndex);
192
+ const fieldLine = typeBody.substring(lineStart, lineEnd !== -1 ? lineEnd : typeBody.length);
193
+
194
+ const updatedLine = fieldLine.replace(/:\s*String(\??)(\s*)$/, `: ${options.returnType}$1$2`);
195
+
196
+ if (updatedLine !== fieldLine) {
197
+ const fullFieldIndex = schemaString.indexOf(typeMatch[0]) + typeMatch[0].indexOf(fieldLine);
198
+ schemaString = schemaString.substring(0, fullFieldIndex) +
199
+ updatedLine +
200
+ schemaString.substring(fullFieldIndex + fieldLine.length);
201
+ }
202
+ }
203
+ }
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ return schemaString;
210
+ } catch (error) {
211
+ console.warn(
212
+ `Failed to weave all archetypes due to duplicate types.\n` +
213
+ `Archetypes being processed: ${archetypeNames.join(', ')}\n` +
214
+ `Error: ${error}`
215
+ );
216
+ return null;
217
+ }
218
+ }