graphql-query-depth-limit-esm 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Lafitte Mehdy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,496 @@
1
+ # graphql-query-depth-limit-esm
2
+
3
+ [![npm version](https://img.shields.io/npm/v/graphql-query-depth-limit-esm.svg)](https://www.npmjs.com/package/graphql-query-depth-limit-esm)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+ [![Build Status](https://github.com/lafittemehdy/graphql-query-depth-limit-esm/actions/workflows/test.yml/badge.svg)](https://github.com/lafittemehdy/graphql-query-depth-limit-esm/actions)
6
+
7
+ Protect your GraphQL API by limiting query depth before execution.
8
+
9
+ Prevents deeply nested queries from overloading your server. A lightweight, zero-dependency library that works with any GraphQL server (Apollo, Yoga, etc.) with native ESM and TypeScript support.
10
+
11
+ ## Features
12
+
13
+ - **Native ESM & TypeScript:** Modern module support with full type safety
14
+ - **Works Anywhere:** Compatible with any GraphQL-compliant server (Apollo, Yoga, etc.)
15
+ - **Fragment Support:** Correctly handles fragment spreads and inline fragments
16
+ - **Flexible Ignore Rules:** Skip specific fields using strings, RegExp, or custom functions
17
+ - **Directive Support:** Field-specific depth limits using `@depth` directive
18
+ - **Zero Dependencies:** Lightweight and focused (small bundle size)
19
+ - **Well Tested:** Comprehensive test suite
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install graphql-query-depth-limit-esm
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ### Apollo Server Integration
30
+
31
+ ```typescript
32
+ import { ApolloServer } from '@apollo/server';
33
+ import { startStandaloneServer } from '@apollo/server/standalone';
34
+ import { depthLimit } from 'graphql-query-depth-limit-esm';
35
+
36
+ // Step 1: Define your schema
37
+ const typeDefs = `#graphql
38
+ directive @depth(max: Int!) on FIELD_DEFINITION
39
+
40
+ type Query {
41
+ # Regular user endpoint with depth limit of 3
42
+ user(id: ID!): User @depth(max: 3)
43
+
44
+ # Admin endpoint with higher depth limit
45
+ adminUser(id: ID!): User @depth(max: 10)
46
+
47
+ # Public endpoint without directive (uses global limit)
48
+ users: [User!]!
49
+ }
50
+
51
+ type User {
52
+ id: ID!
53
+ name: String!
54
+ email: String!
55
+ friends: [User!]!
56
+ posts: [Post!]!
57
+ }
58
+
59
+ type Post {
60
+ id: ID!
61
+ title: String!
62
+ author: User!
63
+ }
64
+ `;
65
+
66
+ // Step 2: Define your resolvers
67
+ const resolvers = {
68
+ Query: {
69
+ user: (_: unknown, { id }: { id: string }) => ({
70
+ id,
71
+ name: "John Doe",
72
+ email: "john@example.com",
73
+ friends: [],
74
+ posts: [],
75
+ }),
76
+ adminUser: (_: unknown, { id }: { id: string }) => ({
77
+ id,
78
+ name: "Admin User",
79
+ email: "admin@example.com",
80
+ friends: [],
81
+ posts: [],
82
+ }),
83
+ users: () => [
84
+ {
85
+ id: "1",
86
+ name: "Alice",
87
+ email: "alice@example.com",
88
+ friends: [],
89
+ posts: [],
90
+ },
91
+ ],
92
+ },
93
+ User: {
94
+ friends: (parent: { id: string }) => [
95
+ {
96
+ id: `${parent.id}-friend`,
97
+ name: "Friend User",
98
+ email: "friend@example.com",
99
+ friends: [],
100
+ posts: [],
101
+ },
102
+ ],
103
+ posts: () => [],
104
+ },
105
+ };
106
+
107
+ // Step 3: Create and start the server with depth limiting
108
+ const server = new ApolloServer({
109
+ typeDefs,
110
+ resolvers,
111
+ validationRules: [
112
+ // Global depth limit of 5 with directive support enabled
113
+ depthLimit(5, { useDirective: true }),
114
+ ],
115
+ });
116
+
117
+ const { url } = await startStandaloneServer(server, {
118
+ listen: { port: 4000 },
119
+ });
120
+
121
+ console.log(`🚀 Server ready at: ${url}`);
122
+ ```
123
+
124
+ ## How It Works
125
+
126
+ The library calculates query depth during GraphQL validation (before execution). Deeply nested queries are rejected before hitting your business logic.
127
+
128
+ **Process:**
129
+ 1. Client sends a query
130
+ 2. Server parses and validates the query
131
+ 3. **Query depth calculation runs** (this library)
132
+ 4. If validation passes, query executes
133
+
134
+ The library traverses the query AST to calculate depth. It correctly handles fragments and introspection fields, counting only fields that actually contribute to nesting.
135
+
136
+ ---
137
+
138
+ ## Usage Examples
139
+
140
+ ### Basic Depth Limiting
141
+
142
+ ```typescript
143
+ import { depthLimit } from 'graphql-query-depth-limit-esm';
144
+ import { ApolloServer } from '@apollo/server';
145
+
146
+ const server = new ApolloServer({
147
+ schema,
148
+ validationRules: [depthLimit(10)], // Maximum depth of 10
149
+ });
150
+ ```
151
+
152
+ ### With Ignore Rules
153
+
154
+ Skip specific fields from depth calculation:
155
+
156
+ ```typescript
157
+ import { depthLimit } from 'graphql-query-depth-limit-esm';
158
+
159
+ const server = new ApolloServer({
160
+ schema,
161
+ validationRules: [
162
+ depthLimit(10, {
163
+ ignore: [
164
+ 'friends', // Exact field name
165
+ /^internal/, // RegExp pattern
166
+ (fieldName) => fieldName.startsWith('_'), // Custom function
167
+ ],
168
+ }),
169
+ ],
170
+ });
171
+ ```
172
+
173
+ ### With Callback
174
+
175
+ Monitor query depths:
176
+
177
+ ```typescript
178
+ import { depthLimit } from 'graphql-query-depth-limit-esm';
179
+
180
+ const server = new ApolloServer({
181
+ schema,
182
+ validationRules: [
183
+ depthLimit(10, undefined, (depths) => {
184
+ console.log('Query depths:', depths);
185
+ // Output: { "GetUser": 5, "GetPosts": 3 }
186
+ }),
187
+ ],
188
+ });
189
+ ```
190
+
191
+ ### With @depth Directive
192
+
193
+ Apply field-specific depth limits using the `@depth` directive:
194
+
195
+ ```typescript
196
+ import { depthLimit } from 'graphql-query-depth-limit-esm';
197
+
198
+ // First, add the directive to your schema
199
+ const typeDefs = `#graphql
200
+ directive @depth(max: Int!) on FIELD_DEFINITION
201
+
202
+ type Query {
203
+ publicUser(id: ID!): User @depth(max: 2)
204
+ adminUser(id: ID!): User @depth(max: 10)
205
+ }
206
+
207
+ type User {
208
+ id: ID!
209
+ name: String!
210
+ friends: [User]
211
+ }
212
+ `;
213
+
214
+ const server = new ApolloServer({
215
+ typeDefs,
216
+ resolvers,
217
+ validationRules: [
218
+ depthLimit(5, { useDirective: true }),
219
+ ],
220
+ });
221
+ ```
222
+
223
+ **How it works:**
224
+ - `publicUser` field has `@depth(max: 2)` - queries can only go 2 levels deep
225
+ - `adminUser` field has `@depth(max: 10)` - queries can go 10 levels deep
226
+ - Fields without `@depth` directive use the global limit (5 in this example)
227
+ - The `@depth` directive can be combined with `@complexity` and other directives
228
+
229
+ ## How Depth is Calculated
230
+
231
+ Depth is measured by counting nested field selections:
232
+
233
+ ```graphql
234
+ query {
235
+ user { # depth 1
236
+ posts { # depth 2
237
+ comments { # depth 3
238
+ author { # depth 4
239
+ name # depth 4 (scalar fields don't add depth)
240
+ }
241
+ }
242
+ }
243
+ }
244
+ }
245
+ # Total depth: 4
246
+ ```
247
+
248
+ ### What Doesn't Add Depth
249
+
250
+ - **Fragment spreads** (`...FragmentName`) - The fragment's fields are counted, not the spread itself
251
+ - **Inline fragments** (`... on Type { }`) - The inline fragment doesn't add depth
252
+ - **Introspection fields** (`__typename`, `__schema`, `__type`, etc.)
253
+ - **Fields matching ignore rules** - Skipped entirely from calculation
254
+ - **Scalar/leaf fields** - Terminal nodes don't increase depth
255
+
256
+ ## Query Depth Examples
257
+
258
+ **Schema:**
259
+ ```graphql
260
+ type Query {
261
+ user(id: ID!): User
262
+ }
263
+
264
+ type User {
265
+ id: ID!
266
+ name: String!
267
+ friends: [User]
268
+ posts: [Post]
269
+ }
270
+
271
+ type Post {
272
+ id: ID!
273
+ title: String!
274
+ comments: [Comment]
275
+ }
276
+
277
+ type Comment {
278
+ id: ID!
279
+ body: String!
280
+ author: User
281
+ }
282
+ ```
283
+
284
+ | Query | Depth | Allowed (max 3)? |
285
+ | :--- | :---: | :---: |
286
+ | `{ user { id } }` | 1 | ✅ |
287
+ | `{ user { friends { name } } }` | 2 | ✅ |
288
+ | `{ user { posts { comments { body } } } }` | 3 | ✅ |
289
+ | `{ user { posts { comments { author { name } } } } }` | 4 | ❌ |
290
+ | `{ user { friends { friends { friends { name } } } } }` | 4 | ❌ |
291
+
292
+ ## Integration Examples
293
+
294
+ ### Apollo Server
295
+
296
+ ```typescript
297
+ import { ApolloServer } from '@apollo/server';
298
+ import { depthLimit } from 'graphql-query-depth-limit-esm';
299
+
300
+ const server = new ApolloServer({
301
+ schema,
302
+ validationRules: [depthLimit(10)],
303
+ });
304
+ ```
305
+
306
+ ### Express GraphQL
307
+
308
+ ```typescript
309
+ import { graphqlHTTP } from 'express-graphql';
310
+ import { depthLimit } from 'graphql-query-depth-limit-esm';
311
+
312
+ app.use(
313
+ '/graphql',
314
+ graphqlHTTP({
315
+ schema,
316
+ validationRules: [depthLimit(10)],
317
+ }),
318
+ );
319
+ ```
320
+
321
+ ### GraphQL Yoga
322
+
323
+ ```typescript
324
+ import { createYoga } from 'graphql-yoga';
325
+ import { depthLimit } from 'graphql-query-depth-limit-esm';
326
+
327
+ const yoga = createYoga({
328
+ schema,
329
+ validationRules: [depthLimit(10)],
330
+ });
331
+ ```
332
+
333
+ ## API Reference
334
+
335
+ ### `depthLimit(maxDepth, options?, callback?)`
336
+
337
+ Creates a GraphQL validation rule that limits query depth.
338
+
339
+ - `maxDepth` (number, **required**) - Maximum allowed depth for queries
340
+ - `options` (object, optional):
341
+ - `caseInsensitiveIgnore` (boolean, optional) - Enable case-insensitive matching for string-based ignore rules. Default: `false`
342
+ - `ignore` (IgnoreRule[], optional) - Fields to exclude from depth calculation
343
+ - String: Exact field name match (case-sensitive by default)
344
+ - RegExp: Pattern matching
345
+ - Function: `(fieldName: string) => boolean` - Custom logic
346
+ - `useDirective` (boolean, optional) - Enable reading depth limits from `@depth` directive on fields. Default: `false`
347
+ - `callback` (DepthCallback, optional) - Called after validation with depth information
348
+ - Receives object mapping operation names to their depths
349
+
350
+ **Returns:** `ValidationRule` - GraphQL validation rule function
351
+
352
+ ### Ignore Rules
353
+
354
+ Control which fields are excluded from depth calculation:
355
+
356
+ ```typescript
357
+ import { depthLimit } from 'graphql-query-depth-limit-esm';
358
+
359
+ const server = new ApolloServer({
360
+ schema,
361
+ validationRules: [
362
+ depthLimit(5, {
363
+ ignore: [
364
+ 'friends', // String: exact match
365
+ /^metadata/, // RegExp: pattern match
366
+ (fieldName) => fieldName.includes('__'), // Function: custom logic
367
+ ],
368
+ }),
369
+ ],
370
+ });
371
+ ```
372
+
373
+ ## Security Considerations
374
+
375
+ This library is designed to protect against denial-of-service (DoS) attacks via deeply nested queries. Recent updates have strengthened the implementation to handle edge cases correctly:
376
+
377
+ ### Correctly Handled Scenarios
378
+
379
+ ✅ **Fragments at Different Depths** - Fragments used multiple times at different nesting levels are calculated correctly. Each usage is evaluated independently to prevent depth limit bypass.
380
+
381
+ ```graphql
382
+ fragment UserInfo on User {
383
+ posts { title } # Adds 2 levels
384
+ }
385
+
386
+ query {
387
+ user {
388
+ ...UserInfo # Depth: 1 + 2 = 3
389
+ friends {
390
+ ...UserInfo # Depth: 2 + 2 = 4 ✓ (calculated independently)
391
+ }
392
+ }
393
+ }
394
+ ```
395
+
396
+ ✅ **Circular Fragment Detection** - Circular fragment references are detected per-path to prevent infinite recursion while maintaining accurate depth calculation.
397
+
398
+ ✅ **Introspection Fields** - All introspection fields (`__typename`, `__schema`, `__type`, etc.) are automatically excluded from depth calculation.
399
+
400
+ ### Known Limitations
401
+
402
+ ⚠️ **Variables in @depth Directive** - The `@depth` directive only supports integer literals. Variables are not supported because their values are not available during the validation phase.
403
+
404
+ ```graphql
405
+ # ✅ Works
406
+ type Query {
407
+ user: User @depth(max: 5)
408
+ }
409
+
410
+ # ❌ Not supported - will fall back to global depth limit
411
+ type Query {
412
+ user: User @depth(max: $maxDepth)
413
+ }
414
+ ```
415
+
416
+ ⚠️ **Case-Sensitive Field Names** - By default, field name matching is case-sensitive (as per GraphQL specification). Use the `caseInsensitiveIgnore` option if you need case-insensitive matching for ignore rules.
417
+
418
+ ```typescript
419
+ // Case-sensitive (default)
420
+ depthLimit(5, { ignore: ['friends'] }) // Only matches 'friends', not 'Friends'
421
+
422
+ // Case-insensitive
423
+ depthLimit(5, {
424
+ caseInsensitiveIgnore: true,
425
+ ignore: ['friends'] // Matches 'friends', 'Friends', 'FRIENDS', etc.
426
+ })
427
+ ```
428
+
429
+ ### Security Best Practices
430
+
431
+ 1. **Always use depth limiting in production** - Even if you think your schema is safe
432
+ 2. **Combine with complexity limiting** - Use both for comprehensive protection
433
+ 3. **Set reasonable limits** - Balance security with legitimate use cases (typically 5-15)
434
+ 4. **Monitor query depths** - Use the callback to log and alert on suspicious patterns
435
+ 5. **Use field-specific limits** - Apply stricter limits to recursive fields via `@depth` directive
436
+
437
+ ## Why Depth Limiting?
438
+
439
+ Deeply nested queries can cause:
440
+
441
+ - **Performance issues** - Exponential data fetching (N+1 problem amplified)
442
+ - **DoS attacks** - Server resource exhaustion
443
+ - **Database overload** - Too many nested joins
444
+
445
+ ### Example Attack
446
+
447
+ ```graphql
448
+ query Attack {
449
+ user {
450
+ friends {
451
+ friends {
452
+ friends {
453
+ friends {
454
+ # ... 100 levels deep
455
+ # This could fetch millions of records!
456
+ }
457
+ }
458
+ }
459
+ }
460
+ }
461
+ }
462
+ ```
463
+
464
+ Without depth limiting, this query could:
465
+ - Fetch 10^100 user records (if each user has 10 friends)
466
+ - Exhaust server memory
467
+ - Crash your database
468
+
469
+ **Depth limiting stops this attack during validation.**
470
+
471
+ ## Comparison with Complexity Limiting
472
+
473
+ | Feature | Depth Limit | Complexity Limit |
474
+ | :--- | :--- | :--- |
475
+ | **Measures** | Nesting level | Total operation cost |
476
+ | **Prevents** | Deep recursion attacks | Wide/expensive queries |
477
+ | **Example Attack** | `user { friends { friends { ... } } }` | `users(limit: 9999) { id name email ... }` |
478
+ | **Use Case** | Recursive relationships | Pagination/list queries |
479
+
480
+ **Best Practice:** Use **both** depth and complexity limiting for comprehensive protection.
481
+
482
+ ---
483
+
484
+ ## Requirements
485
+
486
+ - Node.js 18+
487
+ - GraphQL 16+
488
+
489
+ ## Related Packages
490
+
491
+ - [graphql-query-complexity-esm](https://github.com/lafittemehdy/graphql-query-complexity-esm) - Query complexity limiting
492
+ - [graphql-rate-limit-redis-esm](https://github.com/lafittemehdy/graphql-rate-limit-redis-esm) - Rate limiting with Redis
493
+
494
+ ## License
495
+
496
+ MIT License. This code is free to use. It has no opinions. You, I presume, do. Please use them.
@@ -0,0 +1,41 @@
1
+ import { type ValidationContext } from "graphql";
2
+ import type { DepthCallback, DepthLimitOptions } from "./types.js";
3
+ /**
4
+ * Creates a GraphQL validation rule that limits query depth.
5
+ *
6
+ * This helps prevent DoS attacks and resource exhaustion from excessively deep queries.
7
+ * The depth limit can be configured globally and optionally overridden per-field using
8
+ * the @depth directive.
9
+ *
10
+ * Security considerations:
11
+ * - This validator correctly handles fragments used at different depths
12
+ * - Circular fragment references are detected and handled per-path
13
+ * - Introspection fields (__typename, __schema, etc.) are automatically ignored
14
+ *
15
+ * Limitations:
16
+ * - Variables in @depth directives are not supported (falls back to global limit)
17
+ * - Field names are case-sensitive by default (use caseInsensitiveIgnore option if needed)
18
+ *
19
+ * @param maxDepth - Maximum allowed depth for queries
20
+ * @param options - Optional configuration (ignore rules, directive support, case sensitivity)
21
+ * @param callback - Optional callback invoked with depth information for all operations
22
+ * @returns A GraphQL validation rule function
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * import { depthLimit } from 'graphql-query-depth-limit-esm';
27
+ * import { validate } from 'graphql';
28
+ *
29
+ * const validationRules = [
30
+ * depthLimit(5, {
31
+ * ignore: ['friends', /.*Connection$/],
32
+ * useDirective: true,
33
+ * caseInsensitiveIgnore: false
34
+ * })
35
+ * ];
36
+ *
37
+ * const errors = validate(schema, query, validationRules);
38
+ * ```
39
+ */
40
+ export declare function depthLimit(maxDepth: number, options?: DepthLimitOptions, callback?: DepthCallback): (context: ValidationContext) => {};
41
+ //# sourceMappingURL=depthLimit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"depthLimit.d.ts","sourceRoot":"","sources":["../src/depthLimit.ts"],"names":[],"mappings":"AAAA,OAAO,EAgBL,KAAK,iBAAiB,EACvB,MAAM,SAAS,CAAC;AACjB,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAc,MAAM,YAAY,CAAC;AA8S/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,UAAU,CACxB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,iBAAiB,EAC3B,QAAQ,CAAC,EAAE,aAAa,IAEiB,SAAS,iBAAiB,QAwDpE"}
@@ -0,0 +1,273 @@
1
+ import { GraphQLError, getNamedType, isCompositeType, Kind, } from "graphql";
2
+ /**
3
+ * Determines whether a field should be ignored in depth calculation.
4
+ *
5
+ * @param fieldName - The name of the field to check
6
+ * @param ignore - Array of ignore rules (strings, RegExp, or functions)
7
+ * @param caseInsensitive - Whether to perform case-insensitive matching for string rules
8
+ * @returns True if the field should be ignored, false otherwise
9
+ */
10
+ function shouldIgnoreField(fieldName, ignore, caseInsensitive = false) {
11
+ // Always ignore introspection fields
12
+ if (fieldName.startsWith("__")) {
13
+ return true;
14
+ }
15
+ if (!ignore || ignore.length === 0) {
16
+ return false;
17
+ }
18
+ for (const rule of ignore) {
19
+ if (typeof rule === "string") {
20
+ const matches = caseInsensitive
21
+ ? rule.toLowerCase() === fieldName.toLowerCase()
22
+ : rule === fieldName;
23
+ if (matches) {
24
+ return true;
25
+ }
26
+ }
27
+ else if (rule instanceof RegExp) {
28
+ if (rule.test(fieldName)) {
29
+ return true;
30
+ }
31
+ }
32
+ else if (typeof rule === "function") {
33
+ if (rule(fieldName)) {
34
+ return true;
35
+ }
36
+ }
37
+ }
38
+ return false;
39
+ }
40
+ /**
41
+ * Extracts the depth limit from a @depth directive on a field definition.
42
+ *
43
+ * Note: This function only supports integer literals in the directive.
44
+ * Variables (e.g., @depth(max: $var)) are not supported because variable
45
+ * values are not available during the validation phase. Directives with
46
+ * variables will be ignored and fall back to the global depth limit.
47
+ *
48
+ * @param field - The GraphQL field definition to check
49
+ * @returns The max depth from the directive, or undefined if not found/invalid
50
+ */
51
+ function getDepthFromDirective(field) {
52
+ if (!field?.astNode?.directives) {
53
+ return undefined;
54
+ }
55
+ const depthDirective = field.astNode.directives.find((d) => d.name.value === "depth");
56
+ if (!depthDirective?.arguments) {
57
+ return undefined;
58
+ }
59
+ const maxArg = depthDirective.arguments.find((arg) => arg.name.value === "max");
60
+ if (maxArg?.value.kind === "IntValue") {
61
+ const directiveDepth = Number.parseInt(maxArg.value.value, 10);
62
+ if (Number.isFinite(directiveDepth) && directiveDepth >= 0) {
63
+ return directiveDepth;
64
+ }
65
+ }
66
+ return undefined;
67
+ }
68
+ /**
69
+ * Extracts all fragment definitions from a GraphQL document.
70
+ *
71
+ * @param definitions - Array of definition nodes from the GraphQL document
72
+ * @returns Map of fragment names to their definitions
73
+ */
74
+ function getFragments(definitions) {
75
+ const fragments = new Map();
76
+ for (const definition of definitions) {
77
+ if (definition.kind === Kind.FRAGMENT_DEFINITION) {
78
+ fragments.set(definition.name.value, definition);
79
+ }
80
+ }
81
+ return fragments;
82
+ }
83
+ /**
84
+ * Extracts all operation definitions from a GraphQL document.
85
+ *
86
+ * @param definitions - Array of definition nodes from the GraphQL document
87
+ * @returns Array of operation definitions (queries, mutations, subscriptions)
88
+ */
89
+ function getOperations(definitions) {
90
+ return definitions.filter((def) => def.kind === Kind.OPERATION_DEFINITION);
91
+ }
92
+ /**
93
+ * Recursively calculates the depth of a GraphQL query node.
94
+ *
95
+ * This function handles fragment spreads correctly by creating an independent copy
96
+ * of the visited fragments set for each fragment path. This ensures:
97
+ * 1. Fragments used at different depths are calculated correctly
98
+ * 2. Circular fragment references are detected per-path, not globally
99
+ * 3. The same fragment can be used multiple times without miscalculation
100
+ *
101
+ * @param node - The AST node to calculate depth for
102
+ * @param fragments - Map of all fragment definitions in the document
103
+ * @param schema - GraphQL schema for type resolution
104
+ * @param maxDepth - Maximum allowed depth for this node
105
+ * @param context - Context object containing current depth, parent type, and visited fragments
106
+ * @param options - Options object containing configuration flags and ignore rules
107
+ * @returns Object containing final depth and any violation found
108
+ */
109
+ function calculateDepth(node, fragments, schema, maxDepth, context, options) {
110
+ const { currentDepth, parentType, visitedFragments } = context;
111
+ const { useDirective, caseInsensitiveIgnore, ignore } = options;
112
+ if (currentDepth > maxDepth) {
113
+ return {
114
+ depth: currentDepth,
115
+ violation: { depth: currentDepth, maxDepth },
116
+ };
117
+ }
118
+ if (!node.selectionSet) {
119
+ return { depth: currentDepth, violation: null };
120
+ }
121
+ let maxChildDepth = currentDepth;
122
+ let violation = null;
123
+ for (const selection of node.selectionSet.selections) {
124
+ if (selection.kind === Kind.FIELD) {
125
+ const fieldNode = selection;
126
+ const fieldName = fieldNode.name.value;
127
+ if (shouldIgnoreField(fieldName, ignore, caseInsensitiveIgnore)) {
128
+ continue;
129
+ }
130
+ if (fieldNode.selectionSet) {
131
+ // Check for field-specific depth limit from @depth directive
132
+ let fieldMaxDepth = maxDepth;
133
+ let fieldType;
134
+ if (schema && parentType) {
135
+ const namedType = getNamedType(parentType);
136
+ if (isCompositeType(namedType) && "getFields" in namedType) {
137
+ const fieldDef = namedType.getFields()[fieldName];
138
+ if (fieldDef) {
139
+ fieldType = fieldDef.type;
140
+ if (useDirective) {
141
+ const directiveDepth = getDepthFromDirective(fieldDef);
142
+ if (directiveDepth !== undefined) {
143
+ fieldMaxDepth = directiveDepth;
144
+ }
145
+ }
146
+ }
147
+ }
148
+ }
149
+ const result = calculateDepth(fieldNode, fragments, schema, fieldMaxDepth, {
150
+ currentDepth: currentDepth + 1,
151
+ parentType: fieldType,
152
+ visitedFragments,
153
+ }, options);
154
+ if (result.violation && !violation) {
155
+ violation = result.violation;
156
+ }
157
+ maxChildDepth = Math.max(maxChildDepth, result.depth);
158
+ }
159
+ }
160
+ else if (selection.kind === Kind.FRAGMENT_SPREAD) {
161
+ const spreadNode = selection;
162
+ const fragmentName = spreadNode.name.value;
163
+ // Create a copy of visitedFragments for this path to correctly handle
164
+ // fragments used at different depths. This prevents the bug where a fragment
165
+ // used multiple times would only be calculated based on first occurrence.
166
+ const fragmentVisited = new Set(visitedFragments);
167
+ if (fragmentVisited.has(fragmentName)) {
168
+ // Fragment cycle detected - skip to prevent infinite recursion
169
+ continue;
170
+ }
171
+ const fragment = fragments.get(fragmentName);
172
+ if (fragment) {
173
+ fragmentVisited.add(fragmentName);
174
+ const result = calculateDepth(fragment, fragments, schema, maxDepth, {
175
+ currentDepth,
176
+ parentType,
177
+ visitedFragments: fragmentVisited,
178
+ }, options);
179
+ if (result.violation && !violation) {
180
+ violation = result.violation;
181
+ }
182
+ maxChildDepth = Math.max(maxChildDepth, result.depth);
183
+ }
184
+ }
185
+ else if (selection.kind === Kind.INLINE_FRAGMENT) {
186
+ const inlineFragment = selection;
187
+ // Inline fragments inherit the visitedFragments set since they don't have names
188
+ // and cannot create cycles by themselves
189
+ const result = calculateDepth(inlineFragment, fragments, schema, maxDepth, context, options);
190
+ if (result.violation && !violation) {
191
+ violation = result.violation;
192
+ }
193
+ maxChildDepth = Math.max(maxChildDepth, result.depth);
194
+ }
195
+ }
196
+ return { depth: maxChildDepth, violation };
197
+ }
198
+ /**
199
+ * Creates a GraphQL validation rule that limits query depth.
200
+ *
201
+ * This helps prevent DoS attacks and resource exhaustion from excessively deep queries.
202
+ * The depth limit can be configured globally and optionally overridden per-field using
203
+ * the @depth directive.
204
+ *
205
+ * Security considerations:
206
+ * - This validator correctly handles fragments used at different depths
207
+ * - Circular fragment references are detected and handled per-path
208
+ * - Introspection fields (__typename, __schema, etc.) are automatically ignored
209
+ *
210
+ * Limitations:
211
+ * - Variables in @depth directives are not supported (falls back to global limit)
212
+ * - Field names are case-sensitive by default (use caseInsensitiveIgnore option if needed)
213
+ *
214
+ * @param maxDepth - Maximum allowed depth for queries
215
+ * @param options - Optional configuration (ignore rules, directive support, case sensitivity)
216
+ * @param callback - Optional callback invoked with depth information for all operations
217
+ * @returns A GraphQL validation rule function
218
+ *
219
+ * @example
220
+ * ```typescript
221
+ * import { depthLimit } from 'graphql-query-depth-limit-esm';
222
+ * import { validate } from 'graphql';
223
+ *
224
+ * const validationRules = [
225
+ * depthLimit(5, {
226
+ * ignore: ['friends', /.*Connection$/],
227
+ * useDirective: true,
228
+ * caseInsensitiveIgnore: false
229
+ * })
230
+ * ];
231
+ *
232
+ * const errors = validate(schema, query, validationRules);
233
+ * ```
234
+ */
235
+ export function depthLimit(maxDepth, options, callback) {
236
+ return function depthLimitValidationRule(context) {
237
+ const document = context.getDocument();
238
+ const fragments = getFragments(document.definitions);
239
+ const operations = getOperations(document.definitions);
240
+ const depths = {};
241
+ const schema = context.getSchema();
242
+ const rootTypeMap = {
243
+ mutation: schema.getMutationType() ?? undefined,
244
+ query: schema.getQueryType() ?? undefined,
245
+ subscription: schema.getSubscriptionType() ?? undefined,
246
+ };
247
+ for (const operation of operations) {
248
+ const operationName = operation.name?.value || "anonymous";
249
+ const visitedFragments = new Set();
250
+ const rootType = rootTypeMap[operation.operation];
251
+ const result = calculateDepth(operation, fragments, schema, maxDepth, {
252
+ currentDepth: 0,
253
+ parentType: rootType,
254
+ visitedFragments,
255
+ }, {
256
+ caseInsensitiveIgnore: options?.caseInsensitiveIgnore ?? false,
257
+ ignore: options?.ignore,
258
+ useDirective: options?.useDirective ?? false,
259
+ });
260
+ depths[operationName] = result.depth;
261
+ // Report error if depth limit was exceeded (either field-specific or global)
262
+ if (result.violation) {
263
+ context.reportError(new GraphQLError(`'${operationName}' has depth ${result.violation.depth} which exceeds maximum operation depth of ${result.violation.maxDepth}`, {
264
+ nodes: [operation],
265
+ }));
266
+ }
267
+ }
268
+ if (callback) {
269
+ callback(depths);
270
+ }
271
+ return {};
272
+ };
273
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * GraphQL Query Depth Limiting - ESM Compatible
3
+ *
4
+ * A GraphQL validation rule that limits the depth of queries.
5
+ * Prevents deeply nested queries from overloading your server.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ export { depthLimit } from "./depthLimit.js";
10
+ export type { DepthCallback, DepthLimitFunction, DepthLimitOptions, IgnoreRule, } from "./types.js";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EACV,aAAa,EACb,kBAAkB,EAClB,iBAAiB,EACjB,UAAU,GACX,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * GraphQL Query Depth Limiting - ESM Compatible
3
+ *
4
+ * A GraphQL validation rule that limits the depth of queries.
5
+ * Prevents deeply nested queries from overloading your server.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ export { depthLimit } from "./depthLimit.js";
@@ -0,0 +1,25 @@
1
+ import type { ValidationRule } from "graphql";
2
+ export type IgnoreRule = string | RegExp | ((fieldName: string) => boolean);
3
+ export interface DepthLimitOptions {
4
+ /**
5
+ * Whether to perform case-insensitive matching for string-based ignore rules
6
+ * When true, "user" will match "User", "USER", etc.
7
+ * Note: GraphQL field names are case-sensitive by specification
8
+ * @default false
9
+ */
10
+ caseInsensitiveIgnore?: boolean;
11
+ /**
12
+ * Fields to exclude from depth calculation
13
+ * Can be strings (exact match), RegExp patterns, or custom functions
14
+ */
15
+ ignore?: IgnoreRule[];
16
+ /**
17
+ * Whether to read depth limits from @depth directive on fields
18
+ * When enabled, field-specific depth limits override the global maxDepth
19
+ * @default false
20
+ */
21
+ useDirective?: boolean;
22
+ }
23
+ export type DepthCallback = (depths: Record<string, number>) => void;
24
+ export type DepthLimitFunction = (maxDepth: number, options?: DepthLimitOptions, callback?: DepthCallback) => ValidationRule;
25
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC;AAE5E,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAEhC;;;OAGG;IACH,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC;IAEtB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;AAErE,MAAM,MAAM,kBAAkB,GAAG,CAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,iBAAiB,EAC3B,QAAQ,CAAC,EAAE,aAAa,KACrB,cAAc,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "graphql-query-depth-limit-esm",
3
+ "version": "0.1.0",
4
+ "description": "GraphQL query depth limiting for ESM projects",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "test:coverage": "vitest run --coverage",
24
+ "dev": "tsc --watch",
25
+ "lint": "biome check --unsafe --write && tsc --noEmit",
26
+ "lint:fix": "biome check --write .",
27
+ "format": "biome format --write .",
28
+ "prepublishOnly": "pnpm run build && pnpm run test"
29
+ },
30
+ "keywords": [
31
+ "graphql",
32
+ "depth",
33
+ "limit",
34
+ "validation",
35
+ "security",
36
+ "esm"
37
+ ],
38
+ "author": "Lafitte Mehdy",
39
+ "license": "MIT",
40
+ "publishConfig": {
41
+ "access": "public",
42
+ "provenance": true
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/lafittemehdy/graphql-query-depth-limit-esm.git"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/lafittemehdy/graphql-query-depth-limit-esm/issues"
50
+ },
51
+ "homepage": "https://github.com/lafittemehdy/graphql-query-depth-limit-esm#readme",
52
+ "peerDependencies": {
53
+ "graphql": "^16.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "@biomejs/biome": "^2.3.5",
57
+ "@types/node": "^24.10.1",
58
+ "@vitest/coverage-v8": "^4.0.9",
59
+ "graphql": "^16.12.0",
60
+ "typescript": "^5.9.3",
61
+ "vitest": "^4.0.9"
62
+ },
63
+ "sideEffects": false,
64
+ "engines": {
65
+ "node": ">=18.0.0"
66
+ }
67
+ }