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.
Files changed (257) hide show
  1. package/.claude/settings.local.json +47 -0
  2. package/.claude/skills/update-memory.md +74 -0
  3. package/.prettierrc +4 -0
  4. package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
  5. package/.serena/memories/architecture.md +154 -0
  6. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
  7. package/.serena/memories/code_style_and_conventions.md +76 -0
  8. package/.serena/memories/project_overview.md +43 -0
  9. package/.serena/memories/schema-dsl-plan.md +107 -0
  10. package/.serena/memories/suggested_commands.md +80 -0
  11. package/.serena/memories/typescript-compilation-status.md +54 -0
  12. package/.serena/project.yml +114 -0
  13. package/TODO.md +1 -7
  14. package/bun.lock +150 -4
  15. package/bunfig.toml +10 -0
  16. package/config/cache.config.ts +77 -0
  17. package/config/upload.config.ts +4 -5
  18. package/core/App.ts +870 -123
  19. package/core/ArcheType.ts +2268 -377
  20. package/core/BatchLoader.ts +181 -71
  21. package/core/Config.ts +153 -0
  22. package/core/Decorators.ts +4 -1
  23. package/core/Entity.ts +621 -92
  24. package/core/EntityHookManager.ts +1 -1
  25. package/core/EntityInterface.ts +3 -1
  26. package/core/EntityManager.ts +1 -13
  27. package/core/ErrorHandler.ts +8 -2
  28. package/core/Logger.ts +9 -0
  29. package/core/Middleware.ts +34 -0
  30. package/core/RequestContext.ts +5 -1
  31. package/core/RequestLoaders.ts +227 -93
  32. package/core/SchedulerManager.ts +193 -52
  33. package/core/cache/CacheAnalytics.ts +399 -0
  34. package/core/cache/CacheFactory.ts +145 -0
  35. package/core/cache/CacheManager.ts +520 -0
  36. package/core/cache/CacheProvider.ts +34 -0
  37. package/core/cache/CacheWarmer.ts +157 -0
  38. package/core/cache/CompressionUtils.ts +110 -0
  39. package/core/cache/MemoryCache.ts +251 -0
  40. package/core/cache/MultiLevelCache.ts +180 -0
  41. package/core/cache/NoOpCache.ts +53 -0
  42. package/core/cache/RedisCache.ts +464 -0
  43. package/core/cache/TTLStrategy.ts +254 -0
  44. package/core/cache/index.ts +6 -0
  45. package/core/components/BaseComponent.ts +120 -0
  46. package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
  47. package/core/components/Decorators.ts +88 -0
  48. package/core/components/Interfaces.ts +7 -0
  49. package/core/components/index.ts +5 -0
  50. package/core/decorators/EntityHooks.ts +0 -3
  51. package/core/decorators/IndexedField.ts +26 -0
  52. package/core/decorators/ScheduledTask.ts +0 -47
  53. package/core/events/EntityLifecycleEvents.ts +1 -1
  54. package/core/health.ts +112 -0
  55. package/core/metadata/definitions/ArcheType.ts +14 -0
  56. package/core/metadata/definitions/Component.ts +9 -0
  57. package/core/metadata/definitions/gqlObject.ts +1 -1
  58. package/core/metadata/index.ts +42 -1
  59. package/core/metadata/metadata-storage.ts +28 -2
  60. package/core/middleware/AccessLog.ts +59 -0
  61. package/core/middleware/RequestId.ts +38 -0
  62. package/core/middleware/SecurityHeaders.ts +62 -0
  63. package/core/middleware/index.ts +3 -0
  64. package/core/scheduler/DistributedLock.ts +266 -0
  65. package/core/scheduler/index.ts +15 -0
  66. package/core/validateEnv.ts +92 -0
  67. package/database/DatabaseHelper.ts +416 -40
  68. package/database/IndexingStrategy.ts +342 -0
  69. package/database/PreparedStatementCache.ts +226 -0
  70. package/database/index.ts +32 -7
  71. package/database/sqlHelpers.ts +14 -2
  72. package/endpoints/archetypes.ts +362 -0
  73. package/endpoints/components.ts +58 -0
  74. package/endpoints/entity.ts +80 -0
  75. package/endpoints/index.ts +27 -0
  76. package/endpoints/query.ts +93 -0
  77. package/endpoints/stats.ts +76 -0
  78. package/endpoints/tables.ts +212 -0
  79. package/endpoints/types.ts +155 -0
  80. package/gql/ArchetypeOperations.ts +32 -86
  81. package/gql/Generator.ts +27 -315
  82. package/gql/GeneratorV2.ts +37 -0
  83. package/gql/builders/InputTypeBuilder.ts +99 -0
  84. package/gql/builders/ResolverBuilder.ts +234 -0
  85. package/gql/builders/TypeDefBuilder.ts +105 -0
  86. package/gql/builders/index.ts +3 -0
  87. package/gql/decorators/Upload.ts +1 -1
  88. package/gql/depthLimit.ts +85 -0
  89. package/gql/graph/GraphNode.ts +224 -0
  90. package/gql/graph/SchemaGraph.ts +278 -0
  91. package/gql/helpers.ts +8 -2
  92. package/gql/index.ts +56 -4
  93. package/gql/middleware.ts +79 -0
  94. package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
  95. package/gql/orchestration/index.ts +1 -0
  96. package/gql/scanner/ServiceScanner.ts +347 -0
  97. package/gql/schema/index.ts +458 -0
  98. package/gql/strategies/TypeGenerationStrategy.ts +329 -0
  99. package/gql/types.ts +1 -0
  100. package/gql/utils/TypeSignature.ts +220 -0
  101. package/gql/utils/index.ts +1 -0
  102. package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
  103. package/gql/visitors/DeduplicationVisitor.ts +82 -0
  104. package/gql/visitors/GraphVisitor.ts +78 -0
  105. package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
  106. package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
  107. package/gql/visitors/TypeCollectorVisitor.ts +79 -0
  108. package/gql/visitors/VisitorComposer.ts +96 -0
  109. package/gql/visitors/index.ts +7 -0
  110. package/package.json +59 -37
  111. package/plugins/index.ts +2 -2
  112. package/query/CTENode.ts +97 -0
  113. package/query/ComponentInclusionNode.ts +689 -0
  114. package/query/FilterBuilder.ts +127 -0
  115. package/query/FilterBuilderRegistry.ts +202 -0
  116. package/query/OrNode.ts +517 -0
  117. package/query/OrQuery.ts +42 -0
  118. package/query/Query.ts +1022 -0
  119. package/query/QueryContext.ts +170 -0
  120. package/query/QueryDAG.ts +122 -0
  121. package/query/QueryNode.ts +65 -0
  122. package/query/SourceNode.ts +53 -0
  123. package/query/builders/FullTextSearchBuilder.ts +236 -0
  124. package/query/index.ts +21 -0
  125. package/scheduler/index.ts +40 -8
  126. package/service/Service.ts +2 -1
  127. package/service/ServiceRegistry.ts +6 -5
  128. package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
  129. package/storage/S3StorageProvider.ts +316 -0
  130. package/{core/storage → storage}/StorageProvider.ts +7 -3
  131. package/studio/bun.lock +482 -0
  132. package/studio/index.html +13 -0
  133. package/studio/package.json +39 -0
  134. package/studio/postcss.config.js +6 -0
  135. package/studio/src/components/DataTable.tsx +211 -0
  136. package/studio/src/components/Layout.tsx +13 -0
  137. package/studio/src/components/PageContainer.tsx +9 -0
  138. package/studio/src/components/PageHeader.tsx +13 -0
  139. package/studio/src/components/SearchBar.tsx +57 -0
  140. package/studio/src/components/Sidebar.tsx +294 -0
  141. package/studio/src/components/ui/button.tsx +56 -0
  142. package/studio/src/components/ui/checkbox.tsx +26 -0
  143. package/studio/src/components/ui/input.tsx +25 -0
  144. package/studio/src/hooks/useDataTable.ts +131 -0
  145. package/studio/src/index.css +36 -0
  146. package/studio/src/lib/api.ts +186 -0
  147. package/studio/src/lib/utils.ts +13 -0
  148. package/studio/src/main.tsx +17 -0
  149. package/studio/src/pages/ArcheType.tsx +239 -0
  150. package/studio/src/pages/Components.tsx +124 -0
  151. package/studio/src/pages/EntityInspector.tsx +302 -0
  152. package/studio/src/pages/QueryRunner.tsx +246 -0
  153. package/studio/src/pages/Table.tsx +94 -0
  154. package/studio/src/pages/Welcome.tsx +241 -0
  155. package/studio/src/routes.tsx +45 -0
  156. package/studio/src/store/archeTypeSettings.ts +30 -0
  157. package/studio/src/store/studio.ts +65 -0
  158. package/studio/src/utils/columnHelpers.tsx +114 -0
  159. package/studio/studio-instructions.md +81 -0
  160. package/studio/tailwind.config.js +77 -0
  161. package/studio/tsconfig.json +24 -0
  162. package/studio/utils.ts +54 -0
  163. package/studio/vite.config.js +19 -0
  164. package/swagger/generator.ts +1 -1
  165. package/tests/e2e/http.test.ts +126 -0
  166. package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
  167. package/tests/fixtures/components/TestOrder.ts +23 -0
  168. package/tests/fixtures/components/TestProduct.ts +23 -0
  169. package/tests/fixtures/components/TestUser.ts +20 -0
  170. package/tests/fixtures/components/index.ts +6 -0
  171. package/tests/graphql/SchemaGeneration.test.ts +90 -0
  172. package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
  173. package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
  174. package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
  175. package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
  176. package/tests/integration/entity/Entity.persistence.test.ts +333 -0
  177. package/tests/integration/query/Query.exec.test.ts +523 -0
  178. package/tests/pglite-setup.ts +61 -0
  179. package/tests/setup.ts +164 -0
  180. package/tests/stress/BenchmarkRunner.ts +203 -0
  181. package/tests/stress/DataSeeder.ts +190 -0
  182. package/tests/stress/StressTestReporter.ts +229 -0
  183. package/tests/stress/cursor-perf-test.ts +171 -0
  184. package/tests/stress/fixtures/StressTestComponents.ts +58 -0
  185. package/tests/stress/index.ts +7 -0
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
  187. package/tests/unit/BatchLoader.test.ts +82 -0
  188. package/tests/unit/archetype/ArcheType.test.ts +107 -0
  189. package/tests/unit/cache/CacheManager.test.ts +347 -0
  190. package/tests/unit/cache/MemoryCache.test.ts +260 -0
  191. package/tests/unit/cache/RedisCache.test.ts +411 -0
  192. package/tests/unit/entity/Entity.components.test.ts +244 -0
  193. package/tests/unit/entity/Entity.test.ts +345 -0
  194. package/tests/unit/gql/depthLimit.test.ts +203 -0
  195. package/tests/unit/gql/operationMiddleware.test.ts +293 -0
  196. package/tests/unit/health/Health.test.ts +129 -0
  197. package/tests/unit/middleware/AccessLog.test.ts +37 -0
  198. package/tests/unit/middleware/Middleware.test.ts +98 -0
  199. package/tests/unit/middleware/RequestId.test.ts +54 -0
  200. package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
  201. package/tests/unit/query/FilterBuilder.test.ts +111 -0
  202. package/tests/unit/query/Query.test.ts +308 -0
  203. package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
  204. package/tests/unit/schema/schema-integration.test.ts +426 -0
  205. package/tests/unit/schema/schema.test.ts +580 -0
  206. package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
  207. package/tests/unit/upload/RestUpload.test.ts +267 -0
  208. package/tests/unit/validateEnv.test.ts +82 -0
  209. package/tests/utils/entity-tracker.ts +57 -0
  210. package/tests/utils/index.ts +13 -0
  211. package/tests/utils/test-context.ts +149 -0
  212. package/tsconfig.json +5 -1
  213. package/types/archetype.types.ts +6 -0
  214. package/types/hooks.types.ts +1 -1
  215. package/types/query.types.ts +110 -0
  216. package/types/scheduler.types.ts +68 -7
  217. package/types/upload.types.ts +1 -0
  218. package/{core → upload}/FileValidator.ts +10 -1
  219. package/upload/RestUpload.ts +130 -0
  220. package/{core/components → upload}/UploadComponent.ts +11 -11
  221. package/{core → upload}/UploadManager.ts +3 -3
  222. package/upload/index.ts +23 -7
  223. package/utils/UploadHelper.ts +27 -6
  224. package/utils/cronParser.ts +16 -6
  225. package/.github/workflows/deploy-docs.yml +0 -57
  226. package/core/Components.ts +0 -202
  227. package/core/EntityCache.ts +0 -15
  228. package/core/Query.ts +0 -880
  229. package/docs/README.md +0 -149
  230. package/docs/_coverpage.md +0 -36
  231. package/docs/_sidebar.md +0 -23
  232. package/docs/api/core.md +0 -568
  233. package/docs/api/hooks.md +0 -554
  234. package/docs/api/index.md +0 -222
  235. package/docs/api/query.md +0 -678
  236. package/docs/api/service.md +0 -744
  237. package/docs/core-concepts/archetypes.md +0 -512
  238. package/docs/core-concepts/components.md +0 -498
  239. package/docs/core-concepts/entity.md +0 -314
  240. package/docs/core-concepts/hooks.md +0 -683
  241. package/docs/core-concepts/query.md +0 -588
  242. package/docs/core-concepts/services.md +0 -647
  243. package/docs/examples/code-examples.md +0 -425
  244. package/docs/getting-started.md +0 -337
  245. package/docs/index.html +0 -97
  246. package/tests/bench/insert.bench.ts +0 -60
  247. package/tests/bench/relations.bench.ts +0 -270
  248. package/tests/bench/sorting.bench.ts +0 -416
  249. package/tests/component-hooks-simple.test.ts +0 -117
  250. package/tests/component-hooks.test.ts +0 -1461
  251. package/tests/component.test.ts +0 -339
  252. package/tests/errorHandling.test.ts +0 -155
  253. package/tests/hooks.test.ts +0 -667
  254. package/tests/query-sorting.test.ts +0 -101
  255. package/tests/query.test.ts +0 -81
  256. package/tests/relations.test.ts +0 -170
  257. 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
+ }