@znight/graphql 0.0.0 → 0.2.1

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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1346 -0
  3. package/dist/compiler.d.mts +114 -0
  4. package/dist/compiler.mjs +4599 -0
  5. package/dist/compiler.mjs.map +1 -0
  6. package/dist/index.d.mts +583 -0
  7. package/dist/index.mjs +134 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/dist/names-BlzDM5cF.mjs +111 -0
  10. package/dist/names-BlzDM5cF.mjs.map +1 -0
  11. package/dist/plugin-CR_wVfRU.mjs +3445 -0
  12. package/dist/plugin-CR_wVfRU.mjs.map +1 -0
  13. package/dist/testing.d.mts +101 -0
  14. package/dist/testing.mjs +306 -0
  15. package/dist/testing.mjs.map +1 -0
  16. package/dist/types-DQ713UgI.d.mts +266 -0
  17. package/package.json +97 -1
  18. package/src/compiler/analyze.ts +727 -0
  19. package/src/compiler/builder.ts +1561 -0
  20. package/src/compiler/checks.ts +105 -0
  21. package/src/compiler/conformance.ts +1068 -0
  22. package/src/compiler/constants.ts +80 -0
  23. package/src/compiler/data.ts +83 -0
  24. package/src/compiler/defaults.ts +92 -0
  25. package/src/compiler/discriminators.ts +53 -0
  26. package/src/compiler/docs.ts +55 -0
  27. package/src/compiler/generate.ts +419 -0
  28. package/src/compiler/graphql-js.ts +42 -0
  29. package/src/compiler/ids.ts +128 -0
  30. package/src/compiler/index.ts +98 -0
  31. package/src/compiler/locate.ts +82 -0
  32. package/src/compiler/model.ts +137 -0
  33. package/src/compiler/options.ts +85 -0
  34. package/src/compiler/scalars.ts +138 -0
  35. package/src/compiler/schema-first.ts +595 -0
  36. package/src/compiler/sdl-source.ts +529 -0
  37. package/src/compiler/sdl-view.ts +61 -0
  38. package/src/compiler/suggest.ts +39 -0
  39. package/src/context.ts +181 -0
  40. package/src/decisions.ts +169 -0
  41. package/src/decorators.ts +202 -0
  42. package/src/errors.ts +89 -0
  43. package/src/execution.ts +1064 -0
  44. package/src/format.ts +285 -0
  45. package/src/index.ts +89 -0
  46. package/src/limits.ts +177 -0
  47. package/src/loaders.ts +149 -0
  48. package/src/memory-pubsub.ts +133 -0
  49. package/src/metadata.ts +222 -0
  50. package/src/names.ts +47 -0
  51. package/src/options.ts +331 -0
  52. package/src/pipeline.ts +338 -0
  53. package/src/plugin.ts +579 -0
  54. package/src/pubsub.ts +36 -0
  55. package/src/results.ts +206 -0
  56. package/src/scalars.ts +138 -0
  57. package/src/schema-hash.ts +18 -0
  58. package/src/schema.ts +635 -0
  59. package/src/sdl.ts +530 -0
  60. package/src/testing.ts +454 -0
  61. package/src/tokens.ts +22 -0
  62. package/src/transport/abort.ts +80 -0
  63. package/src/transport/accept.ts +155 -0
  64. package/src/transport/graphiql.ts +106 -0
  65. package/src/transport/http.ts +373 -0
  66. package/src/transport/sse.ts +45 -0
  67. package/src/type-ref.ts +89 -0
  68. package/src/types.ts +225 -0
  69. package/src/validate-args.ts +48 -0
