graphql-query-depth-limit-esm 0.1.0 → 2.0.1
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 +1 -1
- package/README.md +376 -348
- package/dist/index.cjs +587 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +276 -0
- package/dist/index.d.ts +272 -7
- package/dist/index.js +566 -9
- package/dist/index.js.map +1 -0
- package/package.json +75 -65
- package/dist/depthLimit.d.ts +0 -41
- package/dist/depthLimit.d.ts.map +0 -1
- package/dist/depthLimit.js +0 -273
- package/dist/index.d.ts.map +0 -1
- package/dist/types.d.ts +0 -25
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -1
package/README.md
CHANGED
|
@@ -1,496 +1,524 @@
|
|
|
1
1
|
# graphql-query-depth-limit-esm
|
|
2
2
|
|
|
3
|
-
[](https://github.com/lafittemehdy/graphql-query-depth-limit-esm/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/lafittemehdy/graphql-query-depth-limit-esm/actions/workflows/publish.yml)
|
|
5
|
+
[](https://github.com/lafittemehdy/graphql-query-depth-limit-esm/actions/workflows/pages.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/graphql-query-depth-limit-esm)
|
|
7
|
+
[](https://www.npmjs.com/package/graphql-query-depth-limit-esm)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](https://nodejs.org/)
|
|
10
|
+
[](https://www.npmjs.com/package/graphql-query-depth-limit-esm)
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
Production-ready GraphQL query depth limiting as a validation rule. Prevents denial-of-service attacks from deeply nested queries by enforcing a configurable maximum depth.
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
Explore the library's internal function architecture with the **[interactive node-based visualization](https://lafittemehdy.github.io/graphql-query-depth-limit-esm/architecture.html)**.
|
|
10
15
|
|
|
11
16
|
## Features
|
|
12
17
|
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
18
|
+
- **`@depth` directive** — per-field depth overrides via schema directives
|
|
19
|
+
- **Alias-aware paths** — violation paths use aliases when present, matching the response shape
|
|
20
|
+
- **Configurable introspection handling** — control whether `__schema` and `__type` count toward depth
|
|
21
|
+
- **Callback support** — optional callback reports per-operation depth for monitoring
|
|
22
|
+
- **Fragment-safe** — handles named fragments, inline fragments, and circular fragment detection
|
|
23
|
+
- **Ignore rules** — skip fields by name, pattern, or custom function
|
|
24
|
+
- **Interface directive inheritance** — `@depth` directives on interface fields apply to all implementors
|
|
25
|
+
- **Short-circuit traversal** — stops immediately on first violation when no callback is provided
|
|
26
|
+
- **Validation rule** — integrates directly with `graphql`'s `validate()` function
|
|
27
|
+
- **Zero runtime dependencies** — only `graphql ^16` as a peer dependency
|
|
20
28
|
|
|
21
29
|
## Installation
|
|
22
30
|
|
|
23
31
|
```bash
|
|
24
|
-
|
|
32
|
+
pnpm add graphql-query-depth-limit-esm graphql
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install graphql-query-depth-limit-esm graphql
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
yarn add graphql-query-depth-limit-esm graphql
|
|
25
41
|
```
|
|
26
42
|
|
|
27
43
|
## Quick Start
|
|
28
44
|
|
|
29
|
-
|
|
45
|
+
```ts
|
|
46
|
+
import { specifiedRules, validate } from "graphql";
|
|
47
|
+
import { depthLimit } from "graphql-query-depth-limit-esm";
|
|
30
48
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
49
|
+
const errors = validate(schema, document, [
|
|
50
|
+
...specifiedRules,
|
|
51
|
+
depthLimit(7, { useDirective: true }),
|
|
52
|
+
]);
|
|
53
|
+
```
|
|
35
54
|
|
|
36
|
-
|
|
37
|
-
const typeDefs = `#graphql
|
|
38
|
-
directive @depth(max: Int!) on FIELD_DEFINITION
|
|
55
|
+
When calling `validate()` directly, include `specifiedRules` unless you intentionally want to replace GraphQL's default validation rules.
|
|
39
56
|
|
|
40
|
-
|
|
41
|
-
# Regular user endpoint with depth limit of 3
|
|
42
|
-
user(id: ID!): User @depth(max: 3)
|
|
57
|
+
The recommended way to use `graphql-query-depth-limit-esm` is the `@depth` directive — a global maximum protects your API, while per-field overrides can tighten limits (and can opt into deeper nesting when `directiveMode: "override"` is enabled).
|
|
43
58
|
|
|
44
|
-
|
|
45
|
-
adminUser(id: ID!): User @depth(max: 10)
|
|
59
|
+
### Global Limit with Per-Field Overrides
|
|
46
60
|
|
|
47
|
-
|
|
48
|
-
users: [User!]!
|
|
49
|
-
}
|
|
61
|
+
Include [`depthDirectiveTypeDefs`](#depthdirectivetypedefs) in your schema to declare the `@depth` directive, then annotate individual fields:
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
email: String!
|
|
55
|
-
friends: [User!]!
|
|
56
|
-
posts: [Post!]!
|
|
57
|
-
}
|
|
63
|
+
```graphql
|
|
64
|
+
# depthDirectiveTypeDefs provides this automatically:
|
|
65
|
+
# directive @depth(max: Int!) on FIELD_DEFINITION
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
};
|
|
67
|
+
type User {
|
|
68
|
+
name: String!
|
|
69
|
+
profile: Profile!
|
|
106
70
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
71
|
+
# Self-referential — allow up to 3 levels of nesting
|
|
72
|
+
friends: [User!]! @depth(max: 3)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type Post {
|
|
76
|
+
title: String!
|
|
77
|
+
author: User!
|
|
78
|
+
|
|
79
|
+
# Recursive comments — allow up to 5 levels deep
|
|
80
|
+
comments: [Comment!]! @depth(max: 5)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type Comment {
|
|
84
|
+
text: String!
|
|
85
|
+
replies: [Comment!]! @depth(max: 4)
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Build the schema with the directive type defs, then enable directive support via [`depthLimit()`](#depthlimitmaxdepth-options-callback):
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { makeExecutableSchema } from "@graphql-tools/schema";
|
|
93
|
+
import { depthDirectiveTypeDefs, depthLimit } from "graphql-query-depth-limit-esm";
|
|
94
|
+
import { specifiedRules, validate } from "graphql";
|
|
95
|
+
|
|
96
|
+
const schema = makeExecutableSchema({
|
|
97
|
+
typeDefs: [depthDirectiveTypeDefs, yourTypeDefs],
|
|
110
98
|
resolvers,
|
|
111
|
-
validationRules: [
|
|
112
|
-
// Global depth limit of 5 with directive support enabled
|
|
113
|
-
depthLimit(5, { useDirective: true }),
|
|
114
|
-
],
|
|
115
99
|
});
|
|
116
100
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
101
|
+
// Global max of 7 — @depth directives can tighten but never exceed this limit
|
|
102
|
+
const errors = validate(schema, document, [
|
|
103
|
+
...specifiedRules,
|
|
104
|
+
depthLimit(7, { useDirective: true }),
|
|
105
|
+
]);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Nested Directives
|
|
109
|
+
|
|
110
|
+
When multiple `@depth` directives appear along a query path, the effective limit is the **strictest (minimum)** along that path. A child directive can tighten an ancestor's limit but never relax it:
|
|
111
|
+
|
|
112
|
+
```graphql
|
|
113
|
+
type Post {
|
|
114
|
+
# 3 levels from here (absolute max = currentDepth + 3)
|
|
115
|
+
comments: [Comment] @depth(max: 3)
|
|
116
|
+
}
|
|
120
117
|
|
|
121
|
-
|
|
118
|
+
type Comment {
|
|
119
|
+
text: String
|
|
120
|
+
# Wants 5 levels, but capped by the ancestor's limit
|
|
121
|
+
replies: [Comment] @depth(max: 5)
|
|
122
|
+
}
|
|
122
123
|
```
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
This ensures a parent directive remains a hard ceiling for its entire subtree, preventing deeply nested child directives from punching through ancestor limits.
|
|
126
|
+
|
|
127
|
+
> **Note:** By default (`directiveMode: "cap"`), directives can only **tighten** the global `maxDepth`, never relax it. If you need directives to override the global limit for specific subtrees, set `directiveMode: "override"`. See [`directiveMode`](#depthlimitoptions) for details.
|
|
128
|
+
|
|
129
|
+
### Interface Directive Inheritance
|
|
125
130
|
|
|
126
|
-
|
|
131
|
+
When a `@depth` directive is placed on an interface field, it applies to all concrete types implementing that interface — even if the concrete type's field definition has no directive:
|
|
127
132
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
```graphql
|
|
134
|
+
interface Node {
|
|
135
|
+
children: [Node!]! @depth(max: 3)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
type TreeNode implements Node {
|
|
139
|
+
children: [Node!]! # Inherits @depth(max: 3) from Node interface
|
|
140
|
+
label: String!
|
|
141
|
+
}
|
|
142
|
+
```
|
|
133
143
|
|
|
134
|
-
|
|
144
|
+
When multiple interfaces define `@depth` on the same field, the **strictest (lowest)** limit is used.
|
|
135
145
|
|
|
136
|
-
|
|
146
|
+
### Why Explicit Opt-In?
|
|
137
147
|
|
|
138
|
-
|
|
148
|
+
The [`useDirective`](#depthlimitoptions) option is `false` by default. This is intentional:
|
|
149
|
+
|
|
150
|
+
- **Follows GraphQL Convention.** Standard rules like `NoUnusedFragmentsRule` or `KnownDirectivesRule` are configured explicitly, and this library follows that same pattern for consistency.
|
|
151
|
+
- **No hidden side effects.** Users who only need a global depth cap get exactly that, without the engine scanning every field definition for directives they never added.
|
|
152
|
+
- **Predictable behavior.** Enabling directive support is a deliberate choice, making it clear in code review that per-field overrides are in play.
|
|
153
|
+
|
|
154
|
+
## Usage
|
|
139
155
|
|
|
140
156
|
### Basic Depth Limiting
|
|
141
157
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
158
|
+
For straightforward global limiting without per-field overrides, call [`depthLimit()`](#depthlimitmaxdepth-options-callback) with only a maximum depth:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { depthLimit } from "graphql-query-depth-limit-esm";
|
|
162
|
+
import { specifiedRules, validate } from "graphql";
|
|
163
|
+
|
|
164
|
+
// Reject queries deeper than 10 levels
|
|
165
|
+
const rule = depthLimit(10);
|
|
166
|
+
const errors = validate(schema, document, [...specifiedRules, rule]);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### With Apollo Server
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { ApolloServer } from "@apollo/server";
|
|
173
|
+
import { depthLimit } from "graphql-query-depth-limit-esm";
|
|
145
174
|
|
|
146
175
|
const server = new ApolloServer({
|
|
147
176
|
schema,
|
|
148
|
-
validationRules: [depthLimit(
|
|
177
|
+
validationRules: [depthLimit(7, { useDirective: true })],
|
|
149
178
|
});
|
|
150
179
|
```
|
|
151
180
|
|
|
152
|
-
### With
|
|
181
|
+
### With Yoga
|
|
153
182
|
|
|
154
|
-
|
|
183
|
+
```ts
|
|
184
|
+
import { createYoga } from "graphql-yoga";
|
|
185
|
+
import { depthLimit } from "graphql-query-depth-limit-esm";
|
|
155
186
|
|
|
156
|
-
|
|
157
|
-
import { depthLimit } from 'graphql-query-depth-limit-esm';
|
|
158
|
-
|
|
159
|
-
const server = new ApolloServer({
|
|
187
|
+
const yoga = createYoga({
|
|
160
188
|
schema,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
],
|
|
168
|
-
}),
|
|
189
|
+
plugins: [
|
|
190
|
+
{
|
|
191
|
+
onValidate({ addValidationRule }) {
|
|
192
|
+
addValidationRule(depthLimit(7, { useDirective: true }));
|
|
193
|
+
},
|
|
194
|
+
},
|
|
169
195
|
],
|
|
170
196
|
});
|
|
171
197
|
```
|
|
172
198
|
|
|
173
|
-
###
|
|
199
|
+
### Ignore Rules
|
|
174
200
|
|
|
175
|
-
|
|
201
|
+
Skip specific fields during depth calculation using strings, regular expressions, or custom functions. See [`IgnoreRule`](#ignorerule) for the full type definition.
|
|
176
202
|
|
|
177
|
-
```
|
|
178
|
-
|
|
203
|
+
```ts
|
|
204
|
+
const rule = depthLimit(5, {
|
|
205
|
+
ignore: [
|
|
206
|
+
// Exact field name match
|
|
207
|
+
"metadata",
|
|
179
208
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// Output: { "GetUser": 5, "GetPosts": 3 }
|
|
186
|
-
}),
|
|
209
|
+
// Regular expression pattern
|
|
210
|
+
/.*Connection$/,
|
|
211
|
+
|
|
212
|
+
// Custom function
|
|
213
|
+
(fieldName) => fieldName.startsWith("internal"),
|
|
187
214
|
],
|
|
188
215
|
});
|
|
189
216
|
```
|
|
190
217
|
|
|
191
|
-
|
|
218
|
+
> **Warning:** When `ignoreMode: "skip"` is set and a field matches an ignore rule, its **entire subtree** is skipped — not just the depth increment for that field. This means all children, grandchildren, etc. are excluded from depth calculation entirely. Use ignore rules carefully on composite fields, as deeply nested subtrees under an ignored field will bypass depth protection.
|
|
192
219
|
|
|
193
|
-
|
|
220
|
+
### Ignore Mode
|
|
194
221
|
|
|
195
|
-
|
|
196
|
-
import { depthLimit } from 'graphql-query-depth-limit-esm';
|
|
222
|
+
By default, ignored fields only skip the depth increment while still traversing children. Use [`ignoreMode`](#depthlimitoptions) to control this behavior:
|
|
197
223
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
224
|
+
```ts
|
|
225
|
+
// Default (secure): skip only the depth increment, still traverse children
|
|
226
|
+
const rule = depthLimit(5, { ignore: ["metadata"], ignoreMode: "exclude" });
|
|
201
227
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
228
|
+
// Optional: skip the field and its entire subtree (use with caution)
|
|
229
|
+
const rule = depthLimit(5, { ignore: ["metadata"], ignoreMode: "skip" });
|
|
230
|
+
```
|
|
206
231
|
|
|
207
|
-
|
|
208
|
-
id: ID!
|
|
209
|
-
name: String!
|
|
210
|
-
friends: [User]
|
|
211
|
-
}
|
|
212
|
-
`;
|
|
232
|
+
With `"exclude"`, the ignored field does not increment the depth counter, but its children are still traversed and subject to depth limits. This prevents attackers from nesting arbitrarily deep queries under ignored composite fields.
|
|
213
233
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
234
|
+
If the ignored field is recursive (for example, `friends -> friends`), depth can remain flat along that edge because the ignored field keeps suppressing increments. To prevent this, enable [`limitIgnoredRecursion`](#depthlimitoptions), use `ignoreMode: "skip"`, or cap the field with `@depth`.
|
|
235
|
+
|
|
236
|
+
### Recursive Ignore Guard
|
|
237
|
+
|
|
238
|
+
Enable [`limitIgnoredRecursion`](#depthlimitoptions) to prevent unbounded traversal when using `ignoreMode: "exclude"` on recursive fields:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
const rule = depthLimit(10, {
|
|
242
|
+
ignore: ["friends"],
|
|
243
|
+
ignoreMode: "exclude",
|
|
244
|
+
limitIgnoredRecursion: true,
|
|
220
245
|
});
|
|
221
246
|
```
|
|
222
247
|
|
|
223
|
-
|
|
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
|
|
248
|
+
When enabled, the first ignored field occurrence on a path is still excluded, but repeated occurrences on the same path increment depth normally.
|
|
228
249
|
|
|
229
|
-
|
|
250
|
+
### Short-Circuit Control
|
|
230
251
|
|
|
231
|
-
|
|
252
|
+
By default, short-circuit behavior is auto-detected:
|
|
232
253
|
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
```
|
|
254
|
+
- No callback: short-circuits on first violation (`shortCircuit: true`)
|
|
255
|
+
- Callback provided: traverses full tree for exact depth (`shortCircuit: false`)
|
|
247
256
|
|
|
248
|
-
|
|
257
|
+
You can override this explicitly:
|
|
249
258
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
- **Fields matching ignore rules** - Skipped entirely from calculation
|
|
254
|
-
- **Scalar/leaf fields** - Terminal nodes don't increase depth
|
|
259
|
+
```ts
|
|
260
|
+
// Force full traversal even without a callback
|
|
261
|
+
const exactRule = depthLimit(5, { shortCircuit: false });
|
|
255
262
|
|
|
256
|
-
|
|
263
|
+
// Force early bailout even with a callback
|
|
264
|
+
const fastRule = depthLimit(5, { shortCircuit: true }, (depths) => {
|
|
265
|
+
console.log(depths);
|
|
266
|
+
});
|
|
267
|
+
```
|
|
257
268
|
|
|
258
|
-
|
|
259
|
-
```graphql
|
|
260
|
-
type Query {
|
|
261
|
-
user(id: ID!): User
|
|
262
|
-
}
|
|
269
|
+
### Case-Insensitive Matching
|
|
263
270
|
|
|
264
|
-
|
|
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
|
-
}
|
|
271
|
+
Enable [`caseInsensitiveIgnore`](#depthlimitoptions) to match string ignore rules regardless of casing:
|
|
276
272
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
273
|
+
```ts
|
|
274
|
+
const rule = depthLimit(5, {
|
|
275
|
+
caseInsensitiveIgnore: true,
|
|
276
|
+
ignore: ["metadata"], // Matches "metadata", "Metadata", "METADATA", etc.
|
|
277
|
+
});
|
|
282
278
|
```
|
|
283
279
|
|
|
284
|
-
|
|
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 | ❌ |
|
|
280
|
+
### Introspection Handling
|
|
291
281
|
|
|
292
|
-
|
|
282
|
+
By default, only `__typename` is ignored during depth calculation. The `__schema` and `__type` fields are **counted** toward depth, protecting against deeply nested introspection queries.
|
|
293
283
|
|
|
294
|
-
|
|
284
|
+
Note that scalar introspection fields like `__typename` never increment depth on their own (only composite fields with nested selections do). The `ignoreIntrospection` setting controls whether these fields are _recognized as ignored_, which matters for the `ignoreMode` behavior when they appear within composite selections.
|
|
295
285
|
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
|
|
286
|
+
```ts
|
|
287
|
+
// Default: only __typename is ignored
|
|
288
|
+
const rule = depthLimit(10);
|
|
299
289
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
290
|
+
// Ignore all introspection fields and skip their entire subtree
|
|
291
|
+
const rule = depthLimit(10, { ignoreIntrospection: "all" });
|
|
292
|
+
|
|
293
|
+
// Count all fields toward depth, including __typename
|
|
294
|
+
const rule = depthLimit(10, { ignoreIntrospection: "none" });
|
|
304
295
|
```
|
|
305
296
|
|
|
306
|
-
|
|
297
|
+
> **Note:** When `ignoreIntrospection: "all"` is set, introspection fields and their entire subtree are always skipped — regardless of `ignoreMode`. This is a security hardening mode that completely eliminates introspection from depth calculation.
|
|
307
298
|
|
|
308
|
-
|
|
309
|
-
import { graphqlHTTP } from 'express-graphql';
|
|
310
|
-
import { depthLimit } from 'graphql-query-depth-limit-esm';
|
|
299
|
+
### Depth Callback
|
|
311
300
|
|
|
312
|
-
|
|
313
|
-
'/graphql',
|
|
314
|
-
graphqlHTTP({
|
|
315
|
-
schema,
|
|
316
|
-
validationRules: [depthLimit(10)],
|
|
317
|
-
}),
|
|
318
|
-
);
|
|
319
|
-
```
|
|
301
|
+
Monitor query depths with an optional [`callback`](#depthcallback) that receives per-operation depth results:
|
|
320
302
|
|
|
321
|
-
|
|
303
|
+
```ts
|
|
304
|
+
const rule = depthLimit(10, {}, (depths) => {
|
|
305
|
+
// { "GetUser": 3, "ListPosts": 5, "anonymous": 2 }
|
|
306
|
+
for (const [operation, depth] of Object.entries(depths)) {
|
|
307
|
+
console.log(`Operation "${operation}" has depth ${depth}`);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
```
|
|
322
311
|
|
|
323
|
-
|
|
324
|
-
import { createYoga } from 'graphql-yoga';
|
|
325
|
-
import { depthLimit } from 'graphql-query-depth-limit-esm';
|
|
312
|
+
If you do not need options, you can pass the callback as the second argument:
|
|
326
313
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
314
|
+
```ts
|
|
315
|
+
const rule = depthLimit(10, (depths) => {
|
|
316
|
+
console.log(depths);
|
|
330
317
|
});
|
|
331
318
|
```
|
|
332
319
|
|
|
320
|
+
> **Note:** By default, providing a callback disables short-circuiting so the engine can report accurate maximum depths. If you explicitly set `shortCircuit: true`, traversal still bails early and reported depths are lower bounds (`"at least N"`). Without a callback, the engine short-circuits by default for maximum performance.
|
|
321
|
+
|
|
333
322
|
## API Reference
|
|
334
323
|
|
|
335
324
|
### `depthLimit(maxDepth, options?, callback?)`
|
|
336
325
|
|
|
337
326
|
Creates a GraphQL validation rule that limits query depth.
|
|
338
327
|
|
|
339
|
-
|
|
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
|
|
328
|
+
#### Parameters
|
|
349
329
|
|
|
350
|
-
|
|
330
|
+
| Parameter | Type | Required | Description |
|
|
331
|
+
|---|---|---|---|
|
|
332
|
+
| `maxDepth` | `number` | Yes | Maximum allowed depth (non-negative integer) |
|
|
333
|
+
| `options` | [`DepthLimitOptions`](#depthlimitoptions) | No | Configuration options |
|
|
334
|
+
| `callback` | [`DepthCallback`](#depthcallback) | No | Called with per-operation depth results |
|
|
351
335
|
|
|
352
|
-
|
|
336
|
+
#### Returns
|
|
353
337
|
|
|
354
|
-
|
|
338
|
+
A GraphQL `ValidationRule` function.
|
|
355
339
|
|
|
356
|
-
|
|
357
|
-
import { depthLimit } from 'graphql-query-depth-limit-esm';
|
|
340
|
+
#### Throws
|
|
358
341
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}),
|
|
369
|
-
],
|
|
370
|
-
});
|
|
342
|
+
- `Error` if `maxDepth` is not a non-negative integer
|
|
343
|
+
- `TypeError` if options, callback, or ignore rules are invalid
|
|
344
|
+
|
|
345
|
+
### `depthDirectiveTypeDefs`
|
|
346
|
+
|
|
347
|
+
GraphQL SDL string defining the `@depth` directive. Include this in your schema type definitions when using `{ useDirective: true }`.
|
|
348
|
+
|
|
349
|
+
```graphql
|
|
350
|
+
directive @depth(max: Int!) on FIELD_DEFINITION
|
|
371
351
|
```
|
|
372
352
|
|
|
373
|
-
|
|
353
|
+
### `DepthLimitOptions`
|
|
374
354
|
|
|
375
|
-
|
|
355
|
+
| Property | Type | Default | Description |
|
|
356
|
+
|---|---|---|---|
|
|
357
|
+
| `caseInsensitiveIgnore` | `boolean` | `false` | Case-insensitive matching for string ignore rules |
|
|
358
|
+
| `directiveMode` | [`DirectiveMode`](#directivemode) | `"cap"` | Controls how `@depth` directives interact with the global `maxDepth` |
|
|
359
|
+
| `ignore` | [`IgnoreRule \| IgnoreRule[]`](#ignorerule) | `undefined` | Fields to skip during depth calculation |
|
|
360
|
+
| `ignoreIntrospection` | [`IntrospectionMode`](#introspectionmode) | `"typename"` | Controls which introspection fields are ignored |
|
|
361
|
+
| `ignoreMode` | [`IgnoreMode`](#ignoremode) | `"exclude"` | Controls whether ignored fields skip their entire subtree or only the depth increment |
|
|
362
|
+
| `limitIgnoredRecursion` | `boolean` | `false` | In `ignoreMode: "exclude"`, increments depth for repeated ignored fields on the same path to prevent unbounded recursion |
|
|
363
|
+
| `shortCircuit` | `boolean` | `undefined` (auto) | Auto: `true` without callback, `false` with callback. Set explicitly to force early bailout or full traversal |
|
|
364
|
+
| `useDirective` | `boolean` | `false` | Read `@depth(max: Int!)` directives from field definitions. Requires a schema in validation context; without one, directive resolution falls back to the global `maxDepth`. |
|
|
376
365
|
|
|
377
|
-
###
|
|
366
|
+
### `DirectiveMode`
|
|
378
367
|
|
|
379
|
-
|
|
368
|
+
```ts
|
|
369
|
+
type DirectiveMode = "cap" | "override";
|
|
370
|
+
```
|
|
380
371
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
posts { title } # Adds 2 levels
|
|
384
|
-
}
|
|
372
|
+
- `"cap"` — directives can only tighten the limit below the global `maxDepth` (secure default)
|
|
373
|
+
- `"override"` — the first directive replaces the global limit for its subtree
|
|
385
374
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
...UserInfo # Depth: 2 + 2 = 4 ✓ (calculated independently)
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
375
|
+
### `IgnoreMode`
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
type IgnoreMode = "exclude" | "skip";
|
|
394
379
|
```
|
|
395
380
|
|
|
396
|
-
|
|
381
|
+
- `"exclude"` — skip the depth increment but still traverse children (secure default)
|
|
382
|
+
- `"skip"` — skip the field and its entire subtree
|
|
397
383
|
|
|
398
|
-
|
|
384
|
+
### `IntrospectionMode`
|
|
399
385
|
|
|
400
|
-
|
|
386
|
+
```ts
|
|
387
|
+
type IntrospectionMode = "all" | "none" | "typename";
|
|
388
|
+
```
|
|
401
389
|
|
|
402
|
-
|
|
390
|
+
- `"all"` — ignore every `__`-prefixed field (`__typename`, `__schema`, `__type`, etc.)
|
|
391
|
+
- `"typename"` — only ignore `__typename` (secure default)
|
|
392
|
+
- `"none"` — count all introspection fields toward depth
|
|
403
393
|
|
|
404
|
-
|
|
405
|
-
# ✅ Works
|
|
406
|
-
type Query {
|
|
407
|
-
user: User @depth(max: 5)
|
|
408
|
-
}
|
|
394
|
+
### `IgnoreRule`
|
|
409
395
|
|
|
410
|
-
|
|
411
|
-
type
|
|
412
|
-
user: User @depth(max: $maxDepth)
|
|
413
|
-
}
|
|
396
|
+
```ts
|
|
397
|
+
type IgnoreRule = string | RegExp | ((fieldName: string) => boolean);
|
|
414
398
|
```
|
|
415
399
|
|
|
416
|
-
|
|
400
|
+
### `DepthCallback`
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
type DepthCallback = (depths: Record<string, number>) => void;
|
|
404
|
+
```
|
|
417
405
|
|
|
418
|
-
|
|
419
|
-
// Case-sensitive (default)
|
|
420
|
-
depthLimit(5, { ignore: ['friends'] }) // Only matches 'friends', not 'Friends'
|
|
406
|
+
### `ERROR_CODES`
|
|
421
407
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
408
|
+
```ts
|
|
409
|
+
const ERROR_CODES = {
|
|
410
|
+
IGNORE_RULE_ERROR: "IGNORE_RULE_ERROR",
|
|
411
|
+
QUERY_TOO_DEEP: "QUERY_TOO_DEEP",
|
|
412
|
+
} as const;
|
|
427
413
|
```
|
|
428
414
|
|
|
429
|
-
|
|
415
|
+
- `QUERY_TOO_DEEP`: query depth exceeded the allowed limit
|
|
416
|
+
- `IGNORE_RULE_ERROR`: a user-provided ignore rule (`RegExp` or function) threw at validation time
|
|
430
417
|
|
|
431
|
-
|
|
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
|
|
418
|
+
## Error Extensions
|
|
436
419
|
|
|
437
|
-
|
|
420
|
+
Depth violations include structured extensions for programmatic access:
|
|
438
421
|
|
|
439
|
-
|
|
422
|
+
```json
|
|
423
|
+
{
|
|
424
|
+
"message": "'GetUser' has depth 8 which exceeds maximum allowed depth of 5 (at user.friends.friends)",
|
|
425
|
+
"extensions": {
|
|
426
|
+
"code": "QUERY_TOO_DEEP",
|
|
427
|
+
"depth": 8,
|
|
428
|
+
"maxDepth": 5,
|
|
429
|
+
"path": ["user", "friends", "friends"],
|
|
430
|
+
"shortCircuit": false
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
```
|
|
440
434
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
435
|
+
| Field | Type | Description |
|
|
436
|
+
|---|---|---|
|
|
437
|
+
| `code` | `string` | Always `"QUERY_TOO_DEEP"` |
|
|
438
|
+
| `depth` | `number` | The query's calculated depth. If `shortCircuit` is `true`, this is the depth where the violation was detected, which may not be the absolute maximum depth. |
|
|
439
|
+
| `maxDepth` | `number` | The maximum allowed depth that was exceeded |
|
|
440
|
+
| `path` | `string[]` | Field path from the operation root to the violation point (uses aliases when present) |
|
|
441
|
+
| `shortCircuit` | `boolean` | Whether the engine short-circuited. If `true`, `depth` is a lower bound ("at least N"), including when `shortCircuit: true` is explicitly forced with a callback. |
|
|
442
|
+
|
|
443
|
+
If an ignore rule throws, validation reports:
|
|
444
|
+
|
|
445
|
+
```json
|
|
446
|
+
{
|
|
447
|
+
"message": "Ignore rule function threw for field \"friends\": boom",
|
|
448
|
+
"extensions": {
|
|
449
|
+
"code": "IGNORE_RULE_ERROR"
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
## How Depth Is Calculated
|
|
444
455
|
|
|
445
|
-
|
|
456
|
+
- Depth increments for each **composite field** (objects, interfaces, unions)
|
|
457
|
+
- **Scalar and enum fields** do not increment depth
|
|
458
|
+
- **Fragment spreads** contribute the depth of their expanded selections
|
|
459
|
+
- **Inline fragments** contribute the depth of their selections
|
|
460
|
+
- **`__typename`** is ignored by default (configurable via [`ignoreIntrospection`](#introspectionmode))
|
|
461
|
+
- **`__schema` and `__type`** are counted toward depth by default
|
|
462
|
+
- **Circular fragment references** are detected per-path and stop recursion
|
|
463
|
+
|
|
464
|
+
### Example
|
|
446
465
|
|
|
447
466
|
```graphql
|
|
448
|
-
|
|
467
|
+
# Depth: 0
|
|
468
|
+
query {
|
|
469
|
+
# Depth: 1
|
|
449
470
|
user {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
471
|
+
name # Depth: 1 (scalar, no increment)
|
|
472
|
+
# Depth: 2
|
|
473
|
+
posts {
|
|
474
|
+
title # Depth: 2 (scalar, no increment)
|
|
475
|
+
# Depth: 3
|
|
476
|
+
comments {
|
|
477
|
+
text # Depth: 3 (scalar, no increment)
|
|
458
478
|
}
|
|
459
479
|
}
|
|
460
480
|
}
|
|
461
481
|
}
|
|
482
|
+
# Maximum depth: 3
|
|
462
483
|
```
|
|
463
484
|
|
|
464
|
-
|
|
465
|
-
- Fetch 10^100 user records (if each user has 10 friends)
|
|
466
|
-
- Exhaust server memory
|
|
467
|
-
- Crash your database
|
|
485
|
+
## Migrating from v1 to v2
|
|
468
486
|
|
|
469
|
-
**
|
|
487
|
+
v2 introduces three **breaking changes** with more secure defaults:
|
|
470
488
|
|
|
471
|
-
|
|
489
|
+
### 1. Introspection fields are no longer fully ignored
|
|
472
490
|
|
|
473
|
-
|
|
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 |
|
|
491
|
+
**v1:** All `__`-prefixed fields (`__typename`, `__schema`, `__type`) were ignored during depth calculation.
|
|
479
492
|
|
|
480
|
-
**
|
|
493
|
+
**v2:** Only `__typename` is ignored by default. `__schema` and `__type` now count toward depth, preventing deeply nested introspection queries from bypassing the depth limit.
|
|
481
494
|
|
|
482
|
-
|
|
495
|
+
**To restore v1 behavior:**
|
|
483
496
|
|
|
484
|
-
|
|
497
|
+
```ts
|
|
498
|
+
depthLimit(10, { ignoreIntrospection: "all" });
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### 2. `@depth` directives can no longer relax the global limit
|
|
502
|
+
|
|
503
|
+
**v1:** A `@depth(max: 50)` directive could override a global `maxDepth: 10`, allowing that subtree to nest up to 50 levels deep.
|
|
485
504
|
|
|
486
|
-
|
|
487
|
-
- GraphQL 16+
|
|
505
|
+
**v2:** By default (`directiveMode: "cap"`), directives can only **tighten** below the global max. A `@depth(max: 50)` with `maxDepth: 10` caps at 10.
|
|
488
506
|
|
|
489
|
-
|
|
507
|
+
**To restore v1 behavior:**
|
|
508
|
+
|
|
509
|
+
```ts
|
|
510
|
+
depthLimit(10, { directiveMode: "override", useDirective: true });
|
|
511
|
+
```
|
|
490
512
|
|
|
491
|
-
|
|
492
|
-
|
|
513
|
+
### 3. Short-circuit traversal on violations
|
|
514
|
+
|
|
515
|
+
**v1:** The engine always traversed the full query, even after detecting a violation.
|
|
516
|
+
|
|
517
|
+
**v2:** When no callback is provided, the engine stops traversal immediately on the first violation. This is a performance improvement and DoS protection — a deeply nested query with a small `maxDepth` no longer burns CPU traversing thousands of levels.
|
|
518
|
+
|
|
519
|
+
**Impact:** This is transparent to most users. The only observable difference is that error messages may report the depth at the first violation rather than the deepest violation when multiple branches exceed the limit. If you need the true maximum depth, provide a callback.
|
|
493
520
|
|
|
494
521
|
## License
|
|
495
522
|
|
|
496
|
-
MIT
|
|
523
|
+
[MIT](LICENSE)
|
|
524
|
+
|