@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
package/README.md
CHANGED
|
@@ -241,19 +241,24 @@ The codes, and `fieldsError`, are in
|
|
|
241
241
|
A schema with list cycles (`Folder.items` returns items, `Item.folder` returns
|
|
242
242
|
its folder) lets a client nest a query whose cost grows exponentially.
|
|
243
243
|
`maxDepth` and `maxListCost` are graphql-js validation rules that refuse such a
|
|
244
|
-
query before any resolver runs
|
|
244
|
+
query before any resolver runs; `maxFields` bounds the breadth they leave open
|
|
245
|
+
(thousands of aliases of a cheap field).
|
|
245
246
|
|
|
246
247
|
```ts
|
|
247
|
-
import { maxDepth, maxListCost } from '@nxgt/graphql-validation';
|
|
248
|
+
import { maxDepth, maxFields, maxListCost } from '@nxgt/graphql-validation';
|
|
248
249
|
import { parse, specifiedRules, validate } from 'graphql';
|
|
249
250
|
|
|
250
|
-
const rules = [
|
|
251
|
+
const rules = [
|
|
252
|
+
maxDepth({ depth: 8 }),
|
|
253
|
+
maxListCost({ budget: 10_000, defaultFirst: 50 }),
|
|
254
|
+
maxFields({ limit: 500 }),
|
|
255
|
+
];
|
|
251
256
|
const errors = validate(schema, parse(query), [...specifiedRules, ...rules]);
|
|
252
257
|
```
|
|
253
258
|
|
|
254
259
|
```ts
|
|
255
260
|
// graphql-yoga
|
|
256
|
-
import { maxDepth, maxListCost } from '@nxgt/graphql-validation';
|
|
261
|
+
import { maxDepth, maxFields, maxListCost } from '@nxgt/graphql-validation';
|
|
257
262
|
import { createYoga } from 'graphql-yoga';
|
|
258
263
|
|
|
259
264
|
const yoga = createYoga({
|
|
@@ -263,6 +268,7 @@ const yoga = createYoga({
|
|
|
263
268
|
onValidate: ({ addValidationRule }) => {
|
|
264
269
|
addValidationRule(maxDepth({ depth: 8 }));
|
|
265
270
|
addValidationRule(maxListCost({ budget: 10_000, defaultFirst: 50 }));
|
|
271
|
+
addValidationRule(maxFields({ limit: 500 }));
|
|
266
272
|
},
|
|
267
273
|
},
|
|
268
274
|
],
|
|
@@ -270,13 +276,30 @@ const yoga = createYoga({
|
|
|
270
276
|
```
|
|
271
277
|
|
|
272
278
|
One `GraphQLError` per operation: `extensions: { code: 'QUERY_TOO_DEEP', depth,
|
|
273
|
-
limit }
|
|
274
|
-
|
|
275
|
-
|
|
279
|
+
limit }`, `{ code: 'QUERY_TOO_COSTLY', cost, limit }` or `{ code: 'QUERY_TOO_WIDE',
|
|
280
|
+
fields, limit }`. `maxFields` counts every field selection in the document, aliases and
|
|
281
|
+
`__typename` included, at each spread of a fragment. A list field is sized by
|
|
282
|
+
its `first`, `last` or `limit` argument. An argument not given says its SDL
|
|
283
|
+
default; with no argument and no default the size is `defaultFirst`. A variable
|
|
284
|
+
or an unusable literal (a negative number) counts as the largest of that
|
|
285
|
+
default (else `defaultFirst`), the argument's declared `@constraint(max)` or
|
|
286
|
+
`exclusiveMax`, and `variableSize` where some definition declares no bound; the
|
|
287
|
+
operation's own variable default is ignored. A code-first argument has no SDL
|
|
288
|
+
node, so only `variableSize` applies to it. A size is an
|
|
276
289
|
Int, a canonical-integer string or an integral Float; the largest of several
|
|
277
|
-
size arguments wins.
|
|
278
|
-
|
|
279
|
-
|
|
290
|
+
size arguments wins. On a Relay connection the `first` on the object field
|
|
291
|
+
sizes the `edges` and `nodes` lists directly under it (not lists further down,
|
|
292
|
+
nor lists declaring their own size argument):
|
|
293
|
+
|
|
294
|
+
```graphql
|
|
295
|
+
{ items(first: 100) { edges { node { id } } } }
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Trap: any non-list field stating a size sizes the lists declaring none directly
|
|
299
|
+
under it, so an argument that means something else (`limit`, say) and is named in
|
|
300
|
+
`sizeArguments` now sizes them: leave it out of `sizeArguments`.
|
|
301
|
+
Introspection is counted too; add `ignoreIntrospection: true` where it is
|
|
302
|
+
trusted (GraphiQL in development). See
|
|
280
303
|
[Limiting the depth and cost of a query](docs/guide/query-limits.md).
|
|
281
304
|
|
|
282
305
|
## IDE support
|
|
@@ -331,8 +354,9 @@ the directive declared is this package's. Both at once throw
|
|
|
331
354
|
| `FieldIssue`, `FieldError`, `IssueParams`, `Translate`, `InvalidInputOptions`, `FieldsErrorOptions` | their types: an issue you write, a `fields` entry, its params, your translator, the options |
|
|
332
355
|
| `ValidationIssue`, `ConstraintValue`, `BadUserInputExtensions` | the types of `extensions.issues`, of an issue's `value`, and of `extensions` |
|
|
333
356
|
| `maxDepth({ depth, ignoreIntrospection? })` | a graphql-js validation rule refusing an operation nested deeper than `depth` fields, `extensions.code` `QUERY_TOO_DEEP`; introspection counts (the standard query is 15 levels) unless `ignoreIntrospection` |
|
|
334
|
-
| `maxListCost({ budget, defaultFirst, sizeArguments?, ignoreIntrospection? })` | a graphql-js validation rule refusing an operation whose list-weighted cost is over `budget`, `extensions.code` `QUERY_TOO_COSTLY`; `sizeArguments` names the arguments sizing a list (default `first`, `last`, `limit`, the largest wins); `ignoreIntrospection` skips `__schema` and `__type` |
|
|
335
|
-
| `
|
|
357
|
+
| `maxListCost({ budget, defaultFirst, sizeArguments?, ignoreIntrospection?, variableSize? })` | a graphql-js validation rule refusing an operation whose list-weighted cost is over `budget`, `extensions.code` `QUERY_TOO_COSTLY`; `sizeArguments` names the arguments sizing a list (default `first`, `last`, `limit`, the largest wins); `ignoreIntrospection` skips `__schema` and `__type`; `variableSize` is the size assumed for any size argument (`first`, `last`, `limit`, custom `sizeArguments`) sent as a variable or an unusable literal; see [Variables](docs/guide/query-limits.md#variables) for how a declared bound and `variableSize` combine |
|
|
358
|
+
| `maxFields({ limit, ignoreIntrospection? })` | a graphql-js validation rule refusing an operation that selects more than `limit` fields (aliases, `__typename` and every fragment spread counted, `@skip`/`@include` not evaluated), `extensions.code` `QUERY_TOO_WIDE`; introspection counts (the standard query selects 220 fields) unless `ignoreIntrospection` |
|
|
359
|
+
| `MaxDepthOptions`, `MaxListCostOptions`, `MaxFieldsOptions` | their options |
|
|
336
360
|
| `ConstraintArgument` | the type of `issues[].constraint`: `'minLength' \| 'format' \| ...` |
|
|
337
361
|
| `ArgsSchema`, `ArgsSchemaFor`, `SchemaOf` | the types `validated` accepts: the schema or shape, and that or the function building it from the context |
|
|
338
362
|
| `WithValidationOptions`, `FormatSchemas`, `OwnFormats`, `StringSchema`, `FormatName` | the types of `withValidation`'s options: a record of string schemas, with no built-in format name (`FormatName`) |
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type { StringSchema } from './formats/format.js';
|
|
|
6
6
|
export type { FormatSchemas, OwnFormats } from './formats/registry.js';
|
|
7
7
|
export { type FieldError, type FieldIssue, type FieldsErrorOptions, fieldsError, INVALID_INPUT, type InvalidInputOptions, type IssueParams, inputPath, invalidInputError, type Translate, } from './invalid-input.js';
|
|
8
8
|
export { type MaxDepthOptions, maxDepth } from './limits/max-depth.js';
|
|
9
|
+
export { type MaxFieldsOptions, maxFields } from './limits/max-fields.js';
|
|
9
10
|
export { type MaxListCostOptions, maxListCost } from './limits/max-list-cost.js';
|
|
10
11
|
export type { ConstraintArgument } from './rules/index.js';
|
|
11
12
|
export { type ArgsSchema, type ArgsSchemaFor, type SchemaOf, validated, } from './validated.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,sBAAsB,EAC3B,YAAY,EACZ,KAAK,eAAe,EACpB,KAAK,eAAe,GACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC7D,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,YAAY,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EACN,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,WAAW,EACX,aAAa,EACb,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,SAAS,EACT,iBAAiB,EACjB,KAAK,SAAS,GACd,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EAAE,KAAK,kBAAkB,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC9E,YAAY,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EACN,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,SAAS,GACT,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,KAAK,qBAAqB,EAC1B,cAAc,GACd,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,sBAAsB,EAC3B,YAAY,EACZ,KAAK,eAAe,EACpB,KAAK,eAAe,GACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC7D,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,YAAY,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EACN,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,WAAW,EACX,aAAa,EACb,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,SAAS,EACT,iBAAiB,EACjB,KAAK,SAAS,GACd,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAE,KAAK,kBAAkB,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC9E,YAAY,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EACN,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,SAAS,GACT,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,KAAK,qBAAqB,EAC1B,cAAc,GACd,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -137,15 +137,42 @@ import {
|
|
|
137
137
|
} from "graphql";
|
|
138
138
|
|
|
139
139
|
// src/limits/describe.ts
|
|
140
|
+
var CEILING = Number.MAX_SAFE_INTEGER;
|
|
141
|
+
var add = (a, b) => Math.min(CEILING, a + b);
|
|
140
142
|
function operationLabel(operation) {
|
|
141
143
|
return operation.name ? `Operation "${operation.name.value}"` : "Operation";
|
|
142
144
|
}
|
|
143
|
-
function assertInteger(option, value, min, max =
|
|
145
|
+
function assertInteger(option, value, min, max = CEILING) {
|
|
144
146
|
if (!Number.isInteger(value) || value < min || value >= max) {
|
|
145
147
|
throw new RangeError(`${option} must be an integer from ${min} to ${max - 1}.`);
|
|
146
148
|
}
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
// src/limits/fragments.ts
|
|
152
|
+
var NO_KEY = -1;
|
|
153
|
+
function newFragmentWalk(getFragment) {
|
|
154
|
+
return { getFragment, values: new Map, visiting: new Set };
|
|
155
|
+
}
|
|
156
|
+
function fragmentValue(walk, name, key, compute) {
|
|
157
|
+
let known = walk.values.get(name);
|
|
158
|
+
if (!known) {
|
|
159
|
+
known = new Map;
|
|
160
|
+
walk.values.set(name, known);
|
|
161
|
+
}
|
|
162
|
+
const at = typeof key === "number" ? key : key(known);
|
|
163
|
+
const memo = known.get(at);
|
|
164
|
+
if (memo !== undefined)
|
|
165
|
+
return memo;
|
|
166
|
+
const fragment = walk.getFragment(name);
|
|
167
|
+
if (!fragment || walk.visiting.has(name))
|
|
168
|
+
return 0;
|
|
169
|
+
walk.visiting.add(name);
|
|
170
|
+
const result = compute(fragment, at);
|
|
171
|
+
walk.visiting.delete(name);
|
|
172
|
+
known.set(at, result);
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
|
|
149
176
|
// src/limits/introspection.ts
|
|
150
177
|
import {
|
|
151
178
|
isEnumType,
|
|
@@ -230,26 +257,14 @@ function depthOf(depth, selectionSet) {
|
|
|
230
257
|
return deepest;
|
|
231
258
|
}
|
|
232
259
|
function fragmentDepth(depth, name) {
|
|
233
|
-
|
|
234
|
-
if (known !== undefined)
|
|
235
|
-
return known;
|
|
236
|
-
const fragment = depth.getFragment(name);
|
|
237
|
-
if (!fragment || depth.measuring.has(name))
|
|
238
|
-
return 0;
|
|
239
|
-
depth.measuring.add(name);
|
|
240
|
-
const result = depthOf(depth, fragment.selectionSet);
|
|
241
|
-
depth.measuring.delete(name);
|
|
242
|
-
depth.fragmentDepths.set(name, result);
|
|
243
|
-
return result;
|
|
260
|
+
return fragmentValue(depth, name, NO_KEY, (fragment) => depthOf(depth, fragment.selectionSet));
|
|
244
261
|
}
|
|
245
262
|
function maxDepth(options) {
|
|
246
263
|
const limit = options.depth;
|
|
247
264
|
assertInteger("maxDepth: depth", limit, 1);
|
|
248
265
|
return (context) => {
|
|
249
266
|
const walk = {
|
|
250
|
-
|
|
251
|
-
fragmentDepths: new Map,
|
|
252
|
-
measuring: new Set,
|
|
267
|
+
...newFragmentWalk((name) => context.getFragment(name)),
|
|
253
268
|
ignoreIntrospection: options.ignoreIntrospection ?? false
|
|
254
269
|
};
|
|
255
270
|
return {
|
|
@@ -266,21 +281,94 @@ function maxDepth(options) {
|
|
|
266
281
|
};
|
|
267
282
|
};
|
|
268
283
|
}
|
|
284
|
+
// src/limits/max-fields.ts
|
|
285
|
+
import {
|
|
286
|
+
GraphQLError as GraphQLError4
|
|
287
|
+
} from "graphql";
|
|
288
|
+
function countOf(walk, selectionSet) {
|
|
289
|
+
let total = 0;
|
|
290
|
+
for (const selection of selectionSet.selections) {
|
|
291
|
+
if (selection.kind === "Field") {
|
|
292
|
+
if (walk.ignoreIntrospection && isIntrospectionRoot(selection.name.value))
|
|
293
|
+
continue;
|
|
294
|
+
total = add(total, 1);
|
|
295
|
+
if (selection.selectionSet)
|
|
296
|
+
total = add(total, countOf(walk, selection.selectionSet));
|
|
297
|
+
} else if (selection.kind === "InlineFragment") {
|
|
298
|
+
total = add(total, countOf(walk, selection.selectionSet));
|
|
299
|
+
} else {
|
|
300
|
+
total = add(total, fragmentCount(walk, selection.name.value));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return total;
|
|
304
|
+
}
|
|
305
|
+
function fragmentCount(walk, name) {
|
|
306
|
+
return fragmentValue(walk, name, NO_KEY, (fragment) => countOf(walk, fragment.selectionSet));
|
|
307
|
+
}
|
|
308
|
+
function maxFields(options) {
|
|
309
|
+
const limit = options.limit;
|
|
310
|
+
assertInteger("maxFields: limit", limit, 1);
|
|
311
|
+
return (context) => {
|
|
312
|
+
const walk = {
|
|
313
|
+
...newFragmentWalk((name) => context.getFragment(name)),
|
|
314
|
+
ignoreIntrospection: options.ignoreIntrospection ?? false
|
|
315
|
+
};
|
|
316
|
+
return {
|
|
317
|
+
OperationDefinition(operation) {
|
|
318
|
+
const fields = countOf(walk, operation.selectionSet);
|
|
319
|
+
if (fields > limit) {
|
|
320
|
+
context.reportError(new GraphQLError4(`${operationLabel(operation)} selects ${fields} fields, more than the ${limit} allowed`, {
|
|
321
|
+
nodes: [operation],
|
|
322
|
+
extensions: { code: "QUERY_TOO_WIDE", fields, limit }
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
}
|
|
269
330
|
// src/limits/max-list-cost.ts
|
|
270
331
|
import {
|
|
271
|
-
GraphQLError as
|
|
332
|
+
GraphQLError as GraphQLError5,
|
|
272
333
|
getNamedType,
|
|
273
334
|
getNullableType,
|
|
335
|
+
isListType
|
|
336
|
+
} from "graphql";
|
|
337
|
+
|
|
338
|
+
// src/limits/cost-context.ts
|
|
339
|
+
import {
|
|
274
340
|
isInterfaceType as isInterfaceType2,
|
|
275
|
-
isListType,
|
|
276
341
|
isObjectType as isObjectType2
|
|
277
342
|
} from "graphql";
|
|
343
|
+
var multiply = (a, b) => Math.min(CEILING, a * b);
|
|
344
|
+
function fieldOf(cost, parent, name) {
|
|
345
|
+
if (parent === cost.schema.getQueryType() && isIntrospectionRoot(name)) {
|
|
346
|
+
return introspectionRoot(name);
|
|
347
|
+
}
|
|
348
|
+
return isObjectType2(parent) || isInterfaceType2(parent) ? parent.getFields()[name] : undefined;
|
|
349
|
+
}
|
|
350
|
+
function rootType(schema, operation) {
|
|
351
|
+
const type = operation.operation === "query" ? schema.getQueryType() : operation.operation === "mutation" ? schema.getMutationType() : schema.getSubscriptionType();
|
|
352
|
+
return type ?? undefined;
|
|
353
|
+
}
|
|
354
|
+
var EXACT_SIZES = 8;
|
|
355
|
+
var NO_PENDING = NO_KEY;
|
|
356
|
+
function memoSize(known, pending) {
|
|
357
|
+
if (pending === undefined || known.has(pending) || known.size < EXACT_SIZES) {
|
|
358
|
+
return pending;
|
|
359
|
+
}
|
|
360
|
+
let rounded = 2 ** Math.ceil(Math.log2(Math.max(pending, 1)));
|
|
361
|
+
if (rounded < pending)
|
|
362
|
+
rounded *= 2;
|
|
363
|
+
return Math.min(CEILING, rounded);
|
|
364
|
+
}
|
|
278
365
|
|
|
279
366
|
// src/limits/list-size.ts
|
|
280
367
|
import {
|
|
281
368
|
isAbstractType
|
|
282
369
|
} from "graphql";
|
|
283
|
-
|
|
370
|
+
|
|
371
|
+
// src/limits/size-reading.ts
|
|
284
372
|
var CANONICAL_INTEGER = /^(0|[1-9]\d*)$/;
|
|
285
373
|
function sizeOf(value) {
|
|
286
374
|
if (typeof value === "string") {
|
|
@@ -318,6 +406,30 @@ function defaultSizeOf(argument) {
|
|
|
318
406
|
}
|
|
319
407
|
return;
|
|
320
408
|
}
|
|
409
|
+
function boundOf(value) {
|
|
410
|
+
if (value.kind === "StringValue")
|
|
411
|
+
return literalSize(value);
|
|
412
|
+
if (value.kind !== "IntValue" && value.kind !== "FloatValue")
|
|
413
|
+
return;
|
|
414
|
+
const number = Number(value.value);
|
|
415
|
+
return Number.isFinite(number) && number >= 0 ? Math.min(Math.ceil(number), CEILING) : undefined;
|
|
416
|
+
}
|
|
417
|
+
function constraintMaxOf(argument) {
|
|
418
|
+
const directive = argument?.astNode?.directives?.find((node) => node.name.value === "constraint");
|
|
419
|
+
let largest;
|
|
420
|
+
for (const node of directive?.arguments ?? []) {
|
|
421
|
+
if (node.name.value !== "max" && node.name.value !== "exclusiveMax") {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const bound = boundOf(node.value);
|
|
425
|
+
if (bound !== undefined && (largest === undefined || bound > largest)) {
|
|
426
|
+
largest = bound;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return largest;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/limits/list-size.ts
|
|
321
433
|
var profiles = new WeakMap;
|
|
322
434
|
function profileOf(cost, parent, definition) {
|
|
323
435
|
let bySizes = profiles.get(cost.schema);
|
|
@@ -346,13 +458,25 @@ function profileOf(cost, parent, definition) {
|
|
|
346
458
|
largest: cost.sizeArguments.map(() => {
|
|
347
459
|
return;
|
|
348
460
|
}),
|
|
461
|
+
maxima: cost.sizeArguments.map(() => {
|
|
462
|
+
return;
|
|
463
|
+
}),
|
|
464
|
+
someNoMax: cost.sizeArguments.map(() => false),
|
|
349
465
|
someLack: cost.sizeArguments.map(() => false),
|
|
350
|
-
someBare: false
|
|
466
|
+
someBare: false,
|
|
467
|
+
declares: false,
|
|
468
|
+
someUndeclared: false
|
|
351
469
|
};
|
|
352
470
|
for (const field of definitions) {
|
|
353
471
|
let any = false;
|
|
354
472
|
cost.sizeArguments.forEach((name, index) => {
|
|
355
|
-
const
|
|
473
|
+
const argument = field.args.find((arg) => arg.name === name);
|
|
474
|
+
const max = constraintMaxOf(argument);
|
|
475
|
+
if (max === undefined)
|
|
476
|
+
profile.someNoMax[index] = true;
|
|
477
|
+
else
|
|
478
|
+
profile.maxima[index] = Math.max(profile.maxima[index] ?? 0, max);
|
|
479
|
+
const size = defaultSizeOf(argument);
|
|
356
480
|
if (size === undefined) {
|
|
357
481
|
profile.someLack[index] = true;
|
|
358
482
|
return;
|
|
@@ -362,6 +486,11 @@ function profileOf(cost, parent, definition) {
|
|
|
362
486
|
});
|
|
363
487
|
if (!any)
|
|
364
488
|
profile.someBare = true;
|
|
489
|
+
if (field.args.some((arg) => cost.sizeArguments.includes(arg.name))) {
|
|
490
|
+
profile.declares = true;
|
|
491
|
+
} else {
|
|
492
|
+
profile.someUndeclared = true;
|
|
493
|
+
}
|
|
365
494
|
}
|
|
366
495
|
byField.set(key, profile);
|
|
367
496
|
return profile;
|
|
@@ -373,9 +502,18 @@ function argumentSize(cost, profile, index, node) {
|
|
|
373
502
|
const literal = literalSize(node.value);
|
|
374
503
|
if (literal !== undefined)
|
|
375
504
|
return literal;
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
505
|
+
const today = profile.someLack[index] ? Math.max(largest ?? 0, cost.defaultFirst) : largest;
|
|
506
|
+
const bound = unusableBound(cost, profile, index);
|
|
507
|
+
return bound === undefined ? today : Math.max(today ?? 0, bound);
|
|
508
|
+
}
|
|
509
|
+
function unusableBound(cost, profile, index) {
|
|
510
|
+
const assumed = profile.someNoMax[index] ? cost.variableSize : undefined;
|
|
511
|
+
const declared = profile.maxima[index];
|
|
512
|
+
if (assumed === undefined)
|
|
513
|
+
return declared;
|
|
514
|
+
return Math.max(declared ?? 0, assumed);
|
|
515
|
+
}
|
|
516
|
+
function statedSize(cost, parent, definition, args) {
|
|
379
517
|
const profile = profileOf(cost, parent, definition);
|
|
380
518
|
let size;
|
|
381
519
|
let given = false;
|
|
@@ -387,23 +525,50 @@ function listSize(cost, parent, definition, args) {
|
|
|
387
525
|
if (said !== undefined)
|
|
388
526
|
size = Math.max(size ?? 0, said);
|
|
389
527
|
});
|
|
390
|
-
if (!given && profile.someBare)
|
|
391
|
-
|
|
392
|
-
|
|
528
|
+
if (size !== undefined && !given && profile.someBare) {
|
|
529
|
+
return Math.max(size, cost.defaultFirst);
|
|
530
|
+
}
|
|
531
|
+
return size;
|
|
532
|
+
}
|
|
533
|
+
function declaration(cost, parent, definition) {
|
|
534
|
+
const { declares, someUndeclared } = profileOf(cost, parent, definition);
|
|
535
|
+
if (!declares)
|
|
536
|
+
return "none";
|
|
537
|
+
return someUndeclared ? "some" : "all";
|
|
393
538
|
}
|
|
394
539
|
|
|
395
|
-
// src/limits/max-list-cost.ts
|
|
540
|
+
// src/limits/max-list-cost-options.ts
|
|
396
541
|
var DEFAULT_SIZE_ARGUMENTS = ["first", "last", "limit"];
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (
|
|
402
|
-
return
|
|
542
|
+
|
|
543
|
+
// src/limits/pending-size.ts
|
|
544
|
+
function listSizeUnder(cost, parent, definition, args, pending) {
|
|
545
|
+
const own = () => statedSize(cost, parent, definition, args) ?? cost.defaultFirst;
|
|
546
|
+
if (pending === undefined)
|
|
547
|
+
return own();
|
|
548
|
+
const declared = declaration(cost, parent, definition);
|
|
549
|
+
if (declared === "none")
|
|
550
|
+
return pending;
|
|
551
|
+
if (declared === "all")
|
|
552
|
+
return own();
|
|
553
|
+
return Math.max(pending, own());
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/limits/max-list-cost.ts
|
|
557
|
+
function fieldCost(cost, parent, selection, definition, pending) {
|
|
558
|
+
if (!selection.selectionSet)
|
|
559
|
+
return 1;
|
|
560
|
+
const name = selection.name.value;
|
|
561
|
+
const type = getNamedType(definition.type);
|
|
562
|
+
if (!isListType(getNullableType(definition.type))) {
|
|
563
|
+
const stated = statedSize(cost, parent, definition, selection.arguments);
|
|
564
|
+
const inner = costOf(cost, selection.selectionSet, type, stated);
|
|
565
|
+
return add(1, inner);
|
|
403
566
|
}
|
|
404
|
-
|
|
567
|
+
const inner = costOf(cost, selection.selectionSet, type, undefined);
|
|
568
|
+
const size = parent?.name.startsWith("__") ? introspectionSize(cost.schema, name) : listSizeUnder(cost, parent, definition, selection.arguments, pending);
|
|
569
|
+
return add(1, multiply(inner, size));
|
|
405
570
|
}
|
|
406
|
-
function costOf(cost, selectionSet, parent) {
|
|
571
|
+
function costOf(cost, selectionSet, parent, pending) {
|
|
407
572
|
let total = 0;
|
|
408
573
|
for (const selection of selectionSet.selections) {
|
|
409
574
|
if (selection.kind === "Field") {
|
|
@@ -415,40 +580,19 @@ function costOf(cost, selectionSet, parent) {
|
|
|
415
580
|
const definition = fieldOf(cost, parent, name);
|
|
416
581
|
if (!definition)
|
|
417
582
|
continue;
|
|
418
|
-
|
|
419
|
-
if (selection.selectionSet) {
|
|
420
|
-
inner = costOf(cost, selection.selectionSet, getNamedType(definition.type));
|
|
421
|
-
if (isListType(getNullableType(definition.type))) {
|
|
422
|
-
const size = parent?.name.startsWith("__") ? introspectionSize(cost.schema, name) : listSize(cost, parent, definition, selection.arguments);
|
|
423
|
-
inner = multiply(inner, size);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
total = add(total, add(1, inner));
|
|
583
|
+
total = add(total, fieldCost(cost, parent, selection, definition, pending));
|
|
427
584
|
} else if (selection.kind === "InlineFragment") {
|
|
428
585
|
const condition = selection.typeCondition ? cost.schema.getType(selection.typeCondition.name.value) : parent;
|
|
429
|
-
total = add(total, costOf(cost, selection.selectionSet, condition));
|
|
586
|
+
total = add(total, costOf(cost, selection.selectionSet, condition, pending));
|
|
430
587
|
} else {
|
|
431
|
-
total = add(total, fragmentCost(cost, selection.name.value));
|
|
588
|
+
total = add(total, fragmentCost(cost, selection.name.value, pending));
|
|
432
589
|
}
|
|
433
590
|
}
|
|
434
591
|
return total;
|
|
435
592
|
}
|
|
436
|
-
function fragmentCost(cost, name) {
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
return known;
|
|
440
|
-
const fragment = cost.getFragment(name);
|
|
441
|
-
if (!fragment || cost.costing.has(name))
|
|
442
|
-
return 0;
|
|
443
|
-
cost.costing.add(name);
|
|
444
|
-
const result = costOf(cost, fragment.selectionSet, cost.schema.getType(fragment.typeCondition.name.value));
|
|
445
|
-
cost.costing.delete(name);
|
|
446
|
-
cost.fragmentCosts.set(name, result);
|
|
447
|
-
return result;
|
|
448
|
-
}
|
|
449
|
-
function rootType(schema, operation) {
|
|
450
|
-
const type = operation.operation === "query" ? schema.getQueryType() : operation.operation === "mutation" ? schema.getMutationType() : schema.getSubscriptionType();
|
|
451
|
-
return type ?? undefined;
|
|
593
|
+
function fragmentCost(cost, name, pending) {
|
|
594
|
+
const key = (known) => memoSize(known, pending) ?? NO_PENDING;
|
|
595
|
+
return fragmentValue(cost, name, key, (fragment, at) => costOf(cost, fragment.selectionSet, cost.schema.getType(fragment.typeCondition.name.value), at === NO_PENDING ? undefined : at));
|
|
452
596
|
}
|
|
453
597
|
function maxListCost(options) {
|
|
454
598
|
const { budget, defaultFirst } = options;
|
|
@@ -457,22 +601,25 @@ function maxListCost(options) {
|
|
|
457
601
|
]);
|
|
458
602
|
assertInteger("maxListCost: budget", budget, 0);
|
|
459
603
|
assertInteger("maxListCost: defaultFirst", defaultFirst, 0);
|
|
604
|
+
const { variableSize } = options;
|
|
605
|
+
if (variableSize !== undefined) {
|
|
606
|
+
assertInteger("maxListCost: variableSize", variableSize, 0);
|
|
607
|
+
}
|
|
460
608
|
return (context) => {
|
|
461
609
|
const schema = context.getSchema();
|
|
462
610
|
const cost = {
|
|
463
611
|
schema,
|
|
464
612
|
sizeArguments,
|
|
465
613
|
defaultFirst,
|
|
614
|
+
variableSize,
|
|
466
615
|
ignoreIntrospection: options.ignoreIntrospection ?? false,
|
|
467
|
-
|
|
468
|
-
fragmentCosts: new Map,
|
|
469
|
-
costing: new Set
|
|
616
|
+
...newFragmentWalk((name) => context.getFragment(name))
|
|
470
617
|
};
|
|
471
618
|
return {
|
|
472
619
|
OperationDefinition(operation) {
|
|
473
|
-
const total = costOf(cost, operation.selectionSet, rootType(schema, operation));
|
|
620
|
+
const total = costOf(cost, operation.selectionSet, rootType(schema, operation), undefined);
|
|
474
621
|
if (total > budget) {
|
|
475
|
-
context.reportError(new
|
|
622
|
+
context.reportError(new GraphQLError5(`${operationLabel(operation)} costs ${total}, more than the ${budget} allowed`, {
|
|
476
623
|
nodes: [operation],
|
|
477
624
|
extensions: {
|
|
478
625
|
code: "QUERY_TOO_COSTLY",
|
|
@@ -561,10 +708,11 @@ export {
|
|
|
561
708
|
inputPath,
|
|
562
709
|
invalidInputError,
|
|
563
710
|
maxDepth,
|
|
711
|
+
maxFields,
|
|
564
712
|
maxListCost,
|
|
565
713
|
validated,
|
|
566
714
|
withValidation
|
|
567
715
|
};
|
|
568
716
|
|
|
569
|
-
//# debugId=
|
|
717
|
+
//# debugId=B1856E23719B140164756E2164756E21
|
|
570
718
|
//# sourceMappingURL=index.js.map
|