package/src/format.ts ADDED
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Error formatting (RFC 0005 §5.10, §8.8): what clients see of the errors of a result.
3
+ *
4
+ * Execution errors, in order: a `GraphQLException` (or a `GraphQLError` thrown by user code) as is →
5
+ * the error mappers, in order → an `HttpException` mapped by status (5xx masked) → anything else
6
+ * masked as `Unexpected error.` with `INTERNAL_SERVER_ERROR`, logged once (unless `exposeErrors`).
7
+ * The `cause` of a shown `GraphQLException` is logged once too, never sent.
8
+ * Request errors (parse, validation, variables, limits) carry their code and are never logged
9
+ * above `debug`.
10
+ *
11
+ * Kept apart from the import-free `errors.ts`, which the compiler side may reach.
12
+ */
13
+ import type { Container, CoreLogger, Token } from '@znight/core'
14
+ import type {
15
+ ASTNode,
16
+ DocumentNode,
17
+ FieldNode,
18
+ FragmentDefinitionNode,
19
+ GraphQLField,
20
+ GraphQLFormattedError,
21
+ GraphQLNamedType,
22
+ GraphQLSchema,
23
+ OperationDefinitionNode,
24
+ SelectionSetNode,
25
+ } from 'graphql'
26
+ import type { GraphQLContext, GraphQLErrorMapper } from './types.ts'
27
+
28
+ import { isHttpException, ValidationException } from '@znight/http'
29
+ import { GraphQLError, getNamedType, isObjectType, Kind } from 'graphql'
30
+
31
+ import { GraphQLErrorCode, GraphQLException } from './errors.ts'
32
+
33
+ /** A provider resolved once (singleton, instance) or per use, in the current scope (token). */
34
+ export type Resolvable<T> = { readonly instance: T; readonly token?: undefined } | { readonly token: Token }
35
+
36
+ /** Resolves a {@link Resolvable} (in the current scope for scoped providers). */
37
+ export function resolveIn<T>(container: Container, resolvable: Resolvable<T>): T {
38
+ return resolvable.token === undefined ? resolvable.instance : (container.get(resolvable.token) as T)
39
+ }
40
+
41
+ /** Message of masked errors. */
42
+ export const MASKED_MESSAGE = 'Unexpected error.'
43
+
44
+ /** `HttpException` status → `extensions.code` (other 4xx: `BAD_REQUEST`, 5xx: masked). */
45
+ const STATUS_CODES: ReadonlyMap<number, string> = new Map([
46
+ [400, GraphQLErrorCode.BadUserInput],
47
+ [401, GraphQLErrorCode.Unauthenticated],
48
+ [403, GraphQLErrorCode.Forbidden],
49
+ [404, GraphQLErrorCode.NotFound],
50
+ [409, GraphQLErrorCode.Conflict],
51
+ [429, GraphQLErrorCode.TooManyRequests],
52
+ ])
53
+
54
+ /** Options of {@link ErrorFormatter}. */
55
+ export interface ErrorFormatterOptions {
56
+ readonly container: Container
57
+ readonly mappers: readonly Resolvable<GraphQLErrorMapper>[]
58
+ readonly exposeErrors: boolean
59
+ readonly logger: CoreLogger
60
+ readonly schema: GraphQLSchema
61
+ }
62
+
63
+ /** Formats the errors of execution results. One per plugin, built at boot. */
64
+ export class ErrorFormatter {
65
+ readonly #container: Container
66
+ readonly #mappers: readonly Resolvable<GraphQLErrorMapper>[]
67
+ readonly #expose: boolean
68
+ readonly #logger: CoreLogger
69
+ readonly #schema: GraphQLSchema
70
+
71
+ constructor(options: ErrorFormatterOptions) {
72
+ this.#container = options.container
73
+ this.#mappers = options.mappers
74
+ this.#expose = options.exposeErrors
75
+ this.#logger = options.logger
76
+ this.#schema = options.schema
77
+ }
78
+
79
+ /** Formats the errors of an executed operation (field errors). */
80
+ format(errors: readonly GraphQLError[], ctx: GraphQLContext): GraphQLFormattedError[] {
81
+ const result: GraphQLFormattedError[] = new Array(errors.length)
82
+ // the same cause may fail several fields (a loader rejecting every key): logged once
83
+ let logged: Set<unknown> | undefined
84
+ for (let index = 0; index < errors.length; index++) {
85
+ const error = errors[index] as GraphQLError
86
+ const original = error.originalError
87
+ if (original === undefined || original === null) {
88
+ // produced by graphql-js during execution (non-null violation...): about the schema, shown as is
89
+ result[index] = withCode(
90
+ error,
91
+ error.extensions['code'] === undefined ? GraphQLErrorCode.InternalServerError : undefined,
92
+ )
93
+ continue
94
+ }
95
+ const exposed = this.#exposed(original, ctx)
96
+ if (exposed !== undefined) {
97
+ result[index] = render(error, exposed)
98
+ // the original failure behind a presented error (`{ cause }`): logged once, never sent
99
+ const cause = exposed.cause
100
+ if (cause !== undefined) {
101
+ logged ??= new Set()
102
+ if (!logged.has(cause)) {
103
+ logged.add(cause)
104
+ this.#log(error, cause, ctx, exposed.code)
105
+ }
106
+ }
107
+ continue
108
+ }
109
+ if (original instanceof GraphQLError && !isCompletionError(original)) {
110
+ result[index] = { ...original.toJSON(), ...located(error) }
111
+ continue
112
+ }
113
+ logged ??= new Set()
114
+ if (!logged.has(original)) {
115
+ logged.add(original)
116
+ this.#log(error, original, ctx, undefined)
117
+ }
118
+ result[index] = {
119
+ message: this.#expose && original instanceof Error ? original.message : MASKED_MESSAGE,
120
+ ...located(error),
121
+ extensions: { code: GraphQLErrorCode.InternalServerError },
122
+ }
123
+ }
124
+ return result
125
+ }
126
+
127
+ /** The exception shown for `original`, `undefined` when it must be masked. */
128
+ #exposed(original: unknown, ctx: GraphQLContext): GraphQLException | undefined {
129
+ if (original instanceof GraphQLException) return original
130
+ for (const resolvable of this.#mappers) {
131
+ let mapped: GraphQLException | undefined
132
+ try {
133
+ mapped = resolveIn(this.#container, resolvable).map(original, ctx)
134
+ } catch (error) {
135
+ this.#logger.error('GraphQLPlugin: an error mapper failed', { err: error, requestId: ctx.requestId })
136
+ continue
137
+ }
138
+ if (mapped !== undefined) return mapped
139
+ }
140
+ if (isHttpException(original)) {
141
+ const status = original.status
142
+ if (original instanceof ValidationException) {
143
+ return new GraphQLException(original.message, {
144
+ code: GraphQLErrorCode.BadUserInput,
145
+ extensions: { issues: original.details },
146
+ })
147
+ }
148
+ if (status >= 500) return undefined
149
+ return new GraphQLException(original.message, {
150
+ code: STATUS_CODES.get(status) ?? GraphQLErrorCode.BadRequest,
151
+ })
152
+ }
153
+ return undefined
154
+ }
155
+
156
+ /** Logs an unexpected error, or the cause of a presented one (`code`), at `error` level. */
157
+ #log(error: GraphQLError, cause: unknown, ctx: GraphQLContext, code: string | undefined): void {
158
+ const coordinate = coordinateOf(this.#schema, ctx.document, ctx.operationName, error.path)
159
+ const what = code === undefined ? 'unexpected error' : `${code} error`
160
+ this.#logger.error(`GraphQL: ${what}${coordinate === undefined ? '' : ` in ${coordinate}`}`, {
161
+ err: cause,
162
+ requestId: ctx.requestId,
163
+ ...(code === undefined ? {} : { code }),
164
+ ...(coordinate === undefined ? {} : { coordinate }),
165
+ ...(error.path === undefined ? {} : { path: error.path.join('.') }),
166
+ })
167
+ }
168
+ }
169
+
170
+ /**
171
+ * What graphql-js raises while completing a resolver's value: a built-in scalar or an enum that
172
+ * cannot represent it (`String cannot represent value: { passwordHash: ... }`), a list value that
173
+ * is not iterable, a union or interface value that resolves to no member. A server bug whose message
174
+ * holds the value, masked and logged like any unexpected error. graphql-js throws plain
175
+ * `GraphQLError`s without code there, told apart from the ones of user code by their message.
176
+ */
177
+ const COMPLETION_ERROR =
178
+ /^(?:(?:Int|Float|String|Boolean|ID) cannot represent |Enum "[^"]*" cannot represent |Expected Iterable, but did not find one for field "|Abstract type "|Runtime Object type ")/
179
+
180
+ function isCompletionError(error: GraphQLError): boolean {
181
+ return (
182
+ error.constructor === GraphQLError && error.extensions['code'] === undefined && COMPLETION_ERROR.test(error.message)
183
+ )
184
+ }
185
+
186
+ function located(error: GraphQLError): Pick<GraphQLFormattedError, 'locations' | 'path'> {
187
+ return {
188
+ ...(error.locations === undefined ? {} : { locations: error.locations }),
189
+ ...(error.path === undefined ? {} : { path: error.path }),
190
+ }
191
+ }
192
+
193
+ /** `{ message, locations, path, extensions: { ...extensions, code } }`. */
194
+ function render(error: GraphQLError, exception: GraphQLException): GraphQLFormattedError {
195
+ return {
196
+ message: exception.message,
197
+ ...located(error),
198
+ extensions: { ...exception.extensions, code: exception.code },
199
+ }
200
+ }
201
+
202
+ /** A graphql-js error formatted with `extensions.code` set to `code` (kept as is without code). */
203
+ export function withCode(error: GraphQLError, code: string | undefined): GraphQLFormattedError {
204
+ const formatted = error.toJSON()
205
+ if (code === undefined) return formatted
206
+ return { ...formatted, extensions: { ...formatted.extensions, code } }
207
+ }
208
+
209
+ /** A request error (no location): `{ message, extensions: { code } }`. */
210
+ export function requestError(
211
+ message: string,
212
+ code: string,
213
+ nodes?: ASTNode | readonly ASTNode[],
214
+ ): GraphQLFormattedError {
215
+ return withCode(new GraphQLError(message, nodes === undefined ? {} : { nodes }), code)
216
+ }
217
+
218
+ /**
219
+ * The schema coordinate (`Book.author`) of the field at `path`, found by walking the operation's
220
+ * selections (aliases, fragments, inline fragments). Best effort, for logs: `undefined` when the
221
+ * path crosses a union without a type condition.
222
+ */
223
+ export function coordinateOf(
224
+ schema: GraphQLSchema,
225
+ document: DocumentNode,
226
+ operationName: string | undefined,
227
+ path: readonly (string | number)[] | undefined,
228
+ ): string | undefined {
229
+ if (path === undefined || path.length === 0) return undefined
230
+ const fragments = new Map<string, FragmentDefinitionNode>()
231
+ let operation: OperationDefinitionNode | undefined
232
+ for (const definition of document.definitions) {
233
+ if (definition.kind === Kind.FRAGMENT_DEFINITION) fragments.set(definition.name.value, definition)
234
+ else if (
235
+ definition.kind === Kind.OPERATION_DEFINITION &&
236
+ (operationName === undefined || definition.name?.value === operationName)
237
+ ) {
238
+ operation ??= definition
239
+ }
240
+ }
241
+ if (operation === undefined) return undefined
242
+ let type: GraphQLNamedType | undefined | null = schema.getRootType(operation.operation)
243
+ let selections: SelectionSetNode[] = [operation.selectionSet]
244
+ let coordinate: string | undefined
245
+ for (const segment of path) {
246
+ if (typeof segment === 'number') continue
247
+ const found = findField(selections, segment, fragments, type?.name)
248
+ if (found === undefined || type === undefined || type === null) return undefined
249
+ const parent: GraphQLNamedType | undefined | null =
250
+ found.typeCondition === undefined ? type : schema.getType(found.typeCondition)
251
+ if (!isObjectType(parent)) return undefined
252
+ const field: GraphQLField<unknown, unknown> | undefined = parent.getFields()[found.field.name.value]
253
+ coordinate = `${parent.name}.${found.field.name.value}`
254
+ if (field === undefined) return coordinate
255
+ type = getNamedType(field.type)
256
+ selections = found.field.selectionSet === undefined ? [] : [found.field.selectionSet]
257
+ }
258
+ return coordinate
259
+ }
260
+
261
+ function findField(
262
+ sets: readonly SelectionSetNode[],
263
+ key: string,
264
+ fragments: ReadonlyMap<string, FragmentDefinitionNode>,
265
+ typeCondition: string | undefined,
266
+ ): { readonly field: FieldNode; readonly typeCondition: string | undefined } | undefined {
267
+ for (const set of sets) {
268
+ for (const selection of set.selections) {
269
+ if (selection.kind === Kind.FIELD) {
270
+ if ((selection.alias ?? selection.name).value === key) return { field: selection, typeCondition }
271
+ } else {
272
+ const nested = selection.kind === Kind.INLINE_FRAGMENT ? selection : fragments.get(selection.name.value)
273
+ if (nested === undefined) continue
274
+ const found = findField(
275
+ [nested.selectionSet],
276
+ key,
277
+ fragments,
278
+ nested.typeCondition?.name.value ?? typeCondition,
279
+ )
280
+ if (found !== undefined) return found
281
+ }
282
+ }
283
+ }
284
+ return undefined
285
+ }
package/src/index.ts ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * `@znight/graphql`: code-first GraphQL whose schema is derived from TypeScript types at build time
3
+ * (RFC 0005). Resolvers are providers marked with `@Resolver`, `@Query`, `@Mutation`,
4
+ * `@Subscription` and `@ResolveField`; the compiler extension (`@znight/graphql/compiler`) generates
5
+ * `metadata.extensions.graphql`, served by `GraphQLPlugin` on graphql-js 17.
6
+ */
7
+
8
+ export type { StandardSchemaV1 } from '@znight/http'
9
+ export type { Float, ID, Int, JSONValue, ResolverOptions } from './decorators.ts'
10
+ export type { GraphQLRequest, GraphQLResultStream } from './execution.ts'
11
+ export type {
12
+ AnyPubSub,
13
+ GraphQLErrorMapperRef,
14
+ GraphQLGuardRef,
15
+ GraphQLHttpOptions,
16
+ GraphQLLimits,
17
+ GraphQLPluginOptions,
18
+ GraphQLResultMapperRef,
19
+ GraphQLSubscriptionOptions,
20
+ } from './options.ts'
21
+ export type {
22
+ BatchLoad,
23
+ BatchLoadClass,
24
+ BatchLoader,
25
+ GraphQLArgs,
26
+ GraphQLContext,
27
+ GraphQLErrorMapper,
28
+ GraphQLErrorMapperClass,
29
+ GraphQLGuard,
30
+ GraphQLGuardClass,
31
+ GraphQLOperationKind,
32
+ GraphQLResultMapper,
33
+ GraphQLResultMapperClass,
34
+ GraphQLResultTypes,
35
+ GraphQLTransport,
36
+ LoaderKey,
37
+ LoaderOptions,
38
+ LoaderValue,
39
+ ResolverContext,
40
+ ScalarDefinition,
41
+ } from './types.ts'
42
+
43
+ /** `Loader` is both the `@Loader()` parameter decorator and the `Loader<L>` parameter type. */
44
+ export {
45
+ Args,
46
+ Ctx,
47
+ Info,
48
+ Loader,
49
+ Mutation,
50
+ Parent,
51
+ Query,
52
+ ResolveField,
53
+ Resolver,
54
+ Subscription,
55
+ scalar,
56
+ UseGuard,
57
+ } from './decorators.ts'
58
+ export {
59
+ GraphQLErrorCode,
60
+ type GraphQLErrorCodeValue,
61
+ GraphQLException,
62
+ type GraphQLExceptionOptions,
63
+ } from './errors.ts'
64
+ export { type OrderByKeysOptions, orderByKeys } from './loaders.ts'
65
+ export { MemoryPubSub, type MemoryPubSubOptions } from './memory-pubsub.ts'
66
+ export {
67
+ GRAPHQL_METADATA_VERSION,
68
+ type GraphQLArgumentDefinition,
69
+ type GraphQLBuiltinScalarDefinition,
70
+ type GraphQLCustomScalarDefinition,
71
+ type GraphQLDiscriminator,
72
+ type GraphQLEnumTypeDefinition,
73
+ type GraphQLEnumValueDefinition,
74
+ type GraphQLFieldDefinition,
75
+ type GraphQLInputFieldDefinition,
76
+ type GraphQLInputTypeDefinition,
77
+ type GraphQLInterfaceTypeDefinition,
78
+ type GraphQLMetadata,
79
+ type GraphQLObjectTypeDefinition,
80
+ type GraphQLResolverBinding,
81
+ type GraphQLResolverOperation,
82
+ type GraphQLRootTypes,
83
+ type GraphQLTypeDefinition,
84
+ type GraphQLUnionTypeDefinition,
85
+ } from './metadata.ts'
86
+ export { type DirectExecuteOptions, GraphQLPlugin, type MaybePromise } from './plugin.ts'
87
+ export { PubSub, type SubscribeOptions } from './pubsub.ts'
88
+ export { GraphQLTokens } from './tokens.ts'
89
+ export { validateArgs } from './validate-args.ts'
package/src/limits.ts ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Document limits (RFC 0005 §8.9): depth, aliases and directives, checked in one walk of the parsed
3
+ * document **before** the specified validation rules, so that an abusive document is rejected
4
+ * before rules whose cost grows with it (`OverlappingFieldsCanBeMerged`) run. Aliases are counted as
5
+ * written, and, when the document spreads fragments, as each operation executes them (a fragment
6
+ * spread several times counts each time). `maxTokens` is enforced by `parse()` itself.
7
+ */
8
+ import type { DirectiveNode, DocumentNode, FragmentDefinitionNode, SelectionSetNode } from 'graphql'
9
+
10
+ import { GraphQLError, Kind } from 'graphql'
11
+
12
+ import { GraphQLErrorCode } from './errors.ts'
13
+
14
+ /** Resolved limits: `undefined` disables one. */
15
+ export interface DocumentLimits {
16
+ readonly maxDepth: number | undefined
17
+ readonly maxAliases: number | undefined
18
+ readonly maxDirectives: number | undefined
19
+ }
20
+
21
+ /** The limit errors of `document` (`QUERY_TOO_COMPLEX`), `undefined` when it is within its limits. */
22
+ export function checkLimits(document: DocumentNode, limits: DocumentLimits): GraphQLError[] | undefined {
23
+ const { maxDepth, maxAliases, maxDirectives } = limits
24
+ if (maxDepth === undefined && maxAliases === undefined && maxDirectives === undefined) return undefined
25
+ const counter = { aliases: 0, directives: 0, spreads: 0 }
26
+ const fragments = new Map<string, FragmentDefinitionNode>()
27
+ for (const definition of document.definitions) {
28
+ if (definition.kind === Kind.FRAGMENT_DEFINITION) fragments.set(definition.name.value, definition)
29
+ }
30
+ let errors: GraphQLError[] | undefined
31
+ const fail = (message: string): void => {
32
+ errors ??= []
33
+ errors.push(new GraphQLError(message, { extensions: { code: GraphQLErrorCode.QueryTooComplex } }))
34
+ }
35
+ // counts every selection once, where it is written (fragment spreads expanded below for aliases)
36
+ for (const definition of document.definitions) {
37
+ if (definition.kind === Kind.OPERATION_DEFINITION || definition.kind === Kind.FRAGMENT_DEFINITION) {
38
+ countDirectives(definition.directives, counter)
39
+ if (definition.kind === Kind.OPERATION_DEFINITION) {
40
+ for (const variable of definition.variableDefinitions ?? []) countDirectives(variable.directives, counter)
41
+ }
42
+ count(definition.selectionSet, counter)
43
+ }
44
+ }
45
+ if (maxAliases !== undefined) {
46
+ if (counter.aliases > maxAliases) {
47
+ fail(`The document has ${counter.aliases} aliases, more than the limit of ${maxAliases}.`)
48
+ } else if (counter.spreads > 0) {
49
+ // a fragment spread several times executes its aliases each time: counted as executed
50
+ const memo = new Map<string, number>()
51
+ for (const definition of document.definitions) {
52
+ if (definition.kind !== Kind.OPERATION_DEFINITION) continue
53
+ const executed = aliasesOf(definition.selectionSet, fragments, memo, new Set(), maxAliases + 1)
54
+ if (executed > maxAliases) {
55
+ const name = definition.name === undefined ? 'The operation' : `The operation ${definition.name.value}`
56
+ fail(`${name} has more than ${maxAliases} aliases once its fragments are expanded.`)
57
+ break
58
+ }
59
+ }
60
+ }
61
+ }
62
+ if (maxDirectives !== undefined && counter.directives > maxDirectives) {
63
+ fail(`The document has ${counter.directives} directives, more than the limit of ${maxDirectives}.`)
64
+ }
65
+ if (maxDepth !== undefined) {
66
+ const depths = new Map<string, number>()
67
+ for (const definition of document.definitions) {
68
+ if (definition.kind !== Kind.OPERATION_DEFINITION) continue
69
+ const depth = depthOf(definition.selectionSet, fragments, depths, new Set())
70
+ if (depth > maxDepth) {
71
+ const name = definition.name === undefined ? 'The operation' : `The operation ${definition.name.value}`
72
+ fail(`${name} is ${depth} levels deep, more than the limit of ${maxDepth}.`)
73
+ }
74
+ }
75
+ }
76
+ return errors
77
+ }
78
+
79
+ interface Counter {
80
+ aliases: number
81
+ directives: number
82
+ spreads: number
83
+ }
84
+
85
+ function countDirectives(directives: readonly DirectiveNode[] | undefined, counter: Counter): void {
86
+ if (directives !== undefined) counter.directives += directives.length
87
+ }
88
+
89
+ function count(set: SelectionSetNode, counter: Counter): void {
90
+ for (const selection of set.selections) {
91
+ countDirectives(selection.directives, counter)
92
+ if (selection.kind === Kind.FIELD) {
93
+ if (selection.alias !== undefined) counter.aliases++
94
+ if (selection.selectionSet !== undefined) count(selection.selectionSet, counter)
95
+ } else if (selection.kind === Kind.INLINE_FRAGMENT) {
96
+ count(selection.selectionSet, counter)
97
+ } else {
98
+ counter.spreads++
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Aliases a selection set executes, fragment spreads expanded (`a: f { ...F } b: f { ...F }` counts
105
+ * the aliases of `F` twice). Fragment counts are memoised, so the walk stays linear in the document;
106
+ * counts saturate at `cap` (one more than the limit) and a fragment cycle counts 0 (validation
107
+ * rejects it).
108
+ */
109
+ function aliasesOf(
110
+ set: SelectionSetNode,
111
+ fragments: ReadonlyMap<string, FragmentDefinitionNode>,
112
+ memo: Map<string, number>,
113
+ visiting: Set<string>,
114
+ cap: number,
115
+ ): number {
116
+ let total = 0
117
+ for (const selection of set.selections) {
118
+ if (selection.kind === Kind.FIELD) {
119
+ if (selection.alias !== undefined) total++
120
+ if (selection.selectionSet !== undefined)
121
+ total += aliasesOf(selection.selectionSet, fragments, memo, visiting, cap)
122
+ } else if (selection.kind === Kind.INLINE_FRAGMENT) {
123
+ total += aliasesOf(selection.selectionSet, fragments, memo, visiting, cap)
124
+ } else {
125
+ const name = selection.name.value
126
+ let known = memo.get(name)
127
+ if (known === undefined) {
128
+ const fragment = fragments.get(name)
129
+ if (fragment === undefined || visiting.has(name)) continue
130
+ visiting.add(name)
131
+ known = aliasesOf(fragment.selectionSet, fragments, memo, visiting, cap)
132
+ visiting.delete(name)
133
+ memo.set(name, known)
134
+ }
135
+ total += known
136
+ }
137
+ if (total >= cap) return cap
138
+ }
139
+ return total
140
+ }
141
+
142
+ /**
143
+ * Field nesting of a selection set (`{ a { b } }` is 2). Introspection fields (`__schema`,
144
+ * `__type`) do not count: `MaxIntrospectionDepthRule` bounds them. Fragment depths are memoised; a
145
+ * fragment cycle counts 0 (validation rejects it).
146
+ */
147
+ function depthOf(
148
+ set: SelectionSetNode,
149
+ fragments: ReadonlyMap<string, FragmentDefinitionNode>,
150
+ depths: Map<string, number>,
151
+ visiting: Set<string>,
152
+ ): number {
153
+ let max = 0
154
+ for (const selection of set.selections) {
155
+ let depth = 0
156
+ if (selection.kind === Kind.FIELD) {
157
+ if (selection.name.value.startsWith('__')) continue
158
+ depth =
159
+ selection.selectionSet === undefined ? 1 : 1 + depthOf(selection.selectionSet, fragments, depths, visiting)
160
+ } else if (selection.kind === Kind.INLINE_FRAGMENT) {
161
+ depth = depthOf(selection.selectionSet, fragments, depths, visiting)
162
+ } else {
163
+ const name = selection.name.value
164
+ const known = depths.get(name)
165
+ if (known === undefined) {
166
+ const fragment = fragments.get(name)
167
+ if (fragment === undefined || visiting.has(name)) continue
168
+ visiting.add(name)
169
+ depth = depthOf(fragment.selectionSet, fragments, depths, visiting)
170
+ visiting.delete(name)
171
+ depths.set(name, depth)
172
+ } else depth = known
173
+ }
174
+ if (depth > max) max = depth
175
+ }
176
+ return max
177
+ }