@nxgt/graphql-validation 0.6.0 → 0.8.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 +36 -12
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +216 -68
- package/dist/index.js.map +13 -7
- package/dist/limits/cost-context.d.ts +26 -0
- package/dist/limits/cost-context.d.ts.map +1 -0
- package/dist/limits/describe.d.ts +4 -0
- package/dist/limits/describe.d.ts.map +1 -1
- package/dist/limits/fragments.d.ts +19 -0
- package/dist/limits/fragments.d.ts.map +1 -0
- package/dist/limits/list-size.d.ts +15 -5
- package/dist/limits/list-size.d.ts.map +1 -1
- package/dist/limits/max-depth.d.ts.map +1 -1
- package/dist/limits/max-fields.d.ts +35 -0
- package/dist/limits/max-fields.d.ts.map +1 -0
- package/dist/limits/max-list-cost-options.d.ts +38 -0
- package/dist/limits/max-list-cost-options.d.ts.map +1 -0
- package/dist/limits/max-list-cost.d.ts +25 -28
- package/dist/limits/max-list-cost.d.ts.map +1 -1
- package/dist/limits/pending-size.d.ts +13 -0
- package/dist/limits/pending-size.d.ts.map +1 -0
- package/dist/limits/size-reading.d.ts +22 -0
- package/dist/limits/size-reading.d.ts.map +1 -0
- package/docs/README.md +1 -1
- package/docs/guide/query-limits.md +155 -17
- package/docs/roadmap.md +15 -4
- package/docs/troubleshooting.md +46 -9
- package/package.json +1 -1
|
@@ -1,24 +1,6 @@
|
|
|
1
1
|
import { type ValidationRule } from 'graphql';
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
/** The most an operation may cost (at least 0). */
|
|
5
|
-
budget: number;
|
|
6
|
-
/** The size of a list field whose size cannot be read (an integer of at least 0). */
|
|
7
|
-
defaultFirst: number;
|
|
8
|
-
/**
|
|
9
|
-
* The argument names that size a list. Each one given, or defaulted in the
|
|
10
|
-
* SDL, says a size (an Int, a canonical-integer string or an integral Float)
|
|
11
|
-
* and the largest wins. Default `['first', 'last', 'limit']`.
|
|
12
|
-
*/
|
|
13
|
-
sizeArguments?: readonly string[];
|
|
14
|
-
/**
|
|
15
|
-
* Do not cost `__schema` and `__type` (default `false`). Only where
|
|
16
|
-
* introspection is disabled or the client is trusted, e.g. GraphiQL in
|
|
17
|
-
* development: the standard introspection query is costly (on the order of
|
|
18
|
-
* 200,000 on a one-field schema).
|
|
19
|
-
*/
|
|
20
|
-
ignoreIntrospection?: boolean;
|
|
21
|
-
}
|
|
2
|
+
import { type MaxListCostOptions } from './max-list-cost-options.js';
|
|
3
|
+
export type { MaxListCostOptions };
|
|
22
4
|
/**
|
|
23
5
|
* A graphql-js validation rule refusing an operation whose cost is more than
|
|
24
6
|
* `budget`, to stop a client from exploiting list cycles (`Folder.items`,
|
|
@@ -36,15 +18,30 @@ export interface MaxListCostOptions {
|
|
|
36
18
|
* `first`, `last`, `limit`) say. A literal Int, a String holding a canonical
|
|
37
19
|
* integer (`"1000"`) or an integral Float (`1e3`) of at least 0 says that
|
|
38
20
|
* number. Anything else (a negative literal, a variable, since validation has
|
|
39
|
-
* no variable values) says the argument's SDL default when it
|
|
40
|
-
* `defaultFirst` otherwise
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
21
|
+
* no variable values) says the largest of the argument's SDL default when it
|
|
22
|
+
* has a usable one (`defaultFirst` otherwise), its declared bound
|
|
23
|
+
* (`@constraint(max)` or `exclusiveMax`) and, when some definition declares no
|
|
24
|
+
* bound, `variableSize`; an argument not given says its SDL default, or
|
|
25
|
+
* nothing. When nothing is said the size is `defaultFirst`. On an interface
|
|
26
|
+
* the largest default and bound over its possible types is used. Hence no
|
|
27
|
+
* subtree ever costs less than nothing: a negative `first` on one field cannot
|
|
28
|
+
* cancel the cost of its sibling. A field whose type (once unwrapped from
|
|
29
|
+
* non-null) is a list is one list, nested `[[X]]` included.
|
|
46
30
|
*
|
|
47
|
-
*
|
|
31
|
+
* A Relay connection puts the size on a field returning an object, with the
|
|
32
|
+
* list below it (`items(first: 100) { edges { node { id } } }`). A field that
|
|
33
|
+
* is not a list and states a size (by the same reading) hands it to the lists
|
|
34
|
+
* selected directly on its return type, through inline fragments and fragment
|
|
35
|
+
* spreads on that same selection set, and only to those declaring none of the
|
|
36
|
+
* size arguments (`edges`, `nodes`): such a list takes it instead of
|
|
37
|
+
* `defaultFirst`. A list declaring a size argument (`recent(first: Int)`)
|
|
38
|
+
* reads its own, never the pending size. Crossing any other field (an object
|
|
39
|
+
* field, `pageInfo`, a wrapper) ends it. A connection stating nothing hands
|
|
40
|
+
* down nothing: its lists are sized `defaultFirst`.
|
|
41
|
+
*
|
|
42
|
+
* Named fragments are followed, each costed once per pending size (past eight
|
|
43
|
+
* distinct sizes under one fragment a size is rounded up to a power of two, so
|
|
44
|
+
* the work stays linear in the document and a cost only over-counts). Inline fragments and
|
|
48
45
|
* fragments on an abstract type are summed branch by branch, so the cost of a
|
|
49
46
|
* query on an interface or a union is overestimated, never underestimated.
|
|
50
47
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"max-list-cost.d.ts","sourceRoot":"","sources":["../../src/limits/max-list-cost.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"max-list-cost.d.ts","sourceRoot":"","sources":["../../src/limits/max-list-cost.ts"],"names":[],"mappings":"AAAA,OAAO,EAUN,KAAK,cAAc,EACnB,MAAM,SAAS,CAAC;AAajB,OAAO,EAEN,KAAK,kBAAkB,EACvB,MAAM,yBAAyB,CAAC;AAGjC,YAAY,EAAE,kBAAkB,EAAE,CAAC;AAoFnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,cAAc,CAmDvE"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ArgumentNode, GraphQLField, GraphQLNamedType } from 'graphql';
|
|
2
|
+
import { type SizeContext } from './list-size.js';
|
|
3
|
+
/**
|
|
4
|
+
* The size of the list `definition` on `parent`. `pending` is the size of the
|
|
5
|
+
* field whose return type this list is selected on (a connection's `first`):
|
|
6
|
+
* a list that declares none of the size arguments takes it, since the size
|
|
7
|
+
* cannot be stated on the list itself. A list that declares one reads its own
|
|
8
|
+
* arguments, else `defaultFirst`, and never the pending size. On an abstract
|
|
9
|
+
* parent some possible types may declare one and others not: the largest of
|
|
10
|
+
* both readings, since the query cannot say which comes back.
|
|
11
|
+
*/
|
|
12
|
+
export declare function listSizeUnder(cost: SizeContext, parent: GraphQLNamedType | undefined, definition: GraphQLField<unknown, unknown>, args: readonly ArgumentNode[] | undefined, pending: number | undefined): number;
|
|
13
|
+
//# sourceMappingURL=pending-size.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pending-size.d.ts","sourceRoot":"","sources":["../../src/limits/pending-size.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC5E,OAAO,EAAe,KAAK,WAAW,EAAc,MAAM,aAAa,CAAC;AAExE;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC5B,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,gBAAgB,GAAG,SAAS,EACpC,UAAU,EAAE,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,EAC1C,IAAI,EAAE,SAAS,YAAY,EAAE,GAAG,SAAS,EACzC,OAAO,EAAE,MAAM,GAAG,SAAS,GACzB,MAAM,CASR"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { GraphQLArgument, ValueNode } from 'graphql';
|
|
2
|
+
/**
|
|
3
|
+
* The one reading of a size, wherever it comes from (a literal, a default): a
|
|
4
|
+
* non-negative integral number or bigint, or a canonical-integer string,
|
|
5
|
+
* clamped to the ceiling. Anything else says nothing.
|
|
6
|
+
*/
|
|
7
|
+
export declare function sizeOf(value: unknown): number | undefined;
|
|
8
|
+
/** The size a literal gives: an Int, a canonical-integer String or an integral Float (`1e3`). */
|
|
9
|
+
export declare function literalSize(value: ValueNode): number | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* An argument's SDL default as a size. graphql 16 holds it as `defaultValue`;
|
|
12
|
+
* graphql 17 as `default`, `{ value }` or `{ literal }`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function defaultSizeOf(argument: object | undefined): number | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* The bound of an argument's `@constraint` directive, as a size: what the
|
|
17
|
+
* client may send at most. The largest of `max` and `exclusiveMax` (the value
|
|
18
|
+
* itself, a slight over-count), a fractional one rounded up. Read from the SDL
|
|
19
|
+
* node, so a code-first argument (no `astNode`) declares none.
|
|
20
|
+
*/
|
|
21
|
+
export declare function constraintMaxOf(argument: GraphQLArgument | undefined): number | undefined;
|
|
22
|
+
//# sourceMappingURL=size-reading.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"size-reading.d.ts","sourceRoot":"","sources":["../../src/limits/size-reading.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAK1D;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAczD;AAED,iGAAiG;AACjG,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,CAKhE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC5B,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC1B,MAAM,GAAG,SAAS,CAiBpB;AAeD;;;;;GAKG;AACH,wBAAgB,eAAe,CAC9B,QAAQ,EAAE,eAAe,GAAG,SAAS,GACnC,MAAM,GAAG,SAAS,CAepB"}
|
package/docs/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
| --- | --- |
|
|
7
7
|
| [Constraints](guide/constraints.md) | you write `@constraint` in your schema: setup with `withValidation`, every argument and its Zod equivalent, the formats and your own, lists and nested inputs |
|
|
8
8
|
| [Answering invalid input as one error](guide/invalid-input.md) | your API answers invalid input as one 400 error with a list of fields (`invalidInputError`, `fieldIssueOf` for a Zod parse), or a refusal such as a 409 names the fields at fault (`fieldsError`) |
|
|
9
|
-
| [Limiting the depth and cost of a query](guide/query-limits.md) | a schema has list cycles and you want to refuse a query nested too deep or too
|
|
9
|
+
| [Limiting the depth and cost of a query](guide/query-limits.md) | a schema has list cycles and you want to refuse a query nested too deep, too costly or too wide (`maxDepth`, `maxListCost`, `maxFields`), with graphql-js `validate` or graphql-yoga |
|
|
10
10
|
| [Errors](guide/errors.md) | you handle the `BAD_USER_INPUT` error in a client, build it yourself with `badUserInput`, or add rules a directive cannot say with `validated` |
|
|
11
11
|
|
|
12
12
|
## Reference
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Limiting the depth and cost of a query
|
|
2
2
|
|
|
3
|
-
`maxDepth` and `
|
|
3
|
+
`maxDepth`, `maxListCost` and `maxFields` are graphql-js validation rules. They refuse an
|
|
4
4
|
operation before any resolver runs, so a hostile query costs a walk of its
|
|
5
5
|
document and nothing more. For `@constraint`, see
|
|
6
6
|
[Constraints](constraints.md).
|
|
@@ -26,25 +26,31 @@ query {
|
|
|
26
26
|
Four `items` levels of 100 each can ask for 100 million nodes in a query nine
|
|
27
27
|
levels deep (`folder`, four `items`, three `folder`, `id`). `maxDepth` bounds the nesting; `maxListCost` bounds the work.
|
|
28
28
|
|
|
29
|
+
Neither bounds breadth: a query of 2000 aliases of `item(id: "1") { id }` is two
|
|
30
|
+
levels deep and costs 4000, yet selects 4000 fields for the executor to run; so does a
|
|
31
|
+
query of thousands of aliases of `__typename`, which neither rule counts.
|
|
32
|
+
`maxFields` bounds that.
|
|
33
|
+
|
|
29
34
|
## Use
|
|
30
35
|
|
|
31
36
|
With graphql-js, add the rules to the ones `validate` already runs:
|
|
32
37
|
|
|
33
38
|
```ts
|
|
34
|
-
import { maxDepth, maxListCost } from '@nxgt/graphql-validation';
|
|
39
|
+
import { maxDepth, maxFields, maxListCost } from '@nxgt/graphql-validation';
|
|
35
40
|
import { parse, specifiedRules, validate } from 'graphql';
|
|
36
41
|
|
|
37
42
|
const errors = validate(schema, parse(query), [
|
|
38
43
|
...specifiedRules,
|
|
39
44
|
maxDepth({ depth: 8 }),
|
|
40
45
|
maxListCost({ budget: 10_000, defaultFirst: 50 }),
|
|
46
|
+
maxFields({ limit: 500 }),
|
|
41
47
|
]);
|
|
42
48
|
```
|
|
43
49
|
|
|
44
50
|
With graphql-yoga, in a plugin:
|
|
45
51
|
|
|
46
52
|
```ts
|
|
47
|
-
import { maxDepth, maxListCost } from '@nxgt/graphql-validation';
|
|
53
|
+
import { maxDepth, maxFields, maxListCost } from '@nxgt/graphql-validation';
|
|
48
54
|
import { createYoga } from 'graphql-yoga';
|
|
49
55
|
|
|
50
56
|
createYoga({
|
|
@@ -54,6 +60,7 @@ createYoga({
|
|
|
54
60
|
onValidate: ({ addValidationRule }) => {
|
|
55
61
|
addValidationRule(maxDepth({ depth: 8 }));
|
|
56
62
|
addValidationRule(maxListCost({ budget: 10_000, defaultFirst: 50 }));
|
|
63
|
+
addValidationRule(maxFields({ limit: 500 }));
|
|
57
64
|
},
|
|
58
65
|
},
|
|
59
66
|
],
|
|
@@ -61,7 +68,7 @@ createYoga({
|
|
|
61
68
|
```
|
|
62
69
|
|
|
63
70
|
Each rule reports **one** `GraphQLError` per operation, whose `nodes` is the
|
|
64
|
-
operation. An invalid option (`depth: 0`, a fractional `budget`) throws a
|
|
71
|
+
operation. An invalid option (`depth: 0`, a fractional `budget`, `limit: 0`) throws a
|
|
65
72
|
`RangeError` when the rule is created.
|
|
66
73
|
|
|
67
74
|
## `maxDepth({ depth, ignoreIntrospection? })`
|
|
@@ -95,7 +102,7 @@ The refusal:
|
|
|
95
102
|
`depth` is the operation's real depth. An anonymous operation reads
|
|
96
103
|
`Operation is 9 levels deep, ...`.
|
|
97
104
|
|
|
98
|
-
## `maxListCost({ budget, defaultFirst, sizeArguments?, ignoreIntrospection? })`
|
|
105
|
+
## `maxListCost({ budget, defaultFirst, sizeArguments?, ignoreIntrospection?, variableSize? })`
|
|
99
106
|
|
|
100
107
|
### How cost is counted
|
|
101
108
|
|
|
@@ -142,35 +149,113 @@ argument says:
|
|
|
142
149
|
| a bigint (an SDL default held by a code-first schema, or after graphql-tools' `mapSchema`) | that number |
|
|
143
150
|
| an integral Float literal (`first: 1000.0`, `first: 1e3`) | that number; `1.5` and `-1000.0` are unusable |
|
|
144
151
|
| 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 |
|
|
152
|
+
| a variable (`first: $n`) | as if unusable, see below: validation has no variable values |
|
|
146
153
|
| not given | its SDL default, when it has one; else nothing |
|
|
147
154
|
|
|
148
155
|
An SDL default is read the same way: `limit: String = "1000"` and `first:
|
|
149
156
|
Float = 1000.0` are sizes of 1000. Every size is held at most
|
|
150
157
|
`Number.MAX_SAFE_INTEGER`.
|
|
151
158
|
|
|
152
|
-
An argument given but unusable
|
|
153
|
-
any accepted form (`items(first:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
field where no size argument says anything is
|
|
159
|
+
An argument given but unusable (a negative number, a variable) counts for the
|
|
160
|
+
largest of its SDL default when it has one in any accepted form (`items(first:
|
|
161
|
+
Int = 100)`, else `defaultFirst`), its declared bound (`@constraint(max)` or
|
|
162
|
+
`exclusiveMax`) and, where some definition declares no bound, `variableSize`:
|
|
163
|
+
see [Variables](#variables). A field where no size argument says anything is
|
|
164
|
+
sized `defaultFirst`.
|
|
157
165
|
|
|
158
166
|
On an interface or a union, the default is the **largest** over the interface's
|
|
159
167
|
field and the field of every possible type (`Node.kids(first: Int = 1)` next to
|
|
160
168
|
`Folder.kids(first: Int = 100000)` is sized 100,000), since the query cannot
|
|
161
|
-
say which object comes back
|
|
169
|
+
say which object comes back; `defaultFirst` counts too when it is larger and
|
|
170
|
+
some possible type has no default.
|
|
162
171
|
|
|
163
172
|
A negative number is therefore never taken as a size: no subtree costs less than
|
|
164
173
|
nothing, and `items(first: -1000)` next to an expensive sibling cannot cancel
|
|
165
174
|
the sibling's cost.
|
|
166
175
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
176
|
+
### Variables
|
|
177
|
+
|
|
178
|
+
The client can send any value the argument accepts, so a size given as a
|
|
179
|
+
variable (or as an unusable literal) counts as the **largest** of:
|
|
180
|
+
|
|
181
|
+
- its SDL default, else `defaultFirst`: never less than that;
|
|
182
|
+
- the bound of the argument's `@constraint` (the directive `withValidation`
|
|
183
|
+
enforces), read from the schema's SDL: `first: Int = 20 @constraint(max: 100)`
|
|
184
|
+
counts 100 for `first: $n`. `exclusiveMax` is read the same way (the value
|
|
185
|
+
itself, a slight over-count), a fractional bound is rounded up (`max: 100.5`
|
|
186
|
+
counts 101), and the largest of `max` and `exclusiveMax` counts. On an
|
|
187
|
+
interface it is the largest over the interface's field and each possible
|
|
188
|
+
type's field;
|
|
189
|
+
- `variableSize`, where some definition declares no bound:
|
|
190
|
+
`maxListCost({ budget, defaultFirst, variableSize: 100 })` for a server that
|
|
191
|
+
caps every page at 100 in its resolvers. It applies to every size argument
|
|
192
|
+
(`first`, `last`, `limit`, your own `sizeArguments`) and to unusable
|
|
193
|
+
literals. A declared bound wins over it only when every definition (the
|
|
194
|
+
interface's field and each possible type's) declares one; if any lacks one,
|
|
195
|
+
`variableSize` applies too and the largest wins.
|
|
196
|
+
|
|
197
|
+
```graphql
|
|
198
|
+
query ($n: Int) { wards(first: $n) { edges { node { rooms(first: $n) { id } } } } }
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
With `first: Int = 20 @constraint(max: 100)` on both fields, that query is
|
|
202
|
+
costed as the same one written with `first: 100`, not with the defaults. The
|
|
203
|
+
operation's own variable default (`query ($n: Int = 5)`) is ignored: the client
|
|
204
|
+
can override it. An argument not given at all keeps its SDL default.
|
|
205
|
+
|
|
206
|
+
### Relay connections
|
|
207
|
+
|
|
208
|
+
On a Relay connection the size is on a field that returns an **object**, and the
|
|
209
|
+
list is below it:
|
|
210
|
+
|
|
211
|
+
```graphql
|
|
212
|
+
type Query { items(first: Int): ItemConnection! }
|
|
213
|
+
type ItemConnection { edges: [ItemEdge!]! nodes: [Item!]! pageInfo: PageInfo! }
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
```graphql
|
|
217
|
+
{ items(first: 100) { edges { node { id } } nodes { id } } }
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
A field that is not a list and states a size, read as for a list above (a size
|
|
221
|
+
argument given, or declared with a default; on an abstract parent the largest
|
|
222
|
+
over its possible types), hands it down as a **pending size** to the lists
|
|
223
|
+
selected **directly on its return type**:
|
|
224
|
+
|
|
225
|
+
- Only a list that **declares none of the size arguments** takes it, instead of
|
|
226
|
+
`defaultFirst`: `edges` and `nodes` above are both 100. A list that declares
|
|
227
|
+
one (`recent(first: Int)`) reads its own arguments, else `defaultFirst`,
|
|
228
|
+
never the pending size.
|
|
229
|
+
- On an interface, when some possible types declare a size argument on that
|
|
230
|
+
list and others do not, the larger of the pending size and the list's own
|
|
231
|
+
reading is used, since the query cannot say which comes back.
|
|
232
|
+
- It stops at the first field crossed: an object field (`owner`), `pageInfo` or
|
|
233
|
+
a wrapper object passes nothing on, so the lists below them are sized as
|
|
234
|
+
without a connection. In `items(first: 0) { owner { friends { … } } }` the
|
|
235
|
+
`0` does not reach `friends`. A nested field that states a size hands its own
|
|
236
|
+
to the lists directly on it.
|
|
237
|
+
- A connection field that states nothing (no size argument given, none declared
|
|
238
|
+
with a default) hands down nothing: its lists are sized `defaultFirst`, as
|
|
239
|
+
before. A variable, a negative number or an unusable value on it counts as for
|
|
240
|
+
a list, see [Variables](#variables).
|
|
241
|
+
- The rule is structural: any non-list field stating a size sizes the lists
|
|
242
|
+
declaring no size argument directly on its return type, whether or not it is
|
|
243
|
+
a connection. `user(limit: 0) { friends { … } }` sizes `friends` 0. So an
|
|
244
|
+
argument named in `sizeArguments` must size the lists directly under its
|
|
245
|
+
field: when one of yours means something else, leave its name out of
|
|
246
|
+
`sizeArguments` (or rename the argument).
|
|
247
|
+
- Inline fragments, and fragment spreads, on that same selection set pass the
|
|
248
|
+
pending size through. A named fragment is costed once for each distinct
|
|
249
|
+
pending size it is spread under (`...C` under
|
|
250
|
+
`first: 2` and `first: 5` costs differently). Under more than eight distinct
|
|
251
|
+
sizes in one document a fragment's further sizes are rounded up to the next
|
|
252
|
+
power of two, so the work stays linear in the document and the cost only
|
|
253
|
+
over-counts.
|
|
170
254
|
|
|
171
255
|
### Fragments and abstract types
|
|
172
256
|
|
|
173
|
-
Named fragments are followed, each costed once
|
|
257
|
+
Named fragments are followed, each costed once per pending size (see above;
|
|
258
|
+
a fragment's type condition fixes
|
|
174
259
|
its parent type). A fragment spread twice counts twice. Inline fragments and
|
|
175
260
|
fragments on an interface or a union are **summed branch by branch**: a
|
|
176
261
|
selection `... on A { ... } ... on B { ... }` costs A's plus B's, although an
|
|
@@ -186,7 +271,7 @@ never underestimated.
|
|
|
186
271
|
}
|
|
187
272
|
```
|
|
188
273
|
|
|
189
|
-
`budget` and `
|
|
274
|
+
`budget`, `defaultFirst` and `variableSize` must be integers below `Number.MAX_SAFE_INTEGER`
|
|
190
275
|
(a cost never exceeds it, so a budget at or over it would refuse nothing); the
|
|
191
276
|
rule throws a `RangeError` otherwise.
|
|
192
277
|
|
|
@@ -194,8 +279,61 @@ Costs are plain numbers, computed from memoized fragments (no exponential work
|
|
|
194
279
|
on a hostile query) and held at `Number.MAX_SAFE_INTEGER`: `cost` is exact below
|
|
195
280
|
it and never `Infinity`.
|
|
196
281
|
|
|
282
|
+
## `maxFields({ limit, ignoreIntrospection? })`
|
|
283
|
+
|
|
284
|
+
The size of an operation is the number of field selections in its document,
|
|
285
|
+
fragment spreads expanded. It is not what the executor runs: how often a field
|
|
286
|
+
runs inside a list is `maxListCost`'s business, so `maxFields` alone does not
|
|
287
|
+
bound execution.
|
|
288
|
+
|
|
289
|
+
- Every field node counts 1: aliases count (`a1: id a2: id` is 2), and so does
|
|
290
|
+
`__typename`.
|
|
291
|
+
- A named fragment counts **at each spread**: a fragment of 3 fields spread
|
|
292
|
+
twice adds 6. Each fragment is walked once, so the work stays linear in the
|
|
293
|
+
size of the document even when spreads multiply. A fragment cycle counts 0
|
|
294
|
+
(`NoFragmentCyclesRule` reports it).
|
|
295
|
+
- Inline fragments count their contents. No type condition prunes anything:
|
|
296
|
+
every branch of a union or an interface counts, so the figure can only be
|
|
297
|
+
over the document's field count.
|
|
298
|
+
- `@skip` and `@include` are not evaluated (the rules read the document, not its
|
|
299
|
+
variables): a field they would remove is counted anyway.
|
|
300
|
+
- `__schema` and `__type` and everything under them count like any field.
|
|
301
|
+
The standard introspection query (`getIntrospectionQuery()`) selects **220
|
|
302
|
+
fields** on graphql 16 and 17 (the fragments `FullType`, `InputValue` and
|
|
303
|
+
`TypeRef` are counted at each spread), so a limit under 220 refuses it. Where
|
|
304
|
+
introspection is disabled, or the client is trusted (GraphiQL in development),
|
|
305
|
+
pass `ignoreIntrospection: true`.
|
|
306
|
+
|
|
307
|
+
The refusal:
|
|
308
|
+
|
|
309
|
+
```json
|
|
310
|
+
{
|
|
311
|
+
"message": "Operation \"Wide\" selects 4000 fields, more than the 500 allowed",
|
|
312
|
+
"extensions": { "code": "QUERY_TOO_WIDE", "fields": 4000, "limit": 500 }
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`fields` is the operation's real count, held at `Number.MAX_SAFE_INTEGER`
|
|
317
|
+
whatever a hostile query multiplies. An anonymous operation reads
|
|
318
|
+
`Operation selects 4000 fields, ...`. `limit` must be an integer from 1 to
|
|
319
|
+
`Number.MAX_SAFE_INTEGER - 1`, else a `RangeError`.
|
|
320
|
+
|
|
321
|
+
### Choosing a limit
|
|
322
|
+
|
|
323
|
+
Count the fields of your largest legitimate query, spreads included (a page
|
|
324
|
+
that spreads a 10-field fragment on 20 list entries reads 200 plus the rest),
|
|
325
|
+
and add headroom: a few hundred covers most screens. Keep the introspection
|
|
326
|
+
query in mind (220) if you leave it counted. `maxFields` complements the other
|
|
327
|
+
two: `maxDepth` bounds nesting, `maxListCost` bounds list multiplication, and
|
|
328
|
+
`maxFields` bounds how many fields one request may name.
|
|
329
|
+
|
|
197
330
|
## Not covered
|
|
198
331
|
|
|
332
|
+
- A code-first argument has no SDL node, hence no declared bound: only
|
|
333
|
+
`variableSize` applies to a variable on it.
|
|
334
|
+
- A size bounded only by a custom scalar (`first: PageSize`) or by `maxItems`
|
|
335
|
+
or `maxLength` counts as having no bound: declare `@constraint(max)` on the
|
|
336
|
+
argument.
|
|
199
337
|
- A size carried inside an input object (`items(page: { first: 1000 })`) is not
|
|
200
338
|
read: the field has no size argument, so it is sized `defaultFirst`. Name the
|
|
201
339
|
size as an argument, or raise `defaultFirst`.
|
package/docs/roadmap.md
CHANGED
|
@@ -18,10 +18,21 @@ Nothing planned yet.
|
|
|
18
18
|
|
|
19
19
|
## Shipped
|
|
20
20
|
|
|
21
|
-
- **
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
- **Relay connection sizing** — `maxListCost` sizes the `edges` and `nodes` of
|
|
22
|
+
a connection from the connection field's size argument (`items(first: 100)
|
|
23
|
+
{ edges { … } }`), not `defaultFirst`.
|
|
24
|
+
- **Variable sizes counted at their bound** — `maxListCost` counts a size sent
|
|
25
|
+
as a variable (or an unusable literal) at the argument's declared bound
|
|
26
|
+
(`@constraint(max)` or `exclusiveMax`) and the new `variableSize` option. The
|
|
27
|
+
bound wins only when every definition declares one, and never counts below the
|
|
28
|
+
SDL default or `defaultFirst`, so a query cannot hide a large page behind a
|
|
29
|
+
variable.
|
|
30
|
+
- **Query depth, cost and breadth limits** — `maxDepth`, `maxListCost` and
|
|
31
|
+
`maxFields` are graphql-js validation rules (also for graphql-yoga's
|
|
32
|
+
`addValidationRule`) that refuse a query nested too deep, too costly for a
|
|
33
|
+
schema with list cycles, or selecting too many fields (aliases of a cheap
|
|
34
|
+
field), with the stable codes `QUERY_TOO_DEEP`, `QUERY_TOO_COSTLY` and
|
|
35
|
+
`QUERY_TOO_WIDE`.
|
|
25
36
|
- **Schemas that read the context** — 0.5.0. `validated((context, info) =>
|
|
26
37
|
schema, resolver)` builds the schema on each call from the request's context
|
|
27
38
|
and the field's `info`, for checks that need the request (a uniqueness query
|
package/docs/troubleshooting.md
CHANGED
|
@@ -425,11 +425,35 @@ reads `Operation is 9 levels deep, ...`.
|
|
|
425
425
|
**When:** an operation is validated with `maxListCost({ budget: 1000, ... })`.
|
|
426
426
|
**Why:** `extensions` is `{ code: 'QUERY_TOO_COSTLY', cost: 5002, limit: 1000 }`.
|
|
427
427
|
Each field costs 1, times the sizes of the lists enclosing it. A list without a
|
|
428
|
-
usable `first`
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
on
|
|
428
|
+
usable `first` counts for its SDL default, else `defaultFirst`; a variable or
|
|
429
|
+
a negative number counts for the largest of that, the declared bound and
|
|
430
|
+
`variableSize`, so a query that is cheap with the variable's real value can
|
|
431
|
+
still be refused, see [Variables](guide/query-limits.md#variables). A size
|
|
432
|
+
argument on a field returning an object (a connection's `first`) sizes its
|
|
433
|
+
`edges` and `nodes`, and any non-list field stating a size sizes the lists
|
|
434
|
+
declaring none directly under it, see
|
|
435
|
+
[Relay connections](guide/query-limits.md#relay-connections).
|
|
436
|
+
|
|
437
|
+
**Fix:** ask for fewer items with a literal `first`, lower the `@constraint(max)`
|
|
438
|
+
of a size argument, or raise `budget`. A query on an interface or a union is
|
|
439
|
+
overestimated (every branch is summed).
|
|
440
|
+
|
|
441
|
+
### `Operation "Name" selects 4000 fields, more than the 500 allowed`
|
|
442
|
+
|
|
443
|
+
**When:** an operation is validated with `maxFields({ limit: 500 })`.
|
|
444
|
+
**Why:** `extensions` is `{ code: 'QUERY_TOO_WIDE', fields: 4000, limit: 500 }`.
|
|
445
|
+
Every field selection counts 1, aliases and `__typename` included, and a named
|
|
446
|
+
fragment counts at each spread; every branch of an inline fragment counts and
|
|
447
|
+
`@skip`/`@include` are not evaluated, so the figure can be over what runs. An
|
|
448
|
+
anonymous operation reads `Operation selects 4000 fields, ...`.
|
|
449
|
+
**Fix:** ask for fewer fields (a fragment spread many times is many fields), or
|
|
450
|
+
raise `limit`. See [Limiting the depth and cost of a query](guide/query-limits.md).
|
|
451
|
+
|
|
452
|
+
### `maxFields: limit must be an integer from 1 to 9007199254740990.`
|
|
453
|
+
|
|
454
|
+
**When:** `maxFields` is called with `0`, a negative, a fractional or a too
|
|
455
|
+
large `limit`.
|
|
456
|
+
**Fix:** pass an integer in range.
|
|
433
457
|
|
|
434
458
|
### `maxDepth: depth must be an integer from 1 to 9007199254740990.`
|
|
435
459
|
|
|
@@ -448,15 +472,28 @@ budget at or over it would refuse nothing.
|
|
|
448
472
|
**When:** `defaultFirst` is negative, fractional or too large.
|
|
449
473
|
**Fix:** pass the size to assume for a list with no usable size argument.
|
|
450
474
|
|
|
475
|
+
### `maxListCost: variableSize must be an integer from 0 to 9007199254740990.`
|
|
476
|
+
|
|
477
|
+
**When:** `variableSize` is negative, fractional or too large.
|
|
478
|
+
**Why:** it is the size assumed for a variable or unusable size argument, and
|
|
479
|
+
a size is an integer from 0 up to a bound a cost cannot overflow.
|
|
480
|
+
**Fix:** pass an integer in range, such as the page cap of your resolvers:
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
maxListCost({ budget: 10_000, defaultFirst: 50, variableSize: 100 });
|
|
484
|
+
```
|
|
485
|
+
|
|
451
486
|
### A query refused although its `first` is small, or an introspection query refused
|
|
452
487
|
|
|
453
|
-
**When:** `QUERY_TOO_COSTLY` or `
|
|
488
|
+
**When:** `QUERY_TOO_COSTLY`, `QUERY_TOO_DEEP` or `QUERY_TOO_WIDE` for GraphiQL's schema fetch or
|
|
454
489
|
a codegen's introspection query.
|
|
455
490
|
**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.
|
|
491
|
+
deep, selects 220 fields and costs on the order of 200,000 on a one-field schema.
|
|
457
492
|
**Fix:** `ignoreIntrospection: true` on the rule where introspection is
|
|
458
|
-
disabled or the client is trusted. A `first: $n` variable counts for
|
|
459
|
-
|
|
493
|
+
disabled or the client is trusted. A `first: $n` variable counts for its
|
|
494
|
+
declared bound (`@constraint(max)` or `exclusiveMax`, when every definition
|
|
495
|
+
declares one) and `variableSize` otherwise, never below the SDL default or
|
|
496
|
+
`defaultFirst`, not for the value sent.
|
|
460
497
|
|
|
461
498
|
## The bin
|
|
462
499
|
|
package/package.json
CHANGED