@nxgt/graphql-validation 0.5.0 → 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 +54 -6
- 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 +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +380 -10
- package/dist/index.js.map +10 -5
- 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/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 +12 -3
- package/docs/guide/query-limits.md +206 -0
- package/docs/roadmap.md +4 -0
- package/docs/troubleshooting.md +53 -0
- package/package.json +1 -1
package/dist/index.js.map
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/bad-user-input.ts", "../src/invalid-input.ts", "../src/field-issue-of.ts", "../src/validated.ts", "../src/with-validation.ts"],
|
|
3
|
+
"sources": ["../src/bad-user-input.ts", "../src/invalid-input.ts", "../src/field-issue-of.ts", "../src/limits/max-depth.ts", "../src/limits/describe.ts", "../src/limits/introspection.ts", "../src/limits/max-list-cost.ts", "../src/limits/list-size.ts", "../src/validated.ts", "../src/with-validation.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import { GraphQLError } from 'graphql';\nimport type { z } from 'zod';\nimport type { ConstraintArgument } from './rules';\n\n/** One reason an input was refused, at its path inside the arguments. */\nexport interface ValidationIssue {\n\t/** `['input', 'address', 'zip']`: the argument, then the input fields. */\n\treadonly path: readonly (string | number)[];\n\treadonly message: string;\n\t/** Zod's issue code: `too_small`, `invalid_format`, `custom`, ... */\n\treadonly code: string;\n\t/**\n\t * The `@constraint` argument that refused (`minLength`, `format`), when a\n\t * directive did: stable across Zod versions, unlike `code`. Absent for a\n\t * refusal of `validated()`'s own schema.\n\t */\n\treadonly constraint?: ConstraintArgument;\n}\n\n/** The `extensions` of the error an invalid input becomes. */\nexport type BadUserInputExtensions = {\n\treadonly code: 'BAD_USER_INPUT';\n\treadonly issues: readonly ValidationIssue[];\n};\n\n
|
|
5
|
+
"import { GraphQLError } from 'graphql';\nimport type { z } from 'zod';\nimport type { ConstraintArgument } from './rules';\n\n/** One reason an input was refused, at its path inside the arguments. */\nexport interface ValidationIssue {\n\t/** `['input', 'address', 'zip']`: the argument, then the input fields. */\n\treadonly path: readonly (string | number)[];\n\treadonly message: string;\n\t/** Zod's issue code: `too_small`, `invalid_format`, `custom`, ... */\n\treadonly code: string;\n\t/**\n\t * The `@constraint` argument that refused (`minLength`, `format`), when a\n\t * directive did: stable across Zod versions, unlike `code`. Absent for a\n\t * refusal of `validated()`'s own schema.\n\t */\n\treadonly constraint?: ConstraintArgument;\n\t/**\n\t * What that `@constraint` argument was declared with, exactly as written\n\t * (`100` for `maxLength: 100`, `\"email\"` for `format: \"email\"`), so a\n\t * client can say \"at most 100 characters\" without parsing `message`.\n\t * Present exactly when `constraint` is.\n\t */\n\treadonly value?: ConstraintValue;\n}\n\n/** What a `@constraint` argument is declared with: a number or a string. */\nexport type ConstraintValue = string | number;\n\n/** The `extensions` of the error an invalid input becomes. */\nexport type BadUserInputExtensions = {\n\treadonly code: 'BAD_USER_INPUT';\n\treadonly issues: readonly ValidationIssue[];\n};\n\n/**\n * Names the `@constraint` argument behind an issue, and what it was declared\n * with, when there is one.\n */\nexport type ConstraintOf = (\n\tissue: z.core.$ZodIssue,\n) => { argument: ConstraintArgument; value: ConstraintValue } | undefined;\n\nfunction issuesOf(\n\terror: z.ZodError,\n\tconstraintOf?: ConstraintOf,\n): ValidationIssue[] {\n\treturn error.issues.map((issue) => {\n\t\tconst path = issue.path.map((key) =>\n\t\t\ttypeof key === 'number' ? key : String(key),\n\t\t);\n\t\tconst constraint = constraintOf?.(issue);\n\t\treturn constraint\n\t\t\t? {\n\t\t\t\t\tpath,\n\t\t\t\t\tmessage: issue.message,\n\t\t\t\t\tcode: issue.code,\n\t\t\t\t\tconstraint: constraint.argument,\n\t\t\t\t\tvalue: constraint.value,\n\t\t\t\t}\n\t\t\t: { path, message: issue.message, code: issue.code };\n\t});\n}\n\nfunction errorOf(where: string, issues: ValidationIssue[]): GraphQLError {\n\tconst [first] = issues;\n\tconst reason = first\n\t\t? `${first.path.join('.') || 'arguments'}: ${first.message}`\n\t\t: 'refused';\n\tconst extensions: BadUserInputExtensions = { code: 'BAD_USER_INPUT', issues };\n\treturn new GraphQLError(`Invalid arguments for ${where}. ${reason}`, {\n\t\textensions,\n\t});\n}\n\n/**\n * The one error an invalid input becomes: `extensions.code` is\n * `BAD_USER_INPUT`, the code Apollo and most clients already read, and\n * `extensions.issues` lists every reason with its path, so a form can show\n * each one beside its field. The message names the field and the first\n * reason, for a log line.\n */\nexport function badUserInput(where: string, error: z.ZodError): GraphQLError {\n\treturn errorOf(where, issuesOf(error));\n}\n\n/** The parsed arguments, or the error above, its issues named by `constraintOf`. */\nexport async function parseArgs(\n\tschema: z.ZodType,\n\targs: unknown,\n\twhere: string,\n\tconstraintOf?: ConstraintOf,\n): Promise<unknown> {\n\tconst result = await schema.safeParseAsync(args);\n\tif (!result.success)\n\t\tthrow errorOf(where, issuesOf(result.error, constraintOf));\n\treturn result.data;\n}\n",
|
|
6
6
|
"import { GraphQLError, type GraphQLErrorOptions } from 'graphql';\n\n/** The code of the error `invalidInputError` builds, and its default message. */\nexport const INVALID_INPUT = 'validation.errors.invalid-input';\n\n/** Values a message is built from, such as `{ min: 2 }`. */\nexport type IssueParams = Record<string, string | number>;\n\n/** One field at fault: where it is, why, and what the reason's message needs. */\nexport interface FieldIssue {\n\t/** The field inside the input, dotted: `person.firstName`, `tags.1`. */\n\treadonly path: string;\n\t/** A code your client and your translations know: `validation.errors.too-short`. */\n\treadonly code: string;\n\t/** Values the message needs; an object with no key counts as none. */\n\treadonly params?: IssueParams;\n}\n\n/** One entry of `extensions.fields`: a `FieldIssue` with its message. */\nexport interface FieldError {\n\treadonly path: string;\n\treadonly code: string;\n\t/** `translate(code, params)`, or the code itself without a translator. */\n\treadonly message: string;\n\t/** Present only when the issue had params with at least one key. */\n\treadonly params?: IssueParams;\n}\n\n/** Turns a code (and its params) into the text a person reads. */\nexport type Translate = (code: string, params?: IssueParams) => string;\n\n/** The options of `invalidInputError`: GraphQL's own, and a translator. */\nexport type InvalidInputOptions = GraphQLErrorOptions & {\n\treadonly translate?: Translate;\n};\n\n/** The options of `fieldsError`: also an HTTP status and the error's own params. */\nexport type FieldsErrorOptions = InvalidInputOptions & {\n\t/** `extensions.http.status`, for a server that reads it (409, ...). */\n\treadonly status?: number;\n\treadonly params?: IssueParams;\n};\n\n/** `params` itself, or `undefined` when it holds no key: never `{}`. */\nfunction paramsOf(params: IssueParams | undefined): IssueParams | undefined {\n\treturn params && Object.keys(params).length > 0 ? params : undefined;\n}\n\nfunction messageOf(\n\tcode: string,\n\tparams: IssueParams | undefined,\n\ttranslate: Translate | undefined,\n): string {\n\treturn translate?.(code, params) ?? code;\n}\n\nfunction fieldsOf(\n\tissues: readonly FieldIssue[],\n\ttranslate: Translate | undefined,\n): FieldError[] {\n\treturn issues.map(({ path, code, params: given }) => {\n\t\tconst params = paramsOf(given);\n\t\treturn {\n\t\t\tpath,\n\t\t\tcode,\n\t\t\tmessage: messageOf(code, params, translate),\n\t\t\t...(params && { params }),\n\t\t};\n\t});\n}\n\n/**\n * One 400 error listing every field at fault, for an API that answers invalid\n * input this way: `extensions` is `{ code: 'validation.errors.invalid-input',\n * http: { status: 400 }, fields }`, each field with its translated message\n * (the code itself without `translate`), in the order given. GraphQL's\n * options (`originalError`, `path`, `nodes`, ...) are kept; an `extensions`\n * among them is replaced by this one.\n */\nexport function invalidInputError(\n\tissues: readonly FieldIssue[],\n\toptions: InvalidInputOptions = {},\n): GraphQLError {\n\tconst { translate, ...graphql } = options;\n\treturn new GraphQLError(messageOf(INVALID_INPUT, undefined, translate), {\n\t\t...graphql,\n\t\textensions: {\n\t\t\tcode: INVALID_INPUT,\n\t\t\thttp: { status: 400 },\n\t\t\tfields: fieldsOf(issues, translate),\n\t\t},\n\t});\n}\n\n/**\n * A refusal of your own (a 409 conflict, say) that names the fields at\n * fault: `extensions` is `{ code, params?, http?: { status }, fields }`, the\n * fields worded as `invalidInputError` words them, the message\n * `translate(code, params)` or the code. An `extensions` among the options\n * is replaced by this one.\n */\nexport function fieldsError(\n\tcode: string,\n\tissues: readonly FieldIssue[],\n\toptions: FieldsErrorOptions = {},\n): GraphQLError {\n\tconst { translate, status, params: given, ...graphql } = options;\n\tconst params = paramsOf(given);\n\treturn new GraphQLError(messageOf(code, params, translate), {\n\t\t...graphql,\n\t\textensions: {\n\t\t\tcode,\n\t\t\t...(params && { params }),\n\t\t\t...(status && { http: { status } }),\n\t\t\tfields: fieldsOf(issues, translate),\n\t\t},\n\t});\n}\n\n/**\n * A GraphQL or Zod path as a `FieldIssue` path: dotted, without the leading\n * `input` argument when a field follows it, a symbol written as\n * `String(key)`. `['input', 'person', 'firstName']` is `person.firstName`,\n * `['input', 'tags', 1]` is `tags.1`, `['first']` and `['input']` stay as\n * they are.\n */\nexport function inputPath(path: readonly PropertyKey[]): string {\n\tconst [first, ...rest] = path;\n\treturn (first === 'input' && rest.length > 0 ? rest : path)\n\t\t.map((key) => String(key))\n\t\t.join('.');\n}\n",
|
|
7
7
|
"import type { z } from 'zod';\nimport { type FieldIssue, inputPath } from './invalid-input';\n\n/**\n * The codes `fieldIssueOf` gives, each under `validation.errors.`: a string's\n * length (`too-short`, `too-long`), a number's or bigint's bound or multiple\n * (`out-of-range`), a list's or set's size (`too-few`, `too-many`), a named\n * format or a pattern (`invalid-format`), anything else (`invalid`).\n */\nexport const FIELD_CODES = {\n\ttooShort: 'validation.errors.too-short',\n\ttooLong: 'validation.errors.too-long',\n\toutOfRange: 'validation.errors.out-of-range',\n\ttooFew: 'validation.errors.too-few',\n\ttooMany: 'validation.errors.too-many',\n\tinvalidFormat: 'validation.errors.invalid-format',\n\tinvalid: 'validation.errors.invalid',\n} as const;\n\n/** The string checks Zod reports as `invalid_format` that are not a format. */\nconst TEXT_CHECKS: ReadonlySet<string> = new Set([\n\t'starts_with',\n\t'ends_with',\n\t'includes',\n]);\n\n/**\n * A Zod issue as a `FieldIssue`, for `invalidInputError`: its path through\n * `inputPath` (dotted, a leading `input` dropped) and one of `FIELD_CODES`,\n * without params. `too-short` / `too-long` for a string's length,\n * `out-of-range` for a number's or bigint's bound or a multiple, `too-few` /\n * `too-many` for a list's or set's size, `invalid-format` for a named format\n * or a pattern, `invalid` for anything else (a prefix, a suffix or a\n * substring included).\n */\nexport function fieldIssueOf(issue: z.core.$ZodIssue): FieldIssue {\n\treturn { path: inputPath(issue.path), code: codeOf(issue) };\n}\n\nfunction codeOf(issue: z.core.$ZodIssue): string {\n\tswitch (issue.code) {\n\t\tcase 'too_small':\n\t\tcase 'too_big': {\n\t\t\tconst small = issue.code === 'too_small';\n\t\t\tswitch (issue.origin) {\n\t\t\t\tcase 'string':\n\t\t\t\t\treturn small ? FIELD_CODES.tooShort : FIELD_CODES.tooLong;\n\t\t\t\tcase 'number':\n\t\t\t\tcase 'bigint':\n\t\t\t\t\treturn FIELD_CODES.outOfRange;\n\t\t\t\tcase 'array':\n\t\t\t\tcase 'set':\n\t\t\t\t\treturn small ? FIELD_CODES.tooFew : FIELD_CODES.tooMany;\n\t\t\t\tdefault:\n\t\t\t\t\treturn FIELD_CODES.invalid;\n\t\t\t}\n\t\t}\n\t\tcase 'not_multiple_of':\n\t\t\treturn FIELD_CODES.outOfRange;\n\t\tcase 'invalid_format':\n\t\t\t// A prefix, a suffix or a substring is not a format: only a named\n\t\t\t// format (`email`, `uuid`, …) or a pattern is.\n\t\t\treturn TEXT_CHECKS.has(issue.format)\n\t\t\t\t? FIELD_CODES.invalid\n\t\t\t\t: FIELD_CODES.invalidFormat;\n\t\tdefault:\n\t\t\treturn FIELD_CODES.invalid;\n\t}\n}\n",
|
|
8
|
+
"import {\n\ttype FragmentDefinitionNode,\n\tGraphQLError,\n\ttype SelectionSetNode,\n\ttype ValidationContext,\n\ttype ValidationRule,\n} from 'graphql';\nimport { assertInteger, operationLabel } from './describe';\nimport { isIntrospectionRoot } from './introspection';\n\n/** Options of {@link maxDepth}. */\nexport interface MaxDepthOptions {\n\t/** The deepest nesting of field selections an operation may have (at least 1). */\n\tdepth: number;\n\t/**\n\t * Do not count `__schema` and `__type` (default `false`). Only where\n\t * introspection is disabled or the client is trusted, e.g. GraphiQL in\n\t * development: the standard introspection query (`getIntrospectionQuery()`) is 15 levels deep.\n\t */\n\tignoreIntrospection?: boolean;\n}\n\n/** What a walk of one document needs: what it has measured so far. */\ninterface DepthContext {\n\tgetFragment: (name: string) => FragmentDefinitionNode | null | undefined;\n\tfragmentDepths: Map<string, number>;\n\tmeasuring: Set<string>;\n\tignoreIntrospection: boolean;\n}\n\n/** The depth of `selectionSet`: 0 when it selects no field. */\nfunction depthOf(depth: DepthContext, selectionSet: SelectionSetNode): number {\n\tlet deepest = 0;\n\tfor (const selection of selectionSet.selections) {\n\t\tlet here = 0;\n\t\tif (selection.kind === 'Field') {\n\t\t\tconst name = selection.name.value;\n\t\t\tif (name === '__typename') continue;\n\t\t\tif (depth.ignoreIntrospection && isIntrospectionRoot(name)) continue;\n\t\t\there =\n\t\t\t\t1 +\n\t\t\t\t(selection.selectionSet ? depthOf(depth, selection.selectionSet) : 0);\n\t\t} else if (selection.kind === 'InlineFragment') {\n\t\t\there = depthOf(depth, selection.selectionSet);\n\t\t} else {\n\t\t\there = fragmentDepth(depth, selection.name.value);\n\t\t}\n\t\tif (here > deepest) deepest = here;\n\t}\n\treturn deepest;\n}\n\n/** The depth of the named fragment, measured once. */\nfunction fragmentDepth(depth: DepthContext, name: string): number {\n\tconst known = depth.fragmentDepths.get(name);\n\tif (known !== undefined) return known;\n\tconst fragment = depth.getFragment(name);\n\t// A cycle is NoFragmentCyclesRule's to report; here it adds nothing.\n\tif (!fragment || depth.measuring.has(name)) return 0;\n\tdepth.measuring.add(name);\n\tconst result = depthOf(depth, fragment.selectionSet);\n\tdepth.measuring.delete(name);\n\tdepth.fragmentDepths.set(name, result);\n\treturn result;\n}\n\n/**\n * A graphql-js validation rule refusing an operation nested deeper than\n * `depth` fields: a top-level field is depth 1.\n *\n * Named fragments are followed (each measured once), inline fragments add no\n * depth, and `__typename` does not count. One `GraphQLError` per\n * operation, with `extensions: { code: 'QUERY_TOO_DEEP', depth, limit }`,\n * `depth` being the operation's real depth.\n *\n * ```ts\n * validate(schema, document, [...specifiedRules, maxDepth({ depth: 8 })]);\n * // graphql-yoga: onValidate: ({ addValidationRule }) => addValidationRule(maxDepth({ depth: 8 }))\n * ```\n */\nexport function maxDepth(options: MaxDepthOptions): ValidationRule {\n\tconst limit = options.depth;\n\tassertInteger('maxDepth: depth', limit, 1);\n\treturn (context: ValidationContext) => {\n\t\tconst walk: DepthContext = {\n\t\t\tgetFragment: (name) => context.getFragment(name),\n\t\t\tfragmentDepths: new Map(),\n\t\t\tmeasuring: new Set(),\n\t\t\tignoreIntrospection: options.ignoreIntrospection ?? false,\n\t\t};\n\t\treturn {\n\t\t\tOperationDefinition(operation) {\n\t\t\t\tconst depth = depthOf(walk, operation.selectionSet);\n\t\t\t\tif (depth > limit) {\n\t\t\t\t\tcontext.reportError(\n\t\t\t\t\t\tnew GraphQLError(\n\t\t\t\t\t\t\t`${operationLabel(operation)} is ${depth} levels deep, more than the ${limit} allowed`,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tnodes: [operation],\n\t\t\t\t\t\t\t\textensions: { code: 'QUERY_TOO_DEEP', depth, limit },\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t},\n\t\t};\n\t};\n}\n",
|
|
9
|
+
"import type { OperationDefinitionNode } from 'graphql';\n\n/** `Operation \"Name\"`, or `Operation` for an anonymous one. */\nexport function operationLabel(operation: OperationDefinitionNode): string {\n\treturn operation.name ? `Operation \"${operation.name.value}\"` : 'Operation';\n}\n\n/**\n * Throws a `RangeError` unless `value` is an integer from `min` up to, and\n * excluding, `max` (default: `Number.MAX_SAFE_INTEGER`, where a cost is clamped).\n */\nexport function assertInteger(\n\toption: string,\n\tvalue: number,\n\tmin: number,\n\tmax = Number.MAX_SAFE_INTEGER,\n): void {\n\tif (!Number.isInteger(value) || value < min || value >= max) {\n\t\tthrow new RangeError(\n\t\t\t`${option} must be an integer from ${min} to ${max - 1}.`,\n\t\t);\n\t}\n}\n",
|
|
10
|
+
"import {\n\ttype GraphQLField,\n\ttype GraphQLNamedType,\n\ttype GraphQLSchema,\n\tisEnumType,\n\tisInputObjectType,\n\tisInterfaceType,\n\tisObjectType,\n\tisUnionType,\n\tSchemaMetaFieldDef,\n\tTypeMetaFieldDef,\n} from 'graphql';\n\n/** The meta-fields that read the schema; `__typename` is not one. */\nexport function isIntrospectionRoot(name: string): boolean {\n\treturn name === '__schema' || name === '__type';\n}\n\n/** The definition of `__schema` or `__type`, selected on the query root. */\nexport function introspectionRoot(\n\tname: string,\n): GraphQLField<unknown, unknown> | undefined {\n\tif (name === '__schema') return SchemaMetaFieldDef;\n\tif (name === '__type') return TypeMetaFieldDef;\n\treturn undefined;\n}\n\n/** The largest list the introspection types can return for `schema`, by schema. */\nconst largest = new WeakMap<GraphQLSchema, number>();\n\nconst typeCounts = new WeakMap<GraphQLSchema, number>();\n\nfunction typeCount(schema: GraphQLSchema): number {\n\tlet count = typeCounts.get(schema);\n\tif (count === undefined) {\n\t\tcount = Object.keys(schema.getTypeMap()).length;\n\t\ttypeCounts.set(schema, count);\n\t}\n\treturn count;\n}\n\nfunction largestCount(schema: GraphQLSchema): number {\n\tconst known = largest.get(schema);\n\tif (known !== undefined) return known;\n\tlet max = schema.getDirectives().length;\n\tfor (const directive of schema.getDirectives()) {\n\t\tmax = Math.max(max, directive.args.length, directive.locations.length);\n\t}\n\tfor (const type of Object.values(schema.getTypeMap())) {\n\t\tmax = Math.max(max, countsOf(schema, type));\n\t}\n\tlargest.set(schema, max);\n\treturn max;\n}\n\n/** The longest of a type's fields, input fields, enum values, interfaces, possible types and arguments. */\nfunction countsOf(schema: GraphQLSchema, type: GraphQLNamedType): number {\n\tif (isObjectType(type) || isInterfaceType(type)) {\n\t\tconst fields = Object.values(type.getFields());\n\t\treturn Math.max(\n\t\t\tfields.length,\n\t\t\ttype.getInterfaces().length,\n\t\t\tisInterfaceType(type) ? schema.getPossibleTypes(type).length : 0,\n\t\t\t...fields.map((field) => field.args.length),\n\t\t);\n\t}\n\tif (isInputObjectType(type)) return Object.keys(type.getFields()).length;\n\tif (isEnumType(type)) return type.getValues().length;\n\tif (isUnionType(type)) return type.getTypes().length;\n\treturn 0;\n}\n\n/**\n * The most items the introspection list `parent.field` can hold in `schema`:\n * the number of types for `__Schema.types`, else the largest list anywhere in\n * the schema. Not `defaultFirst`: a query asks no size of them.\n */\nexport function introspectionSize(\n\tschema: GraphQLSchema,\n\tfield: string,\n): number {\n\treturn field === 'types' ? typeCount(schema) : largestCount(schema);\n}\n",
|
|
11
|
+
"import {\n\ttype FragmentDefinitionNode,\n\tGraphQLError,\n\ttype GraphQLField,\n\ttype GraphQLNamedType,\n\ttype GraphQLSchema,\n\tgetNamedType,\n\tgetNullableType,\n\tisInterfaceType,\n\tisListType,\n\tisObjectType,\n\ttype OperationDefinitionNode,\n\ttype SelectionSetNode,\n\ttype ValidationContext,\n\ttype ValidationRule,\n} from 'graphql';\nimport { assertInteger, operationLabel } from './describe';\nimport {\n\tintrospectionRoot,\n\tintrospectionSize,\n\tisIntrospectionRoot,\n} from './introspection';\nimport { listSize, type SizeContext } from './list-size';\n\n/** Options of {@link maxListCost}. */\nexport interface MaxListCostOptions {\n\t/** The most an operation may cost (at least 0). */\n\tbudget: number;\n\t/** The size of a list field whose size cannot be read (an integer of at least 0). */\n\tdefaultFirst: number;\n\t/**\n\t * The argument names that size a list. Each one given, or defaulted in the\n\t * SDL, says a size (an Int, a canonical-integer string or an integral Float)\n\t * and the largest wins. Default `['first', 'last', 'limit']`.\n\t */\n\tsizeArguments?: readonly string[];\n\t/**\n\t * Do not cost `__schema` and `__type` (default `false`). Only where\n\t * introspection is disabled or the client is trusted, e.g. GraphiQL in\n\t * development: the standard introspection query is costly (on the order of\n\t * 200,000 on a one-field schema).\n\t */\n\tignoreIntrospection?: boolean;\n}\n\nconst DEFAULT_SIZE_ARGUMENTS = ['first', 'last', 'limit'] as const;\n\n/** Costs are plain numbers, held below this so a hostile query never reaches `Infinity`. */\nconst CEILING = Number.MAX_SAFE_INTEGER;\n\nconst add = (a: number, b: number) => Math.min(CEILING, a + b);\nconst multiply = (a: number, b: number) => Math.min(CEILING, a * b);\n\n/** What a walk of one document needs: the options, and what it has measured so far. */\ninterface CostContext extends SizeContext {\n\tignoreIntrospection: boolean;\n\tgetFragment: (name: string) => FragmentDefinitionNode | null | undefined;\n\tfragmentCosts: Map<string, number>;\n\tcosting: Set<string>;\n}\n\n/** The definition of the field `name` on `parent`, meta-fields of the query root included. */\nfunction fieldOf(\n\tcost: CostContext,\n\tparent: GraphQLNamedType | undefined,\n\tname: string,\n): GraphQLField<unknown, unknown> | undefined {\n\tif (parent === cost.schema.getQueryType() && isIntrospectionRoot(name)) {\n\t\treturn introspectionRoot(name);\n\t}\n\treturn isObjectType(parent) || isInterfaceType(parent)\n\t\t? parent.getFields()[name]\n\t\t: undefined;\n}\n\n/** The cost of `selectionSet` selected on `parent`. */\nfunction costOf(\n\tcost: CostContext,\n\tselectionSet: SelectionSetNode,\n\tparent: GraphQLNamedType | undefined,\n): number {\n\tlet total = 0;\n\tfor (const selection of selectionSet.selections) {\n\t\tif (selection.kind === 'Field') {\n\t\t\tconst name = selection.name.value;\n\t\t\tif (name === '__typename') continue;\n\t\t\tif (isIntrospectionRoot(name) && cost.ignoreIntrospection) continue;\n\t\t\tconst definition = fieldOf(cost, parent, name);\n\t\t\t// An unknown field is another rule's to report.\n\t\t\tif (!definition) continue;\n\t\t\tlet inner = 0;\n\t\t\tif (selection.selectionSet) {\n\t\t\t\tinner = costOf(\n\t\t\t\t\tcost,\n\t\t\t\t\tselection.selectionSet,\n\t\t\t\t\tgetNamedType(definition.type),\n\t\t\t\t);\n\t\t\t\tif (isListType(getNullableType(definition.type))) {\n\t\t\t\t\tconst size = parent?.name.startsWith('__')\n\t\t\t\t\t\t? introspectionSize(cost.schema, name)\n\t\t\t\t\t\t: listSize(cost, parent, definition, selection.arguments);\n\t\t\t\t\tinner = multiply(inner, size);\n\t\t\t\t}\n\t\t\t}\n\t\t\ttotal = add(total, add(1, inner));\n\t\t} else if (selection.kind === 'InlineFragment') {\n\t\t\tconst condition = selection.typeCondition\n\t\t\t\t? cost.schema.getType(selection.typeCondition.name.value)\n\t\t\t\t: parent;\n\t\t\ttotal = add(total, costOf(cost, selection.selectionSet, condition));\n\t\t} else {\n\t\t\ttotal = add(total, fragmentCost(cost, selection.name.value));\n\t\t}\n\t}\n\treturn total;\n}\n\n/** The cost of the named fragment, computed once. */\nfunction fragmentCost(cost: CostContext, name: string): number {\n\tconst known = cost.fragmentCosts.get(name);\n\tif (known !== undefined) return known;\n\tconst fragment = cost.getFragment(name);\n\t// A cycle is NoFragmentCyclesRule's to report; here it adds nothing.\n\tif (!fragment || cost.costing.has(name)) return 0;\n\tcost.costing.add(name);\n\tconst result = costOf(\n\t\tcost,\n\t\tfragment.selectionSet,\n\t\tcost.schema.getType(fragment.typeCondition.name.value),\n\t);\n\tcost.costing.delete(name);\n\tcost.fragmentCosts.set(name, result);\n\treturn result;\n}\n\n/** The type an operation selects on. */\nfunction rootType(schema: GraphQLSchema, operation: OperationDefinitionNode) {\n\tconst type =\n\t\toperation.operation === 'query'\n\t\t\t? schema.getQueryType()\n\t\t\t: operation.operation === 'mutation'\n\t\t\t\t? schema.getMutationType()\n\t\t\t\t: schema.getSubscriptionType();\n\treturn type ?? undefined;\n}\n\n/**\n * A graphql-js validation rule refusing an operation whose cost is more than\n * `budget`, to stop a client from exploiting list cycles (`Folder.items`,\n * `Item.folder`, `Folder.items`, …) for a cost growing exponentially.\n *\n * The cost of an operation is the sum, over every selected field, of the\n * product of the sizes of the list fields enclosing it: a field costs 1, and a\n * list field multiplies the cost of its sub-selection by its size.\n * `__typename` costs nothing. `__schema` and `__type` are costed like any\n * field, their lists sized by the schema itself (`__Schema.types` is the number\n * of types, the other lists the longest list anywhere in the schema), unless\n * `ignoreIntrospection` is set.\n *\n * The size of a list field is the largest size its `sizeArguments` (default\n * `first`, `last`, `limit`) say. A literal Int, a String holding a canonical\n * integer (`\"1000\"`) or an integral Float (`1e3`) of at least 0 says that\n * number. Anything else (a negative literal, a variable, since validation has\n * no variable values) says the argument's SDL default when it has a usable one,\n * `defaultFirst` otherwise; an argument not given says its SDL default, or\n * nothing. When nothing is said the size is `defaultFirst`. On an interface the\n * largest default over its possible types is used. Hence no subtree ever costs\n * less than nothing: a negative `first` on one field cannot cancel the cost of\n * its sibling. A field whose type (once unwrapped from non-null) is a list is\n * one list, nested `[[X]]` included.\n *\n * Named fragments are followed, each costed once. Inline fragments and\n * fragments on an abstract type are summed branch by branch, so the cost of a\n * query on an interface or a union is overestimated, never underestimated.\n *\n * One `GraphQLError` per operation, with\n * `extensions: { code: 'QUERY_TOO_COSTLY', cost, limit }`. `cost` is exact up\n * to `Number.MAX_SAFE_INTEGER`, which it never exceeds.\n *\n * ```ts\n * const rule = maxListCost({ budget: 10_000, defaultFirst: 50 });\n * validate(schema, document, [...specifiedRules, rule]);\n * // graphql-yoga: onValidate: ({ addValidationRule }) => addValidationRule(rule)\n * ```\n */\nexport function maxListCost(options: MaxListCostOptions): ValidationRule {\n\tconst { budget, defaultFirst } = options;\n\t// A copy: a caller changing its array later must not desync the schema\n\t// cache keyed by it.\n\tconst sizeArguments = Object.freeze([\n\t\t...(options.sizeArguments ?? DEFAULT_SIZE_ARGUMENTS),\n\t]);\n\tassertInteger('maxListCost: budget', budget, 0);\n\tassertInteger('maxListCost: defaultFirst', defaultFirst, 0);\n\n\treturn (context: ValidationContext) => {\n\t\tconst schema = context.getSchema();\n\t\tconst cost: CostContext = {\n\t\t\tschema,\n\t\t\tsizeArguments,\n\t\t\tdefaultFirst,\n\t\t\tignoreIntrospection: options.ignoreIntrospection ?? false,\n\t\t\tgetFragment: (name) => context.getFragment(name),\n\t\t\tfragmentCosts: new Map(),\n\t\t\tcosting: new Set(),\n\t\t};\n\t\treturn {\n\t\t\tOperationDefinition(operation) {\n\t\t\t\tconst total = costOf(\n\t\t\t\t\tcost,\n\t\t\t\t\toperation.selectionSet,\n\t\t\t\t\trootType(schema, operation),\n\t\t\t\t);\n\t\t\t\tif (total > budget) {\n\t\t\t\t\tcontext.reportError(\n\t\t\t\t\t\tnew GraphQLError(\n\t\t\t\t\t\t\t`${operationLabel(operation)} costs ${total}, more than the ${budget} allowed`,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tnodes: [operation],\n\t\t\t\t\t\t\t\textensions: {\n\t\t\t\t\t\t\t\t\tcode: 'QUERY_TOO_COSTLY',\n\t\t\t\t\t\t\t\t\tcost: total,\n\t\t\t\t\t\t\t\t\tlimit: budget,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t},\n\t\t};\n\t};\n}\n",
|
|
12
|
+
"import {\n\ttype ArgumentNode,\n\ttype GraphQLField,\n\ttype GraphQLNamedType,\n\ttype GraphQLSchema,\n\tisAbstractType,\n\ttype ValueNode,\n} from 'graphql';\n\n/** Sizes are plain numbers, held below this so a hostile query never reaches `Infinity`. */\nconst CEILING = Number.MAX_SAFE_INTEGER;\n\n/** What sizing a list needs: the schema and the options of the rule. */\nexport interface SizeContext {\n\tschema: GraphQLSchema;\n\tsizeArguments: readonly string[];\n\tdefaultFirst: number;\n}\n\nconst CANONICAL_INTEGER = /^(0|[1-9]\\d*)$/;\n\n/**\n * The one reading of a size, wherever it comes from (a literal, a default): a\n * non-negative integral number or bigint, or a canonical-integer string,\n * clamped to the ceiling. Anything else says nothing.\n */\nfunction sizeOf(value: unknown): number | undefined {\n\tif (typeof value === 'string') {\n\t\treturn CANONICAL_INTEGER.test(value)\n\t\t\t? Math.min(Number(value), CEILING)\n\t\t\t: undefined;\n\t}\n\t// A bigint is how a Long scalar's default is held once its real scalar is\n\t// swapped in.\n\tif (typeof value === 'bigint') {\n\t\treturn value >= 0n ? Math.min(Number(value), CEILING) : undefined;\n\t}\n\treturn typeof value === 'number' && Number.isInteger(value) && value >= 0\n\t\t? Math.min(value, CEILING)\n\t\t: undefined;\n}\n\n/** The size a literal gives: an Int, a canonical-integer String or an integral Float (`1e3`). */\nfunction literalSize(value: ValueNode): number | undefined {\n\tif (value.kind === 'IntValue' || value.kind === 'FloatValue') {\n\t\treturn sizeOf(Number(value.value));\n\t}\n\treturn value.kind === 'StringValue' ? sizeOf(value.value) : undefined;\n}\n\n/**\n * An argument's SDL default as a size. graphql 16 holds it as `defaultValue`;\n * graphql 17 as `default`, `{ value }` or `{ literal }`.\n */\nfunction defaultSizeOf(argument: object | undefined): number | undefined {\n\tif (!argument) return undefined;\n\tif ('defaultValue' in argument) {\n\t\tconst size = sizeOf(argument.defaultValue);\n\t\tif (size !== undefined) return size;\n\t}\n\tif (!('default' in argument)) return undefined;\n\tconst fallback = argument.default;\n\tif (typeof fallback !== 'object' || fallback === null) return undefined;\n\tif ('value' in fallback) return sizeOf(fallback.value);\n\tif ('literal' in fallback && typeof fallback.literal === 'object') {\n\t\tconst literal = fallback.literal;\n\t\treturn literal !== null && 'kind' in literal\n\t\t\t? literalSize(literal as ValueNode)\n\t\t\t: undefined;\n\t}\n\treturn undefined;\n}\n\n/**\n * What the schema alone says of one list field and the fields implementing it,\n * per size argument (in `sizeArguments` order): the largest default, whether\n * some definition has none, and whether some definition has no default at all.\n */\ninterface Profile {\n\tlargest: (number | undefined)[];\n\tsomeLack: boolean[];\n\tsomeBare: boolean;\n}\n\nconst profiles = new WeakMap<\n\tGraphQLSchema,\n\tWeakMap<readonly string[], Map<string, Profile>>\n>();\n\nfunction profileOf(\n\tcost: SizeContext,\n\tparent: GraphQLNamedType | undefined,\n\tdefinition: GraphQLField<unknown, unknown>,\n): Profile {\n\tlet bySizes = profiles.get(cost.schema);\n\tif (!bySizes) {\n\t\tbySizes = new WeakMap();\n\t\tprofiles.set(cost.schema, bySizes);\n\t}\n\tlet byField = bySizes.get(cost.sizeArguments);\n\tif (!byField) {\n\t\tbyField = new Map();\n\t\tbySizes.set(cost.sizeArguments, byField);\n\t}\n\tconst key = `${parent?.name}.${definition.name}`;\n\tconst known = byField.get(key);\n\tif (known) return known;\n\n\t// On an interface the object types' own fields count too: each may\n\t// declare another default, and the query cannot say which comes back.\n\tconst definitions = [definition];\n\tif (isAbstractType(parent)) {\n\t\tfor (const type of cost.schema.getPossibleTypes(parent)) {\n\t\t\tconst implemented = type.getFields()[definition.name];\n\t\t\tif (implemented) definitions.push(implemented);\n\t\t}\n\t}\n\tconst profile: Profile = {\n\t\tlargest: cost.sizeArguments.map(() => undefined),\n\t\tsomeLack: cost.sizeArguments.map(() => false),\n\t\tsomeBare: false,\n\t};\n\tfor (const field of definitions) {\n\t\tlet any = false;\n\t\tcost.sizeArguments.forEach((name, index) => {\n\t\t\tconst size = defaultSizeOf(field.args.find((arg) => arg.name === name));\n\t\t\tif (size === undefined) {\n\t\t\t\tprofile.someLack[index] = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tany = true;\n\t\t\tprofile.largest[index] = Math.max(profile.largest[index] ?? 0, size);\n\t\t});\n\t\tif (!any) profile.someBare = true;\n\t}\n\tbyField.set(key, profile);\n\treturn profile;\n}\n\n/** The size a size argument says, `undefined` when it says nothing. */\nfunction argumentSize(\n\tcost: SizeContext,\n\tprofile: Profile,\n\tindex: number,\n\tnode: ArgumentNode | undefined,\n): number | undefined {\n\tconst largest = profile.largest[index];\n\tif (!node) return largest;\n\tconst literal = literalSize(node.value);\n\tif (literal !== undefined) return literal;\n\t// Given but unusable (a variable, a negative number): the argument's own\n\t// default, else defaultFirst.\n\treturn profile.someLack[index]\n\t\t? Math.max(largest ?? 0, cost.defaultFirst)\n\t\t: largest;\n}\n\n/**\n * The size of the list `definition` returns on `parent`, as selected by\n * `args`: the largest its size arguments say, and `defaultFirst` when none\n * says anything. The schema's part is memoized, so a selection costs\n * `sizeArguments.length` steps whatever the number of possible types.\n */\nexport function listSize(\n\tcost: SizeContext,\n\tparent: GraphQLNamedType | undefined,\n\tdefinition: GraphQLField<unknown, unknown>,\n\targs: readonly ArgumentNode[] | undefined,\n): number {\n\tconst profile = profileOf(cost, parent, definition);\n\tlet size: number | undefined;\n\tlet given = false;\n\tcost.sizeArguments.forEach((name, index) => {\n\t\tconst node = args?.find((arg) => arg.name.value === name);\n\t\tif (node) given = true;\n\t\tconst said = argumentSize(cost, profile, index, node);\n\t\tif (said !== undefined) size = Math.max(size ?? 0, said);\n\t});\n\t// Nothing given: a definition without any default is sized defaultFirst.\n\tif (!given && profile.someBare) size = Math.max(size ?? 0, cost.defaultFirst);\n\treturn size ?? cost.defaultFirst;\n}\n",
|
|
8
13
|
"import type { GraphQLFieldResolver, GraphQLResolveInfo } from 'graphql';\nimport { z } from 'zod';\nimport { parseArgs } from './bad-user-input';\n\n/** An object schema of the arguments, or its shape. */\nexport type ArgsSchema = z.ZodType<Record<string, unknown>> | z.ZodRawShape;\n\n/** The schema `ArgsSchema` stands for. */\nexport type SchemaOf<S extends ArgsSchema> = S extends z.ZodType\n\t? S\n\t: S extends z.ZodRawShape\n\t\t? // Mapped, not `S & z.ZodRawShape`: the intersection's index signature\n\t\t\t// would type every argument `unknown`.\n\t\t\tz.ZodObject<{ [K in keyof S]: S[K] }>\n\t\t: never;\n\n/**\n * What `validated` takes: the schema, or a function that builds it for each\n * call from the request's context and the field's `info`, for checks that\n * need them (a uniqueness query in the request's database, a rule that\n * depends on the viewer).\n */\nexport type ArgsSchemaFor<S extends ArgsSchema, TContext = unknown> =\n\t| S\n\t| SchemaFactory<S, TContext>;\n\ntype SchemaFactory<S extends ArgsSchema, TContext> = (\n\tcontext: TContext,\n\tinfo: GraphQLResolveInfo,\n) => S | Promise<S>;\n\n/**\n * A resolver whose arguments are checked by `schema` before it runs, for what\n * a `@constraint` cannot say: a rule across arguments, a business refinement,\n * an async check. It receives the parsed arguments (`z.output`), defaults and\n * transforms applied; an invalid input is the same `BAD_USER_INPUT` error as\n * the directives'.\n *\n * ```ts\n * signUp: validated(\n * z.object({ input: signUpSchema }).refine(({ input }) => input.password !== input.email),\n * (_, { input }) => users.create(input),\n * )\n * ```\n *\n * A check that needs the request takes a function in place of the schema:\n * it is called on each call, with the context and `info`, and returns the\n * schema (or a promise of it). What it throws reaches the client as it is,\n * not as `BAD_USER_INPUT`.\n *\n * ```ts\n * rename: validated(\n * (ctx: Context) => ({\n * name: z.string().refine(async (name) => !(await ctx.db.users.exists({ name })), 'taken'),\n * }),\n * (_, { name }, ctx) => ctx.db.users.rename(ctx.viewer, name),\n * )\n * ```\n */\nexport function validated<\n\tS extends ArgsSchema,\n\tTSource = unknown,\n\tTContext = unknown,\n\tTResult = unknown,\n>(\n\tschema: SchemaFactory<S, TContext>,\n\tresolve: (\n\t\tsource: TSource,\n\t\targs: z.output<SchemaOf<S>>,\n\t\tcontext: TContext,\n\t\tinfo: GraphQLResolveInfo,\n\t) => TResult,\n): GraphQLFieldResolver<\n\tTSource,\n\tTContext,\n\tz.input<SchemaOf<S>>,\n\tPromise<Awaited<TResult>>\n>;\nexport function validated<\n\tS extends ArgsSchema,\n\tTSource = unknown,\n\tTContext = unknown,\n\tTResult = unknown,\n>(\n\tschema: S,\n\tresolve: (\n\t\tsource: TSource,\n\t\targs: z.output<SchemaOf<S>>,\n\t\tcontext: TContext,\n\t\tinfo: GraphQLResolveInfo,\n\t) => TResult,\n): GraphQLFieldResolver<\n\tTSource,\n\tTContext,\n\tz.input<SchemaOf<S>>,\n\tPromise<Awaited<TResult>>\n>;\nexport function validated<\n\tS extends ArgsSchema,\n\tTSource = unknown,\n\tTContext = unknown,\n\tTResult = unknown,\n>(\n\tschema: ArgsSchemaFor<S, TContext>,\n\tresolve: (\n\t\tsource: TSource,\n\t\targs: z.output<SchemaOf<S>>,\n\t\tcontext: TContext,\n\t\tinfo: GraphQLResolveInfo,\n\t) => TResult,\n): GraphQLFieldResolver<\n\tTSource,\n\tTContext,\n\tz.input<SchemaOf<S>>,\n\tPromise<Awaited<TResult>>\n>;\nexport function validated<\n\tS extends ArgsSchema,\n\tTSource = unknown,\n\tTContext = unknown,\n\tTResult = unknown,\n>(\n\tschema: ArgsSchemaFor<S, TContext>,\n\tresolve: (\n\t\tsource: TSource,\n\t\targs: z.output<SchemaOf<S>>,\n\t\tcontext: TContext,\n\t\tinfo: GraphQLResolveInfo,\n\t) => TResult,\n): GraphQLFieldResolver<\n\tTSource,\n\tTContext,\n\tz.input<SchemaOf<S>>,\n\tPromise<Awaited<TResult>>\n> {\n\tconst fixed = typeof schema === 'function' ? undefined : zodOf(schema);\n\treturn async (source, args, context, info): Promise<Awaited<TResult>> => {\n\t\tconst where = `${info.parentType.name}.${info.fieldName}`;\n\t\tconst checked =\n\t\t\tfixed ??\n\t\t\tzodOf(await (schema as SchemaFactory<S, TContext>)(context, info));\n\t\tconst parsed = (await parseArgs(checked, args, where)) as z.output<\n\t\t\tSchemaOf<S>\n\t\t>;\n\t\treturn await resolve(source, parsed, context, info);\n\t};\n}\n\n/** The schema itself, or the object schema of a shape. */\nfunction zodOf(schema: ArgsSchema): z.ZodType {\n\t// `_zod`, not `instanceof`: a schema from another copy of zod is still one.\n\treturn (\n\t\t'_zod' in schema ? schema : z.object(schema as z.ZodRawShape)\n\t) as z.ZodType;\n}\n",
|
|
9
|
-
"import {\n\tdefaultFieldResolver,\n\ttype GraphQLField,\n\ttype GraphQLFieldResolver,\n\ttype GraphQLSchema,\n} from 'graphql';\nimport type { z } from 'zod';\nimport { parseArgs } from './bad-user-input';\nimport { checkConstraints } from './builder/check-constraints';\nimport type { FormatSchemas, OwnFormats } from './formats/registry';\nimport { constraintOf } from './rules';\n\n// Symbol.for: two copies of the package in one tree still wrap a field once.\nconst wrapped = Symbol.for('@nxgt/graphql-validation/wrapped');\n// The formats a schema was wrapped with, kept on the schema itself.\nconst wrappedWith = Symbol.for('@nxgt/graphql-validation/formats');\n\n/** What `withValidation` takes besides the schema. */\nexport interface WithValidationOptions<\n\tF extends FormatSchemas = FormatSchemas,\n> {\n\t/**\n\t * The application's own formats, which `@constraint(format: \"...\")` may\n\t * name beside the built-in ones: one Zod string schema each, keyed by its\n\t * name (lowercase letters, digits and hyphens). A built-in name is a type\n\t * error, and refused at startup.\n\t */\n\treadonly formats?: OwnFormats<F>;\n}\n\ntype Resolver = GraphQLFieldResolver<unknown, unknown> & { [wrapped]?: true };\n\nfunction checking(\n\tschema: z.ZodType,\n\twhere: string,\n\tresolve: Resolver,\n): Resolver {\n\tconst check: Resolver = async (source, args, context, info) =>\n\t\tawait resolve(\n\t\t\tsource,\n\t\t\tawait parseArgs(schema, args, where, constraintOf),\n\t\t\tcontext,\n\t\t\tinfo,\n\t\t);\n\tcheck[wrapped] = true;\n\treturn check;\n}\n\n/**\n * Wraps a field once. A subscription field is checked in `subscribe`, which\n * runs once per subscription — graphql's default one when the field has none\n * — and its `resolve` runs once per event, with the same arguments, already\n * checked.\n */\nfunction wrap(\n\tfield: GraphQLField<unknown, unknown>,\n\targs: z.ZodType,\n\twhere: string,\n\tsubscription: boolean,\n): void {\n\tconst subscribe = (field.subscribe ??\n\t\t(subscription ? defaultFieldResolver : undefined)) as Resolver | undefined;\n\tif (subscribe) {\n\t\tif (!subscribe[wrapped])
|
|
14
|
+
"import {\n\tdefaultFieldResolver,\n\ttype GraphQLField,\n\ttype GraphQLFieldResolver,\n\ttype GraphQLSchema,\n} from 'graphql';\nimport type { z } from 'zod';\nimport { type ConstraintOf, parseArgs } from './bad-user-input';\nimport { checkConstraints } from './builder/check-constraints';\nimport type { FormatSchemas, OwnFormats } from './formats/registry';\nimport { constraintOf } from './rules';\n\n// Symbol.for: two copies of the package in one tree still wrap a field once.\nconst wrapped = Symbol.for('@nxgt/graphql-validation/wrapped');\n// The formats a schema was wrapped with, kept on the schema itself.\nconst wrappedWith = Symbol.for('@nxgt/graphql-validation/formats');\n\n/** What `withValidation` takes besides the schema. */\nexport interface WithValidationOptions<\n\tF extends FormatSchemas = FormatSchemas,\n> {\n\t/**\n\t * The application's own formats, which `@constraint(format: \"...\")` may\n\t * name beside the built-in ones: one Zod string schema each, keyed by its\n\t * name (lowercase letters, digits and hyphens). A built-in name is a type\n\t * error, and refused at startup.\n\t */\n\treadonly formats?: OwnFormats<F>;\n}\n\ntype Resolver = GraphQLFieldResolver<unknown, unknown> & { [wrapped]?: true };\n\nfunction checking(\n\tschema: z.ZodType,\n\twhere: string,\n\tresolve: Resolver,\n\tconstraintOf: ConstraintOf,\n): Resolver {\n\tconst check: Resolver = async (source, args, context, info) =>\n\t\tawait resolve(\n\t\t\tsource,\n\t\t\tawait parseArgs(schema, args, where, constraintOf),\n\t\t\tcontext,\n\t\t\tinfo,\n\t\t);\n\tcheck[wrapped] = true;\n\treturn check;\n}\n\n/**\n * Wraps a field once. A subscription field is checked in `subscribe`, which\n * runs once per subscription — graphql's default one when the field has none\n * — and its `resolve` runs once per event, with the same arguments, already\n * checked.\n */\nfunction wrap(\n\tfield: GraphQLField<unknown, unknown>,\n\targs: z.ZodType,\n\twhere: string,\n\tsubscription: boolean,\n\tconstraintOf: ConstraintOf,\n): void {\n\tconst subscribe = (field.subscribe ??\n\t\t(subscription ? defaultFieldResolver : undefined)) as Resolver | undefined;\n\tif (subscribe) {\n\t\tif (!subscribe[wrapped])\n\t\t\tfield.subscribe = checking(args, where, subscribe, constraintOf);\n\t\treturn;\n\t}\n\tconst resolve = (field.resolve ?? defaultFieldResolver) as Resolver;\n\tif (!resolve[wrapped])\n\t\tfield.resolve = checking(args, where, resolve, constraintOf);\n}\n\n/**\n * Checks every `@constraint` of `schema` before the resolvers run. Each\n * field with a constrained argument — directly, or anywhere inside an input\n * type it takes — gets its resolver (and its `subscribe`) wrapped: the\n * arguments are parsed by the Zod schema the directives describe, the\n * resolver receives the parsed value, and an invalid input becomes one\n * `BAD_USER_INPUT` error. Fields with nothing to check are left untouched.\n *\n * The schema is built here, once: a constraint that cannot apply (a\n * `minLength` on an `Int`, an unknown format, a bad pattern) throws now, not\n * on the first request. The resolvers are wrapped in place and the same\n * schema is returned, so it goes wherever it went before; calling it twice\n * wraps nothing twice. Call it last, once every resolver is attached: a\n * resolver set on a field afterwards replaces the check.\n *\n * A `@constraint` on an interface's argument must be repeated on each\n * object's field, which is the one that resolves; a field that drops it\n * fails here.\n *\n * The directives are read from the SDL (`astNode`), so a schema built from\n * type definitions — `buildSchema`, `makeExecutableSchema`, Yoga, Apollo —\n * is checked; a code-first schema with no SDL has no `@constraint` to read.\n *\n * `options.formats` adds the application's own formats; wrapping a schema\n * again throws unless the record has the same names, each the very same\n * schema (no formats after some, or some after none, throws too), since its\n * fields are already checked against the first ones.\n */\nexport function withValidation<\n\tS extends GraphQLSchema,\n\t// biome-ignore lint/complexity/noBannedTypes: no formats of its own by default\n\tconst F extends FormatSchemas = {},\n>(schema: S, options: WithValidationOptions<F> = {}): S {\n\tconst formats: FormatSchemas = options.formats ?? {};\n\tconst holder = schema as unknown as { [wrappedWith]?: FormatSchemas };\n\tconst before = holder[wrappedWith];\n\tif (before && !sameFormats(before, formats)) {\n\t\tthrow new Error(\n\t\t\t'withValidation: this schema is already wrapped with other formats. Call withValidation once, with every format.',\n\t\t);\n\t}\n\tconst directive = checkConstraints(\n\t\tschema,\n\t\t(type, field, args, declared) => {\n\t\t\tconst subscription = type === schema.getSubscriptionType();\n\t\t\t// The rule that refused, with the value this field's SDL declares at the path.\n\t\t\tconst named: ConstraintOf = (issue) => {\n\t\t\t\tconst argument = constraintOf(issue);\n\t\t\t\tif (!argument) return undefined;\n\t\t\t\t// Found: the SDL carries the constraints (interfaces repeat theirs,\n\t\t\t\t// assertInterfaceConstraintsKept), so a miss drops `constraint` by design.\n\t\t\t\tconst found = declared(issue.path).find(\n\t\t\t\t\t({ rule }) => rule.argument === argument,\n\t\t\t\t);\n\t\t\t\treturn found && { argument, value: found.value };\n\t\t\t};\n\t\t\twrap(field, args, `${type.name}.${field.name}`, subscription, named);\n\t\t},\n\t\tformats,\n\t);\n\tif (!directive) {\n\t\tthrow new Error(\n\t\t\t'withValidation: this schema declares no @constraint directive. Add constraintTypeDefs to its type definitions.',\n\t\t);\n\t}\n\tif (!before) Object.defineProperty(schema, wrappedWith, { value: formats });\n\treturn schema;\n}\n\n/** The same names, each the very same schema. */\nfunction sameFormats(a: FormatSchemas, b: FormatSchemas): boolean {\n\tconst names = Object.keys(a);\n\treturn (\n\t\tnames.length === Object.keys(b).length &&\n\t\tnames.every((name) => Object.hasOwn(b, name) && a[name] === b[name])\n\t);\n}\n"
|
|
10
15
|
],
|
|
11
|
-
"mappings": "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",
|
|
12
|
-
"debugId": "
|
|
16
|
+
"mappings": "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",
|
|
17
|
+
"debugId": "E43868C11AE8F6A464756E2164756E21",
|
|
13
18
|
"names": []
|
|
14
19
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { OperationDefinitionNode } from 'graphql';
|
|
2
|
+
/** `Operation "Name"`, or `Operation` for an anonymous one. */
|
|
3
|
+
export declare function operationLabel(operation: OperationDefinitionNode): string;
|
|
4
|
+
/**
|
|
5
|
+
* Throws a `RangeError` unless `value` is an integer from `min` up to, and
|
|
6
|
+
* excluding, `max` (default: `Number.MAX_SAFE_INTEGER`, where a cost is clamped).
|
|
7
|
+
*/
|
|
8
|
+
export declare function assertInteger(option: string, value: number, min: number, max?: number): void;
|
|
9
|
+
//# sourceMappingURL=describe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"describe.d.ts","sourceRoot":"","sources":["../../src/limits/describe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAEvD,+DAA+D;AAC/D,wBAAgB,cAAc,CAAC,SAAS,EAAE,uBAAuB,GAAG,MAAM,CAEzE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC5B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,GAAG,SAA0B,GAC3B,IAAI,CAMN"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type GraphQLField, type GraphQLSchema } from 'graphql';
|
|
2
|
+
/** The meta-fields that read the schema; `__typename` is not one. */
|
|
3
|
+
export declare function isIntrospectionRoot(name: string): boolean;
|
|
4
|
+
/** The definition of `__schema` or `__type`, selected on the query root. */
|
|
5
|
+
export declare function introspectionRoot(name: string): GraphQLField<unknown, unknown> | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* The most items the introspection list `parent.field` can hold in `schema`:
|
|
8
|
+
* the number of types for `__Schema.types`, else the largest list anywhere in
|
|
9
|
+
* the schema. Not `defaultFirst`: a query asks no size of them.
|
|
10
|
+
*/
|
|
11
|
+
export declare function introspectionSize(schema: GraphQLSchema, field: string): number;
|
|
12
|
+
//# sourceMappingURL=introspection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"introspection.d.ts","sourceRoot":"","sources":["../../src/limits/introspection.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,YAAY,EAEjB,KAAK,aAAa,EAQlB,MAAM,SAAS,CAAC;AAEjB,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,4EAA4E;AAC5E,wBAAgB,iBAAiB,CAChC,IAAI,EAAE,MAAM,GACV,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,SAAS,CAI5C;AA+CD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAChC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,MAAM,GACX,MAAM,CAER"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ArgumentNode, type GraphQLField, type GraphQLNamedType, type GraphQLSchema } from 'graphql';
|
|
2
|
+
/** What sizing a list needs: the schema and the options of the rule. */
|
|
3
|
+
export interface SizeContext {
|
|
4
|
+
schema: GraphQLSchema;
|
|
5
|
+
sizeArguments: readonly string[];
|
|
6
|
+
defaultFirst: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The size of the list `definition` returns on `parent`, as selected by
|
|
10
|
+
* `args`: the largest its size arguments say, and `defaultFirst` when none
|
|
11
|
+
* says anything. The schema's part is memoized, so a selection costs
|
|
12
|
+
* `sizeArguments.length` steps whatever the number of possible types.
|
|
13
|
+
*/
|
|
14
|
+
export declare function listSize(cost: SizeContext, parent: GraphQLNamedType | undefined, definition: GraphQLField<unknown, unknown>, args: readonly ArgumentNode[] | undefined): number;
|
|
15
|
+
//# sourceMappingURL=list-size.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-size.d.ts","sourceRoot":"","sources":["../../src/limits/list-size.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAGlB,MAAM,SAAS,CAAC;AAKjB,wEAAwE;AACxE,MAAM,WAAW,WAAW;IAC3B,MAAM,EAAE,aAAa,CAAC;IACtB,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;CACrB;AA4ID;;;;;GAKG;AACH,wBAAgB,QAAQ,CACvB,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,GACvC,MAAM,CAaR"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ValidationRule } from 'graphql';
|
|
2
|
+
/** Options of {@link maxDepth}. */
|
|
3
|
+
export interface MaxDepthOptions {
|
|
4
|
+
/** The deepest nesting of field selections an operation may have (at least 1). */
|
|
5
|
+
depth: number;
|
|
6
|
+
/**
|
|
7
|
+
* Do not count `__schema` and `__type` (default `false`). Only where
|
|
8
|
+
* introspection is disabled or the client is trusted, e.g. GraphiQL in
|
|
9
|
+
* development: the standard introspection query (`getIntrospectionQuery()`) is 15 levels deep.
|
|
10
|
+
*/
|
|
11
|
+
ignoreIntrospection?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A graphql-js validation rule refusing an operation nested deeper than
|
|
15
|
+
* `depth` fields: a top-level field is depth 1.
|
|
16
|
+
*
|
|
17
|
+
* Named fragments are followed (each measured once), inline fragments add no
|
|
18
|
+
* depth, and `__typename` does not count. One `GraphQLError` per
|
|
19
|
+
* operation, with `extensions: { code: 'QUERY_TOO_DEEP', depth, limit }`,
|
|
20
|
+
* `depth` being the operation's real depth.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* validate(schema, document, [...specifiedRules, maxDepth({ depth: 8 })]);
|
|
24
|
+
* // graphql-yoga: onValidate: ({ addValidationRule }) => addValidationRule(maxDepth({ depth: 8 }))
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function maxDepth(options: MaxDepthOptions): ValidationRule;
|
|
28
|
+
//# sourceMappingURL=max-depth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"max-depth.d.ts","sourceRoot":"","sources":["../../src/limits/max-depth.ts"],"names":[],"mappings":"AAAA,OAAO,EAKN,KAAK,cAAc,EACnB,MAAM,SAAS,CAAC;AAIjB,mCAAmC;AACnC,MAAM,WAAW,eAAe;IAC/B,kFAAkF;IAClF,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC9B;AA8CD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,cAAc,CA4BjE"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type ValidationRule } from 'graphql';
|
|
2
|
+
/** Options of {@link maxListCost}. */
|
|
3
|
+
export interface MaxListCostOptions {
|
|
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
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A graphql-js validation rule refusing an operation whose cost is more than
|
|
24
|
+
* `budget`, to stop a client from exploiting list cycles (`Folder.items`,
|
|
25
|
+
* `Item.folder`, `Folder.items`, …) for a cost growing exponentially.
|
|
26
|
+
*
|
|
27
|
+
* The cost of an operation is the sum, over every selected field, of the
|
|
28
|
+
* product of the sizes of the list fields enclosing it: a field costs 1, and a
|
|
29
|
+
* list field multiplies the cost of its sub-selection by its size.
|
|
30
|
+
* `__typename` costs nothing. `__schema` and `__type` are costed like any
|
|
31
|
+
* field, their lists sized by the schema itself (`__Schema.types` is the number
|
|
32
|
+
* of types, the other lists the longest list anywhere in the schema), unless
|
|
33
|
+
* `ignoreIntrospection` is set.
|
|
34
|
+
*
|
|
35
|
+
* The size of a list field is the largest size its `sizeArguments` (default
|
|
36
|
+
* `first`, `last`, `limit`) say. A literal Int, a String holding a canonical
|
|
37
|
+
* integer (`"1000"`) or an integral Float (`1e3`) of at least 0 says that
|
|
38
|
+
* number. Anything else (a negative literal, a variable, since validation has
|
|
39
|
+
* no variable values) says the argument's SDL default when it has a usable one,
|
|
40
|
+
* `defaultFirst` otherwise; an argument not given says its SDL default, or
|
|
41
|
+
* nothing. When nothing is said the size is `defaultFirst`. On an interface the
|
|
42
|
+
* largest default over its possible types is used. Hence no subtree ever costs
|
|
43
|
+
* less than nothing: a negative `first` on one field cannot cancel the cost of
|
|
44
|
+
* its sibling. A field whose type (once unwrapped from non-null) is a list is
|
|
45
|
+
* one list, nested `[[X]]` included.
|
|
46
|
+
*
|
|
47
|
+
* Named fragments are followed, each costed once. Inline fragments and
|
|
48
|
+
* fragments on an abstract type are summed branch by branch, so the cost of a
|
|
49
|
+
* query on an interface or a union is overestimated, never underestimated.
|
|
50
|
+
*
|
|
51
|
+
* One `GraphQLError` per operation, with
|
|
52
|
+
* `extensions: { code: 'QUERY_TOO_COSTLY', cost, limit }`. `cost` is exact up
|
|
53
|
+
* to `Number.MAX_SAFE_INTEGER`, which it never exceeds.
|
|
54
|
+
*
|
|
55
|
+
* ```ts
|
|
56
|
+
* const rule = maxListCost({ budget: 10_000, defaultFirst: 50 });
|
|
57
|
+
* validate(schema, document, [...specifiedRules, rule]);
|
|
58
|
+
* // graphql-yoga: onValidate: ({ addValidationRule }) => addValidationRule(rule)
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare function maxListCost(options: MaxListCostOptions): ValidationRule;
|
|
62
|
+
//# sourceMappingURL=max-list-cost.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"max-list-cost.d.ts","sourceRoot":"","sources":["../../src/limits/max-list-cost.ts"],"names":[],"mappings":"AAAA,OAAO,EAcN,KAAK,cAAc,EACnB,MAAM,SAAS,CAAC;AASjB,sCAAsC;AACtC,MAAM,WAAW,kBAAkB;IAClC,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,qFAAqF;IACrF,YAAY,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAuGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,cAAc,CA+CvE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"with-validation.d.ts","sourceRoot":"","sources":["../src/with-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAIN,KAAK,aAAa,EAClB,MAAM,SAAS,CAAC;AAIjB,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAQpE,sDAAsD;AACtD,MAAM,WAAW,qBAAqB,CACrC,CAAC,SAAS,aAAa,GAAG,aAAa;IAEvC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;CACjC;
|
|
1
|
+
{"version":3,"file":"with-validation.d.ts","sourceRoot":"","sources":["../src/with-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAIN,KAAK,aAAa,EAClB,MAAM,SAAS,CAAC;AAIjB,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAQpE,sDAAsD;AACtD,MAAM,WAAW,qBAAqB,CACrC,CAAC,SAAS,aAAa,GAAG,aAAa;IAEvC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;CACjC;AA8CD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,cAAc,CAC7B,CAAC,SAAS,aAAa,EAEvB,KAAK,CAAC,CAAC,SAAS,aAAa,GAAG,EAAE,EACjC,MAAM,EAAE,CAAC,EAAE,OAAO,GAAE,qBAAqB,CAAC,CAAC,CAAM,GAAG,CAAC,CAmCtD"}
|
package/docs/README.md
CHANGED
|
@@ -6,6 +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 costly (`maxDepth`, `maxListCost`), with graphql-js `validate` or graphql-yoga |
|
|
9
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` |
|
|
10
11
|
|
|
11
12
|
## Reference
|
|
@@ -313,11 +313,11 @@ What a format of your own is:
|
|
|
313
313
|
- **Its messages are yours.** A refusal reads as your schema writes it (the
|
|
314
314
|
package's own messages never name the value; yours may, so keep the input
|
|
315
315
|
out of them if your logs or clients should not see it). Every issue your
|
|
316
|
-
schema raises carries `constraint: "format"
|
|
317
|
-
`.regex()` inside your format is the format's refusal, not `pattern`'s:
|
|
316
|
+
schema raises carries `constraint: "format"` and `value` the format's name, whatever
|
|
317
|
+
its Zod `code` — a `.regex()` inside your format is the format's refusal, not `pattern`'s:
|
|
318
318
|
|
|
319
319
|
```json
|
|
320
|
-
{ "path": ["input", "siret"], "message": "Invalid SIRET: write its 14 digits", "code": "invalid_format", "constraint": "format" }
|
|
320
|
+
{ "path": ["input", "siret"], "message": "Invalid SIRET: write its 14 digits", "code": "invalid_format", "constraint": "format", "value": "siret" }
|
|
321
321
|
```
|
|
322
322
|
|
|
323
323
|
- **Named in lowercase letters, digits and hyphens**, starting with a letter
|
package/docs/guide/errors.md
CHANGED
|
@@ -24,9 +24,9 @@ An invalid input is **one** `GraphQLError`, thrown before your resolver runs:
|
|
|
24
24
|
"extensions": {
|
|
25
25
|
"code": "BAD_USER_INPUT",
|
|
26
26
|
"issues": [
|
|
27
|
-
{ "path": ["input", "email"], "message": "Invalid email address", "code": "invalid_format", "constraint": "format" },
|
|
28
|
-
{ "path": ["input", "name"], "message": "Too small: expected string to have >=2 characters", "code": "too_small", "constraint": "minLength" },
|
|
29
|
-
{ "path": ["input", "tags"], "message": "Too big: expected array to have <=2 items", "code": "too_big", "constraint": "maxItems" }
|
|
27
|
+
{ "path": ["input", "email"], "message": "Invalid email address", "code": "invalid_format", "constraint": "format", "value": "email" },
|
|
28
|
+
{ "path": ["input", "name"], "message": "Too small: expected string to have >=2 characters", "code": "too_small", "constraint": "minLength", "value": 2 },
|
|
29
|
+
{ "path": ["input", "tags"], "message": "Too big: expected array to have <=2 items", "code": "too_big", "constraint": "maxItems", "value": 2 }
|
|
30
30
|
]
|
|
31
31
|
}
|
|
32
32
|
}
|
|
@@ -48,6 +48,13 @@ meant for display in every language: map on `constraint` and `path` if you
|
|
|
48
48
|
translate. Prefer `constraint` to `code`: it is the name in your schema, while
|
|
49
49
|
`code` is Zod's and can change when Zod does across a major version.
|
|
50
50
|
|
|
51
|
+
`value` is what that argument was declared with, exactly as written: `100` for
|
|
52
|
+
`maxLength: 100`, `"^[0-9]{5}$"` for a `pattern`, `"email"` for
|
|
53
|
+
`format: "email"`. A client can say "at most 100 characters" from
|
|
54
|
+
`constraint` and `value` without parsing `message`. It is present exactly when
|
|
55
|
+
`constraint` is, so a refusal of a [`validated`](#validated) schema has
|
|
56
|
+
neither.
|
|
57
|
+
|
|
51
58
|
### Types
|
|
52
59
|
|
|
53
60
|
```ts
|
|
@@ -57,6 +64,8 @@ interface ValidationIssue {
|
|
|
57
64
|
readonly code: string;
|
|
58
65
|
/** The `@constraint` argument that refused; absent for `validated`'s own schema. */
|
|
59
66
|
readonly constraint?: ConstraintArgument;
|
|
67
|
+
/** What that argument was declared with, as written; present exactly when `constraint` is. */
|
|
68
|
+
readonly value?: string | number;
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
type ConstraintArgument =
|
|
@@ -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
|
@@ -18,6 +18,10 @@ Nothing planned yet.
|
|
|
18
18
|
|
|
19
19
|
## Shipped
|
|
20
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`.
|
|
21
25
|
- **Schemas that read the context** — 0.5.0. `validated((context, info) =>
|
|
22
26
|
schema, resolver)` builds the schema on each call from the request's context
|
|
23
27
|
and the field's `info`, for checks that need the request (a uniqueness query
|