bunsane 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +47 -0
- package/.claude/skills/update-memory.md +74 -0
- package/.prettierrc +4 -0
- package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
- package/.serena/memories/architecture.md +154 -0
- package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
- package/.serena/memories/code_style_and_conventions.md +76 -0
- package/.serena/memories/project_overview.md +43 -0
- package/.serena/memories/schema-dsl-plan.md +107 -0
- package/.serena/memories/suggested_commands.md +80 -0
- package/.serena/memories/typescript-compilation-status.md +54 -0
- package/.serena/project.yml +114 -0
- package/TODO.md +1 -7
- package/bun.lock +150 -4
- package/bunfig.toml +10 -0
- package/config/cache.config.ts +77 -0
- package/config/upload.config.ts +4 -5
- package/core/App.ts +870 -123
- package/core/ArcheType.ts +2268 -377
- package/core/BatchLoader.ts +181 -71
- package/core/Config.ts +153 -0
- package/core/Decorators.ts +4 -1
- package/core/Entity.ts +621 -92
- package/core/EntityHookManager.ts +1 -1
- package/core/EntityInterface.ts +3 -1
- package/core/EntityManager.ts +1 -13
- package/core/ErrorHandler.ts +8 -2
- package/core/Logger.ts +9 -0
- package/core/Middleware.ts +34 -0
- package/core/RequestContext.ts +5 -1
- package/core/RequestLoaders.ts +227 -93
- package/core/SchedulerManager.ts +193 -52
- package/core/cache/CacheAnalytics.ts +399 -0
- package/core/cache/CacheFactory.ts +145 -0
- package/core/cache/CacheManager.ts +520 -0
- package/core/cache/CacheProvider.ts +34 -0
- package/core/cache/CacheWarmer.ts +157 -0
- package/core/cache/CompressionUtils.ts +110 -0
- package/core/cache/MemoryCache.ts +251 -0
- package/core/cache/MultiLevelCache.ts +180 -0
- package/core/cache/NoOpCache.ts +53 -0
- package/core/cache/RedisCache.ts +464 -0
- package/core/cache/TTLStrategy.ts +254 -0
- package/core/cache/index.ts +6 -0
- package/core/components/BaseComponent.ts +120 -0
- package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
- package/core/components/Decorators.ts +88 -0
- package/core/components/Interfaces.ts +7 -0
- package/core/components/index.ts +5 -0
- package/core/decorators/EntityHooks.ts +0 -3
- package/core/decorators/IndexedField.ts +26 -0
- package/core/decorators/ScheduledTask.ts +0 -47
- package/core/events/EntityLifecycleEvents.ts +1 -1
- package/core/health.ts +112 -0
- package/core/metadata/definitions/ArcheType.ts +14 -0
- package/core/metadata/definitions/Component.ts +9 -0
- package/core/metadata/definitions/gqlObject.ts +1 -1
- package/core/metadata/index.ts +42 -1
- package/core/metadata/metadata-storage.ts +28 -2
- package/core/middleware/AccessLog.ts +59 -0
- package/core/middleware/RequestId.ts +38 -0
- package/core/middleware/SecurityHeaders.ts +62 -0
- package/core/middleware/index.ts +3 -0
- package/core/scheduler/DistributedLock.ts +266 -0
- package/core/scheduler/index.ts +15 -0
- package/core/validateEnv.ts +92 -0
- package/database/DatabaseHelper.ts +416 -40
- package/database/IndexingStrategy.ts +342 -0
- package/database/PreparedStatementCache.ts +226 -0
- package/database/index.ts +32 -7
- package/database/sqlHelpers.ts +14 -2
- package/endpoints/archetypes.ts +362 -0
- package/endpoints/components.ts +58 -0
- package/endpoints/entity.ts +80 -0
- package/endpoints/index.ts +27 -0
- package/endpoints/query.ts +93 -0
- package/endpoints/stats.ts +76 -0
- package/endpoints/tables.ts +212 -0
- package/endpoints/types.ts +155 -0
- package/gql/ArchetypeOperations.ts +32 -86
- package/gql/Generator.ts +27 -315
- package/gql/GeneratorV2.ts +37 -0
- package/gql/builders/InputTypeBuilder.ts +99 -0
- package/gql/builders/ResolverBuilder.ts +234 -0
- package/gql/builders/TypeDefBuilder.ts +105 -0
- package/gql/builders/index.ts +3 -0
- package/gql/decorators/Upload.ts +1 -1
- package/gql/depthLimit.ts +85 -0
- package/gql/graph/GraphNode.ts +224 -0
- package/gql/graph/SchemaGraph.ts +278 -0
- package/gql/helpers.ts +8 -2
- package/gql/index.ts +56 -4
- package/gql/middleware.ts +79 -0
- package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
- package/gql/orchestration/index.ts +1 -0
- package/gql/scanner/ServiceScanner.ts +347 -0
- package/gql/schema/index.ts +458 -0
- package/gql/strategies/TypeGenerationStrategy.ts +329 -0
- package/gql/types.ts +1 -0
- package/gql/utils/TypeSignature.ts +220 -0
- package/gql/utils/index.ts +1 -0
- package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
- package/gql/visitors/DeduplicationVisitor.ts +82 -0
- package/gql/visitors/GraphVisitor.ts +78 -0
- package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
- package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
- package/gql/visitors/TypeCollectorVisitor.ts +79 -0
- package/gql/visitors/VisitorComposer.ts +96 -0
- package/gql/visitors/index.ts +7 -0
- package/package.json +59 -37
- package/plugins/index.ts +2 -2
- package/query/CTENode.ts +97 -0
- package/query/ComponentInclusionNode.ts +689 -0
- package/query/FilterBuilder.ts +127 -0
- package/query/FilterBuilderRegistry.ts +202 -0
- package/query/OrNode.ts +517 -0
- package/query/OrQuery.ts +42 -0
- package/query/Query.ts +1022 -0
- package/query/QueryContext.ts +170 -0
- package/query/QueryDAG.ts +122 -0
- package/query/QueryNode.ts +65 -0
- package/query/SourceNode.ts +53 -0
- package/query/builders/FullTextSearchBuilder.ts +236 -0
- package/query/index.ts +21 -0
- package/scheduler/index.ts +40 -8
- package/service/Service.ts +2 -1
- package/service/ServiceRegistry.ts +6 -5
- package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
- package/storage/S3StorageProvider.ts +316 -0
- package/{core/storage → storage}/StorageProvider.ts +7 -3
- package/studio/bun.lock +482 -0
- package/studio/index.html +13 -0
- package/studio/package.json +39 -0
- package/studio/postcss.config.js +6 -0
- package/studio/src/components/DataTable.tsx +211 -0
- package/studio/src/components/Layout.tsx +13 -0
- package/studio/src/components/PageContainer.tsx +9 -0
- package/studio/src/components/PageHeader.tsx +13 -0
- package/studio/src/components/SearchBar.tsx +57 -0
- package/studio/src/components/Sidebar.tsx +294 -0
- package/studio/src/components/ui/button.tsx +56 -0
- package/studio/src/components/ui/checkbox.tsx +26 -0
- package/studio/src/components/ui/input.tsx +25 -0
- package/studio/src/hooks/useDataTable.ts +131 -0
- package/studio/src/index.css +36 -0
- package/studio/src/lib/api.ts +186 -0
- package/studio/src/lib/utils.ts +13 -0
- package/studio/src/main.tsx +17 -0
- package/studio/src/pages/ArcheType.tsx +239 -0
- package/studio/src/pages/Components.tsx +124 -0
- package/studio/src/pages/EntityInspector.tsx +302 -0
- package/studio/src/pages/QueryRunner.tsx +246 -0
- package/studio/src/pages/Table.tsx +94 -0
- package/studio/src/pages/Welcome.tsx +241 -0
- package/studio/src/routes.tsx +45 -0
- package/studio/src/store/archeTypeSettings.ts +30 -0
- package/studio/src/store/studio.ts +65 -0
- package/studio/src/utils/columnHelpers.tsx +114 -0
- package/studio/studio-instructions.md +81 -0
- package/studio/tailwind.config.js +77 -0
- package/studio/tsconfig.json +24 -0
- package/studio/utils.ts +54 -0
- package/studio/vite.config.js +19 -0
- package/swagger/generator.ts +1 -1
- package/tests/e2e/http.test.ts +126 -0
- package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
- package/tests/fixtures/components/TestOrder.ts +23 -0
- package/tests/fixtures/components/TestProduct.ts +23 -0
- package/tests/fixtures/components/TestUser.ts +20 -0
- package/tests/fixtures/components/index.ts +6 -0
- package/tests/graphql/SchemaGeneration.test.ts +90 -0
- package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
- package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
- package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
- package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
- package/tests/integration/entity/Entity.persistence.test.ts +333 -0
- package/tests/integration/query/Query.exec.test.ts +523 -0
- package/tests/pglite-setup.ts +61 -0
- package/tests/setup.ts +164 -0
- package/tests/stress/BenchmarkRunner.ts +203 -0
- package/tests/stress/DataSeeder.ts +190 -0
- package/tests/stress/StressTestReporter.ts +229 -0
- package/tests/stress/cursor-perf-test.ts +171 -0
- package/tests/stress/fixtures/StressTestComponents.ts +58 -0
- package/tests/stress/index.ts +7 -0
- package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
- package/tests/unit/BatchLoader.test.ts +82 -0
- package/tests/unit/archetype/ArcheType.test.ts +107 -0
- package/tests/unit/cache/CacheManager.test.ts +347 -0
- package/tests/unit/cache/MemoryCache.test.ts +260 -0
- package/tests/unit/cache/RedisCache.test.ts +411 -0
- package/tests/unit/entity/Entity.components.test.ts +244 -0
- package/tests/unit/entity/Entity.test.ts +345 -0
- package/tests/unit/gql/depthLimit.test.ts +203 -0
- package/tests/unit/gql/operationMiddleware.test.ts +293 -0
- package/tests/unit/health/Health.test.ts +129 -0
- package/tests/unit/middleware/AccessLog.test.ts +37 -0
- package/tests/unit/middleware/Middleware.test.ts +98 -0
- package/tests/unit/middleware/RequestId.test.ts +54 -0
- package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
- package/tests/unit/query/FilterBuilder.test.ts +111 -0
- package/tests/unit/query/Query.test.ts +308 -0
- package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
- package/tests/unit/schema/schema-integration.test.ts +426 -0
- package/tests/unit/schema/schema.test.ts +580 -0
- package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
- package/tests/unit/upload/RestUpload.test.ts +267 -0
- package/tests/unit/validateEnv.test.ts +82 -0
- package/tests/utils/entity-tracker.ts +57 -0
- package/tests/utils/index.ts +13 -0
- package/tests/utils/test-context.ts +149 -0
- package/tsconfig.json +5 -1
- package/types/archetype.types.ts +6 -0
- package/types/hooks.types.ts +1 -1
- package/types/query.types.ts +110 -0
- package/types/scheduler.types.ts +68 -7
- package/types/upload.types.ts +1 -0
- package/{core → upload}/FileValidator.ts +10 -1
- package/upload/RestUpload.ts +130 -0
- package/{core/components → upload}/UploadComponent.ts +11 -11
- package/{core → upload}/UploadManager.ts +3 -3
- package/upload/index.ts +23 -7
- package/utils/UploadHelper.ts +27 -6
- package/utils/cronParser.ts +16 -6
- package/.github/workflows/deploy-docs.yml +0 -57
- package/core/Components.ts +0 -202
- package/core/EntityCache.ts +0 -15
- package/core/Query.ts +0 -880
- package/docs/README.md +0 -149
- package/docs/_coverpage.md +0 -36
- package/docs/_sidebar.md +0 -23
- package/docs/api/core.md +0 -568
- package/docs/api/hooks.md +0 -554
- package/docs/api/index.md +0 -222
- package/docs/api/query.md +0 -678
- package/docs/api/service.md +0 -744
- package/docs/core-concepts/archetypes.md +0 -512
- package/docs/core-concepts/components.md +0 -498
- package/docs/core-concepts/entity.md +0 -314
- package/docs/core-concepts/hooks.md +0 -683
- package/docs/core-concepts/query.md +0 -588
- package/docs/core-concepts/services.md +0 -647
- package/docs/examples/code-examples.md +0 -425
- package/docs/getting-started.md +0 -337
- package/docs/index.html +0 -97
- package/tests/bench/insert.bench.ts +0 -60
- package/tests/bench/relations.bench.ts +0 -270
- package/tests/bench/sorting.bench.ts +0 -416
- package/tests/component-hooks-simple.test.ts +0 -117
- package/tests/component-hooks.test.ts +0 -1461
- package/tests/component.test.ts +0 -339
- package/tests/errorHandling.test.ts +0 -155
- package/tests/hooks.test.ts +0 -667
- package/tests/query-sorting.test.ts +0 -101
- package/tests/query.test.ts +0 -81
- package/tests/relations.test.ts +0 -170
- package/tests/scheduler.test.ts +0 -724
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
import type { TypeGenerationStrategy } from "../strategies/TypeGenerationStrategy";
|
|
2
|
+
import { ZodTypeStrategy } from "../strategies/TypeGenerationStrategy";
|
|
3
|
+
import { GraphVisitor } from "./GraphVisitor";
|
|
4
|
+
import type { TypeNode, OperationNode, FieldNode, InputNode, ScalarNode } from "../graph/GraphNode";
|
|
5
|
+
import { logger as MainLogger } from "../../core/Logger";
|
|
6
|
+
import * as z from "zod";
|
|
7
|
+
import { ZodWeaver, asObjectType } from "@gqloom/zod";
|
|
8
|
+
import { weave } from "@gqloom/core";
|
|
9
|
+
import type { ZodType } from "zod";
|
|
10
|
+
import { GraphQLSchema, printSchema } from "graphql";
|
|
11
|
+
import BaseArcheType from "../../core/ArcheType";
|
|
12
|
+
import { getMetadataStorage } from "../../core/metadata";
|
|
13
|
+
import { getArchetypeSchema } from "../../core/ArcheType";
|
|
14
|
+
import { isSchemaInput, collectNestedTypeDefs, type SchemaType } from "../schema";
|
|
15
|
+
|
|
16
|
+
const logger = MainLogger.child({ scope: 'SchemaGeneratorVisitor' });
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Visitor that generates the final GraphQL schema type definitions string.
|
|
20
|
+
* Replicates the V1 generation logic exactly to ensure identical output.
|
|
21
|
+
*/
|
|
22
|
+
export class SchemaGeneratorVisitor extends GraphVisitor {
|
|
23
|
+
private typeDefs: string = '\n ';
|
|
24
|
+
private queryFields: string[] = [];
|
|
25
|
+
private mutationFields: string[] = [];
|
|
26
|
+
private subscriptionFields: string[] = [];
|
|
27
|
+
private definedTypes: Set<string> = new Set();
|
|
28
|
+
private finalized: boolean = false;
|
|
29
|
+
private inputTypes: string = '';
|
|
30
|
+
private scalarTypes: Set<string> = new Set();
|
|
31
|
+
|
|
32
|
+
constructor() {
|
|
33
|
+
super();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Add archetype type definitions before visiting nodes (like V1 does)
|
|
38
|
+
*/
|
|
39
|
+
override beforeVisit(): void {
|
|
40
|
+
// Import required functions
|
|
41
|
+
const { weaveAllArchetypes, getAllArchetypeSchemas } = require('../../core/ArcheType');
|
|
42
|
+
|
|
43
|
+
// Add archetype types first (exactly like V1)
|
|
44
|
+
const fullSchema = weaveAllArchetypes();
|
|
45
|
+
if (fullSchema) {
|
|
46
|
+
const typeDefinitions = this.extractTypeDefinitions(fullSchema);
|
|
47
|
+
this.typeDefs += this.deduplicateTypeDefs(typeDefinitions.join(''));
|
|
48
|
+
} else {
|
|
49
|
+
const schemas = getAllArchetypeSchemas();
|
|
50
|
+
for (const { graphqlSchema } of schemas) {
|
|
51
|
+
const typeDefinitions = this.extractTypeDefinitions(graphqlSchema);
|
|
52
|
+
this.typeDefs += this.deduplicateTypeDefs(typeDefinitions.join(''));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Add default scalars
|
|
57
|
+
this.typeDefs += 'scalar Upload\n';
|
|
58
|
+
this.typeDefs += 'scalar Date\n';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Extract type definitions from a GraphQL schema string (V1's extractTypeDefinitions)
|
|
63
|
+
*/
|
|
64
|
+
private extractTypeDefinitions(schemaString: string): string[] {
|
|
65
|
+
const lines = schemaString.split('\n');
|
|
66
|
+
const typeDefinitions: string[] = [];
|
|
67
|
+
let currentType = '';
|
|
68
|
+
let inTypeDefinition = false;
|
|
69
|
+
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
const trimmed = line.trim();
|
|
72
|
+
if (trimmed.startsWith('type ') || trimmed.startsWith('input ') || trimmed.startsWith('enum ') ||
|
|
73
|
+
trimmed.startsWith('scalar ') || trimmed.startsWith('interface ') || trimmed.startsWith('union ')) {
|
|
74
|
+
if (inTypeDefinition) {
|
|
75
|
+
typeDefinitions.push(currentType);
|
|
76
|
+
}
|
|
77
|
+
currentType = line + '\n';
|
|
78
|
+
inTypeDefinition = true;
|
|
79
|
+
} else if (inTypeDefinition) {
|
|
80
|
+
currentType += line + '\n';
|
|
81
|
+
if (trimmed === '}') {
|
|
82
|
+
typeDefinitions.push(currentType);
|
|
83
|
+
currentType = '';
|
|
84
|
+
inTypeDefinition = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (inTypeDefinition) {
|
|
89
|
+
typeDefinitions.push(currentType);
|
|
90
|
+
}
|
|
91
|
+
return typeDefinitions;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Deduplicate all type definitions (V1's deduplicateTypeDefs)
|
|
96
|
+
*/
|
|
97
|
+
private deduplicateTypeDefs(typeDefs: string): string {
|
|
98
|
+
const lines = typeDefs.split('\n');
|
|
99
|
+
const typeDefinitions = new Map<string, string>();
|
|
100
|
+
let currentType = '';
|
|
101
|
+
let currentTypeName = '';
|
|
102
|
+
let inTypeDefinition = false;
|
|
103
|
+
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
const trimmed = line.trim();
|
|
106
|
+
const typeMatch = trimmed.match(/^(type|input|enum|scalar|interface|union)\s+(\w+)/);
|
|
107
|
+
|
|
108
|
+
if (typeMatch) {
|
|
109
|
+
// Save previous type if we were in one
|
|
110
|
+
if (inTypeDefinition && currentTypeName && !typeDefinitions.has(currentTypeName)) {
|
|
111
|
+
typeDefinitions.set(currentTypeName, currentType);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Start new type
|
|
115
|
+
currentTypeName = typeMatch[2] || '';
|
|
116
|
+
currentType = line + '\n';
|
|
117
|
+
inTypeDefinition = true;
|
|
118
|
+
} else if (inTypeDefinition) {
|
|
119
|
+
currentType += line + '\n';
|
|
120
|
+
|
|
121
|
+
// Check if this is the closing brace
|
|
122
|
+
if (trimmed === '}') {
|
|
123
|
+
if (!typeDefinitions.has(currentTypeName)) {
|
|
124
|
+
typeDefinitions.set(currentTypeName, currentType);
|
|
125
|
+
}
|
|
126
|
+
currentType = '';
|
|
127
|
+
currentTypeName = '';
|
|
128
|
+
inTypeDefinition = false;
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
// Other lines, like comments or empty lines
|
|
132
|
+
if (!typeDefinitions.has('__other')) {
|
|
133
|
+
typeDefinitions.set('__other', '');
|
|
134
|
+
}
|
|
135
|
+
typeDefinitions.set('__other', typeDefinitions.get('__other')! + line + '\n');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Handle last type if file doesn't end with closing brace
|
|
140
|
+
if (inTypeDefinition && currentTypeName && !typeDefinitions.has(currentTypeName)) {
|
|
141
|
+
typeDefinitions.set(currentTypeName, currentType);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const result = Array.from(typeDefinitions.values()).join('');
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
visitScalarNode(node: ScalarNode): void {
|
|
149
|
+
if (!this.definedTypes.has(node.id)) {
|
|
150
|
+
this.typeDefs += `scalar ${node.name}\n`;
|
|
151
|
+
this.scalarTypes.add(node.name);
|
|
152
|
+
this.definedTypes.add(node.id);
|
|
153
|
+
logger.trace(`Added scalar type: ${node.name}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
visitTypeNode(node: TypeNode): void {
|
|
158
|
+
// Archetype TypeNodes are handled by archetype weaving in beforeVisit().
|
|
159
|
+
// TypeNodes from @GraphQLObjectType must be emitted here if not already defined.
|
|
160
|
+
if (!this.definedTypes.has(node.name)) {
|
|
161
|
+
this.typeDefs += node.typeDef + '\n';
|
|
162
|
+
this.definedTypes.add(node.name);
|
|
163
|
+
logger.trace(`Added type definition: ${node.name}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
visitOperationNode(node: OperationNode): void {
|
|
168
|
+
// operationType is a property of OperationNode, not in metadata
|
|
169
|
+
const operationType = node.operationType;
|
|
170
|
+
|
|
171
|
+
// Build proper field definition from metadata (V1 style)
|
|
172
|
+
const fieldDef = this.buildFieldDefinition(node);
|
|
173
|
+
|
|
174
|
+
// OperationType enum values are QUERY, MUTATION, SUBSCRIPTION (all caps)
|
|
175
|
+
if (operationType === 'QUERY') {
|
|
176
|
+
this.queryFields.push(fieldDef);
|
|
177
|
+
logger.trace(`Added query field: ${fieldDef}`);
|
|
178
|
+
} else if (operationType === 'MUTATION') {
|
|
179
|
+
this.mutationFields.push(fieldDef);
|
|
180
|
+
logger.trace(`Added mutation field: ${fieldDef}`);
|
|
181
|
+
} else if (operationType === 'SUBSCRIPTION') {
|
|
182
|
+
this.subscriptionFields.push(fieldDef);
|
|
183
|
+
logger.trace(`Added subscription field: ${fieldDef}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
visitSubscriptionNode(node: OperationNode): void {
|
|
188
|
+
// Build proper field definition from metadata (V1 style)
|
|
189
|
+
const fieldDef = this.buildFieldDefinition(node);
|
|
190
|
+
|
|
191
|
+
this.subscriptionFields.push(fieldDef);
|
|
192
|
+
logger.trace(`Added subscription field: ${fieldDef}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
visitFieldNode(node: FieldNode): void {
|
|
196
|
+
// Field nodes don't directly contribute to schema, they're used by resolvers
|
|
197
|
+
logger.trace(`Visited field node: ${node.id}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
visitInputNode(node: InputNode): void {
|
|
201
|
+
if (!this.definedTypes.has(node.name)) {
|
|
202
|
+
this.inputTypes += node.typeDef + '\n';
|
|
203
|
+
this.definedTypes.add(node.name);
|
|
204
|
+
logger.trace(`Added input type: ${node.name}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Build a complete GraphQL field definition exactly like V1.
|
|
210
|
+
* Format: "fieldName(input: InputType!): OutputType"
|
|
211
|
+
*/
|
|
212
|
+
private buildFieldDefinition(node: OperationNode): string {
|
|
213
|
+
const name = node.name;
|
|
214
|
+
const { input, output, scalarTypes } = node.metadata;
|
|
215
|
+
let fieldDef = name;
|
|
216
|
+
|
|
217
|
+
// Handle input: Schema DSL → Zod (legacy) → Record (fallback)
|
|
218
|
+
if (input) {
|
|
219
|
+
const inputTypeName = `${name}Input`;
|
|
220
|
+
|
|
221
|
+
if (isSchemaInput(input)) {
|
|
222
|
+
// Schema DSL path
|
|
223
|
+
const result = this.generateInputFromSchema(input as Record<string, SchemaType>, inputTypeName);
|
|
224
|
+
if (!this.definedTypes.has(inputTypeName)) {
|
|
225
|
+
this.typeDefs += result.typeDefs;
|
|
226
|
+
this.definedTypes.add(inputTypeName);
|
|
227
|
+
}
|
|
228
|
+
fieldDef += `(input: ${inputTypeName}!)`;
|
|
229
|
+
} else if (input && typeof input === 'object' && '_def' in input) {
|
|
230
|
+
// Legacy Zod schema path (deprecated — use Schema DSL `t.` API instead)
|
|
231
|
+
logger.warn(`Operation "${name}" uses a Zod schema as input. This is deprecated and will be removed in a future version. Use the Schema DSL (t.string(), t.object(), etc.) instead.`);
|
|
232
|
+
const inputTypeDef = this.generateInputTypeFromZod(input as ZodType, inputTypeName, scalarTypes || new Set());
|
|
233
|
+
if (inputTypeDef) {
|
|
234
|
+
this.typeDefs += inputTypeDef;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let inputNullability = '!';
|
|
238
|
+
try {
|
|
239
|
+
const allowsUndefined = !!(input && typeof (input as any).safeParse === 'function' && (input as any).safeParse(undefined).success);
|
|
240
|
+
const allowsNull = !!(input && typeof (input as any).safeParse === 'function' && (input as any).safeParse(null).success);
|
|
241
|
+
if (allowsUndefined || allowsNull) inputNullability = '';
|
|
242
|
+
} catch (e) {
|
|
243
|
+
inputNullability = '!';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
fieldDef += `(input: ${inputTypeName}${inputNullability})`;
|
|
247
|
+
} else if (typeof input === 'object') {
|
|
248
|
+
// Fallback: Record<string, GraphQLType> format
|
|
249
|
+
const inputTypeDef = `input ${inputTypeName} {\n${Object.entries(input).map(([k, v]) => ` ${k}: ${v}`).join('\n')}\n}\n`;
|
|
250
|
+
if (!this.definedTypes.has(inputTypeName)) {
|
|
251
|
+
this.typeDefs += inputTypeDef;
|
|
252
|
+
this.definedTypes.add(inputTypeName);
|
|
253
|
+
}
|
|
254
|
+
fieldDef += `(input: ${inputTypeName}!)`;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Handle output exactly like V1
|
|
259
|
+
const outputType = this.extractOutputType(output);
|
|
260
|
+
fieldDef += `: ${outputType}`;
|
|
261
|
+
|
|
262
|
+
return fieldDef;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Extract output type exactly like V1
|
|
267
|
+
*/
|
|
268
|
+
private extractOutputType(output: any): string {
|
|
269
|
+
if (typeof output === 'string') {
|
|
270
|
+
return output;
|
|
271
|
+
} else if (Array.isArray(output)) {
|
|
272
|
+
// Handle array of archetypes: [serviceAreaArcheType]
|
|
273
|
+
const archetypeInstance = output[0];
|
|
274
|
+
const typeName = this.getArchetypeTypeName(archetypeInstance);
|
|
275
|
+
if (typeName) {
|
|
276
|
+
return `[${typeName}]`;
|
|
277
|
+
} else {
|
|
278
|
+
logger.warn(`Invalid array output type, expected archetype instance`);
|
|
279
|
+
return '[Any]';
|
|
280
|
+
}
|
|
281
|
+
} else if (output instanceof BaseArcheType) {
|
|
282
|
+
// Handle single archetype instance: serviceAreaArcheType
|
|
283
|
+
const typeName = this.getArchetypeTypeName(output);
|
|
284
|
+
if (typeName) {
|
|
285
|
+
return typeName;
|
|
286
|
+
} else {
|
|
287
|
+
logger.warn(`Could not determine type name for archetype`);
|
|
288
|
+
return 'Any';
|
|
289
|
+
}
|
|
290
|
+
} else if (typeof output === 'object' && output !== null) {
|
|
291
|
+
// Legacy object output format - V1 would generate an output type
|
|
292
|
+
// For now, return String as fallback (V1 would create outputName+"Output")
|
|
293
|
+
return 'String';
|
|
294
|
+
} else {
|
|
295
|
+
// Default case when output is not specified - assume String (like V1)
|
|
296
|
+
return 'String';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get archetype type name exactly like V1's getArchetypeTypeName
|
|
302
|
+
*/
|
|
303
|
+
private getArchetypeTypeName(archetypeInstance: any): string | null {
|
|
304
|
+
if (!archetypeInstance || !(archetypeInstance instanceof BaseArcheType)) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const storage = getMetadataStorage();
|
|
309
|
+
const className = archetypeInstance.constructor.name;
|
|
310
|
+
|
|
311
|
+
// Look up the archetype metadata by class name to get the custom name
|
|
312
|
+
const archetypeMetadata = storage.archetypes.find(a => a.target?.name === className);
|
|
313
|
+
|
|
314
|
+
if (archetypeMetadata?.name) {
|
|
315
|
+
// Use the custom name from @ArcheType("CustomName") decorator
|
|
316
|
+
logger.trace(`Found custom archetype name: ${archetypeMetadata.name} for class ${className}`);
|
|
317
|
+
|
|
318
|
+
// Ensure schema is generated and cached
|
|
319
|
+
try {
|
|
320
|
+
if (!getArchetypeSchema(archetypeMetadata.name)) {
|
|
321
|
+
archetypeInstance.getZodObjectSchema();
|
|
322
|
+
}
|
|
323
|
+
} catch (error) {
|
|
324
|
+
logger.warn(`Failed to generate schema for archetype ${archetypeMetadata.name}: ${error}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return archetypeMetadata.name;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Fallback: infer from class name
|
|
331
|
+
const inferredName = className.replace(/ArcheType$/, '');
|
|
332
|
+
logger.trace(`Using inferred archetype name: ${inferredName} for class ${className}`);
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
if (!getArchetypeSchema(inferredName)) {
|
|
336
|
+
archetypeInstance.getZodObjectSchema();
|
|
337
|
+
}
|
|
338
|
+
} catch (error) {
|
|
339
|
+
logger.warn(`Failed to generate schema for archetype ${inferredName}: ${error}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return inferredName;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Generate input type from Schema DSL definitions.
|
|
347
|
+
* Collects nested type definitions (depth-first) and builds a Zod schema for validation.
|
|
348
|
+
*/
|
|
349
|
+
private generateInputFromSchema(
|
|
350
|
+
input: Record<string, SchemaType>,
|
|
351
|
+
inputName: string,
|
|
352
|
+
): { typeDefs: string; zodSchema: ZodType } {
|
|
353
|
+
const collected = collectNestedTypeDefs(input);
|
|
354
|
+
|
|
355
|
+
// Build the top-level input type
|
|
356
|
+
const fields = Object.entries(input)
|
|
357
|
+
.map(([key, schema]) => ` ${key}: ${schema.toGraphQL()}`)
|
|
358
|
+
.join("\n");
|
|
359
|
+
collected.set(inputName, `input ${inputName} {\n${fields}\n}`);
|
|
360
|
+
|
|
361
|
+
// Build Zod schema for runtime validation
|
|
362
|
+
const zodShape: Record<string, ZodType> = {};
|
|
363
|
+
for (const [key, schema] of Object.entries(input)) {
|
|
364
|
+
zodShape[key] = schema.toZod();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
typeDefs: Array.from(collected.values()).join("\n\n") + "\n",
|
|
369
|
+
zodSchema: z.object(zodShape),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Generate input type from Zod schema exactly like V1
|
|
375
|
+
* Returns the generated typedef string, or empty string if already defined
|
|
376
|
+
*/
|
|
377
|
+
private generateInputTypeFromZod(zodSchema: ZodType, inputName: string, scalarTypes: Set<string>): string {
|
|
378
|
+
try {
|
|
379
|
+
// Store original input for validation (before any preprocessing)
|
|
380
|
+
let originalInput: any = zodSchema;
|
|
381
|
+
|
|
382
|
+
// Add __typename to input zod object, handling optional schemas
|
|
383
|
+
let innerInput: any = zodSchema;
|
|
384
|
+
const wasOptional = zodSchema instanceof z.ZodOptional;
|
|
385
|
+
if (wasOptional) {
|
|
386
|
+
innerInput = (zodSchema as any).unwrap();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Preprocess schema: Replace z.union containing scalar literals with z.string() recursively
|
|
390
|
+
// This is needed because GQLoom's weave doesn't handle z.union or z.literal well
|
|
391
|
+
// We'll replace the String type with the actual scalar type after weaving
|
|
392
|
+
const scalarFieldNames = new Map<string, string>(); // Track which fields should be scalars (full path)
|
|
393
|
+
|
|
394
|
+
// Recursive function to process schemas and handle all Zod wrapper types
|
|
395
|
+
const processSchema = (schema: any, path: string[] = []): any => {
|
|
396
|
+
if (!schema) return schema;
|
|
397
|
+
|
|
398
|
+
// Handle the case where _def is missing or empty (can happen with .omit()/.extend())
|
|
399
|
+
if (!schema._def || (typeof schema._def === 'object' && Object.keys(schema._def).length === 0)) {
|
|
400
|
+
// Try to identify the schema type by checking other properties
|
|
401
|
+
// In Zod v4, some schemas have a 'parse' method and other identifying features
|
|
402
|
+
const hasShape = typeof schema.shape === 'object' || typeof schema.shape === 'function';
|
|
403
|
+
const hasElement = typeof schema.element === 'object';
|
|
404
|
+
const schemaStr = typeof schema === 'string' ? `"${schema}"` : (typeof schema === 'object' ? JSON.stringify(schema).slice(0, 200) : String(schema));
|
|
405
|
+
logger.trace(`Schema at path ${path.join('.')} has empty _def, hasShape=${hasShape}, hasElement=${hasElement}, type=${typeof schema}, value=${schemaStr}`);
|
|
406
|
+
|
|
407
|
+
// If the schema is actually a string literal value (not a Zod schema), it's likely a z.literal value
|
|
408
|
+
if (typeof schema === 'string') {
|
|
409
|
+
logger.trace(`Found string literal at ${path.join('.')}: "${schema}", converting to z.string()`);
|
|
410
|
+
return z.string();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// If the schema is an object without _def, it might be the raw element value, not a Zod schema
|
|
414
|
+
// For arrays from .extend(), the element type might be lost - use z.any() as fallback
|
|
415
|
+
if (typeof schema === 'object' && !schema._def) {
|
|
416
|
+
// Check if it's an array-like object (has numeric keys)
|
|
417
|
+
const numericKeys = Object.keys(schema).filter(k => !isNaN(Number(k)));
|
|
418
|
+
if (numericKeys.length > 0) {
|
|
419
|
+
// This might be a tuple or array with positional elements - treat as z.any()
|
|
420
|
+
logger.trace(`Found array-like object at ${path.join('.')}, using z.any()`);
|
|
421
|
+
return z.any();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// If it looks like an object with a shape, try to process it as an object
|
|
425
|
+
if (hasShape) {
|
|
426
|
+
const shape = typeof schema.shape === 'function' ? schema.shape() : schema.shape;
|
|
427
|
+
if (shape && Object.keys(shape).length > 0) {
|
|
428
|
+
const processedShape: any = {};
|
|
429
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
430
|
+
processedShape[key] = processSchema(value, [...path, key]);
|
|
431
|
+
}
|
|
432
|
+
const newSchema = z.object(processedShape);
|
|
433
|
+
const baseInputName = inputName.replace(/Input$/, '');
|
|
434
|
+
const nestedTypeName = `${baseInputName}_${path.join('_')}Input`;
|
|
435
|
+
const registered = newSchema.register(asObjectType, { name: nestedTypeName });
|
|
436
|
+
logger.trace(`Recovered broken object schema at ${path.join('.')}, registered as ${nestedTypeName}`);
|
|
437
|
+
return registered;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Last resort - return z.any() to prevent crashes
|
|
443
|
+
logger.trace(`Could not recover schema at ${path.join('.')}, using z.any() as fallback`);
|
|
444
|
+
return z.any();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const typeName = schema._def?.typeName || schema._def?.type;
|
|
448
|
+
|
|
449
|
+
// Handle wrapper types (optional, nullable, default, etc.) - process inner type and re-wrap
|
|
450
|
+
if (typeName === 'ZodOptional' || typeName === 'optional') {
|
|
451
|
+
const inner = processSchema(schema._def.innerType, path);
|
|
452
|
+
return inner.optional();
|
|
453
|
+
}
|
|
454
|
+
if (typeName === 'ZodNullable' || typeName === 'nullable') {
|
|
455
|
+
const inner = processSchema(schema._def.innerType, path);
|
|
456
|
+
return inner.nullable();
|
|
457
|
+
}
|
|
458
|
+
if (typeName === 'ZodDefault' || typeName === 'default') {
|
|
459
|
+
const inner = processSchema(schema._def.innerType, path);
|
|
460
|
+
return inner.default(schema._def.defaultValue());
|
|
461
|
+
}
|
|
462
|
+
if (typeName === 'ZodEffects' || typeName === 'effects') {
|
|
463
|
+
// Effects (transform, refine, etc.) - process the inner schema
|
|
464
|
+
const inner = processSchema(schema._def.schema, path);
|
|
465
|
+
// Return as-is since we can't easily re-apply effects
|
|
466
|
+
return inner;
|
|
467
|
+
}
|
|
468
|
+
if (typeName === 'ZodArray' || typeName === 'array') {
|
|
469
|
+
// Arrays with complex nested objects can have broken _def.type
|
|
470
|
+
// In such cases, return the original schema and let GQLoom handle it
|
|
471
|
+
const elementSchema = schema._def.type;
|
|
472
|
+
if (!elementSchema || !elementSchema._def || Object.keys(elementSchema._def).length === 0) {
|
|
473
|
+
logger.trace(`Array at path ${path.join('.')} has broken element schema, returning original to let GQLoom handle it`);
|
|
474
|
+
return schema;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const elementTypeName = elementSchema._def.typeName || elementSchema._def.type;
|
|
478
|
+
logger.trace(`Array at path ${path.join('.')} (inputName: ${inputName}): element typeName = ${elementTypeName}`);
|
|
479
|
+
|
|
480
|
+
// For array elements, process and register nested objects
|
|
481
|
+
const arrayElementPath = path.length > 0 ? path : ['Item'];
|
|
482
|
+
const inner = processSchema(elementSchema, arrayElementPath);
|
|
483
|
+
logger.trace(`Processed array element at path ${path.join('.')}, inner type: ${inner._def?.typeName || inner._def?.type}, has _zod: ${!!(inner as any)._zod}`);
|
|
484
|
+
return z.array(inner);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Handle unions containing scalar literals
|
|
488
|
+
if (typeName === 'ZodUnion' || typeName === 'union') {
|
|
489
|
+
const options = schema._def?.options || [];
|
|
490
|
+
let foundScalarName = null;
|
|
491
|
+
|
|
492
|
+
for (const option of options) {
|
|
493
|
+
const optionTypeName = option._def?.typeName || option._def?.type;
|
|
494
|
+
if (optionTypeName === 'ZodLiteral' || optionTypeName === 'literal') {
|
|
495
|
+
const value = option._def?.value ?? (option._def?.values ? option._def.values[0] : undefined);
|
|
496
|
+
if (typeof value === 'string' && scalarTypes.has(value)) {
|
|
497
|
+
foundScalarName = value;
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (foundScalarName) {
|
|
504
|
+
scalarFieldNames.set(path.join('.') || path[path.length - 1] || 'root', foundScalarName);
|
|
505
|
+
return z.string();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Process each option in the union recursively (for non-scalar unions)
|
|
509
|
+
const processedOptions = options.map((opt: any) => processSchema(opt, path));
|
|
510
|
+
// Can't easily recreate union, return original if no scalar found
|
|
511
|
+
return schema;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Handle objects recursively - ALWAYS register nested objects with GQLoom
|
|
515
|
+
// because after .omit()/.extend() the schema tree loses _zod metadata
|
|
516
|
+
if (typeName === 'ZodObject' || typeName === 'object') {
|
|
517
|
+
// Only check for matching registered types on NESTED objects (path.length > 0)
|
|
518
|
+
// Root-level input types should always use their own generated name
|
|
519
|
+
// This prevents operations like reverseGeocoding({ latitude, longitude }) from
|
|
520
|
+
// incorrectly matching ST_PointInput
|
|
521
|
+
if (path.length > 0) {
|
|
522
|
+
const { findMatchingInputType } = require('../../core/ArcheType');
|
|
523
|
+
const matchingTypeName = findMatchingInputType(schema);
|
|
524
|
+
|
|
525
|
+
if (matchingTypeName) {
|
|
526
|
+
// Reuse the registered type - register with the existing type name
|
|
527
|
+
const registeredSchema = schema.register(asObjectType, { name: matchingTypeName });
|
|
528
|
+
logger.trace(`Matched nested object to registered type: ${matchingTypeName} at path ${path.join('.')}`);
|
|
529
|
+
return registeredSchema;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
let shape;
|
|
534
|
+
try {
|
|
535
|
+
shape = typeof schema._def.shape === 'function' ? schema._def.shape() : schema._def.shape;
|
|
536
|
+
} catch (error) {
|
|
537
|
+
// Zod v4 can throw errors when accessing .shape() on schemas modified with .partial().extend()
|
|
538
|
+
// In this case, try alternative methods to access the shape
|
|
539
|
+
logger.warn(`Failed to access shape at path ${path.join('.')}: ${error instanceof Error ? error.message : String(error)}`);
|
|
540
|
+
|
|
541
|
+
// Try alternative: use the schema's shape property directly (without calling _def.shape)
|
|
542
|
+
try {
|
|
543
|
+
if (schema.shape && typeof schema.shape === 'object') {
|
|
544
|
+
shape = schema.shape;
|
|
545
|
+
} else if (schema._shape && typeof schema._shape === 'object') {
|
|
546
|
+
shape = schema._shape;
|
|
547
|
+
}
|
|
548
|
+
} catch (altError) {
|
|
549
|
+
logger.warn(`Alternative shape access also failed: ${altError instanceof Error ? altError.message : String(altError)}`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// If all attempts failed, return the original schema and let GQLoom handle it
|
|
553
|
+
if (!shape) {
|
|
554
|
+
logger.trace(`Could not access shape, returning original schema at path ${path.join('.')}`);
|
|
555
|
+
return schema;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (shape) {
|
|
560
|
+
const processedShape: any = {};
|
|
561
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
562
|
+
const processed = processSchema(value, [...path, key]);
|
|
563
|
+
processedShape[key] = processed;
|
|
564
|
+
}
|
|
565
|
+
// Always create new object and register with GQLoom
|
|
566
|
+
// This ensures all nested objects have proper _zod metadata
|
|
567
|
+
const newSchema = z.object(processedShape);
|
|
568
|
+
|
|
569
|
+
// Check again after processing children - the processed schema might match a registered type
|
|
570
|
+
// Only for nested objects (path.length > 0), not root-level inputs
|
|
571
|
+
if (path.length > 0) {
|
|
572
|
+
const { findMatchingInputType } = require('../../core/ArcheType');
|
|
573
|
+
const processedMatchingTypeName = findMatchingInputType(newSchema);
|
|
574
|
+
if (processedMatchingTypeName) {
|
|
575
|
+
const registeredSchema = newSchema.register(asObjectType, { name: processedMatchingTypeName });
|
|
576
|
+
logger.trace(`Matched processed nested object to registered type: ${processedMatchingTypeName} at path ${path.join('.')}`);
|
|
577
|
+
return registeredSchema;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Remove 'Input' suffix from inputName to avoid duplication when building nested type name
|
|
582
|
+
const baseInputName = inputName.replace(/Input$/, '');
|
|
583
|
+
const nestedTypeName = path.length > 0 ? `${baseInputName}_${path.join('_')}Input` : inputName;
|
|
584
|
+
const registeredSchema = newSchema.register(asObjectType, { name: nestedTypeName });
|
|
585
|
+
logger.trace(`Registered nested object ${nestedTypeName} for input ${inputName}, path: ${path.join('.')}, has _zod: ${!!(registeredSchema as any)._zod}`);
|
|
586
|
+
return registeredSchema;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return schema;
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
innerInput = processSchema(innerInput);
|
|
594
|
+
|
|
595
|
+
// Debug: log the processed schema structure for problematic inputs
|
|
596
|
+
if (inputName === 'uploadDriverDocumentInput' || inputName === 'getFaresInput') {
|
|
597
|
+
try {
|
|
598
|
+
const debugSchema = (s: any, indent: string = ''): string => {
|
|
599
|
+
if (!s) return 'null';
|
|
600
|
+
const def = s._def || {};
|
|
601
|
+
const typeName = def.typeName || def.type || 'unknown';
|
|
602
|
+
let result = `${typeName}`;
|
|
603
|
+
if (typeName === 'ZodObject' || typeName === 'object') {
|
|
604
|
+
const shape = typeof def.shape === 'function' ? def.shape() : def.shape;
|
|
605
|
+
if (shape) {
|
|
606
|
+
const entries = Object.entries(shape);
|
|
607
|
+
result += ` { ${entries.map(([k, v]) => `${k}: ${debugSchema(v, '')}`).join(', ')} }`;
|
|
608
|
+
}
|
|
609
|
+
} else if (typeName === 'ZodArray' || typeName === 'array') {
|
|
610
|
+
const elem = def.type;
|
|
611
|
+
result += `[${debugSchema(elem, '')}]`;
|
|
612
|
+
} else if (typeName === 'ZodOptional' || typeName === 'optional') {
|
|
613
|
+
result = `Optional<${debugSchema(def.innerType, '')}>`;
|
|
614
|
+
}
|
|
615
|
+
result += `(_zod:${!!(s as any)._zod})`;
|
|
616
|
+
return result;
|
|
617
|
+
};
|
|
618
|
+
logger.trace(`DEBUG getFaresInput structure: ${debugSchema(innerInput)}`);
|
|
619
|
+
} catch (e) {
|
|
620
|
+
logger.trace(`DEBUG getFaresInput error: ${e}`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// DO NOT call .extend() or .register() after processSchema - they create new schema instances
|
|
625
|
+
// and can destroy the _zod metadata on nested objects that we just registered.
|
|
626
|
+
// The weave() function will handle the root level registration automatically.
|
|
627
|
+
|
|
628
|
+
const input = wasOptional ? innerInput.optional() : innerInput;
|
|
629
|
+
|
|
630
|
+
logger.trace(`Weaving Zod schema for ${inputName}`);
|
|
631
|
+
const gqlInputSchema = weave(ZodWeaver, input as ZodType) as GraphQLSchema;
|
|
632
|
+
const schemaString = printSchema(gqlInputSchema);
|
|
633
|
+
logger.trace(`Schema string for ${inputName}: ${schemaString}`);
|
|
634
|
+
|
|
635
|
+
// Collect custom type names
|
|
636
|
+
const typeNames: string[] = [];
|
|
637
|
+
schemaString.replace(/type (\w+)/g, (match, name) => {
|
|
638
|
+
typeNames.push(name);
|
|
639
|
+
return match;
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// Convert all type definitions to input types with Input suffix for non-Input types
|
|
643
|
+
let inputTypeDefs = schemaString.replace(/\btype\b/g, 'input');
|
|
644
|
+
inputTypeDefs = inputTypeDefs.replace(/input (\w+)/g, (match, name) => {
|
|
645
|
+
if (name.endsWith('Input')) {
|
|
646
|
+
return `input ${name}`;
|
|
647
|
+
} else {
|
|
648
|
+
return `input ${name}Input`;
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// Update field types for custom types
|
|
653
|
+
inputTypeDefs = inputTypeDefs.replace(/: (\[?)(\w+)([!\[\]]*)(\s|$)/g, (match, bracketStart, type, suffix, end) => {
|
|
654
|
+
if (typeNames.includes(type)) {
|
|
655
|
+
return `: ${bracketStart}${type.endsWith('Input') ? type : type + 'Input'}${suffix}${end}`;
|
|
656
|
+
} else {
|
|
657
|
+
return match;
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// Replace String with scalar types for fields we tracked during preprocessing
|
|
662
|
+
for (const [fieldPath, scalarName] of scalarFieldNames.entries()) {
|
|
663
|
+
const fieldName = fieldPath.split('.').pop()!;
|
|
664
|
+
inputTypeDefs = inputTypeDefs.replace(
|
|
665
|
+
new RegExp(`(\\s+${fieldName}:\\s+)String(!?)`, 'g'),
|
|
666
|
+
`$1${scalarName}$2`
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Post-process to handle z.literal scalars
|
|
671
|
+
// Use the original input before __typename was added
|
|
672
|
+
let schemaToTraverse: any = originalInput;
|
|
673
|
+
|
|
674
|
+
// Unwrap optional if needed
|
|
675
|
+
const defType = (schemaToTraverse._def as any)?.typeName || (schemaToTraverse._def as any)?.type;
|
|
676
|
+
if (defType === 'ZodOptional' || defType === 'optional') {
|
|
677
|
+
schemaToTraverse = (schemaToTraverse as any)._def.innerType;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Find all z.literal fields that match scalar names
|
|
681
|
+
const literalFields: Record<string, string> = {};
|
|
682
|
+
function traverseZod(obj: any, path: string[] = []) {
|
|
683
|
+
if (!obj || !obj._def) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const typeName = (obj._def as any).typeName || (obj._def as any).type;
|
|
687
|
+
if (typeName === 'ZodLiteral' || typeName === 'literal') {
|
|
688
|
+
// Zod v3 uses 'value', Zod v4 uses 'values' array
|
|
689
|
+
const defObj = obj._def as any;
|
|
690
|
+
const value = defObj.value ?? (defObj.values ? defObj.values[0] : undefined);
|
|
691
|
+
if (typeof value === 'string' && scalarTypes.has(value)) {
|
|
692
|
+
literalFields[path.join('.')] = value;
|
|
693
|
+
}
|
|
694
|
+
} else if (typeName === 'ZodUnion' || typeName === 'union') {
|
|
695
|
+
// Handle z.union - check if any option is a literal scalar
|
|
696
|
+
const options = (obj._def as any).options || [];
|
|
697
|
+
for (const option of options) {
|
|
698
|
+
const optionTypeName = (option._def as any)?.typeName || (option._def as any)?.type;
|
|
699
|
+
if (optionTypeName === 'ZodLiteral' || optionTypeName === 'literal') {
|
|
700
|
+
const defObj = option._def as any;
|
|
701
|
+
const value = defObj.value ?? (defObj.values ? defObj.values[0] : undefined);
|
|
702
|
+
if (typeof value === 'string' && scalarTypes.has(value)) {
|
|
703
|
+
literalFields[path.join('.')] = value;
|
|
704
|
+
break; // Found a scalar literal, use it
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
} else if (typeName === 'ZodObject' || typeName === 'object') {
|
|
709
|
+
const shape = typeof obj._def.shape === 'function' ? obj._def.shape() : obj._def.shape;
|
|
710
|
+
if (shape) {
|
|
711
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
712
|
+
traverseZod(value, [...path, key]);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
traverseZod(schemaToTraverse);
|
|
718
|
+
|
|
719
|
+
// Replace in typeDefs
|
|
720
|
+
for (const [fieldPath, scalarName] of Object.entries(literalFields)) {
|
|
721
|
+
const fieldName = fieldPath.split('.').pop()!;
|
|
722
|
+
inputTypeDefs = inputTypeDefs.replace(new RegExp(`(\\s+${fieldName}:\\s+)String!`, 'g'), `$1${scalarName}!`);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Deduplicate input types - deduplicateInputTypes updates this.definedTypes and returns only new types
|
|
726
|
+
const deduplicatedInputTypeDefs = this.deduplicateInputTypes(inputTypeDefs);
|
|
727
|
+
|
|
728
|
+
logger.trace(`Successfully generated input types for ${inputName}`);
|
|
729
|
+
return deduplicatedInputTypeDefs;
|
|
730
|
+
} catch (error) {
|
|
731
|
+
logger.error(`Failed to weave Zod schema for ${inputName}: ${error}`);
|
|
732
|
+
logger.error(`Error stack: ${error instanceof Error ? error.stack : 'No stack'}`);
|
|
733
|
+
// Fallback: generate basic input type only if not already defined
|
|
734
|
+
if (!this.definedTypes.has(inputName)) {
|
|
735
|
+
this.definedTypes.add(inputName);
|
|
736
|
+
return `input ${inputName} { _placeholder: String }\n`;
|
|
737
|
+
}
|
|
738
|
+
return '';
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Deduplicate input type definitions exactly like V1's deduplicateInputTypes
|
|
744
|
+
*/
|
|
745
|
+
private deduplicateInputTypes(inputTypeDefs: string): string {
|
|
746
|
+
const lines = inputTypeDefs.split('\n');
|
|
747
|
+
const result: string[] = [];
|
|
748
|
+
let currentType = '';
|
|
749
|
+
let currentTypeName = '';
|
|
750
|
+
let inTypeDefinition = false;
|
|
751
|
+
|
|
752
|
+
for (const line of lines) {
|
|
753
|
+
const trimmed = line.trim();
|
|
754
|
+
|
|
755
|
+
// Check if this is the start of an input/enum definition
|
|
756
|
+
const typeMatch = trimmed.match(/^(input|enum)\s+(\w+)/);
|
|
757
|
+
|
|
758
|
+
if (typeMatch) {
|
|
759
|
+
// Save previous type if we were in one
|
|
760
|
+
if (inTypeDefinition && currentTypeName && !this.definedTypes.has(currentTypeName)) {
|
|
761
|
+
result.push(currentType);
|
|
762
|
+
this.definedTypes.add(currentTypeName);
|
|
763
|
+
logger.trace(`Added input type definition: ${currentTypeName}`);
|
|
764
|
+
} else if (inTypeDefinition && currentTypeName && this.definedTypes.has(currentTypeName)) {
|
|
765
|
+
logger.trace(`Skipped duplicate input type definition: ${currentTypeName}`);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Start new type
|
|
769
|
+
currentTypeName = typeMatch[2] || '';
|
|
770
|
+
currentType = line + '\n';
|
|
771
|
+
inTypeDefinition = true;
|
|
772
|
+
} else if (inTypeDefinition) {
|
|
773
|
+
currentType += line + '\n';
|
|
774
|
+
|
|
775
|
+
// Check if this is the closing brace
|
|
776
|
+
if (trimmed === '}' || trimmed === '') {
|
|
777
|
+
// End of type definition (closing brace or empty line after enum)
|
|
778
|
+
if (trimmed === '}' && !this.definedTypes.has(currentTypeName)) {
|
|
779
|
+
result.push(currentType);
|
|
780
|
+
this.definedTypes.add(currentTypeName);
|
|
781
|
+
logger.trace(`Added input type definition: ${currentTypeName}`);
|
|
782
|
+
} else if (trimmed === '}' && this.definedTypes.has(currentTypeName)) {
|
|
783
|
+
logger.trace(`Skipped duplicate input type definition: ${currentTypeName}`);
|
|
784
|
+
}
|
|
785
|
+
currentType = '';
|
|
786
|
+
currentTypeName = '';
|
|
787
|
+
inTypeDefinition = false;
|
|
788
|
+
}
|
|
789
|
+
} else {
|
|
790
|
+
// Not in a type definition, just add the line (could be comments, etc.)
|
|
791
|
+
if (trimmed !== '') {
|
|
792
|
+
result.push(line + '\n');
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Handle last type if file doesn't end with closing brace
|
|
798
|
+
if (inTypeDefinition && currentTypeName && !this.definedTypes.has(currentTypeName)) {
|
|
799
|
+
result.push(currentType);
|
|
800
|
+
this.definedTypes.add(currentTypeName);
|
|
801
|
+
logger.trace(`Added input type definition: ${currentTypeName}`);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return result.join('');
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Get the complete typeDefs string exactly like V1
|
|
809
|
+
*/
|
|
810
|
+
getTypeDefs(): string {
|
|
811
|
+
// Only finalize once to avoid duplicating Query/Mutation/Subscription types
|
|
812
|
+
if (!this.finalized) {
|
|
813
|
+
// Add Query/Mutation/Subscription types
|
|
814
|
+
if (this.queryFields.length > 0) {
|
|
815
|
+
// Sort fields alphabetically (like V1)
|
|
816
|
+
this.queryFields.sort();
|
|
817
|
+
this.typeDefs += `type Query {\n${this.queryFields.map(f => ` ${f}`).join('\n')}\n}\n`;
|
|
818
|
+
} else {
|
|
819
|
+
// Always add at least an empty Query type for GraphQL schema validity
|
|
820
|
+
this.typeDefs += `type Query {\n _empty: String\n}\n`;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (this.mutationFields.length > 0) {
|
|
824
|
+
// Sort fields alphabetically (like V1)
|
|
825
|
+
this.mutationFields.sort();
|
|
826
|
+
this.typeDefs += `type Mutation {\n${this.mutationFields.map(f => ` ${f}`).join('\n')}\n}\n`;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
if (this.subscriptionFields.length > 0) {
|
|
830
|
+
// Sort fields alphabetically (like V1)
|
|
831
|
+
this.subscriptionFields.sort();
|
|
832
|
+
this.typeDefs += `type Subscription {\n${this.subscriptionFields.map(f => ` ${f}`).join('\n')}\n}\n`;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
this.finalized = true;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return this.typeDefs;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Get results in the format expected by the orchestrator
|
|
843
|
+
*/
|
|
844
|
+
getResults(): { typeDefs: string; inputTypes?: string; scalarTypes?: string[] } {
|
|
845
|
+
return {
|
|
846
|
+
typeDefs: this.getTypeDefs(),
|
|
847
|
+
inputTypes: this.inputTypes,
|
|
848
|
+
scalarTypes: Array.from(this.scalarTypes)
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
}
|