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,278 @@
|
|
|
1
|
+
import { GraphNode, NodeType } from './GraphNode';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Directed graph data structure for managing GraphQL schema components and their dependencies.
|
|
5
|
+
* Provides topological sorting and dependency resolution for schema generation.
|
|
6
|
+
*/
|
|
7
|
+
export class SchemaGraph {
|
|
8
|
+
private nodes: Map<string, GraphNode> = new Map();
|
|
9
|
+
private adjacencyList: Map<string, Set<string>> = new Map(); // nodeId -> set of dependent nodeIds
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Add a node to the graph
|
|
13
|
+
*/
|
|
14
|
+
addNode(node: GraphNode): void {
|
|
15
|
+
if (this.nodes.has(node.id)) {
|
|
16
|
+
throw new Error(`Node with id '${node.id}' already exists in graph`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
this.nodes.set(node.id, node);
|
|
20
|
+
this.adjacencyList.set(node.id, new Set());
|
|
21
|
+
|
|
22
|
+
// Add reverse edges for dependencies (what depends on this node)
|
|
23
|
+
for (const depId of node.dependencies) {
|
|
24
|
+
if (!this.adjacencyList.has(depId)) {
|
|
25
|
+
this.adjacencyList.set(depId, new Set());
|
|
26
|
+
}
|
|
27
|
+
this.adjacencyList.get(depId)!.add(node.id);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get a node by its ID
|
|
33
|
+
*/
|
|
34
|
+
getNode(nodeId: string): GraphNode | undefined {
|
|
35
|
+
return this.nodes.get(nodeId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if a node exists in the graph
|
|
40
|
+
*/
|
|
41
|
+
hasNode(nodeId: string): boolean {
|
|
42
|
+
return this.nodes.has(nodeId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Remove a node from the graph
|
|
47
|
+
*/
|
|
48
|
+
removeNode(nodeId: string): boolean {
|
|
49
|
+
const node = this.nodes.get(nodeId);
|
|
50
|
+
if (!node) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Remove from nodes map
|
|
55
|
+
this.nodes.delete(nodeId);
|
|
56
|
+
|
|
57
|
+
// Remove from adjacency list
|
|
58
|
+
this.adjacencyList.delete(nodeId);
|
|
59
|
+
|
|
60
|
+
// Remove this node from other nodes' dependency lists
|
|
61
|
+
for (const [otherNodeId, dependents] of this.adjacencyList) {
|
|
62
|
+
dependents.delete(nodeId);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get all nodes in the graph
|
|
70
|
+
*/
|
|
71
|
+
getAllNodes(): GraphNode[] {
|
|
72
|
+
return Array.from(this.nodes.values());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get nodes of a specific type
|
|
77
|
+
*/
|
|
78
|
+
getNodesByType(nodeType: NodeType): GraphNode[] {
|
|
79
|
+
return this.getAllNodes().filter(node => node.nodeType === nodeType);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get direct dependencies of a node (nodes this node depends on)
|
|
84
|
+
*/
|
|
85
|
+
getDependencies(nodeId: string): GraphNode[] {
|
|
86
|
+
const node = this.getNode(nodeId);
|
|
87
|
+
if (!node) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return node.dependencies
|
|
92
|
+
.map(depId => this.getNode(depId))
|
|
93
|
+
.filter((dep): dep is GraphNode => dep !== undefined);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get nodes that depend on the given node (reverse dependencies)
|
|
98
|
+
*/
|
|
99
|
+
getDependents(nodeId: string): GraphNode[] {
|
|
100
|
+
const dependents = this.adjacencyList.get(nodeId);
|
|
101
|
+
if (!dependents) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return Array.from(dependents)
|
|
106
|
+
.map(depId => this.getNode(depId))
|
|
107
|
+
.filter((dep): dep is GraphNode => dep !== undefined);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Add a dependency edge between two nodes
|
|
112
|
+
*/
|
|
113
|
+
addDependency(fromNodeId: string, toNodeId: string): void {
|
|
114
|
+
const fromNode = this.getNode(fromNodeId);
|
|
115
|
+
const toNode = this.getNode(toNodeId);
|
|
116
|
+
|
|
117
|
+
if (!fromNode) {
|
|
118
|
+
throw new Error(`Source node '${fromNodeId}' does not exist`);
|
|
119
|
+
}
|
|
120
|
+
if (!toNode) {
|
|
121
|
+
throw new Error(`Target node '${toNodeId}' does not exist`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fromNode.addDependency(toNodeId);
|
|
125
|
+
|
|
126
|
+
// Update adjacency list for reverse lookup
|
|
127
|
+
if (!this.adjacencyList.has(toNodeId)) {
|
|
128
|
+
this.adjacencyList.set(toNodeId, new Set());
|
|
129
|
+
}
|
|
130
|
+
this.adjacencyList.get(toNodeId)!.add(fromNodeId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Remove a dependency edge between two nodes
|
|
135
|
+
*/
|
|
136
|
+
removeDependency(fromNodeId: string, toNodeId: string): void {
|
|
137
|
+
const fromNode = this.getNode(fromNodeId);
|
|
138
|
+
if (!fromNode) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fromNode.removeDependency(toNodeId);
|
|
143
|
+
|
|
144
|
+
// Update adjacency list
|
|
145
|
+
const dependents = this.adjacencyList.get(toNodeId);
|
|
146
|
+
if (dependents) {
|
|
147
|
+
dependents.delete(fromNodeId);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Perform topological sort of the graph
|
|
153
|
+
* Returns nodes in dependency order (dependencies first)
|
|
154
|
+
*/
|
|
155
|
+
topologicalSort(): GraphNode[] {
|
|
156
|
+
const visited = new Set<string>();
|
|
157
|
+
const visiting = new Set<string>(); // For cycle detection
|
|
158
|
+
const result: GraphNode[] = [];
|
|
159
|
+
|
|
160
|
+
const visit = (nodeId: string): void => {
|
|
161
|
+
if (visited.has(nodeId)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (visiting.has(nodeId)) {
|
|
166
|
+
throw new Error(`Cycle detected in graph involving node '${nodeId}'`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
visiting.add(nodeId);
|
|
170
|
+
|
|
171
|
+
// Visit all dependencies first
|
|
172
|
+
const node = this.getNode(nodeId);
|
|
173
|
+
if (node) {
|
|
174
|
+
for (const depId of node.dependencies) {
|
|
175
|
+
if (this.hasNode(depId)) {
|
|
176
|
+
visit(depId);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
visiting.delete(nodeId);
|
|
182
|
+
visited.add(nodeId);
|
|
183
|
+
|
|
184
|
+
if (node) {
|
|
185
|
+
result.push(node);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Visit all nodes
|
|
190
|
+
for (const nodeId of this.nodes.keys()) {
|
|
191
|
+
if (!visited.has(nodeId)) {
|
|
192
|
+
visit(nodeId);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get nodes in dependency order (topologically sorted)
|
|
201
|
+
*/
|
|
202
|
+
getNodesInDependencyOrder(): GraphNode[] {
|
|
203
|
+
return this.topologicalSort();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Clear all nodes and edges from the graph
|
|
208
|
+
*/
|
|
209
|
+
clear(): void {
|
|
210
|
+
this.nodes.clear();
|
|
211
|
+
this.adjacencyList.clear();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get graph statistics
|
|
216
|
+
*/
|
|
217
|
+
getStats(): {
|
|
218
|
+
nodeCount: number;
|
|
219
|
+
edgeCount: number;
|
|
220
|
+
nodesByType: Record<NodeType, number>;
|
|
221
|
+
} {
|
|
222
|
+
const nodesByType = Object.values(NodeType).reduce((acc, type) => {
|
|
223
|
+
acc[type] = 0;
|
|
224
|
+
return acc;
|
|
225
|
+
}, {} as Record<NodeType, number>);
|
|
226
|
+
|
|
227
|
+
for (const node of this.nodes.values()) {
|
|
228
|
+
nodesByType[node.nodeType]++;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let edgeCount = 0;
|
|
232
|
+
for (const node of this.nodes.values()) {
|
|
233
|
+
edgeCount += node.dependencies.length;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
nodeCount: this.nodes.size,
|
|
238
|
+
edgeCount,
|
|
239
|
+
nodesByType
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Validate the graph structure
|
|
245
|
+
* Checks for missing dependencies and other consistency issues
|
|
246
|
+
*/
|
|
247
|
+
validate(): { isValid: boolean; errors: string[] } {
|
|
248
|
+
const errors: string[] = [];
|
|
249
|
+
|
|
250
|
+
for (const [nodeId, node] of this.nodes) {
|
|
251
|
+
// Check that all dependencies exist
|
|
252
|
+
for (const depId of node.dependencies) {
|
|
253
|
+
if (!this.hasNode(depId)) {
|
|
254
|
+
errors.push(`Node '${nodeId}' depends on non-existent node '${depId}'`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check for cycles (this will throw an error if cycles exist)
|
|
260
|
+
try {
|
|
261
|
+
this.topologicalSort();
|
|
262
|
+
} catch (error) {
|
|
263
|
+
errors.push(`Graph contains cycles: ${(error as Error).message}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
isValid: errors.length === 0,
|
|
268
|
+
errors
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get the number of nodes in the graph
|
|
274
|
+
*/
|
|
275
|
+
size(): number {
|
|
276
|
+
return this.nodes.size;
|
|
277
|
+
}
|
|
278
|
+
}
|
package/gql/helpers.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GraphQLFieldTypes } from './types';
|
|
2
|
+
import { z } from 'zod';
|
|
2
3
|
|
|
3
4
|
export type GraphQLType =
|
|
4
5
|
| GraphQLFieldTypes // e.g., "ID!", "String"
|
|
@@ -12,7 +13,7 @@ export function isValidGraphQLType(type: string): type is GraphQLType {
|
|
|
12
13
|
return enumValues.includes(type as GraphQLFieldTypes) ||
|
|
13
14
|
/^(\w+|\[\w+\])(!)?$/.test(type); // Simple regex for custom types/lists
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
+
/** @deprecated */
|
|
16
17
|
export type TypeFromGraphQL<T extends GraphQLType> =
|
|
17
18
|
T extends GraphQLFieldTypes.ID_REQUIRED | GraphQLFieldTypes.ID ? string :
|
|
18
19
|
T extends GraphQLFieldTypes.STRING_REQUIRED ? string :
|
|
@@ -26,6 +27,11 @@ export type TypeFromGraphQL<T extends GraphQLType> =
|
|
|
26
27
|
T extends `[${string}]` | `[${string}]!` ? any[] :
|
|
27
28
|
any;
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
*
|
|
32
|
+
* @deprecated
|
|
33
|
+
* @reason Use `ArcheType.getInputSchema()` instead.
|
|
34
|
+
*/
|
|
29
35
|
export type ResolverInput<T extends Record<string, GraphQLType>> = {
|
|
30
36
|
[K in keyof T]: TypeFromGraphQL<T[K]>;
|
|
31
37
|
};
|
|
@@ -64,4 +70,4 @@ function isPathSelected(selectionSet: any, path: string[]): boolean {
|
|
|
64
70
|
}
|
|
65
71
|
}
|
|
66
72
|
return false;
|
|
67
|
-
}
|
|
73
|
+
}
|
package/gql/index.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {createSchema, createYoga, type Plugin} from 'graphql-yoga';
|
|
2
|
+
import { useValidationRule } from '@envelop/core';
|
|
2
3
|
import { GraphQLSchema, GraphQLError } from 'graphql';
|
|
3
|
-
import {
|
|
4
|
+
import { depthLimitRule } from './depthLimit';
|
|
5
|
+
import { GraphQLObjectType, GraphQLField, GraphQLOperation, GraphQLScalarType, GraphQLSubscription } from './Generator';
|
|
4
6
|
import {GraphQLFieldTypes} from "./types"
|
|
5
|
-
import {logger as MainLogger} from "core/Logger"
|
|
7
|
+
import {logger as MainLogger} from "../core/Logger"
|
|
6
8
|
import { isFieldRequested } from './helpers';
|
|
9
|
+
import * as z from "zod";
|
|
7
10
|
|
|
8
11
|
const logger = MainLogger.child({scope: "GQL"});
|
|
9
12
|
|
|
@@ -19,11 +22,16 @@ export {
|
|
|
19
22
|
GraphQLObjectType,
|
|
20
23
|
GraphQLField,
|
|
21
24
|
GraphQLOperation,
|
|
25
|
+
GraphQLSubscription,
|
|
22
26
|
GraphQLFieldTypes,
|
|
23
27
|
isValidGraphQLType,
|
|
24
28
|
GraphQLScalarType,
|
|
25
29
|
isFieldRequested
|
|
26
30
|
}
|
|
31
|
+
export { GraphQLSchemaOrchestrator } from "./orchestration";
|
|
32
|
+
export { generateGraphQLSchemaV2 } from "./GeneratorV2";
|
|
33
|
+
export { Middleware, composeOperationMiddleware } from "./middleware";
|
|
34
|
+
export type { OperationMiddleware } from "./middleware";
|
|
27
35
|
export type {
|
|
28
36
|
GraphQLType,
|
|
29
37
|
TypeFromGraphQL,
|
|
@@ -90,6 +98,21 @@ const maskError = (error: any, message: string): GraphQLError => {
|
|
|
90
98
|
}
|
|
91
99
|
});
|
|
92
100
|
}
|
|
101
|
+
|
|
102
|
+
// Handle GraphQL validation errors for missing required fields
|
|
103
|
+
if (error.message.includes('was not provided')) {
|
|
104
|
+
const match = error.message.match(/Field "([^"]+)" of required type "([^"]+)" was not provided/);
|
|
105
|
+
if (match) {
|
|
106
|
+
const fieldName = match[1];
|
|
107
|
+
return new GraphQLError(`Missing required field: ${fieldName}`, {
|
|
108
|
+
extensions: {
|
|
109
|
+
code: 'VALIDATION_ERROR',
|
|
110
|
+
field: fieldName,
|
|
111
|
+
originalMessage: error.message
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
93
116
|
|
|
94
117
|
if (process.env.NODE_ENV === 'production') {
|
|
95
118
|
logger.error("GraphQL Error:", error);
|
|
@@ -104,14 +127,41 @@ const maskError = (error: any, message: string): GraphQLError => {
|
|
|
104
127
|
return error instanceof GraphQLError ? error : new GraphQLError(message, { originalError: error });
|
|
105
128
|
};
|
|
106
129
|
|
|
107
|
-
export
|
|
130
|
+
export interface YogaInstanceOptions {
|
|
131
|
+
cors?: {
|
|
132
|
+
origin?: string | string[] | ((origin: string) => boolean);
|
|
133
|
+
credentials?: boolean;
|
|
134
|
+
allowedHeaders?: string[];
|
|
135
|
+
methods?: string[];
|
|
136
|
+
};
|
|
137
|
+
maxDepth?: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function createYogaInstance(
|
|
141
|
+
schema?: GraphQLSchema,
|
|
142
|
+
plugins: Plugin[] = [],
|
|
143
|
+
contextFactory?: (context: any) => any,
|
|
144
|
+
options?: YogaInstanceOptions
|
|
145
|
+
) {
|
|
146
|
+
// Prepend depth limit plugin if configured
|
|
147
|
+
const allPlugins: Plugin[] = [];
|
|
148
|
+
if (options?.maxDepth) {
|
|
149
|
+
allPlugins.push(useValidationRule(depthLimitRule(options.maxDepth)) as Plugin);
|
|
150
|
+
}
|
|
151
|
+
allPlugins.push(...plugins);
|
|
152
|
+
|
|
108
153
|
const yogaConfig: any = {
|
|
109
|
-
plugins,
|
|
154
|
+
plugins: allPlugins,
|
|
110
155
|
maskedErrors: {
|
|
111
156
|
maskError,
|
|
112
157
|
},
|
|
113
158
|
};
|
|
114
159
|
|
|
160
|
+
// Add CORS if provided
|
|
161
|
+
if (options?.cors) {
|
|
162
|
+
yogaConfig.cors = options.cors;
|
|
163
|
+
}
|
|
164
|
+
|
|
115
165
|
// Add context factory if provided
|
|
116
166
|
if (contextFactory) {
|
|
117
167
|
yogaConfig.context = contextFactory;
|
|
@@ -129,4 +179,6 @@ export function createYogaInstance(schema?: GraphQLSchema, plugins: Plugin[] = [
|
|
|
129
179
|
}
|
|
130
180
|
}
|
|
131
181
|
|
|
182
|
+
export const Upload = z.union([z.literal("Upload"), z.any()]);
|
|
183
|
+
|
|
132
184
|
export const yoga = createYogaInstance();
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { GraphQLError } from "graphql";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Middleware function for GraphQL operations (onion model).
|
|
5
|
+
*
|
|
6
|
+
* Call `next()` to continue to the next middleware or the resolver.
|
|
7
|
+
* Throw a `GraphQLError` to short-circuit the chain.
|
|
8
|
+
* Return the result of `next()` (or transform it).
|
|
9
|
+
*/
|
|
10
|
+
export type OperationMiddleware = (
|
|
11
|
+
args: any,
|
|
12
|
+
context: any,
|
|
13
|
+
info: any,
|
|
14
|
+
next: () => Promise<any>,
|
|
15
|
+
) => Promise<any>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Compose an array of OperationMiddleware into a single chain.
|
|
19
|
+
* The final `handler` is the original service method.
|
|
20
|
+
*/
|
|
21
|
+
export function composeOperationMiddleware(
|
|
22
|
+
middlewares: OperationMiddleware[],
|
|
23
|
+
handler: (...args: any[]) => Promise<any>,
|
|
24
|
+
thisArg: any,
|
|
25
|
+
): (args: any, context: any, info: any) => Promise<any> {
|
|
26
|
+
return (args: any, context: any, info: any) => {
|
|
27
|
+
let index = 0;
|
|
28
|
+
const dispatch = (): Promise<any> => {
|
|
29
|
+
if (index >= middlewares.length) {
|
|
30
|
+
return handler.call(thisArg, args, context, info);
|
|
31
|
+
}
|
|
32
|
+
const mw = middlewares[index++]!;
|
|
33
|
+
return mw(args, context, info, dispatch);
|
|
34
|
+
};
|
|
35
|
+
return dispatch();
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Decorator that attaches an ordered chain of middleware to a GraphQL operation.
|
|
41
|
+
*
|
|
42
|
+
* Middleware execute in array order (left-to-right), wrapping the resolver
|
|
43
|
+
* in an onion model identical to HTTP middleware.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* const Authenticate: OperationMiddleware = async (args, ctx, info, next) => {
|
|
48
|
+
* if (!ctx.user) throw new GraphQLError("Unauthenticated", {
|
|
49
|
+
* extensions: { code: "UNAUTHENTICATED", http: { status: 401 } }
|
|
50
|
+
* });
|
|
51
|
+
* return next();
|
|
52
|
+
* };
|
|
53
|
+
*
|
|
54
|
+
* function Authorize(...permissions: string[]): OperationMiddleware {
|
|
55
|
+
* return async (args, ctx, info, next) => {
|
|
56
|
+
* if (!permissions.every(p => ctx.user.permissions?.includes(p)))
|
|
57
|
+
* throw new GraphQLError("Forbidden", {
|
|
58
|
+
* extensions: { code: "FORBIDDEN", http: { status: 403 } }
|
|
59
|
+
* });
|
|
60
|
+
* return next();
|
|
61
|
+
* };
|
|
62
|
+
* }
|
|
63
|
+
*
|
|
64
|
+
* class UserService extends BaseService {
|
|
65
|
+
* @Middleware([Authenticate, Authorize("users.read")])
|
|
66
|
+
* @GraphQLOperation({ type: "Query", output: "User", input: { id: "ID!" } })
|
|
67
|
+
* async getUser(args, context, info) { ... }
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function Middleware(middlewares: OperationMiddleware[]) {
|
|
72
|
+
return function (_target: any, _propertyKey: string, descriptor: PropertyDescriptor) {
|
|
73
|
+
const original = descriptor.value;
|
|
74
|
+
descriptor.value = function (this: any, args: any, context: any, info: any) {
|
|
75
|
+
const chain = composeOperationMiddleware(middlewares, original, this);
|
|
76
|
+
return chain(args, context, info);
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
}
|