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