@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.
Files changed (164) hide show
  1. package/README.md +100 -0
  2. package/builder/index.cjs +1431 -0
  3. package/builder/index.cjs.map +1 -0
  4. package/builder/index.d.cts +259 -0
  5. package/{dist/builder/pipe-api.d.ts → builder/index.d.ts} +49 -21
  6. package/builder/index.js +1390 -0
  7. package/builder/index.js.map +1 -0
  8. package/index.cjs +3419 -0
  9. package/index.cjs.map +1 -0
  10. package/index.d.cts +523 -0
  11. package/index.d.ts +523 -0
  12. package/index.js +3242 -0
  13. package/index.js.map +1 -0
  14. package/package.json +19 -28
  15. package/schema-builder-Cvdq7Kz_.d.cts +963 -0
  16. package/schema-builder-Cvdq7Kz_.d.ts +963 -0
  17. package/server/index.cjs +1555 -0
  18. package/server/index.cjs.map +1 -0
  19. package/server/index.d.cts +680 -0
  20. package/server/index.d.ts +680 -0
  21. package/server/index.js +1524 -0
  22. package/server/index.js.map +1 -0
  23. package/dist/analyzer-extension.d.ts +0 -105
  24. package/dist/analyzer-extension.d.ts.map +0 -1
  25. package/dist/analyzer-extension.js +0 -137
  26. package/dist/analyzer-extension.js.map +0 -1
  27. package/dist/builder/execute.d.ts +0 -26
  28. package/dist/builder/execute.d.ts.map +0 -1
  29. package/dist/builder/execute.js +0 -104
  30. package/dist/builder/execute.js.map +0 -1
  31. package/dist/builder/field-builders.d.ts +0 -30
  32. package/dist/builder/field-builders.d.ts.map +0 -1
  33. package/dist/builder/field-builders.js +0 -200
  34. package/dist/builder/field-builders.js.map +0 -1
  35. package/dist/builder/index.d.ts +0 -7
  36. package/dist/builder/index.d.ts.map +0 -1
  37. package/dist/builder/index.js +0 -31
  38. package/dist/builder/index.js.map +0 -1
  39. package/dist/builder/pipe-api.d.ts.map +0 -1
  40. package/dist/builder/pipe-api.js +0 -151
  41. package/dist/builder/pipe-api.js.map +0 -1
  42. package/dist/builder/schema-builder.d.ts +0 -301
  43. package/dist/builder/schema-builder.d.ts.map +0 -1
  44. package/dist/builder/schema-builder.js +0 -566
  45. package/dist/builder/schema-builder.js.map +0 -1
  46. package/dist/builder/type-registry.d.ts +0 -80
  47. package/dist/builder/type-registry.d.ts.map +0 -1
  48. package/dist/builder/type-registry.js +0 -505
  49. package/dist/builder/type-registry.js.map +0 -1
  50. package/dist/builder/types.d.ts +0 -283
  51. package/dist/builder/types.d.ts.map +0 -1
  52. package/dist/builder/types.js +0 -3
  53. package/dist/builder/types.js.map +0 -1
  54. package/dist/cli/generate-schema.d.ts +0 -29
  55. package/dist/cli/generate-schema.d.ts.map +0 -1
  56. package/dist/cli/generate-schema.js +0 -233
  57. package/dist/cli/generate-schema.js.map +0 -1
  58. package/dist/cli/index.d.ts +0 -19
  59. package/dist/cli/index.d.ts.map +0 -1
  60. package/dist/cli/index.js +0 -24
  61. package/dist/cli/index.js.map +0 -1
  62. package/dist/context.d.ts +0 -18
  63. package/dist/context.d.ts.map +0 -1
  64. package/dist/context.js +0 -11
  65. package/dist/context.js.map +0 -1
  66. package/dist/error.d.ts +0 -45
  67. package/dist/error.d.ts.map +0 -1
  68. package/dist/error.js +0 -29
  69. package/dist/error.js.map +0 -1
  70. package/dist/extensions.d.ts +0 -130
  71. package/dist/extensions.d.ts.map +0 -1
  72. package/dist/extensions.js +0 -78
  73. package/dist/extensions.js.map +0 -1
  74. package/dist/index.d.ts +0 -12
  75. package/dist/index.d.ts.map +0 -1
  76. package/dist/index.js +0 -47
  77. package/dist/index.js.map +0 -1
  78. package/dist/loader.d.ts +0 -169
  79. package/dist/loader.d.ts.map +0 -1
  80. package/dist/loader.js +0 -237
  81. package/dist/loader.js.map +0 -1
  82. package/dist/resolver-context.d.ts +0 -154
  83. package/dist/resolver-context.d.ts.map +0 -1
  84. package/dist/resolver-context.js +0 -184
  85. package/dist/resolver-context.js.map +0 -1
  86. package/dist/schema-mapping.d.ts +0 -30
  87. package/dist/schema-mapping.d.ts.map +0 -1
  88. package/dist/schema-mapping.js +0 -280
  89. package/dist/schema-mapping.js.map +0 -1
  90. package/dist/server/cache-control.d.ts +0 -96
  91. package/dist/server/cache-control.d.ts.map +0 -1
  92. package/dist/server/cache-control.js +0 -308
  93. package/dist/server/cache-control.js.map +0 -1
  94. package/dist/server/complexity.d.ts +0 -165
  95. package/dist/server/complexity.d.ts.map +0 -1
  96. package/dist/server/complexity.js +0 -433
  97. package/dist/server/complexity.js.map +0 -1
  98. package/dist/server/config.d.ts +0 -66
  99. package/dist/server/config.d.ts.map +0 -1
  100. package/dist/server/config.js +0 -104
  101. package/dist/server/config.js.map +0 -1
  102. package/dist/server/graphiql.d.ts +0 -5
  103. package/dist/server/graphiql.d.ts.map +0 -1
  104. package/dist/server/graphiql.js +0 -43
  105. package/dist/server/graphiql.js.map +0 -1
  106. package/dist/server/index.d.ts +0 -18
  107. package/dist/server/index.d.ts.map +0 -1
  108. package/dist/server/index.js +0 -48
  109. package/dist/server/index.js.map +0 -1
  110. package/dist/server/router.d.ts +0 -79
  111. package/dist/server/router.d.ts.map +0 -1
  112. package/dist/server/router.js +0 -232
  113. package/dist/server/router.js.map +0 -1
  114. package/dist/server/schema-builder-extensions.d.ts +0 -42
  115. package/dist/server/schema-builder-extensions.d.ts.map +0 -1
  116. package/dist/server/schema-builder-extensions.js +0 -48
  117. package/dist/server/schema-builder-extensions.js.map +0 -1
  118. package/dist/server/sse-adapter.d.ts +0 -64
  119. package/dist/server/sse-adapter.d.ts.map +0 -1
  120. package/dist/server/sse-adapter.js +0 -227
  121. package/dist/server/sse-adapter.js.map +0 -1
  122. package/dist/server/sse-types.d.ts +0 -192
  123. package/dist/server/sse-types.d.ts.map +0 -1
  124. package/dist/server/sse-types.js +0 -63
  125. package/dist/server/sse-types.js.map +0 -1
  126. package/dist/server/ws-adapter.d.ts +0 -39
  127. package/dist/server/ws-adapter.d.ts.map +0 -1
  128. package/dist/server/ws-adapter.js +0 -247
  129. package/dist/server/ws-adapter.js.map +0 -1
  130. package/dist/server/ws-types.d.ts +0 -169
  131. package/dist/server/ws-types.d.ts.map +0 -1
  132. package/dist/server/ws-types.js +0 -11
  133. package/dist/server/ws-types.js.map +0 -1
  134. package/dist/server/ws-utils.d.ts +0 -42
  135. package/dist/server/ws-utils.d.ts.map +0 -1
  136. package/dist/server/ws-utils.js +0 -99
  137. package/dist/server/ws-utils.js.map +0 -1
  138. package/src/analyzer-extension.ts +0 -254
  139. package/src/builder/execute.ts +0 -153
  140. package/src/builder/field-builders.ts +0 -322
  141. package/src/builder/index.ts +0 -48
  142. package/src/builder/pipe-api.ts +0 -312
  143. package/src/builder/schema-builder.ts +0 -970
  144. package/src/builder/type-registry.ts +0 -670
  145. package/src/builder/types.ts +0 -305
  146. package/src/context.ts +0 -23
  147. package/src/error.ts +0 -32
  148. package/src/extensions.ts +0 -240
  149. package/src/index.ts +0 -32
  150. package/src/loader.ts +0 -363
  151. package/src/resolver-context.ts +0 -253
  152. package/src/schema-mapping.ts +0 -307
  153. package/src/server/cache-control.ts +0 -590
  154. package/src/server/complexity.ts +0 -774
  155. package/src/server/config.ts +0 -174
  156. package/src/server/graphiql.ts +0 -38
  157. package/src/server/index.ts +0 -96
  158. package/src/server/router.ts +0 -432
  159. package/src/server/schema-builder-extensions.ts +0 -51
  160. package/src/server/sse-adapter.ts +0 -327
  161. package/src/server/sse-types.ts +0 -234
  162. package/src/server/ws-adapter.ts +0 -355
  163. package/src/server/ws-types.ts +0 -192
  164. package/src/server/ws-utils.ts +0 -136
@@ -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
- }