@nxgt/graphql-validation 0.4.1 → 0.6.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/README.md +59 -8
- package/dist/bad-user-input.d.ts +17 -2
- package/dist/bad-user-input.d.ts.map +1 -1
- package/dist/builder/check-constraints.d.ts +4 -2
- package/dist/builder/check-constraints.d.ts.map +1 -1
- package/dist/builder/declared.d.ts +12 -0
- package/dist/builder/declared.d.ts.map +1 -0
- package/dist/chunks/{index-xcaf9kes.js → index-atwkcfg3.js} +5 -5
- package/dist/chunks/{index-xcaf9kes.js.map → index-atwkcfg3.js.map} +2 -2
- package/dist/chunks/{index-twevz7yc.js → index-w9pw7fsk.js} +68 -34
- package/dist/chunks/{index-twevz7yc.js.map → index-w9pw7fsk.js.map} +6 -5
- package/dist/cli.js +1 -1
- package/dist/codegen.js +2 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +385 -11
- package/dist/index.js.map +11 -6
- package/dist/limits/describe.d.ts +9 -0
- package/dist/limits/describe.d.ts.map +1 -0
- package/dist/limits/introspection.d.ts +12 -0
- package/dist/limits/introspection.d.ts.map +1 -0
- package/dist/limits/list-size.d.ts +15 -0
- package/dist/limits/list-size.d.ts.map +1 -0
- package/dist/limits/max-depth.d.ts +28 -0
- package/dist/limits/max-depth.d.ts.map +1 -0
- package/dist/limits/max-list-cost.d.ts +62 -0
- package/dist/limits/max-list-cost.d.ts.map +1 -0
- package/dist/validated.d.ts +28 -1
- package/dist/validated.d.ts.map +1 -1
- package/dist/with-validation.d.ts.map +1 -1
- package/docs/README.md +1 -0
- package/docs/guide/constraints.md +3 -3
- package/docs/guide/errors.md +78 -4
- package/docs/guide/query-limits.md +206 -0
- package/docs/roadmap.md +13 -2
- package/docs/troubleshooting.md +53 -0
- package/package.json +1 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Limiting the depth and cost of a query
|
|
2
|
+
|
|
3
|
+
`maxDepth` and `maxListCost` are graphql-js validation rules. They refuse an
|
|
4
|
+
operation before any resolver runs, so a hostile query costs a walk of its
|
|
5
|
+
document and nothing more. For `@constraint`, see
|
|
6
|
+
[Constraints](constraints.md).
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
A schema with a list cycle lets a client multiply a cost at each level:
|
|
11
|
+
|
|
12
|
+
```graphql
|
|
13
|
+
type Folder { id: ID! items(first: Int = 100): [Item!]! }
|
|
14
|
+
type Item { id: ID! folder: Folder! }
|
|
15
|
+
type Query { folder(id: ID!): Folder }
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```graphql
|
|
19
|
+
query {
|
|
20
|
+
folder(id: "1") {
|
|
21
|
+
items { folder { items { folder { items { folder { items { id } } } } } } }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Four `items` levels of 100 each can ask for 100 million nodes in a query nine
|
|
27
|
+
levels deep (`folder`, four `items`, three `folder`, `id`). `maxDepth` bounds the nesting; `maxListCost` bounds the work.
|
|
28
|
+
|
|
29
|
+
## Use
|
|
30
|
+
|
|
31
|
+
With graphql-js, add the rules to the ones `validate` already runs:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { maxDepth, maxListCost } from '@nxgt/graphql-validation';
|
|
35
|
+
import { parse, specifiedRules, validate } from 'graphql';
|
|
36
|
+
|
|
37
|
+
const errors = validate(schema, parse(query), [
|
|
38
|
+
...specifiedRules,
|
|
39
|
+
maxDepth({ depth: 8 }),
|
|
40
|
+
maxListCost({ budget: 10_000, defaultFirst: 50 }),
|
|
41
|
+
]);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
With graphql-yoga, in a plugin:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { maxDepth, maxListCost } from '@nxgt/graphql-validation';
|
|
48
|
+
import { createYoga } from 'graphql-yoga';
|
|
49
|
+
|
|
50
|
+
createYoga({
|
|
51
|
+
schema,
|
|
52
|
+
plugins: [
|
|
53
|
+
{
|
|
54
|
+
onValidate: ({ addValidationRule }) => {
|
|
55
|
+
addValidationRule(maxDepth({ depth: 8 }));
|
|
56
|
+
addValidationRule(maxListCost({ budget: 10_000, defaultFirst: 50 }));
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Each rule reports **one** `GraphQLError` per operation, whose `nodes` is the
|
|
64
|
+
operation. An invalid option (`depth: 0`, a fractional `budget`) throws a
|
|
65
|
+
`RangeError` when the rule is created.
|
|
66
|
+
|
|
67
|
+
## `maxDepth({ depth, ignoreIntrospection? })`
|
|
68
|
+
|
|
69
|
+
The depth of an operation is the nesting of its field selections: a top-level
|
|
70
|
+
field is depth 1.
|
|
71
|
+
|
|
72
|
+
- Named fragments are followed, each measured once. A fragment cycle adds
|
|
73
|
+
nothing (`NoFragmentCyclesRule` reports it).
|
|
74
|
+
- Inline fragments add no depth.
|
|
75
|
+
- `__typename` does not count. `__schema` and `__type` count like any field:
|
|
76
|
+
the standard introspection query (`getIntrospectionQuery()`) is **15 levels
|
|
77
|
+
deep**, so `maxDepth({ depth: 8 })` refuses it. Where introspection is
|
|
78
|
+
disabled, or the client is trusted (GraphiQL in development), pass
|
|
79
|
+
`ignoreIntrospection: true`: `__schema` and `__type` and everything under
|
|
80
|
+
them are then left out.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
maxDepth({ depth: 8, ignoreIntrospection: process.env.NODE_ENV !== 'production' });
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The refusal:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"message": "Operation \"Deep\" is 9 levels deep, more than the 8 allowed",
|
|
91
|
+
"extensions": { "code": "QUERY_TOO_DEEP", "depth": 9, "limit": 8 }
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`depth` is the operation's real depth. An anonymous operation reads
|
|
96
|
+
`Operation is 9 levels deep, ...`.
|
|
97
|
+
|
|
98
|
+
## `maxListCost({ budget, defaultFirst, sizeArguments?, ignoreIntrospection? })`
|
|
99
|
+
|
|
100
|
+
### How cost is counted
|
|
101
|
+
|
|
102
|
+
A field costs 1, times the sizes of the list fields enclosing it. A list field
|
|
103
|
+
multiplies the cost of its sub-selection by its size; a field that is not a list
|
|
104
|
+
multiplies by 1. A list field that selects nothing below it (`tags: [String!]!`)
|
|
105
|
+
costs 1.
|
|
106
|
+
|
|
107
|
+
```graphql
|
|
108
|
+
{ folder(id: "1") { id items(first: 5) { id } } }
|
|
109
|
+
# folder 1 + id 1 + items 1 + 5 * id 1 = 8
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
A type that is a list once unwrapped from non-null is one list, nested
|
|
113
|
+
`[[X]]` included. `__typename` costs nothing.
|
|
114
|
+
|
|
115
|
+
`__schema` and `__type` are costed like any field, their lists sized by the
|
|
116
|
+
schema itself: `__Schema.types` is the number of types of the schema, and
|
|
117
|
+
`fields`, `inputFields`, `enumValues`, `args`, `interfaces`, `possibleTypes`,
|
|
118
|
+
`directives` and `locations` are the longest such list anywhere in the schema.
|
|
119
|
+
A query repeating `a1: __schema { ... }` hundreds of times is refused like any
|
|
120
|
+
other. The standard introspection query costs on the order of 200,000 even on a
|
|
121
|
+
schema of one field, so it is refused by a budget
|
|
122
|
+
of 10,000: pass `ignoreIntrospection: true` where introspection is disabled or
|
|
123
|
+
the client is trusted, such as GraphiQL in development.
|
|
124
|
+
|
|
125
|
+
### How a list is sized
|
|
126
|
+
|
|
127
|
+
A list field is sized by its **size arguments**, `['first', 'last', 'limit']`
|
|
128
|
+
unless `sizeArguments` says otherwise:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
maxListCost({ budget: 10_000, defaultFirst: 50, sizeArguments: ['first', 'pageSize'] });
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Each size argument the field gives or declares a default for says a size, and
|
|
135
|
+
the **largest** wins: `items(first: 0, limit: 1000000)` is a million items. An
|
|
136
|
+
argument says:
|
|
137
|
+
|
|
138
|
+
| Argument | Size |
|
|
139
|
+
| --- | --- |
|
|
140
|
+
| a literal Int, 0 or more (`first: 5`) | that number |
|
|
141
|
+
| a string literal holding a canonical integer (`limit: "100000000"`, for a `Long`-like scalar) | that number: digits only, no sign, no leading zero |
|
|
142
|
+
| a bigint (an SDL default held by a code-first schema, or after graphql-tools' `mapSchema`) | that number |
|
|
143
|
+
| an integral Float literal (`first: 1000.0`, `first: 1e3`) | that number; `1.5` and `-1000.0` are unusable |
|
|
144
|
+
| a negative literal (`first: -1000`), another string | as if unusable, see below |
|
|
145
|
+
| a variable (`first: $n`) | as if unusable: validation has no variable values |
|
|
146
|
+
| not given | its SDL default, when it has one; else nothing |
|
|
147
|
+
|
|
148
|
+
An SDL default is read the same way: `limit: String = "1000"` and `first:
|
|
149
|
+
Float = 1000.0` are sizes of 1000. Every size is held at most
|
|
150
|
+
`Number.MAX_SAFE_INTEGER`.
|
|
151
|
+
|
|
152
|
+
An argument given but unusable counts for its SDL default when it has one in
|
|
153
|
+
any accepted form (`items(first: Int = 100)`), and `defaultFirst` otherwise. On
|
|
154
|
+
an abstract parent that is the largest default, or `defaultFirst` when it is
|
|
155
|
+
larger and some possible type has no default. A
|
|
156
|
+
field where no size argument says anything is sized `defaultFirst`.
|
|
157
|
+
|
|
158
|
+
On an interface or a union, the default is the **largest** over the interface's
|
|
159
|
+
field and the field of every possible type (`Node.kids(first: Int = 1)` next to
|
|
160
|
+
`Folder.kids(first: Int = 100000)` is sized 100,000), since the query cannot
|
|
161
|
+
say which object comes back.
|
|
162
|
+
|
|
163
|
+
A negative number is therefore never taken as a size: no subtree costs less than
|
|
164
|
+
nothing, and `items(first: -1000)` next to an expensive sibling cannot cancel
|
|
165
|
+
the sibling's cost.
|
|
166
|
+
|
|
167
|
+
Pick `defaultFirst` knowing a variable counts for it: a client using
|
|
168
|
+
`first: $n` is charged the default whatever it sends. If your server caps a size
|
|
169
|
+
argument, set `defaultFirst` (or the SDL default) at that cap.
|
|
170
|
+
|
|
171
|
+
### Fragments and abstract types
|
|
172
|
+
|
|
173
|
+
Named fragments are followed, each costed once (a fragment's type condition fixes
|
|
174
|
+
its parent type). A fragment spread twice counts twice. Inline fragments and
|
|
175
|
+
fragments on an interface or a union are **summed branch by branch**: a
|
|
176
|
+
selection `... on A { ... } ... on B { ... }` costs A's plus B's, although an
|
|
177
|
+
object is one of them. The cost of a query on an abstract type is overestimated,
|
|
178
|
+
never underestimated.
|
|
179
|
+
|
|
180
|
+
### The refusal
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"message": "Operation \"Big\" costs 5002, more than the 1000 allowed",
|
|
185
|
+
"extensions": { "code": "QUERY_TOO_COSTLY", "cost": 5002, "limit": 1000 }
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`budget` and `defaultFirst` must be integers below `Number.MAX_SAFE_INTEGER`
|
|
190
|
+
(a cost never exceeds it, so a budget at or over it would refuse nothing); the
|
|
191
|
+
rule throws a `RangeError` otherwise.
|
|
192
|
+
|
|
193
|
+
Costs are plain numbers, computed from memoized fragments (no exponential work
|
|
194
|
+
on a hostile query) and held at `Number.MAX_SAFE_INTEGER`: `cost` is exact below
|
|
195
|
+
it and never `Infinity`.
|
|
196
|
+
|
|
197
|
+
## Not covered
|
|
198
|
+
|
|
199
|
+
- A size carried inside an input object (`items(page: { first: 1000 })`) is not
|
|
200
|
+
read: the field has no size argument, so it is sized `defaultFirst`. Name the
|
|
201
|
+
size as an argument, or raise `defaultFirst`.
|
|
202
|
+
- A nested list `[[X]]` is sized **once**: the inner lists are not multiplied. A
|
|
203
|
+
schema returning a list of lists should size it with its own argument.
|
|
204
|
+
- The rules read the document, not its variables' values, and not what a
|
|
205
|
+
resolver does: a resolver can still return more than `first` items. They
|
|
206
|
+
complement a page-size cap in the resolver; they do not replace it.
|
package/docs/roadmap.md
CHANGED
|
@@ -8,8 +8,7 @@ Nothing in progress.
|
|
|
8
8
|
|
|
9
9
|
## Next
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
that need the request (uniqueness in a database, permissions).
|
|
11
|
+
Nothing planned yet.
|
|
13
12
|
|
|
14
13
|
## Later
|
|
15
14
|
|
|
@@ -19,6 +18,18 @@ Nothing in progress.
|
|
|
19
18
|
|
|
20
19
|
## Shipped
|
|
21
20
|
|
|
21
|
+
- **Query depth and cost limits** — `maxDepth` and `maxListCost` are graphql-js
|
|
22
|
+
validation rules (also for graphql-yoga's `addValidationRule`) that refuse a
|
|
23
|
+
query nested too deep or too costly for a schema with list cycles, with the
|
|
24
|
+
stable codes `QUERY_TOO_DEEP` and `QUERY_TOO_COSTLY`.
|
|
25
|
+
- **Schemas that read the context** — 0.5.0. `validated((context, info) =>
|
|
26
|
+
schema, resolver)` builds the schema on each call from the request's context
|
|
27
|
+
and the field's `info`, for checks that need the request (a uniqueness query
|
|
28
|
+
in the request's database, a rule depending on the viewer). Its result is
|
|
29
|
+
parsed like a fixed schema; what it throws reaches the client as it is. The
|
|
30
|
+
function does not receive the arguments: the schema it returns checks them,
|
|
31
|
+
and a `refine` sees them. A shape's arguments are now typed in the resolver (they
|
|
32
|
+
were `unknown`).
|
|
22
33
|
- **Answering invalid input as one error** — 0.4.0.
|
|
23
34
|
`invalidInputError(issues, { translate })` builds one 400 error whose
|
|
24
35
|
`extensions.fields` lists every field at fault with your own codes,
|
package/docs/troubleshooting.md
CHANGED
|
@@ -8,6 +8,7 @@ is the one a client sees.
|
|
|
8
8
|
- [Startup](#startup)
|
|
9
9
|
- [Silent traps](#silent-traps)
|
|
10
10
|
- [Runtime](#runtime)
|
|
11
|
+
- [Query limits](#query-limits)
|
|
11
12
|
- [The bin](#the-bin)
|
|
12
13
|
|
|
13
14
|
## Install
|
|
@@ -405,6 +406,58 @@ every issue. The resolver did not run.
|
|
|
405
406
|
| `Too small: expected number to be >=18` | `min: 18` |
|
|
406
407
|
| `Invalid string: must match pattern /.../` | `pattern` |
|
|
407
408
|
|
|
409
|
+
## Query limits
|
|
410
|
+
|
|
411
|
+
### `Operation "Name" is 9 levels deep, more than the 8 allowed`
|
|
412
|
+
|
|
413
|
+
**When:** an operation is validated with `maxDepth({ depth: 8 })`.
|
|
414
|
+
**Why:** `extensions` is `{ code: 'QUERY_TOO_DEEP', depth: 9, limit: 8 }`. A
|
|
415
|
+
top-level field is depth 1; named fragments are followed, inline fragments add
|
|
416
|
+
nothing, `__typename` does not count. `__schema` and `__type` do: the standard
|
|
417
|
+
introspection query is 15 levels deep (pass `ignoreIntrospection: true` where
|
|
418
|
+
it is trusted). An anonymous operation
|
|
419
|
+
reads `Operation is 9 levels deep, ...`.
|
|
420
|
+
**Fix:** flatten the query, or raise `depth`. See
|
|
421
|
+
[Limiting the depth and cost of a query](guide/query-limits.md).
|
|
422
|
+
|
|
423
|
+
### `Operation "Name" costs 5002, more than the 1000 allowed`
|
|
424
|
+
|
|
425
|
+
**When:** an operation is validated with `maxListCost({ budget: 1000, ... })`.
|
|
426
|
+
**Why:** `extensions` is `{ code: 'QUERY_TOO_COSTLY', cost: 5002, limit: 1000 }`.
|
|
427
|
+
Each field costs 1, times the sizes of the lists enclosing it. A list without a
|
|
428
|
+
usable `first` (a variable, a negative number, nothing) counts for its SDL
|
|
429
|
+
default, else `defaultFirst`, so a query that is cheap with the variable's real
|
|
430
|
+
value can still be refused.
|
|
431
|
+
**Fix:** ask for fewer items with a literal `first`, or raise `budget`. A query
|
|
432
|
+
on an interface or a union is overestimated (every branch is summed).
|
|
433
|
+
|
|
434
|
+
### `maxDepth: depth must be an integer from 1 to 9007199254740990.`
|
|
435
|
+
|
|
436
|
+
**When:** `maxDepth` is called with `0`, a negative or a fractional `depth`.
|
|
437
|
+
**Fix:** pass an integer in range.
|
|
438
|
+
|
|
439
|
+
### `maxListCost: budget must be an integer from 0 to 9007199254740990.`
|
|
440
|
+
|
|
441
|
+
**When:** `maxListCost` is called with a negative, fractional or too large
|
|
442
|
+
`budget`. A cost never exceeds `Number.MAX_SAFE_INTEGER` (9007199254740991), so a
|
|
443
|
+
budget at or over it would refuse nothing.
|
|
444
|
+
**Fix:** pass an integer in range.
|
|
445
|
+
|
|
446
|
+
### `maxListCost: defaultFirst must be an integer from 0 to 9007199254740990.`
|
|
447
|
+
|
|
448
|
+
**When:** `defaultFirst` is negative, fractional or too large.
|
|
449
|
+
**Fix:** pass the size to assume for a list with no usable size argument.
|
|
450
|
+
|
|
451
|
+
### A query refused although its `first` is small, or an introspection query refused
|
|
452
|
+
|
|
453
|
+
**When:** `QUERY_TOO_COSTLY` or `QUERY_TOO_DEEP` for GraphiQL's schema fetch or
|
|
454
|
+
a codegen's introspection query.
|
|
455
|
+
**Why:** `__schema` is counted: the standard introspection query is 15 levels
|
|
456
|
+
deep and costs on the order of 200,000 on a one-field schema.
|
|
457
|
+
**Fix:** `ignoreIntrospection: true` on the rule where introspection is
|
|
458
|
+
disabled or the client is trusted. A `first: $n` variable counts for the SDL
|
|
459
|
+
default, not for the value sent.
|
|
460
|
+
|
|
408
461
|
## The bin
|
|
409
462
|
|
|
410
463
|
### `typedefs failed: <message>`
|
package/package.json
CHANGED