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