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 +21 -0
- package/README.md +496 -0
- package/dist/depthLimit.d.ts +41 -0
- package/dist/depthLimit.d.ts.map +1 -0
- package/dist/depthLimit.js +273 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/types.d.ts +25 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +67 -0
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
|
+
[](https://www.npmjs.com/package/graphql-query-depth-limit-esm)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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";
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|