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