@effect-gql/core 0.1.0 → 1.0.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/README.md +100 -0
- package/builder/index.cjs +1431 -0
- package/builder/index.cjs.map +1 -0
- package/builder/index.d.cts +259 -0
- package/{dist/builder/pipe-api.d.ts → builder/index.d.ts} +49 -21
- package/builder/index.js +1390 -0
- package/builder/index.js.map +1 -0
- package/index.cjs +3419 -0
- package/index.cjs.map +1 -0
- package/index.d.cts +523 -0
- package/index.d.ts +523 -0
- package/index.js +3242 -0
- package/index.js.map +1 -0
- package/package.json +19 -28
- package/schema-builder-Cvdq7Kz_.d.cts +963 -0
- package/schema-builder-Cvdq7Kz_.d.ts +963 -0
- package/server/index.cjs +1555 -0
- package/server/index.cjs.map +1 -0
- package/server/index.d.cts +680 -0
- package/server/index.d.ts +680 -0
- package/server/index.js +1524 -0
- package/server/index.js.map +1 -0
- package/dist/analyzer-extension.d.ts +0 -105
- package/dist/analyzer-extension.d.ts.map +0 -1
- package/dist/analyzer-extension.js +0 -137
- package/dist/analyzer-extension.js.map +0 -1
- package/dist/builder/execute.d.ts +0 -26
- package/dist/builder/execute.d.ts.map +0 -1
- package/dist/builder/execute.js +0 -104
- package/dist/builder/execute.js.map +0 -1
- package/dist/builder/field-builders.d.ts +0 -30
- package/dist/builder/field-builders.d.ts.map +0 -1
- package/dist/builder/field-builders.js +0 -200
- package/dist/builder/field-builders.js.map +0 -1
- package/dist/builder/index.d.ts +0 -7
- package/dist/builder/index.d.ts.map +0 -1
- package/dist/builder/index.js +0 -31
- package/dist/builder/index.js.map +0 -1
- package/dist/builder/pipe-api.d.ts.map +0 -1
- package/dist/builder/pipe-api.js +0 -151
- package/dist/builder/pipe-api.js.map +0 -1
- package/dist/builder/schema-builder.d.ts +0 -301
- package/dist/builder/schema-builder.d.ts.map +0 -1
- package/dist/builder/schema-builder.js +0 -566
- package/dist/builder/schema-builder.js.map +0 -1
- package/dist/builder/type-registry.d.ts +0 -80
- package/dist/builder/type-registry.d.ts.map +0 -1
- package/dist/builder/type-registry.js +0 -505
- package/dist/builder/type-registry.js.map +0 -1
- package/dist/builder/types.d.ts +0 -283
- package/dist/builder/types.d.ts.map +0 -1
- package/dist/builder/types.js +0 -3
- package/dist/builder/types.js.map +0 -1
- package/dist/cli/generate-schema.d.ts +0 -29
- package/dist/cli/generate-schema.d.ts.map +0 -1
- package/dist/cli/generate-schema.js +0 -233
- package/dist/cli/generate-schema.js.map +0 -1
- package/dist/cli/index.d.ts +0 -19
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -24
- package/dist/cli/index.js.map +0 -1
- package/dist/context.d.ts +0 -18
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -11
- package/dist/context.js.map +0 -1
- package/dist/error.d.ts +0 -45
- package/dist/error.d.ts.map +0 -1
- package/dist/error.js +0 -29
- package/dist/error.js.map +0 -1
- package/dist/extensions.d.ts +0 -130
- package/dist/extensions.d.ts.map +0 -1
- package/dist/extensions.js +0 -78
- package/dist/extensions.js.map +0 -1
- package/dist/index.d.ts +0 -12
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -47
- package/dist/index.js.map +0 -1
- package/dist/loader.d.ts +0 -169
- package/dist/loader.d.ts.map +0 -1
- package/dist/loader.js +0 -237
- package/dist/loader.js.map +0 -1
- package/dist/resolver-context.d.ts +0 -154
- package/dist/resolver-context.d.ts.map +0 -1
- package/dist/resolver-context.js +0 -184
- package/dist/resolver-context.js.map +0 -1
- package/dist/schema-mapping.d.ts +0 -30
- package/dist/schema-mapping.d.ts.map +0 -1
- package/dist/schema-mapping.js +0 -280
- package/dist/schema-mapping.js.map +0 -1
- package/dist/server/cache-control.d.ts +0 -96
- package/dist/server/cache-control.d.ts.map +0 -1
- package/dist/server/cache-control.js +0 -308
- package/dist/server/cache-control.js.map +0 -1
- package/dist/server/complexity.d.ts +0 -165
- package/dist/server/complexity.d.ts.map +0 -1
- package/dist/server/complexity.js +0 -433
- package/dist/server/complexity.js.map +0 -1
- package/dist/server/config.d.ts +0 -66
- package/dist/server/config.d.ts.map +0 -1
- package/dist/server/config.js +0 -104
- package/dist/server/config.js.map +0 -1
- package/dist/server/graphiql.d.ts +0 -5
- package/dist/server/graphiql.d.ts.map +0 -1
- package/dist/server/graphiql.js +0 -43
- package/dist/server/graphiql.js.map +0 -1
- package/dist/server/index.d.ts +0 -18
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/index.js +0 -48
- package/dist/server/index.js.map +0 -1
- package/dist/server/router.d.ts +0 -79
- package/dist/server/router.d.ts.map +0 -1
- package/dist/server/router.js +0 -232
- package/dist/server/router.js.map +0 -1
- package/dist/server/schema-builder-extensions.d.ts +0 -42
- package/dist/server/schema-builder-extensions.d.ts.map +0 -1
- package/dist/server/schema-builder-extensions.js +0 -48
- package/dist/server/schema-builder-extensions.js.map +0 -1
- package/dist/server/sse-adapter.d.ts +0 -64
- package/dist/server/sse-adapter.d.ts.map +0 -1
- package/dist/server/sse-adapter.js +0 -227
- package/dist/server/sse-adapter.js.map +0 -1
- package/dist/server/sse-types.d.ts +0 -192
- package/dist/server/sse-types.d.ts.map +0 -1
- package/dist/server/sse-types.js +0 -63
- package/dist/server/sse-types.js.map +0 -1
- package/dist/server/ws-adapter.d.ts +0 -39
- package/dist/server/ws-adapter.d.ts.map +0 -1
- package/dist/server/ws-adapter.js +0 -247
- package/dist/server/ws-adapter.js.map +0 -1
- package/dist/server/ws-types.d.ts +0 -169
- package/dist/server/ws-types.d.ts.map +0 -1
- package/dist/server/ws-types.js +0 -11
- package/dist/server/ws-types.js.map +0 -1
- package/dist/server/ws-utils.d.ts +0 -42
- package/dist/server/ws-utils.d.ts.map +0 -1
- package/dist/server/ws-utils.js +0 -99
- package/dist/server/ws-utils.js.map +0 -1
- package/src/analyzer-extension.ts +0 -254
- package/src/builder/execute.ts +0 -153
- package/src/builder/field-builders.ts +0 -322
- package/src/builder/index.ts +0 -48
- package/src/builder/pipe-api.ts +0 -312
- package/src/builder/schema-builder.ts +0 -970
- package/src/builder/type-registry.ts +0 -670
- package/src/builder/types.ts +0 -305
- package/src/context.ts +0 -23
- package/src/error.ts +0 -32
- package/src/extensions.ts +0 -240
- package/src/index.ts +0 -32
- package/src/loader.ts +0 -363
- package/src/resolver-context.ts +0 -253
- package/src/schema-mapping.ts +0 -307
- package/src/server/cache-control.ts +0 -590
- package/src/server/complexity.ts +0 -774
- package/src/server/config.ts +0 -174
- package/src/server/graphiql.ts +0 -38
- package/src/server/index.ts +0 -96
- package/src/server/router.ts +0 -432
- package/src/server/schema-builder-extensions.ts +0 -51
- package/src/server/sse-adapter.ts +0 -327
- package/src/server/sse-types.ts +0 -234
- package/src/server/ws-adapter.ts +0 -355
- package/src/server/ws-types.ts +0 -192
- package/src/server/ws-utils.ts +0 -136
package/src/server/complexity.ts
DELETED
|
@@ -1,774 +0,0 @@
|
|
|
1
|
-
import { Effect, Option, Config, Data } from "effect"
|
|
2
|
-
import {
|
|
3
|
-
DocumentNode,
|
|
4
|
-
OperationDefinitionNode,
|
|
5
|
-
FieldNode,
|
|
6
|
-
FragmentDefinitionNode,
|
|
7
|
-
FragmentSpreadNode,
|
|
8
|
-
InlineFragmentNode,
|
|
9
|
-
SelectionSetNode,
|
|
10
|
-
GraphQLSchema,
|
|
11
|
-
GraphQLObjectType,
|
|
12
|
-
GraphQLOutputType,
|
|
13
|
-
GraphQLNonNull,
|
|
14
|
-
GraphQLList,
|
|
15
|
-
Kind,
|
|
16
|
-
parse,
|
|
17
|
-
} from "graphql"
|
|
18
|
-
|
|
19
|
-
// ============================================================================
|
|
20
|
-
// Errors
|
|
21
|
-
// ============================================================================
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Error thrown when query complexity exceeds configured limits
|
|
25
|
-
*/
|
|
26
|
-
export class ComplexityLimitExceededError extends Data.TaggedError("ComplexityLimitExceededError")<{
|
|
27
|
-
readonly message: string
|
|
28
|
-
readonly limit: number
|
|
29
|
-
readonly actual: number
|
|
30
|
-
readonly limitType: "depth" | "complexity" | "aliases" | "fields"
|
|
31
|
-
}> {}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Error thrown when complexity analysis fails
|
|
35
|
-
*/
|
|
36
|
-
export class ComplexityAnalysisError extends Data.TaggedError("ComplexityAnalysisError")<{
|
|
37
|
-
readonly message: string
|
|
38
|
-
readonly cause?: unknown
|
|
39
|
-
}> {}
|
|
40
|
-
|
|
41
|
-
// ============================================================================
|
|
42
|
-
// Types
|
|
43
|
-
// ============================================================================
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Result of complexity analysis for a GraphQL operation
|
|
47
|
-
*/
|
|
48
|
-
export interface ComplexityResult {
|
|
49
|
-
/** Maximum depth of the query */
|
|
50
|
-
readonly depth: number
|
|
51
|
-
/** Total complexity score */
|
|
52
|
-
readonly complexity: number
|
|
53
|
-
/** Number of field selections (including nested) */
|
|
54
|
-
readonly fieldCount: number
|
|
55
|
-
/** Number of aliased fields */
|
|
56
|
-
readonly aliasCount: number
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Information provided to complexity calculators
|
|
61
|
-
*/
|
|
62
|
-
export interface ComplexityAnalysisInfo {
|
|
63
|
-
/** Parsed GraphQL document */
|
|
64
|
-
readonly document: DocumentNode
|
|
65
|
-
/** The operation being executed */
|
|
66
|
-
readonly operation: OperationDefinitionNode
|
|
67
|
-
/** Variables provided with the query */
|
|
68
|
-
readonly variables?: Record<string, unknown>
|
|
69
|
-
/** The GraphQL schema */
|
|
70
|
-
readonly schema: GraphQLSchema
|
|
71
|
-
/** Field complexity definitions from the builder */
|
|
72
|
-
readonly fieldComplexities: FieldComplexityMap
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Information provided when complexity limit is exceeded
|
|
77
|
-
*/
|
|
78
|
-
export interface ComplexityExceededInfo {
|
|
79
|
-
/** The computed complexity result */
|
|
80
|
-
readonly result: ComplexityResult
|
|
81
|
-
/** Which limit was exceeded */
|
|
82
|
-
readonly exceededLimit: "depth" | "complexity" | "aliases" | "fields"
|
|
83
|
-
/** The limit value */
|
|
84
|
-
readonly limit: number
|
|
85
|
-
/** The actual value */
|
|
86
|
-
readonly actual: number
|
|
87
|
-
/** The query that exceeded limits */
|
|
88
|
-
readonly query: string
|
|
89
|
-
/** Operation name if provided */
|
|
90
|
-
readonly operationName?: string
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Complexity value for a field - can be static or dynamic based on arguments
|
|
95
|
-
*/
|
|
96
|
-
export type FieldComplexity = number | ((args: Record<string, unknown>) => number)
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Map of type.field -> complexity
|
|
100
|
-
*/
|
|
101
|
-
export type FieldComplexityMap = Map<string, FieldComplexity>
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Custom complexity calculator function.
|
|
105
|
-
* Must be self-contained (no service requirements).
|
|
106
|
-
*/
|
|
107
|
-
export type ComplexityCalculator = (
|
|
108
|
-
info: ComplexityAnalysisInfo
|
|
109
|
-
) => Effect.Effect<ComplexityResult, ComplexityAnalysisError, never>
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Configuration for query complexity limiting
|
|
113
|
-
*/
|
|
114
|
-
export interface ComplexityConfig {
|
|
115
|
-
/**
|
|
116
|
-
* Maximum allowed query depth.
|
|
117
|
-
* Depth is the deepest nesting level in the query.
|
|
118
|
-
* @example
|
|
119
|
-
* // Depth 3:
|
|
120
|
-
* // { user { posts { comments { text } } } }
|
|
121
|
-
*/
|
|
122
|
-
readonly maxDepth?: number
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Maximum allowed total complexity score.
|
|
126
|
-
* Complexity is calculated by summing field costs.
|
|
127
|
-
*/
|
|
128
|
-
readonly maxComplexity?: number
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Maximum number of field aliases allowed.
|
|
132
|
-
* Prevents response explosion attacks via aliases.
|
|
133
|
-
*/
|
|
134
|
-
readonly maxAliases?: number
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Maximum total number of fields in the query.
|
|
138
|
-
* Includes all nested field selections.
|
|
139
|
-
*/
|
|
140
|
-
readonly maxFields?: number
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Default complexity cost for fields without explicit costs.
|
|
144
|
-
* @default 1
|
|
145
|
-
*/
|
|
146
|
-
readonly defaultFieldComplexity?: number
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Custom complexity calculator.
|
|
150
|
-
* If provided, this is used instead of the default calculator.
|
|
151
|
-
* Can be used to implement custom cost algorithms.
|
|
152
|
-
*/
|
|
153
|
-
readonly calculator?: ComplexityCalculator
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Hook called when a limit is exceeded.
|
|
157
|
-
* Useful for logging, metrics, or custom handling.
|
|
158
|
-
* This is called BEFORE the error is thrown.
|
|
159
|
-
* Must be self-contained (no service requirements).
|
|
160
|
-
*/
|
|
161
|
-
readonly onExceeded?: (info: ComplexityExceededInfo) => Effect.Effect<void, never, never>
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// ============================================================================
|
|
165
|
-
// Default Calculator
|
|
166
|
-
// ============================================================================
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Default complexity calculator that walks the AST and computes:
|
|
170
|
-
* - depth: Maximum nesting level
|
|
171
|
-
* - complexity: Sum of field costs
|
|
172
|
-
* - fieldCount: Total number of field selections
|
|
173
|
-
* - aliasCount: Number of aliased fields
|
|
174
|
-
*/
|
|
175
|
-
export const defaultComplexityCalculator = (defaultCost: number = 1): ComplexityCalculator => {
|
|
176
|
-
return (info: ComplexityAnalysisInfo) =>
|
|
177
|
-
Effect.try({
|
|
178
|
-
try: () => {
|
|
179
|
-
const fragments = new Map<string, FragmentDefinitionNode>()
|
|
180
|
-
|
|
181
|
-
// Collect fragment definitions
|
|
182
|
-
for (const definition of info.document.definitions) {
|
|
183
|
-
if (definition.kind === Kind.FRAGMENT_DEFINITION) {
|
|
184
|
-
fragments.set(definition.name.value, definition)
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Get the root type for the operation
|
|
189
|
-
const rootType = getRootType(info.schema, info.operation.operation)
|
|
190
|
-
if (!rootType) {
|
|
191
|
-
throw new Error(`No root type found for operation: ${info.operation.operation}`)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Analyze the selection set
|
|
195
|
-
const result = analyzeSelectionSet(
|
|
196
|
-
info.operation.selectionSet,
|
|
197
|
-
rootType,
|
|
198
|
-
info.schema,
|
|
199
|
-
fragments,
|
|
200
|
-
info.fieldComplexities,
|
|
201
|
-
info.variables ?? {},
|
|
202
|
-
defaultCost,
|
|
203
|
-
1, // Starting depth
|
|
204
|
-
new Set() // Visited fragments to prevent infinite loops
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
return result
|
|
208
|
-
},
|
|
209
|
-
catch: (error) =>
|
|
210
|
-
new ComplexityAnalysisError({
|
|
211
|
-
message: `Failed to analyze query complexity: ${error}`,
|
|
212
|
-
cause: error,
|
|
213
|
-
}),
|
|
214
|
-
})
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Get the root type for an operation
|
|
219
|
-
*/
|
|
220
|
-
function getRootType(
|
|
221
|
-
schema: GraphQLSchema,
|
|
222
|
-
operation: "query" | "mutation" | "subscription"
|
|
223
|
-
): GraphQLObjectType | null {
|
|
224
|
-
switch (operation) {
|
|
225
|
-
case "query":
|
|
226
|
-
return schema.getQueryType() ?? null
|
|
227
|
-
case "mutation":
|
|
228
|
-
return schema.getMutationType() ?? null
|
|
229
|
-
case "subscription":
|
|
230
|
-
return schema.getSubscriptionType() ?? null
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Get the named type from a potentially wrapped type
|
|
236
|
-
*/
|
|
237
|
-
function getNamedType(type: GraphQLOutputType): GraphQLObjectType | null {
|
|
238
|
-
if (type instanceof GraphQLNonNull || type instanceof GraphQLList) {
|
|
239
|
-
return getNamedType(type.ofType as GraphQLOutputType)
|
|
240
|
-
}
|
|
241
|
-
if (type instanceof GraphQLObjectType) {
|
|
242
|
-
return type
|
|
243
|
-
}
|
|
244
|
-
return null
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Merge a child result into an accumulator (mutates accumulator)
|
|
249
|
-
*/
|
|
250
|
-
function accumulateResult(
|
|
251
|
-
acc: { maxDepth: number; complexity: number; fieldCount: number; aliasCount: number },
|
|
252
|
-
result: ComplexityResult
|
|
253
|
-
): void {
|
|
254
|
-
acc.maxDepth = Math.max(acc.maxDepth, result.depth)
|
|
255
|
-
acc.complexity += result.complexity
|
|
256
|
-
acc.fieldCount += result.fieldCount
|
|
257
|
-
acc.aliasCount += result.aliasCount
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Analysis context passed through the recursive analysis functions
|
|
262
|
-
*/
|
|
263
|
-
interface AnalysisContext {
|
|
264
|
-
readonly schema: GraphQLSchema
|
|
265
|
-
readonly fragments: Map<string, FragmentDefinitionNode>
|
|
266
|
-
readonly fieldComplexities: FieldComplexityMap
|
|
267
|
-
readonly variables: Record<string, unknown>
|
|
268
|
-
readonly defaultCost: number
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Analyze a selection set and return complexity metrics
|
|
273
|
-
*/
|
|
274
|
-
function analyzeSelectionSet(
|
|
275
|
-
selectionSet: SelectionSetNode,
|
|
276
|
-
parentType: GraphQLObjectType,
|
|
277
|
-
schema: GraphQLSchema,
|
|
278
|
-
fragments: Map<string, FragmentDefinitionNode>,
|
|
279
|
-
fieldComplexities: FieldComplexityMap,
|
|
280
|
-
variables: Record<string, unknown>,
|
|
281
|
-
defaultCost: number,
|
|
282
|
-
currentDepth: number,
|
|
283
|
-
visitedFragments: Set<string>
|
|
284
|
-
): ComplexityResult {
|
|
285
|
-
const ctx: AnalysisContext = { schema, fragments, fieldComplexities, variables, defaultCost }
|
|
286
|
-
const acc = { maxDepth: currentDepth, complexity: 0, fieldCount: 0, aliasCount: 0 }
|
|
287
|
-
|
|
288
|
-
for (const selection of selectionSet.selections) {
|
|
289
|
-
const result = analyzeSelection(selection, parentType, ctx, currentDepth, visitedFragments)
|
|
290
|
-
accumulateResult(acc, result)
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return {
|
|
294
|
-
depth: acc.maxDepth,
|
|
295
|
-
complexity: acc.complexity,
|
|
296
|
-
fieldCount: acc.fieldCount,
|
|
297
|
-
aliasCount: acc.aliasCount,
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Analyze a single selection node (field, fragment spread, or inline fragment)
|
|
303
|
-
*/
|
|
304
|
-
function analyzeSelection(
|
|
305
|
-
selection: FieldNode | FragmentSpreadNode | InlineFragmentNode,
|
|
306
|
-
parentType: GraphQLObjectType,
|
|
307
|
-
ctx: AnalysisContext,
|
|
308
|
-
currentDepth: number,
|
|
309
|
-
visitedFragments: Set<string>
|
|
310
|
-
): ComplexityResult {
|
|
311
|
-
switch (selection.kind) {
|
|
312
|
-
case Kind.FIELD:
|
|
313
|
-
return analyzeField(selection, parentType, ctx, currentDepth, visitedFragments)
|
|
314
|
-
case Kind.FRAGMENT_SPREAD:
|
|
315
|
-
return analyzeFragmentSpread(selection, ctx, currentDepth, visitedFragments)
|
|
316
|
-
case Kind.INLINE_FRAGMENT:
|
|
317
|
-
return analyzeInlineFragment(selection, parentType, ctx, currentDepth, visitedFragments)
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Analyze a field node
|
|
323
|
-
*/
|
|
324
|
-
function analyzeField(
|
|
325
|
-
field: FieldNode,
|
|
326
|
-
parentType: GraphQLObjectType,
|
|
327
|
-
ctx: AnalysisContext,
|
|
328
|
-
currentDepth: number,
|
|
329
|
-
visitedFragments: Set<string>
|
|
330
|
-
): ComplexityResult {
|
|
331
|
-
const fieldName = field.name.value
|
|
332
|
-
const aliasCount = field.alias ? 1 : 0
|
|
333
|
-
|
|
334
|
-
// Introspection fields
|
|
335
|
-
if (fieldName.startsWith("__")) {
|
|
336
|
-
return { depth: currentDepth, complexity: 0, fieldCount: 1, aliasCount }
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Get the field from the schema
|
|
340
|
-
const schemaField = parentType.getFields()[fieldName]
|
|
341
|
-
if (!schemaField) {
|
|
342
|
-
// Field not found - skip (will be caught by validation)
|
|
343
|
-
return { depth: currentDepth, complexity: ctx.defaultCost, fieldCount: 1, aliasCount }
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Calculate field arguments
|
|
347
|
-
const args = resolveFieldArguments(field, ctx.variables)
|
|
348
|
-
|
|
349
|
-
// Get field complexity
|
|
350
|
-
const complexityKey = `${parentType.name}.${fieldName}`
|
|
351
|
-
const fieldComplexity = ctx.fieldComplexities.get(complexityKey)
|
|
352
|
-
const cost =
|
|
353
|
-
fieldComplexity !== undefined
|
|
354
|
-
? typeof fieldComplexity === "function"
|
|
355
|
-
? fieldComplexity(args)
|
|
356
|
-
: fieldComplexity
|
|
357
|
-
: ctx.defaultCost
|
|
358
|
-
|
|
359
|
-
// If the field has a selection set, analyze it
|
|
360
|
-
if (field.selectionSet) {
|
|
361
|
-
const fieldType = getNamedType(schemaField.type)
|
|
362
|
-
if (fieldType) {
|
|
363
|
-
const nestedResult = analyzeSelectionSet(
|
|
364
|
-
field.selectionSet,
|
|
365
|
-
fieldType,
|
|
366
|
-
ctx.schema,
|
|
367
|
-
ctx.fragments,
|
|
368
|
-
ctx.fieldComplexities,
|
|
369
|
-
ctx.variables,
|
|
370
|
-
ctx.defaultCost,
|
|
371
|
-
currentDepth + 1,
|
|
372
|
-
visitedFragments
|
|
373
|
-
)
|
|
374
|
-
return {
|
|
375
|
-
depth: nestedResult.depth,
|
|
376
|
-
complexity: cost + nestedResult.complexity,
|
|
377
|
-
fieldCount: 1 + nestedResult.fieldCount,
|
|
378
|
-
aliasCount: aliasCount + nestedResult.aliasCount,
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
return { depth: currentDepth, complexity: cost, fieldCount: 1, aliasCount }
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Analyze a fragment spread
|
|
388
|
-
*/
|
|
389
|
-
function analyzeFragmentSpread(
|
|
390
|
-
spread: FragmentSpreadNode,
|
|
391
|
-
ctx: AnalysisContext,
|
|
392
|
-
currentDepth: number,
|
|
393
|
-
visitedFragments: Set<string>
|
|
394
|
-
): ComplexityResult {
|
|
395
|
-
const fragmentName = spread.name.value
|
|
396
|
-
|
|
397
|
-
// Prevent infinite loops with fragment cycles
|
|
398
|
-
if (visitedFragments.has(fragmentName)) {
|
|
399
|
-
return { depth: currentDepth, complexity: 0, fieldCount: 0, aliasCount: 0 }
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const fragment = ctx.fragments.get(fragmentName)
|
|
403
|
-
if (!fragment) {
|
|
404
|
-
return { depth: currentDepth, complexity: 0, fieldCount: 0, aliasCount: 0 }
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const fragmentType = ctx.schema.getType(fragment.typeCondition.name.value)
|
|
408
|
-
if (!(fragmentType instanceof GraphQLObjectType)) {
|
|
409
|
-
return { depth: currentDepth, complexity: 0, fieldCount: 0, aliasCount: 0 }
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const newVisited = new Set(visitedFragments)
|
|
413
|
-
newVisited.add(fragmentName)
|
|
414
|
-
|
|
415
|
-
return analyzeSelectionSet(
|
|
416
|
-
fragment.selectionSet,
|
|
417
|
-
fragmentType,
|
|
418
|
-
ctx.schema,
|
|
419
|
-
ctx.fragments,
|
|
420
|
-
ctx.fieldComplexities,
|
|
421
|
-
ctx.variables,
|
|
422
|
-
ctx.defaultCost,
|
|
423
|
-
currentDepth,
|
|
424
|
-
newVisited
|
|
425
|
-
)
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Analyze an inline fragment
|
|
430
|
-
*/
|
|
431
|
-
function analyzeInlineFragment(
|
|
432
|
-
fragment: InlineFragmentNode,
|
|
433
|
-
parentType: GraphQLObjectType,
|
|
434
|
-
ctx: AnalysisContext,
|
|
435
|
-
currentDepth: number,
|
|
436
|
-
visitedFragments: Set<string>
|
|
437
|
-
): ComplexityResult {
|
|
438
|
-
let targetType = parentType
|
|
439
|
-
|
|
440
|
-
if (fragment.typeCondition) {
|
|
441
|
-
const conditionType = ctx.schema.getType(fragment.typeCondition.name.value)
|
|
442
|
-
if (conditionType instanceof GraphQLObjectType) {
|
|
443
|
-
targetType = conditionType
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
return analyzeSelectionSet(
|
|
448
|
-
fragment.selectionSet,
|
|
449
|
-
targetType,
|
|
450
|
-
ctx.schema,
|
|
451
|
-
ctx.fragments,
|
|
452
|
-
ctx.fieldComplexities,
|
|
453
|
-
ctx.variables,
|
|
454
|
-
ctx.defaultCost,
|
|
455
|
-
currentDepth,
|
|
456
|
-
visitedFragments
|
|
457
|
-
)
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Resolve field arguments, substituting variables
|
|
462
|
-
*/
|
|
463
|
-
function resolveFieldArguments(
|
|
464
|
-
field: FieldNode,
|
|
465
|
-
variables: Record<string, unknown>
|
|
466
|
-
): Record<string, unknown> {
|
|
467
|
-
const args: Record<string, unknown> = {}
|
|
468
|
-
|
|
469
|
-
if (!field.arguments) {
|
|
470
|
-
return args
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
for (const arg of field.arguments) {
|
|
474
|
-
const value = arg.value
|
|
475
|
-
switch (value.kind) {
|
|
476
|
-
case Kind.VARIABLE:
|
|
477
|
-
args[arg.name.value] = variables[value.name.value]
|
|
478
|
-
break
|
|
479
|
-
case Kind.INT:
|
|
480
|
-
args[arg.name.value] = parseInt(value.value, 10)
|
|
481
|
-
break
|
|
482
|
-
case Kind.FLOAT:
|
|
483
|
-
args[arg.name.value] = parseFloat(value.value)
|
|
484
|
-
break
|
|
485
|
-
case Kind.STRING:
|
|
486
|
-
args[arg.name.value] = value.value
|
|
487
|
-
break
|
|
488
|
-
case Kind.BOOLEAN:
|
|
489
|
-
args[arg.name.value] = value.value
|
|
490
|
-
break
|
|
491
|
-
case Kind.NULL:
|
|
492
|
-
args[arg.name.value] = null
|
|
493
|
-
break
|
|
494
|
-
case Kind.ENUM:
|
|
495
|
-
args[arg.name.value] = value.value
|
|
496
|
-
break
|
|
497
|
-
case Kind.LIST:
|
|
498
|
-
// Simplified - just use empty array for complexity calculation
|
|
499
|
-
args[arg.name.value] = []
|
|
500
|
-
break
|
|
501
|
-
case Kind.OBJECT:
|
|
502
|
-
// Simplified - just use empty object for complexity calculation
|
|
503
|
-
args[arg.name.value] = {}
|
|
504
|
-
break
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
return args
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// ============================================================================
|
|
512
|
-
// Validation
|
|
513
|
-
// ============================================================================
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* Validate query complexity against configured limits.
|
|
517
|
-
* Returns the complexity result if within limits, or fails with ComplexityLimitExceededError.
|
|
518
|
-
*/
|
|
519
|
-
export const validateComplexity = (
|
|
520
|
-
query: string,
|
|
521
|
-
operationName: string | undefined,
|
|
522
|
-
variables: Record<string, unknown> | undefined,
|
|
523
|
-
schema: GraphQLSchema,
|
|
524
|
-
fieldComplexities: FieldComplexityMap,
|
|
525
|
-
config: ComplexityConfig
|
|
526
|
-
): Effect.Effect<ComplexityResult, ComplexityLimitExceededError | ComplexityAnalysisError, never> =>
|
|
527
|
-
Effect.gen(function* () {
|
|
528
|
-
// Parse the query
|
|
529
|
-
const document = yield* Effect.try({
|
|
530
|
-
try: () => parse(query),
|
|
531
|
-
catch: (error) =>
|
|
532
|
-
new ComplexityAnalysisError({
|
|
533
|
-
message: `Failed to parse query: ${error}`,
|
|
534
|
-
cause: error,
|
|
535
|
-
}),
|
|
536
|
-
})
|
|
537
|
-
|
|
538
|
-
// Find the operation
|
|
539
|
-
const operation = yield* Effect.try({
|
|
540
|
-
try: () => {
|
|
541
|
-
const operations = document.definitions.filter(
|
|
542
|
-
(d): d is OperationDefinitionNode => d.kind === Kind.OPERATION_DEFINITION
|
|
543
|
-
)
|
|
544
|
-
|
|
545
|
-
if (operations.length === 0) {
|
|
546
|
-
throw new Error("No operation found in query")
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
if (operationName) {
|
|
550
|
-
const op = operations.find((o) => o.name?.value === operationName)
|
|
551
|
-
if (!op) {
|
|
552
|
-
throw new Error(`Operation "${operationName}" not found`)
|
|
553
|
-
}
|
|
554
|
-
return op
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
if (operations.length > 1) {
|
|
558
|
-
throw new Error("Multiple operations found - operationName required")
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
return operations[0]
|
|
562
|
-
},
|
|
563
|
-
catch: (error) =>
|
|
564
|
-
new ComplexityAnalysisError({
|
|
565
|
-
message: String(error),
|
|
566
|
-
cause: error,
|
|
567
|
-
}),
|
|
568
|
-
})
|
|
569
|
-
|
|
570
|
-
// Calculate complexity
|
|
571
|
-
const calculator =
|
|
572
|
-
config.calculator ?? defaultComplexityCalculator(config.defaultFieldComplexity ?? 1)
|
|
573
|
-
|
|
574
|
-
const result = yield* calculator({
|
|
575
|
-
document,
|
|
576
|
-
operation,
|
|
577
|
-
variables,
|
|
578
|
-
schema,
|
|
579
|
-
fieldComplexities,
|
|
580
|
-
})
|
|
581
|
-
|
|
582
|
-
// Check limits
|
|
583
|
-
const checkLimit = (
|
|
584
|
-
limitType: "depth" | "complexity" | "aliases" | "fields",
|
|
585
|
-
limit: number | undefined,
|
|
586
|
-
actual: number
|
|
587
|
-
) =>
|
|
588
|
-
Effect.gen(function* () {
|
|
589
|
-
if (limit !== undefined && actual > limit) {
|
|
590
|
-
const exceededInfo: ComplexityExceededInfo = {
|
|
591
|
-
result,
|
|
592
|
-
exceededLimit: limitType,
|
|
593
|
-
limit,
|
|
594
|
-
actual,
|
|
595
|
-
query,
|
|
596
|
-
operationName,
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Call onExceeded hook if provided
|
|
600
|
-
if (config.onExceeded) {
|
|
601
|
-
yield* config.onExceeded(exceededInfo)
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
yield* Effect.fail(
|
|
605
|
-
new ComplexityLimitExceededError({
|
|
606
|
-
message: `Query ${limitType} of ${actual} exceeds maximum allowed ${limitType} of ${limit}`,
|
|
607
|
-
limit,
|
|
608
|
-
actual,
|
|
609
|
-
limitType,
|
|
610
|
-
})
|
|
611
|
-
)
|
|
612
|
-
}
|
|
613
|
-
})
|
|
614
|
-
|
|
615
|
-
yield* checkLimit("depth", config.maxDepth, result.depth)
|
|
616
|
-
yield* checkLimit("complexity", config.maxComplexity, result.complexity)
|
|
617
|
-
yield* checkLimit("aliases", config.maxAliases, result.aliasCount)
|
|
618
|
-
yield* checkLimit("fields", config.maxFields, result.fieldCount)
|
|
619
|
-
|
|
620
|
-
return result
|
|
621
|
-
})
|
|
622
|
-
|
|
623
|
-
// ============================================================================
|
|
624
|
-
// Environment Configuration
|
|
625
|
-
// ============================================================================
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* Effect Config for loading complexity configuration from environment variables.
|
|
629
|
-
*
|
|
630
|
-
* Environment variables:
|
|
631
|
-
* - GRAPHQL_MAX_DEPTH: Maximum query depth
|
|
632
|
-
* - GRAPHQL_MAX_COMPLEXITY: Maximum complexity score
|
|
633
|
-
* - GRAPHQL_MAX_ALIASES: Maximum number of aliases
|
|
634
|
-
* - GRAPHQL_MAX_FIELDS: Maximum number of fields
|
|
635
|
-
* - GRAPHQL_DEFAULT_FIELD_COMPLEXITY: Default field complexity (default: 1)
|
|
636
|
-
*/
|
|
637
|
-
export const ComplexityConfigFromEnv: Config.Config<ComplexityConfig> = Config.all({
|
|
638
|
-
maxDepth: Config.number("GRAPHQL_MAX_DEPTH").pipe(Config.option),
|
|
639
|
-
maxComplexity: Config.number("GRAPHQL_MAX_COMPLEXITY").pipe(Config.option),
|
|
640
|
-
maxAliases: Config.number("GRAPHQL_MAX_ALIASES").pipe(Config.option),
|
|
641
|
-
maxFields: Config.number("GRAPHQL_MAX_FIELDS").pipe(Config.option),
|
|
642
|
-
defaultFieldComplexity: Config.number("GRAPHQL_DEFAULT_FIELD_COMPLEXITY").pipe(
|
|
643
|
-
Config.withDefault(1)
|
|
644
|
-
),
|
|
645
|
-
}).pipe(
|
|
646
|
-
Config.map(({ maxDepth, maxComplexity, maxAliases, maxFields, defaultFieldComplexity }) => ({
|
|
647
|
-
maxDepth: Option.getOrUndefined(maxDepth),
|
|
648
|
-
maxComplexity: Option.getOrUndefined(maxComplexity),
|
|
649
|
-
maxAliases: Option.getOrUndefined(maxAliases),
|
|
650
|
-
maxFields: Option.getOrUndefined(maxFields),
|
|
651
|
-
defaultFieldComplexity,
|
|
652
|
-
}))
|
|
653
|
-
)
|
|
654
|
-
|
|
655
|
-
// ============================================================================
|
|
656
|
-
// Utility Calculators
|
|
657
|
-
// ============================================================================
|
|
658
|
-
|
|
659
|
-
/**
|
|
660
|
-
* A simple depth-only calculator that only tracks query depth.
|
|
661
|
-
* Use this when you only care about depth limiting and want fast validation.
|
|
662
|
-
*/
|
|
663
|
-
export const depthOnlyCalculator: ComplexityCalculator = (info) =>
|
|
664
|
-
Effect.try({
|
|
665
|
-
try: () => {
|
|
666
|
-
const fragments = new Map<string, FragmentDefinitionNode>()
|
|
667
|
-
for (const definition of info.document.definitions) {
|
|
668
|
-
if (definition.kind === Kind.FRAGMENT_DEFINITION) {
|
|
669
|
-
fragments.set(definition.name.value, definition)
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
const depth = calculateMaxDepth(info.operation.selectionSet, fragments, 1, new Set())
|
|
674
|
-
|
|
675
|
-
return {
|
|
676
|
-
depth,
|
|
677
|
-
complexity: 0,
|
|
678
|
-
fieldCount: 0,
|
|
679
|
-
aliasCount: 0,
|
|
680
|
-
}
|
|
681
|
-
},
|
|
682
|
-
catch: (error) =>
|
|
683
|
-
new ComplexityAnalysisError({
|
|
684
|
-
message: `Failed to analyze query depth: ${error}`,
|
|
685
|
-
cause: error,
|
|
686
|
-
}),
|
|
687
|
-
})
|
|
688
|
-
|
|
689
|
-
function calculateMaxDepth(
|
|
690
|
-
selectionSet: SelectionSetNode,
|
|
691
|
-
fragments: Map<string, FragmentDefinitionNode>,
|
|
692
|
-
currentDepth: number,
|
|
693
|
-
visitedFragments: Set<string>
|
|
694
|
-
): number {
|
|
695
|
-
let maxDepth = currentDepth
|
|
696
|
-
|
|
697
|
-
for (const selection of selectionSet.selections) {
|
|
698
|
-
switch (selection.kind) {
|
|
699
|
-
case Kind.FIELD:
|
|
700
|
-
if (selection.selectionSet) {
|
|
701
|
-
const nestedDepth = calculateMaxDepth(
|
|
702
|
-
selection.selectionSet,
|
|
703
|
-
fragments,
|
|
704
|
-
currentDepth + 1,
|
|
705
|
-
visitedFragments
|
|
706
|
-
)
|
|
707
|
-
maxDepth = Math.max(maxDepth, nestedDepth)
|
|
708
|
-
}
|
|
709
|
-
break
|
|
710
|
-
|
|
711
|
-
case Kind.FRAGMENT_SPREAD: {
|
|
712
|
-
const fragmentName = selection.name.value
|
|
713
|
-
if (!visitedFragments.has(fragmentName)) {
|
|
714
|
-
const fragment = fragments.get(fragmentName)
|
|
715
|
-
if (fragment) {
|
|
716
|
-
const newVisited = new Set(visitedFragments)
|
|
717
|
-
newVisited.add(fragmentName)
|
|
718
|
-
const fragmentDepth = calculateMaxDepth(
|
|
719
|
-
fragment.selectionSet,
|
|
720
|
-
fragments,
|
|
721
|
-
currentDepth,
|
|
722
|
-
newVisited
|
|
723
|
-
)
|
|
724
|
-
maxDepth = Math.max(maxDepth, fragmentDepth)
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
break
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
case Kind.INLINE_FRAGMENT: {
|
|
731
|
-
const inlineDepth = calculateMaxDepth(
|
|
732
|
-
selection.selectionSet,
|
|
733
|
-
fragments,
|
|
734
|
-
currentDepth,
|
|
735
|
-
visitedFragments
|
|
736
|
-
)
|
|
737
|
-
maxDepth = Math.max(maxDepth, inlineDepth)
|
|
738
|
-
break
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
return maxDepth
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
/**
|
|
747
|
-
* Combine multiple calculators - returns the maximum values from all calculators.
|
|
748
|
-
*/
|
|
749
|
-
export const combineCalculators = (
|
|
750
|
-
...calculators: ComplexityCalculator[]
|
|
751
|
-
): ComplexityCalculator => {
|
|
752
|
-
return (info) =>
|
|
753
|
-
Effect.gen(function* () {
|
|
754
|
-
let maxDepth = 0
|
|
755
|
-
let maxComplexity = 0
|
|
756
|
-
let maxFieldCount = 0
|
|
757
|
-
let maxAliasCount = 0
|
|
758
|
-
|
|
759
|
-
for (const calculator of calculators) {
|
|
760
|
-
const result = yield* calculator(info)
|
|
761
|
-
maxDepth = Math.max(maxDepth, result.depth)
|
|
762
|
-
maxComplexity = Math.max(maxComplexity, result.complexity)
|
|
763
|
-
maxFieldCount = Math.max(maxFieldCount, result.fieldCount)
|
|
764
|
-
maxAliasCount = Math.max(maxAliasCount, result.aliasCount)
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
return {
|
|
768
|
-
depth: maxDepth,
|
|
769
|
-
complexity: maxComplexity,
|
|
770
|
-
fieldCount: maxFieldCount,
|
|
771
|
-
aliasCount: maxAliasCount,
|
|
772
|
-
}
|
|
773
|
-
})
|
|
774
|
-
}
|