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,234 @@
|
|
|
1
|
+
import { GraphQLError } from "graphql";
|
|
2
|
+
import { logger } from "../../core/Logger";
|
|
3
|
+
import { type ZodType } from "zod";
|
|
4
|
+
import * as z from "zod";
|
|
5
|
+
|
|
6
|
+
/** Check if error is a GraphQLError (handles cross-package version mismatches) */
|
|
7
|
+
function isGraphQLError(error: unknown): error is GraphQLError {
|
|
8
|
+
return error instanceof GraphQLError ||
|
|
9
|
+
(error !== null && typeof error === 'object' && 'extensions' in error &&
|
|
10
|
+
'message' in error && typeof (error as any).message === 'string');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ResolverDefinition {
|
|
14
|
+
name: string;
|
|
15
|
+
type: "Query" | "Mutation" | "Subscription";
|
|
16
|
+
service: any;
|
|
17
|
+
propertyKey: string;
|
|
18
|
+
zodSchema?: ZodType;
|
|
19
|
+
hasInput?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ResolverBuilder {
|
|
23
|
+
private resolvers: Record<string, Record<string, Function>> = {
|
|
24
|
+
Query: {},
|
|
25
|
+
Mutation: {},
|
|
26
|
+
Subscription: {}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Add a resolver definition
|
|
31
|
+
*/
|
|
32
|
+
addResolver(definition: ResolverDefinition): void {
|
|
33
|
+
const { name, type, service, propertyKey, zodSchema, hasInput } = definition;
|
|
34
|
+
|
|
35
|
+
let resolver: any;
|
|
36
|
+
|
|
37
|
+
if (type === "Subscription") {
|
|
38
|
+
// Subscriptions need special handling with subscribe/resolve pattern
|
|
39
|
+
resolver = hasInput
|
|
40
|
+
? this.createSubscriptionResolverWithInput(service, propertyKey, zodSchema)
|
|
41
|
+
: this.createSubscriptionResolverWithoutInput(service, propertyKey);
|
|
42
|
+
} else {
|
|
43
|
+
// Queries and Mutations use regular resolver pattern
|
|
44
|
+
resolver = hasInput
|
|
45
|
+
? this.createResolverWithInput(service, propertyKey, zodSchema)
|
|
46
|
+
: this.createResolverWithoutInput(service, propertyKey);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Ensure the resolver category exists (should always exist due to initialization)
|
|
50
|
+
if (!this.resolvers[type]) {
|
|
51
|
+
this.resolvers[type] = {};
|
|
52
|
+
}
|
|
53
|
+
this.resolvers[type][name] = resolver;
|
|
54
|
+
logger.trace(`Added ${type} resolver: ${name}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create a resolver that expects input arguments
|
|
59
|
+
*/
|
|
60
|
+
private createResolverWithInput(service: any, propertyKey: string, zodSchema?: ZodType): Function {
|
|
61
|
+
return async (_: any, args: any, context: any, info: any) => {
|
|
62
|
+
try {
|
|
63
|
+
const inputArgs = args.input || args;
|
|
64
|
+
|
|
65
|
+
// Automatically validate with Zod schema if provided
|
|
66
|
+
if (zodSchema) {
|
|
67
|
+
try {
|
|
68
|
+
const validated = zodSchema.parse(inputArgs);
|
|
69
|
+
return await service[propertyKey](validated, context, info);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error instanceof z.ZodError) {
|
|
72
|
+
// Let handleGraphQLError convert Zod errors to user-friendly messages
|
|
73
|
+
const { handleGraphQLError } = await import("../../core/ErrorHandler");
|
|
74
|
+
handleGraphQLError(error);
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
return await service[propertyKey](inputArgs, context, info);
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
logger.error(`Error in resolver with input:`);
|
|
83
|
+
logger.error(error);
|
|
84
|
+
if (isGraphQLError(error)) {
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
throw new GraphQLError(`Internal error`, {
|
|
88
|
+
extensions: {
|
|
89
|
+
code: "INTERNAL_ERROR",
|
|
90
|
+
originalError: process.env.NODE_ENV === 'development' ? error : undefined
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create a resolver that doesn't expect input arguments
|
|
99
|
+
*/
|
|
100
|
+
private createResolverWithoutInput(service: any, propertyKey: string): Function {
|
|
101
|
+
return async (_: any, args: any, context: any, info: any) => {
|
|
102
|
+
try {
|
|
103
|
+
const result = await service[propertyKey]({}, context, info);
|
|
104
|
+
return result;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
logger.error(`Error in resolver without input:`);
|
|
107
|
+
logger.error(error);
|
|
108
|
+
if (isGraphQLError(error)) {
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
throw new GraphQLError(`Internal error`, {
|
|
112
|
+
extensions: {
|
|
113
|
+
code: "INTERNAL_ERROR",
|
|
114
|
+
originalError: process.env.NODE_ENV === 'development' ? error : undefined
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create a subscription resolver with input (returns { subscribe, resolve })
|
|
123
|
+
*/
|
|
124
|
+
private createSubscriptionResolverWithInput(service: any, propertyKey: string, zodSchema?: ZodType): any {
|
|
125
|
+
return {
|
|
126
|
+
subscribe: async (_: any, args: any, context: any, info: any) => {
|
|
127
|
+
try {
|
|
128
|
+
const inputArgs = args.input || args;
|
|
129
|
+
|
|
130
|
+
// Automatically validate with Zod schema if provided
|
|
131
|
+
if (zodSchema) {
|
|
132
|
+
try {
|
|
133
|
+
const validated = zodSchema.parse(inputArgs);
|
|
134
|
+
return await service[propertyKey](validated, context, info);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error instanceof z.ZodError) {
|
|
137
|
+
const { handleGraphQLError } = await import("../../core/ErrorHandler");
|
|
138
|
+
handleGraphQLError(error);
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
return await service[propertyKey](inputArgs, context, info);
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
logger.error(`Error in subscription with input:`);
|
|
147
|
+
logger.error(error);
|
|
148
|
+
if (isGraphQLError(error)) {
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
throw new GraphQLError(`Internal error in subscription`, {
|
|
152
|
+
extensions: {
|
|
153
|
+
code: "INTERNAL_ERROR",
|
|
154
|
+
originalError: process.env.NODE_ENV === 'development' ? error : undefined
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
resolve: (payload: any) => payload
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Create a subscription resolver without input (returns { subscribe, resolve })
|
|
165
|
+
*/
|
|
166
|
+
private createSubscriptionResolverWithoutInput(service: any, propertyKey: string): any {
|
|
167
|
+
return {
|
|
168
|
+
subscribe: async (_: any, args: any, context: any, info: any) => {
|
|
169
|
+
try {
|
|
170
|
+
return await service[propertyKey]({}, context, info);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
logger.error(`Error in subscription without input:`);
|
|
173
|
+
logger.error(error);
|
|
174
|
+
if (isGraphQLError(error)) {
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
throw new GraphQLError(`Internal error in subscription`, {
|
|
178
|
+
extensions: {
|
|
179
|
+
code: "INTERNAL_ERROR",
|
|
180
|
+
originalError: process.env.NODE_ENV === 'development' ? error : undefined
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
resolve: (payload: any) => payload
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get all built resolvers
|
|
191
|
+
*/
|
|
192
|
+
getResolvers(): Record<string, Record<string, Function>> {
|
|
193
|
+
return { ...this.resolvers };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get resolvers for a specific type
|
|
198
|
+
*/
|
|
199
|
+
getResolversForType(type: "Query" | "Mutation" | "Subscription"): Record<string, Function> {
|
|
200
|
+
return { ...this.resolvers[type] };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Clear all resolvers (for reuse)
|
|
205
|
+
*/
|
|
206
|
+
clear(): void {
|
|
207
|
+
this.resolvers = {
|
|
208
|
+
Query: {},
|
|
209
|
+
Mutation: {},
|
|
210
|
+
Subscription: {}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Add a scalar resolver
|
|
216
|
+
*/
|
|
217
|
+
addScalarResolver(name: string, resolver: any): void {
|
|
218
|
+
if (!this.resolvers[name]) {
|
|
219
|
+
this.resolvers[name] = resolver;
|
|
220
|
+
logger.trace(`Added scalar resolver: ${name}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get statistics
|
|
226
|
+
*/
|
|
227
|
+
getStats(): { queries: number; mutations: number; subscriptions: number } {
|
|
228
|
+
return {
|
|
229
|
+
queries: Object.keys(this.resolvers.Query ?? {}).length,
|
|
230
|
+
mutations: Object.keys(this.resolvers.Mutation ?? {}).length,
|
|
231
|
+
subscriptions: Object.keys(this.resolvers.Subscription ?? {}).length
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { logger } from "../../core/Logger";
|
|
2
|
+
|
|
3
|
+
export interface OperationField {
|
|
4
|
+
name: string;
|
|
5
|
+
fieldDef: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class TypeDefBuilder {
|
|
9
|
+
private queryFields: string[] = [];
|
|
10
|
+
private mutationFields: string[] = [];
|
|
11
|
+
private subscriptionFields: string[] = [];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Add a query field
|
|
15
|
+
*/
|
|
16
|
+
addQueryField(field: OperationField): void {
|
|
17
|
+
this.queryFields.push(field.fieldDef);
|
|
18
|
+
logger.trace(`Added query field: ${field.fieldDef}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Add a mutation field
|
|
23
|
+
*/
|
|
24
|
+
addMutationField(field: OperationField): void {
|
|
25
|
+
this.mutationFields.push(field.fieldDef);
|
|
26
|
+
logger.trace(`Added mutation field: ${field.fieldDef}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Add a subscription field
|
|
31
|
+
*/
|
|
32
|
+
addSubscriptionField(field: OperationField): void {
|
|
33
|
+
this.subscriptionFields.push(field.fieldDef);
|
|
34
|
+
logger.trace(`Added subscription field: ${field.fieldDef}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build the Query type definition
|
|
39
|
+
*/
|
|
40
|
+
buildQueryType(): string {
|
|
41
|
+
if (this.queryFields.length === 0) {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const sortedFields = this.queryFields.sort();
|
|
46
|
+
return `type Query {\n${sortedFields.map(f => ` ${f}`).join('\n')}\n}\n`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build the Mutation type definition
|
|
51
|
+
*/
|
|
52
|
+
buildMutationType(): string {
|
|
53
|
+
if (this.mutationFields.length === 0) {
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const sortedFields = this.mutationFields.sort();
|
|
58
|
+
return `type Mutation {\n${sortedFields.map(f => ` ${f}`).join('\n')}\n}\n`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build the Subscription type definition
|
|
63
|
+
*/
|
|
64
|
+
buildSubscriptionType(): string {
|
|
65
|
+
if (this.subscriptionFields.length === 0) {
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const sortedFields = this.subscriptionFields.sort();
|
|
70
|
+
return `type Subscription {\n${sortedFields.map(f => ` ${f}`).join('\n')}\n}\n`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build all operation types
|
|
75
|
+
*/
|
|
76
|
+
buildAllOperationTypes(): string {
|
|
77
|
+
let typeDefs = '';
|
|
78
|
+
|
|
79
|
+
typeDefs += this.buildQueryType();
|
|
80
|
+
typeDefs += this.buildMutationType();
|
|
81
|
+
typeDefs += this.buildSubscriptionType();
|
|
82
|
+
|
|
83
|
+
return typeDefs;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Clear all fields (for reuse)
|
|
88
|
+
*/
|
|
89
|
+
clear(): void {
|
|
90
|
+
this.queryFields = [];
|
|
91
|
+
this.mutationFields = [];
|
|
92
|
+
this.subscriptionFields = [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get statistics
|
|
97
|
+
*/
|
|
98
|
+
getStats(): { queries: number; mutations: number; subscriptions: number } {
|
|
99
|
+
return {
|
|
100
|
+
queries: this.queryFields.length,
|
|
101
|
+
mutations: this.mutationFields.length,
|
|
102
|
+
subscriptions: this.subscriptionFields.length
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
package/gql/decorators/Upload.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
2
|
import type { UploadDecoratorConfig } from "../../types/upload.types";
|
|
3
|
-
import { UploadManager } from "../../
|
|
3
|
+
import { UploadManager } from "../../upload";
|
|
4
4
|
import { logger as MainLogger } from "../../core/Logger";
|
|
5
5
|
|
|
6
6
|
const logger = MainLogger.child({ scope: "UploadDecorator" });
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ASTVisitor,
|
|
3
|
+
type DocumentNode,
|
|
4
|
+
type FragmentDefinitionNode,
|
|
5
|
+
type ValidationContext,
|
|
6
|
+
GraphQLError,
|
|
7
|
+
Kind,
|
|
8
|
+
} from "graphql";
|
|
9
|
+
|
|
10
|
+
export function depthLimitRule(maxDepth: number) {
|
|
11
|
+
return function DepthLimitValidationRule(
|
|
12
|
+
context: ValidationContext,
|
|
13
|
+
): ASTVisitor {
|
|
14
|
+
const document: DocumentNode = context.getDocument();
|
|
15
|
+
|
|
16
|
+
// Build fragment map
|
|
17
|
+
const fragments = new Map<string, FragmentDefinitionNode>();
|
|
18
|
+
for (const def of document.definitions) {
|
|
19
|
+
if (def.kind === Kind.FRAGMENT_DEFINITION) {
|
|
20
|
+
fragments.set(def.name.value, def);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function measureDepth(
|
|
25
|
+
node: { selectionSet?: { selections: readonly any[] } },
|
|
26
|
+
depth: number,
|
|
27
|
+
visited: Set<string>,
|
|
28
|
+
): number {
|
|
29
|
+
if (!node.selectionSet) return depth;
|
|
30
|
+
|
|
31
|
+
let max = depth;
|
|
32
|
+
for (const selection of node.selectionSet.selections) {
|
|
33
|
+
if (selection.kind === Kind.FIELD) {
|
|
34
|
+
const fieldDepth = measureDepth(
|
|
35
|
+
selection,
|
|
36
|
+
depth + 1,
|
|
37
|
+
visited,
|
|
38
|
+
);
|
|
39
|
+
if (fieldDepth > max) max = fieldDepth;
|
|
40
|
+
} else if (selection.kind === Kind.INLINE_FRAGMENT) {
|
|
41
|
+
const fragDepth = measureDepth(selection, depth, visited);
|
|
42
|
+
if (fragDepth > max) max = fragDepth;
|
|
43
|
+
} else if (selection.kind === Kind.FRAGMENT_SPREAD) {
|
|
44
|
+
const name = selection.name.value;
|
|
45
|
+
if (visited.has(name)) continue;
|
|
46
|
+
visited.add(name);
|
|
47
|
+
const fragment = fragments.get(name);
|
|
48
|
+
if (fragment) {
|
|
49
|
+
const fragDepth = measureDepth(
|
|
50
|
+
fragment,
|
|
51
|
+
depth,
|
|
52
|
+
visited,
|
|
53
|
+
);
|
|
54
|
+
if (fragDepth > max) max = fragDepth;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return max;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
OperationDefinition(node) {
|
|
63
|
+
// Skip introspection queries — they are inherently deep (13+ levels)
|
|
64
|
+
// and must be allowed for tooling (codegen, IDE autocomplete, etc.)
|
|
65
|
+
if (node.selectionSet) {
|
|
66
|
+
const isIntrospection = node.selectionSet.selections.every(
|
|
67
|
+
(sel) =>
|
|
68
|
+
sel.kind === Kind.FIELD &&
|
|
69
|
+
sel.name.value.startsWith("__"),
|
|
70
|
+
);
|
|
71
|
+
if (isIntrospection) return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const depth = measureDepth(node, 0, new Set());
|
|
75
|
+
if (depth > maxDepth) {
|
|
76
|
+
context.reportError(
|
|
77
|
+
new GraphQLError(
|
|
78
|
+
`Query depth ${depth} exceeds maximum allowed depth of ${maxDepth}`,
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core graph data structures for GraphQL schema generation.
|
|
3
|
+
* Following Gall's Law: Start with a simple system that works.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export enum NodeType {
|
|
7
|
+
TYPE = 'TYPE',
|
|
8
|
+
OPERATION = 'OPERATION',
|
|
9
|
+
FIELD = 'FIELD',
|
|
10
|
+
INPUT = 'INPUT',
|
|
11
|
+
SCALAR = 'SCALAR'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export enum GraphQLTypeKind {
|
|
15
|
+
SCALAR = 'SCALAR',
|
|
16
|
+
OBJECT = 'OBJECT',
|
|
17
|
+
INTERFACE = 'INTERFACE',
|
|
18
|
+
UNION = 'UNION',
|
|
19
|
+
ENUM = 'ENUM',
|
|
20
|
+
INPUT_OBJECT = 'INPUT_OBJECT'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export enum OperationType {
|
|
24
|
+
QUERY = 'QUERY',
|
|
25
|
+
MUTATION = 'MUTATION',
|
|
26
|
+
SUBSCRIPTION = 'SUBSCRIPTION'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Base class for all graph nodes in the GraphQL schema generation system.
|
|
31
|
+
* Each node represents a component of the GraphQL schema with its dependencies.
|
|
32
|
+
*/
|
|
33
|
+
export abstract class GraphNode {
|
|
34
|
+
public readonly id: string;
|
|
35
|
+
public readonly nodeType: NodeType;
|
|
36
|
+
public readonly metadata: Record<string, any>;
|
|
37
|
+
public readonly dependencies: string[]; // Array of node IDs this node depends on
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
id: string,
|
|
41
|
+
nodeType: NodeType,
|
|
42
|
+
metadata: Record<string, any> = {},
|
|
43
|
+
dependencies: string[] = []
|
|
44
|
+
) {
|
|
45
|
+
this.id = id;
|
|
46
|
+
this.nodeType = nodeType;
|
|
47
|
+
this.metadata = { ...metadata };
|
|
48
|
+
this.dependencies = [...dependencies];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get a human-readable description of this node
|
|
53
|
+
*/
|
|
54
|
+
abstract getDescription(): string;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if this node depends on another node
|
|
58
|
+
*/
|
|
59
|
+
dependsOn(nodeId: string): boolean {
|
|
60
|
+
return this.dependencies.includes(nodeId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Add a dependency to this node
|
|
65
|
+
*/
|
|
66
|
+
addDependency(nodeId: string): void {
|
|
67
|
+
if (!this.dependencies.includes(nodeId)) {
|
|
68
|
+
this.dependencies.push(nodeId);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Remove a dependency from this node
|
|
74
|
+
*/
|
|
75
|
+
removeDependency(nodeId: string): void {
|
|
76
|
+
const index = this.dependencies.indexOf(nodeId);
|
|
77
|
+
if (index > -1) {
|
|
78
|
+
this.dependencies.splice(index, 1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Represents a GraphQL type definition (scalar, object, interface, union, enum)
|
|
85
|
+
*/
|
|
86
|
+
export class TypeNode extends GraphNode {
|
|
87
|
+
public readonly kind: GraphQLTypeKind;
|
|
88
|
+
public readonly name: string;
|
|
89
|
+
public readonly typeDef: string; // GraphQL type definition string
|
|
90
|
+
|
|
91
|
+
constructor(
|
|
92
|
+
id: string,
|
|
93
|
+
name: string,
|
|
94
|
+
kind: GraphQLTypeKind,
|
|
95
|
+
typeDef: string,
|
|
96
|
+
metadata: Record<string, any> = {},
|
|
97
|
+
dependencies: string[] = []
|
|
98
|
+
) {
|
|
99
|
+
super(id, NodeType.TYPE, metadata, dependencies);
|
|
100
|
+
this.name = name;
|
|
101
|
+
this.kind = kind;
|
|
102
|
+
this.typeDef = typeDef;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getDescription(): string {
|
|
106
|
+
return `Type ${this.name} (${this.kind})`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Represents a GraphQL operation (Query, Mutation, Subscription)
|
|
112
|
+
*/
|
|
113
|
+
export class OperationNode extends GraphNode {
|
|
114
|
+
public readonly operationType: OperationType;
|
|
115
|
+
public readonly name: string;
|
|
116
|
+
public readonly fieldDef: string; // GraphQL field definition string
|
|
117
|
+
public readonly inputNodeId?: string; // ID of input node if any
|
|
118
|
+
public readonly outputNodeId?: string; // ID of output node if any
|
|
119
|
+
|
|
120
|
+
constructor(
|
|
121
|
+
id: string,
|
|
122
|
+
name: string,
|
|
123
|
+
operationType: OperationType,
|
|
124
|
+
fieldDef: string,
|
|
125
|
+
inputNodeId?: string,
|
|
126
|
+
outputNodeId?: string,
|
|
127
|
+
metadata: Record<string, any> = {},
|
|
128
|
+
dependencies: string[] = []
|
|
129
|
+
) {
|
|
130
|
+
super(id, NodeType.OPERATION, metadata, dependencies);
|
|
131
|
+
this.name = name;
|
|
132
|
+
this.operationType = operationType;
|
|
133
|
+
this.fieldDef = fieldDef;
|
|
134
|
+
this.inputNodeId = inputNodeId;
|
|
135
|
+
this.outputNodeId = outputNodeId;
|
|
136
|
+
|
|
137
|
+
// Auto-add dependencies based on input/output nodes
|
|
138
|
+
if (inputNodeId && !dependencies.includes(inputNodeId)) {
|
|
139
|
+
this.addDependency(inputNodeId);
|
|
140
|
+
}
|
|
141
|
+
if (outputNodeId && !dependencies.includes(outputNodeId)) {
|
|
142
|
+
this.addDependency(outputNodeId);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
getDescription(): string {
|
|
147
|
+
return `${this.operationType} operation ${this.name}`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Represents a GraphQL field resolver
|
|
153
|
+
*/
|
|
154
|
+
export class FieldNode extends GraphNode {
|
|
155
|
+
public readonly typeName: string; // The type this field belongs to
|
|
156
|
+
public readonly fieldName: string;
|
|
157
|
+
public readonly fieldDef: string; // GraphQL field definition string
|
|
158
|
+
|
|
159
|
+
constructor(
|
|
160
|
+
id: string,
|
|
161
|
+
typeName: string,
|
|
162
|
+
fieldName: string,
|
|
163
|
+
fieldDef: string,
|
|
164
|
+
metadata: Record<string, any> = {},
|
|
165
|
+
dependencies: string[] = []
|
|
166
|
+
) {
|
|
167
|
+
super(id, NodeType.FIELD, metadata, dependencies);
|
|
168
|
+
this.typeName = typeName;
|
|
169
|
+
this.fieldName = fieldName;
|
|
170
|
+
this.fieldDef = fieldDef;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
getDescription(): string {
|
|
174
|
+
return `Field ${this.fieldName} on type ${this.typeName}`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Represents a GraphQL input type definition
|
|
180
|
+
*/
|
|
181
|
+
export class InputNode extends GraphNode {
|
|
182
|
+
public readonly name: string;
|
|
183
|
+
public readonly typeDef: string; // GraphQL input type definition string
|
|
184
|
+
public readonly isOptional: boolean; // Whether this input is optional
|
|
185
|
+
|
|
186
|
+
constructor(
|
|
187
|
+
id: string,
|
|
188
|
+
name: string,
|
|
189
|
+
typeDef: string,
|
|
190
|
+
isOptional: boolean = false,
|
|
191
|
+
metadata: Record<string, any> = {},
|
|
192
|
+
dependencies: string[] = []
|
|
193
|
+
) {
|
|
194
|
+
super(id, NodeType.INPUT, metadata, dependencies);
|
|
195
|
+
this.name = name;
|
|
196
|
+
this.typeDef = typeDef;
|
|
197
|
+
this.isOptional = isOptional;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
getDescription(): string {
|
|
201
|
+
return `Input type ${this.name}${this.isOptional ? ' (optional)' : ''}`;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Represents a GraphQL scalar type
|
|
207
|
+
*/
|
|
208
|
+
export class ScalarNode extends GraphNode {
|
|
209
|
+
public readonly name: string;
|
|
210
|
+
|
|
211
|
+
constructor(
|
|
212
|
+
id: string,
|
|
213
|
+
name: string,
|
|
214
|
+
metadata: Record<string, any> = {},
|
|
215
|
+
dependencies: string[] = []
|
|
216
|
+
) {
|
|
217
|
+
super(id, NodeType.SCALAR, metadata, dependencies);
|
|
218
|
+
this.name = name;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
getDescription(): string {
|
|
222
|
+
return `Scalar type ${this.name}`;
|
|
223
|
+
}
|
|
224
|
+
}
|