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,241 @@
|
|
|
1
|
+
import { GraphQLSchema } from "graphql";
|
|
2
|
+
import { createSchema } from "graphql-yoga";
|
|
3
|
+
import { logger } from "../../core/Logger";
|
|
4
|
+
import { ServiceScanner } from "../scanner/ServiceScanner";
|
|
5
|
+
import { SchemaGraph } from "../graph/SchemaGraph";
|
|
6
|
+
import { VisitorComposer } from "../visitors/VisitorComposer";
|
|
7
|
+
import { ArchetypePreprocessorVisitor } from "../visitors/ArchetypePreprocessorVisitor";
|
|
8
|
+
import { DeduplicationVisitor } from "../visitors/DeduplicationVisitor";
|
|
9
|
+
import { SchemaGeneratorVisitor } from "../visitors/SchemaGeneratorVisitor";
|
|
10
|
+
import { ResolverGeneratorVisitor } from "../visitors/ResolverGeneratorVisitor";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Orchestrates the complete GraphQL schema generation process using the graph-based architecture.
|
|
14
|
+
* Coordinates service scanning, visitor execution, and final schema assembly.
|
|
15
|
+
*
|
|
16
|
+
* This class implements the high-level workflow for Phase 6 of the refactor.
|
|
17
|
+
*/
|
|
18
|
+
export class GraphQLSchemaOrchestrator {
|
|
19
|
+
private serviceScanner: ServiceScanner;
|
|
20
|
+
private schemaGraph: SchemaGraph;
|
|
21
|
+
private services: any[] = [];
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
this.schemaGraph = new SchemaGraph();
|
|
25
|
+
this.serviceScanner = new ServiceScanner(this.schemaGraph);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generate a complete GraphQL schema from service instances.
|
|
30
|
+
* This is the main entry point that orchestrates the entire generation process.
|
|
31
|
+
*/
|
|
32
|
+
generateSchema(services: any[]): GraphQLSchema | null {
|
|
33
|
+
try {
|
|
34
|
+
logger.info("Starting GraphQL schema generation with orchestrator");
|
|
35
|
+
|
|
36
|
+
// Store services for use in visitors
|
|
37
|
+
this.services = services;
|
|
38
|
+
|
|
39
|
+
// Phase 1: Build graph from services
|
|
40
|
+
this.buildGraphFromServices(services);
|
|
41
|
+
|
|
42
|
+
// Phase 2: Run preprocessing visitors
|
|
43
|
+
this.runPreprocessingVisitors();
|
|
44
|
+
|
|
45
|
+
// Phase 3: Run generation visitors
|
|
46
|
+
const generationResults = this.runGenerationVisitors();
|
|
47
|
+
|
|
48
|
+
// Phase 4: Sort operations alphabetically
|
|
49
|
+
this.sortOperationsAlphabetically(generationResults);
|
|
50
|
+
|
|
51
|
+
// Phase 5: Create final GraphQL schema
|
|
52
|
+
const schema = this.createGraphQLSchema(generationResults);
|
|
53
|
+
|
|
54
|
+
logger.info("GraphQL schema generation completed successfully");
|
|
55
|
+
return schema;
|
|
56
|
+
|
|
57
|
+
} catch (error) {
|
|
58
|
+
logger.error(`Failed to generate GraphQL schema: ${error instanceof Error ? error.message : String(error)}`);
|
|
59
|
+
throw new Error(`Schema generation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Phase 1: Build the schema graph from service instances using the ServiceScanner.
|
|
65
|
+
*/
|
|
66
|
+
private buildGraphFromServices(services: any[]): void {
|
|
67
|
+
|
|
68
|
+
// Clear any existing nodes
|
|
69
|
+
this.schemaGraph.clear();
|
|
70
|
+
|
|
71
|
+
// Scan all services - this adds nodes directly to the graph
|
|
72
|
+
this.serviceScanner.scanServices(services);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Phase 2: Run preprocessing visitors to prepare the graph for generation.
|
|
77
|
+
*/
|
|
78
|
+
private runPreprocessingVisitors(): void {
|
|
79
|
+
|
|
80
|
+
const composer = new VisitorComposer();
|
|
81
|
+
|
|
82
|
+
// Add preprocessing visitors
|
|
83
|
+
composer.addVisitor(new ArchetypePreprocessorVisitor());
|
|
84
|
+
composer.addVisitor(new DeduplicationVisitor());
|
|
85
|
+
|
|
86
|
+
// Run visitors on the graph
|
|
87
|
+
composer.visitGraph(this.schemaGraph);
|
|
88
|
+
|
|
89
|
+
// Get results and apply any necessary modifications
|
|
90
|
+
const results = composer.getResults();
|
|
91
|
+
|
|
92
|
+
// Log preprocessing results
|
|
93
|
+
const archetypeResults = results["visitor-0"];
|
|
94
|
+
const deduplicationResults = results["visitor-1"];
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Phase 3: Run generation visitors to produce typeDefs and resolvers.
|
|
101
|
+
*/
|
|
102
|
+
private runGenerationVisitors(): {
|
|
103
|
+
typeDefs: string;
|
|
104
|
+
resolvers: Record<string, any>;
|
|
105
|
+
} {
|
|
106
|
+
|
|
107
|
+
const composer = new VisitorComposer();
|
|
108
|
+
|
|
109
|
+
// Add generation visitors
|
|
110
|
+
composer.addVisitor(new SchemaGeneratorVisitor());
|
|
111
|
+
composer.addVisitor(new ResolverGeneratorVisitor(this.services));
|
|
112
|
+
|
|
113
|
+
// Run visitors on the graph
|
|
114
|
+
composer.visitGraph(this.schemaGraph);
|
|
115
|
+
|
|
116
|
+
// Get results
|
|
117
|
+
const results = composer.getResults();
|
|
118
|
+
const schemaResults = results["visitor-0"];
|
|
119
|
+
const resolverResults = results["visitor-1"];
|
|
120
|
+
|
|
121
|
+
// Add field resolvers from services (for archetype field resolvers)
|
|
122
|
+
this.addFieldResolvers(resolverResults);
|
|
123
|
+
|
|
124
|
+
// Filter out empty resolver types to avoid schema validation errors
|
|
125
|
+
const filteredResolvers: Record<string, any> = {};
|
|
126
|
+
for (const [type, typeResolvers] of Object.entries(resolverResults || {})) {
|
|
127
|
+
if (typeResolvers && Object.keys(typeResolvers).length > 0) {
|
|
128
|
+
filteredResolvers[type] = typeResolvers;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
typeDefs: schemaResults?.typeDefs || "",
|
|
136
|
+
resolvers: filteredResolvers
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Add field resolvers from services (registered via archetype.registerFieldResolvers)
|
|
142
|
+
*/
|
|
143
|
+
private addFieldResolvers(resolvers: Record<string, any>): void {
|
|
144
|
+
for (const service of this.services) {
|
|
145
|
+
const fields = service.__graphqlFields || service.constructor.prototype.__graphqlFields;
|
|
146
|
+
if (!fields) continue;
|
|
147
|
+
|
|
148
|
+
for (const fieldMeta of fields) {
|
|
149
|
+
const { type, field, propertyKey } = fieldMeta;
|
|
150
|
+
|
|
151
|
+
// Ensure the type exists in resolvers
|
|
152
|
+
if (!resolvers[type]) {
|
|
153
|
+
resolvers[type] = {};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Add field resolver
|
|
157
|
+
resolvers[type][field] = async (parent: any, args: any, context: any, info: any) => {
|
|
158
|
+
try {
|
|
159
|
+
return await service[propertyKey](parent, args, context, info);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
logger.error(`Error in field resolver ${type}.${field}: ${error instanceof Error ? error.message : String(error)}`);
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Phase 4: Sort operations alphabetically within each operation type.
|
|
171
|
+
*/
|
|
172
|
+
private sortOperationsAlphabetically(generationResults: {
|
|
173
|
+
typeDefs: string;
|
|
174
|
+
resolvers: Record<string, any>;
|
|
175
|
+
}): void {
|
|
176
|
+
|
|
177
|
+
// Sort resolvers alphabetically within each type
|
|
178
|
+
const operationTypes = ["Query", "Mutation", "Subscription"] as const;
|
|
179
|
+
|
|
180
|
+
for (const operationType of operationTypes) {
|
|
181
|
+
if (generationResults.resolvers[operationType]) {
|
|
182
|
+
const sortedResolvers: Record<string, any> = {};
|
|
183
|
+
|
|
184
|
+
// Sort operation names alphabetically
|
|
185
|
+
const sortedKeys = Object.keys(generationResults.resolvers[operationType]).sort();
|
|
186
|
+
|
|
187
|
+
for (const key of sortedKeys) {
|
|
188
|
+
sortedResolvers[key] = generationResults.resolvers[operationType][key];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
generationResults.resolvers[operationType] = sortedResolvers;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Phase 5: Create the final GraphQL schema using the generated typeDefs and resolvers.
|
|
199
|
+
*/
|
|
200
|
+
private createGraphQLSchema(generationResults: {
|
|
201
|
+
typeDefs: string;
|
|
202
|
+
resolvers: Record<string, any>;
|
|
203
|
+
}): GraphQLSchema | null {
|
|
204
|
+
// Check if there are any operations to create a schema for
|
|
205
|
+
const hasOperations = Object.values(generationResults.resolvers).some(
|
|
206
|
+
(typeResolvers: any) => Object.keys(typeResolvers).length > 0
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (!hasOperations && generationResults.typeDefs.trim() === "") {
|
|
210
|
+
logger.warn("No operations or type definitions found, returning null schema");
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const schema = createSchema({
|
|
216
|
+
typeDefs: generationResults.typeDefs,
|
|
217
|
+
resolvers: generationResults.resolvers
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return schema;
|
|
221
|
+
|
|
222
|
+
} catch (error) {
|
|
223
|
+
logger.error({error},"Failed to create GraphQL schema");
|
|
224
|
+
throw new Error(`Schema creation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get the current schema graph (for debugging/testing purposes).
|
|
230
|
+
*/
|
|
231
|
+
getSchemaGraph(): SchemaGraph {
|
|
232
|
+
return this.schemaGraph;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Clear the orchestrator state for reuse.
|
|
237
|
+
*/
|
|
238
|
+
clear(): void {
|
|
239
|
+
this.schemaGraph.clear();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GraphQLSchemaOrchestrator } from "./GraphQLSchemaOrchestrator";
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import type BaseService from "../../service/Service";
|
|
2
|
+
import type { GraphQLObjectTypeMeta, GraphQLOperationMeta, GraphQLSubscriptionMeta } from "../Generator";
|
|
3
|
+
import { SchemaGraph } from "../graph/SchemaGraph";
|
|
4
|
+
import { ScalarNode, TypeNode, OperationNode, FieldNode, InputNode } from "../graph/GraphNode";
|
|
5
|
+
import { OperationType, GraphQLTypeKind } from "../graph/GraphNode";
|
|
6
|
+
import { isSchemaInput } from "../schema";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ServiceScanner extracts GraphQL metadata from services and converts it into graph nodes.
|
|
10
|
+
* This class is responsible for scanning service instances and their prototypes to collect
|
|
11
|
+
* all GraphQL-related metadata and create corresponding nodes in the schema graph.
|
|
12
|
+
*/
|
|
13
|
+
export class ServiceScanner {
|
|
14
|
+
private graph: SchemaGraph;
|
|
15
|
+
private allScalarTypes: Set<string> = new Set(['Upload']); // Include default scalars
|
|
16
|
+
|
|
17
|
+
constructor(graph: SchemaGraph) {
|
|
18
|
+
this.graph = graph;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Scans all provided services and extracts their GraphQL metadata into graph nodes.
|
|
23
|
+
* @param services Array of service instances to scan
|
|
24
|
+
*/
|
|
25
|
+
public scanServices(services: BaseService[]): void {
|
|
26
|
+
// First pass: collect all scalar types
|
|
27
|
+
for (const service of services) {
|
|
28
|
+
const scalarTypes = this.getServiceMetadata(service, '__graphqlScalarTypes') as string[];
|
|
29
|
+
if (scalarTypes) {
|
|
30
|
+
for (const scalarName of scalarTypes) {
|
|
31
|
+
this.allScalarTypes.add(scalarName);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Second pass: scan services with scalar types available
|
|
37
|
+
for (const service of services) {
|
|
38
|
+
this.scanService(service);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Scans a single service instance for GraphQL metadata.
|
|
44
|
+
* @param service The service instance to scan
|
|
45
|
+
*/
|
|
46
|
+
private scanService(service: BaseService): void {
|
|
47
|
+
// Extract scalar types
|
|
48
|
+
this.extractScalarTypes(service);
|
|
49
|
+
|
|
50
|
+
// Extract object types
|
|
51
|
+
this.extractObjectTypes(service);
|
|
52
|
+
|
|
53
|
+
// Extract operations (queries/mutations)
|
|
54
|
+
this.extractOperations(service);
|
|
55
|
+
|
|
56
|
+
// Extract subscriptions
|
|
57
|
+
this.extractSubscriptions(service);
|
|
58
|
+
|
|
59
|
+
// Extract fields
|
|
60
|
+
this.extractFields(service);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Extracts scalar type definitions from a service.
|
|
65
|
+
* @param service The service to extract scalar types from
|
|
66
|
+
*/
|
|
67
|
+
public extractScalarTypes(service: BaseService): void {
|
|
68
|
+
const scalarTypes = this.getServiceMetadata(service, '__graphqlScalarTypes') as string[];
|
|
69
|
+
if (!scalarTypes) return;
|
|
70
|
+
|
|
71
|
+
for (const scalarName of scalarTypes) {
|
|
72
|
+
const scalarNode = new ScalarNode(scalarName, scalarName, {
|
|
73
|
+
serviceName: service.constructor.name,
|
|
74
|
+
description: `Scalar type defined in ${service.constructor.name}`
|
|
75
|
+
});
|
|
76
|
+
this.graph.addNode(scalarNode);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Extracts object type definitions from a service.
|
|
82
|
+
* @param service The service to extract object types from
|
|
83
|
+
*/
|
|
84
|
+
public extractObjectTypes(service: BaseService): void {
|
|
85
|
+
const objectTypes = this.getServiceMetadata(service, '__graphqlObjectType') as GraphQLObjectTypeMeta[];
|
|
86
|
+
if (!objectTypes) return;
|
|
87
|
+
|
|
88
|
+
for (const meta of objectTypes) {
|
|
89
|
+
const typeNode = new TypeNode(meta.name, meta.name, GraphQLTypeKind.OBJECT, `type ${meta.name} {\n${Object.entries(meta.fields).map(([k, v]) => ` ${k}: ${v}`).join('\n')}\n}`, {
|
|
90
|
+
serviceName: service.constructor.name,
|
|
91
|
+
fields: meta.fields,
|
|
92
|
+
description: `Object type defined in ${service.constructor.name}`
|
|
93
|
+
});
|
|
94
|
+
this.graph.addNode(typeNode);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Extracts GraphQL operations (queries and mutations) from a service.
|
|
100
|
+
* @param service The service to extract operations from
|
|
101
|
+
*/
|
|
102
|
+
public extractOperations(service: BaseService): void {
|
|
103
|
+
const operations = this.getServiceMetadata(service, '__graphqlOperations') as GraphQLOperationMeta[];
|
|
104
|
+
if (!operations) return;
|
|
105
|
+
|
|
106
|
+
for (const op of operations) {
|
|
107
|
+
// Create input node if input is a plain object (not Zod schema, not Schema DSL)
|
|
108
|
+
let inputNodeId: string | undefined;
|
|
109
|
+
if (op.input && typeof op.input === 'object' && !Array.isArray(op.input) && !('_def' in op.input) && !isSchemaInput(op.input)) {
|
|
110
|
+
const inputTypeName = this.extractTypeNameFromInput(op.input, op.name!);
|
|
111
|
+
|
|
112
|
+
// Only create node if it doesn't already exist
|
|
113
|
+
if (inputTypeName && !this.graph.getNode(inputTypeName)) {
|
|
114
|
+
const inputFields = Object.entries(op.input as Record<string, any>)
|
|
115
|
+
.map(([key, type]) => ` ${key}: ${type}`)
|
|
116
|
+
.join('\n');
|
|
117
|
+
const inputTypeDef = `input ${inputTypeName} {\n${inputFields}\n}`;
|
|
118
|
+
const inputNode = new InputNode(
|
|
119
|
+
inputTypeName,
|
|
120
|
+
inputTypeName,
|
|
121
|
+
inputTypeDef,
|
|
122
|
+
false,
|
|
123
|
+
{
|
|
124
|
+
serviceName: service.constructor.name,
|
|
125
|
+
operationName: op.name,
|
|
126
|
+
description: `Input type for ${op.name} operation`
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
this.graph.addNode(inputNode);
|
|
130
|
+
}
|
|
131
|
+
inputNodeId = inputTypeName || undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const operationType = op.type.toLowerCase() === 'query' ? OperationType.QUERY : OperationType.MUTATION;
|
|
135
|
+
const operationNode = new OperationNode(
|
|
136
|
+
op.name!,
|
|
137
|
+
op.name!,
|
|
138
|
+
operationType,
|
|
139
|
+
`${op.name}: String`, // Placeholder field definition
|
|
140
|
+
inputNodeId,
|
|
141
|
+
undefined, // outputNodeId
|
|
142
|
+
{
|
|
143
|
+
serviceName: service.constructor.name,
|
|
144
|
+
propertyKey: op.propertyKey,
|
|
145
|
+
input: op.input,
|
|
146
|
+
output: op.output,
|
|
147
|
+
scalarTypes: this.allScalarTypes,
|
|
148
|
+
description: `${op.type} operation defined in ${service.constructor.name}`
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
this.graph.addNode(operationNode);
|
|
152
|
+
|
|
153
|
+
// Add dependencies: operation depends on input and output types
|
|
154
|
+
this.addOperationDependencies(operationNode, op);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Extracts GraphQL subscriptions from a service.
|
|
160
|
+
* @param service The service to extract subscriptions from
|
|
161
|
+
*/
|
|
162
|
+
public extractSubscriptions(service: BaseService): void {
|
|
163
|
+
const subscriptions = this.getServiceMetadata(service, '__graphqlSubscriptions') as GraphQLSubscriptionMeta[];
|
|
164
|
+
if (!subscriptions) return;
|
|
165
|
+
|
|
166
|
+
for (const sub of subscriptions) {
|
|
167
|
+
const subscriptionNode = new OperationNode(
|
|
168
|
+
sub.name!,
|
|
169
|
+
sub.name!,
|
|
170
|
+
OperationType.SUBSCRIPTION,
|
|
171
|
+
`${sub.name}: String`, // Placeholder field definition
|
|
172
|
+
undefined, // inputNodeId
|
|
173
|
+
undefined, // outputNodeId
|
|
174
|
+
{
|
|
175
|
+
serviceName: service.constructor.name,
|
|
176
|
+
propertyKey: sub.propertyKey,
|
|
177
|
+
input: sub.input,
|
|
178
|
+
output: sub.output,
|
|
179
|
+
scalarTypes: this.allScalarTypes,
|
|
180
|
+
description: `Subscription defined in ${service.constructor.name}`
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
this.graph.addNode(subscriptionNode);
|
|
184
|
+
|
|
185
|
+
// Add dependencies: subscription depends on input and output types
|
|
186
|
+
this.addSubscriptionDependencies(subscriptionNode, sub);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Extracts GraphQL field definitions from a service.
|
|
192
|
+
* @param service The service to extract fields from
|
|
193
|
+
*/
|
|
194
|
+
public extractFields(service: BaseService): void {
|
|
195
|
+
const fields = this.getServiceMetadata(service, '__graphqlFields') as any[];
|
|
196
|
+
if (!fields) return;
|
|
197
|
+
|
|
198
|
+
for (const field of fields) {
|
|
199
|
+
// Create a unique ID for the field node to avoid conflicts across services
|
|
200
|
+
const fieldId = `${service.constructor.name}-${field.field}`;
|
|
201
|
+
|
|
202
|
+
// Skip if field node already exists (duplicate field definition)
|
|
203
|
+
if (this.graph.getNode(fieldId)) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const fieldNode = new FieldNode(
|
|
208
|
+
fieldId,
|
|
209
|
+
'unknown', // typeName - we don't know the parent type yet
|
|
210
|
+
field.field,
|
|
211
|
+
field.type,
|
|
212
|
+
{
|
|
213
|
+
serviceName: service.constructor.name,
|
|
214
|
+
propertyKey: field.propertyKey,
|
|
215
|
+
description: `Field defined in ${service.constructor.name}`
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
this.graph.addNode(fieldNode);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Adds dependency edges for an operation node based on its input and output types.
|
|
224
|
+
* @param operationNode The operation node to add dependencies for
|
|
225
|
+
* @param operationMeta The operation metadata
|
|
226
|
+
*/
|
|
227
|
+
private addOperationDependencies(operationNode: OperationNode, operationMeta: GraphQLOperationMeta): void {
|
|
228
|
+
// Add dependency on input type if it exists
|
|
229
|
+
if (operationMeta.input) {
|
|
230
|
+
const inputTypeName = this.extractTypeNameFromInput(operationMeta.input, operationMeta.name!);
|
|
231
|
+
if (inputTypeName && this.graph.getNode(inputTypeName)) {
|
|
232
|
+
this.graph.addDependency(operationNode.id, inputTypeName);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Add dependency on output type if it exists in the graph
|
|
237
|
+
// Note: Archetype types may not exist yet - they're added by ArchetypePreprocessorVisitor
|
|
238
|
+
const outputTypeName = this.extractTypeNameFromOutput(operationMeta.output);
|
|
239
|
+
if (outputTypeName && this.graph.getNode(outputTypeName)) {
|
|
240
|
+
this.graph.addDependency(operationNode.id, outputTypeName);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Adds dependency edges for a subscription node based on its input and output types.
|
|
246
|
+
* @param subscriptionNode The subscription node to add dependencies for
|
|
247
|
+
* @param subscriptionMeta The subscription metadata
|
|
248
|
+
*/
|
|
249
|
+
private addSubscriptionDependencies(subscriptionNode: OperationNode, subscriptionMeta: GraphQLSubscriptionMeta): void {
|
|
250
|
+
// Add dependency on input type if it exists
|
|
251
|
+
if (subscriptionMeta.input) {
|
|
252
|
+
const inputTypeName = this.extractTypeNameFromInput(subscriptionMeta.input, subscriptionMeta.name!);
|
|
253
|
+
if (inputTypeName && this.graph.getNode(inputTypeName)) {
|
|
254
|
+
this.graph.addDependency(subscriptionNode.id, inputTypeName);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Add dependency on output type if it exists in the graph
|
|
259
|
+
// Note: Archetype types may not exist yet - they're added by ArchetypePreprocessorVisitor
|
|
260
|
+
const outputTypeName = this.extractTypeNameFromOutput(subscriptionMeta.output);
|
|
261
|
+
if (outputTypeName && this.graph.getNode(outputTypeName)) {
|
|
262
|
+
this.graph.addDependency(subscriptionNode.id, outputTypeName);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Extracts a type name from operation input metadata.
|
|
268
|
+
* @param input The input metadata
|
|
269
|
+
* @param operationName The operation name for naming derived types
|
|
270
|
+
* @returns The type name or null if not found
|
|
271
|
+
*/
|
|
272
|
+
private extractTypeNameFromInput(input: any, operationName: string): string | null {
|
|
273
|
+
if (typeof input === 'string') {
|
|
274
|
+
return input;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// For Zod schemas, generate input type name
|
|
278
|
+
if (input && typeof input === 'object' && '_def' in input) {
|
|
279
|
+
return `${operationName}Input`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// For object inputs, use operation name
|
|
283
|
+
if (typeof input === 'object') {
|
|
284
|
+
return `${operationName}Input`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Extracts a type name from operation output metadata.
|
|
292
|
+
* @param output The output metadata
|
|
293
|
+
* @returns The type name or null if not found
|
|
294
|
+
*/
|
|
295
|
+
private extractTypeNameFromOutput(output: any): string | null {
|
|
296
|
+
if (typeof output === 'string') {
|
|
297
|
+
return output;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Handle arrays of archetypes first
|
|
301
|
+
if (Array.isArray(output) && output.length > 0) {
|
|
302
|
+
const firstItem = output[0];
|
|
303
|
+
if (firstItem && typeof firstItem === 'object' && firstItem.constructor) {
|
|
304
|
+
const constructorName = firstItem.constructor.name;
|
|
305
|
+
if (constructorName && constructorName !== 'Object') {
|
|
306
|
+
return constructorName;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Handle BaseArcheType instances
|
|
312
|
+
if (output && typeof output === 'object' && output.constructor) {
|
|
313
|
+
const constructorName = output.constructor.name;
|
|
314
|
+
if (constructorName && constructorName !== 'Object') {
|
|
315
|
+
return constructorName;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Retrieves metadata from a service instance or its prototype chain.
|
|
324
|
+
* @param service The service instance
|
|
325
|
+
* @param key The metadata key to retrieve
|
|
326
|
+
* @returns The metadata value or null if not found
|
|
327
|
+
*/
|
|
328
|
+
private getServiceMetadata(service: BaseService, key: string): any {
|
|
329
|
+
// Check instance first
|
|
330
|
+
if (service[key as keyof BaseService]) {
|
|
331
|
+
return service[key as keyof BaseService];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Check prototype
|
|
335
|
+
const prototype = service.constructor.prototype;
|
|
336
|
+
if (prototype && prototype[key]) {
|
|
337
|
+
return prototype[key];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Check constructor
|
|
341
|
+
if (service.constructor[key as keyof typeof service.constructor]) {
|
|
342
|
+
return service.constructor[key as keyof typeof service.constructor];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
}
|