@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 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 = [maxDepth({ depth: 8 }), maxListCost({ budget: 10_000, defaultFirst: 50 })];
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 }` or `{ code: 'QUERY_TOO_COSTLY', cost, limit }`. A list field is sized by
274
- its `first`, `last` or `limit` argument; a variable, a negative number or no
275
- argument uses the argument's SDL default, else `defaultFirst`. A size is an
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. Introspection is
278
- counted too; add `ignoreIntrospection: true` where it is trusted (GraphiQL in
279
- development). See
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
- | `MaxDepthOptions`, `MaxListCostOptions` | their options |
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';
@@ -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 = Number.MAX_SAFE_INTEGER) {
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
- const known = depth.fragmentDepths.get(name);
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
- getFragment: (name) => context.getFragment(name),
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 GraphQLError4,
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
- var CEILING = Number.MAX_SAFE_INTEGER;
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 size = defaultSizeOf(field.args.find((arg) => arg.name === name));
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
- return profile.someLack[index] ? Math.max(largest ?? 0, cost.defaultFirst) : largest;
377
- }
378
- function listSize(cost, parent, definition, args) {
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
- size = Math.max(size ?? 0, cost.defaultFirst);
392
- return size ?? cost.defaultFirst;
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
- var CEILING2 = Number.MAX_SAFE_INTEGER;
398
- var add = (a, b) => Math.min(CEILING2, a + b);
399
- var multiply = (a, b) => Math.min(CEILING2, a * b);
400
- function fieldOf(cost, parent, name) {
401
- if (parent === cost.schema.getQueryType() && isIntrospectionRoot(name)) {
402
- return introspectionRoot(name);
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
- return isObjectType2(parent) || isInterfaceType2(parent) ? parent.getFields()[name] : undefined;
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
- let inner = 0;
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 known = cost.fragmentCosts.get(name);
438
- if (known !== undefined)
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
- getFragment: (name) => context.getFragment(name),
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 GraphQLError4(`${operationLabel(operation)} costs ${total}, more than the ${budget} allowed`, {
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=E43868C11AE8F6A464756E2164756E21
717
+ //# debugId=B1856E23719B140164756E2164756E21
570
718
  //# sourceMappingURL=index.js.map