@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/options.ts ADDED
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Options of `GraphQLPlugin` (RFC 0005 §5.11): their types, defaults and validation (ZNIGHT066).
3
+ */
4
+ import type { ByteSize, GuardRef, RouteMetaEntry } from '@znight/http'
5
+ import type { SubscribeOptions } from './pubsub.ts'
6
+ import type {
7
+ GraphQLErrorMapper,
8
+ GraphQLErrorMapperClass,
9
+ GraphQLGuard,
10
+ GraphQLGuardClass,
11
+ GraphQLResultMapper,
12
+ GraphQLResultMapperClass,
13
+ GraphQLResultTypes,
14
+ } from './types.ts'
15
+
16
+ import { ErrorCode, ZnightError } from '@znight/core'
17
+ import { parseByteSize } from '@znight/http'
18
+
19
+ /**
20
+ * A `PubSub` of any event map, for the `pubsub` option: `PubSub<Events>` types are not assignable
21
+ * to each other (generic methods over `keyof Events`), so the option accepts the structure.
22
+ */
23
+ export interface AnyPubSub {
24
+ publish(topic: never, payload: never): Promise<void>
25
+ subscribe(topic: never, options?: SubscribeOptions): AsyncIterableIterator<unknown>
26
+ }
27
+
28
+ /** A global GraphQL guard: a provider class or an instance. */
29
+ export type GraphQLGuardRef = GraphQLGuardClass | GraphQLGuard
30
+
31
+ /** An error mapper: a provider class or an instance. */
32
+ export type GraphQLErrorMapperRef = GraphQLErrorMapperClass | GraphQLErrorMapper
33
+
34
+ /**
35
+ * A result mapper: a provider class with `static types`, or an instance (its `types` property, or the
36
+ * `static types` of its class).
37
+ */
38
+ export type GraphQLResultMapperRef = GraphQLResultMapperClass | (GraphQLResultMapper & Partial<GraphQLResultTypes>)
39
+
40
+ /** Options of the GraphQL-over-HTTP endpoint. */
41
+ export interface GraphQLHttpOptions {
42
+ /** Route metadata of the endpoint (`[Public.meta()]`), seen by HTTP guards and the route decision check. */
43
+ readonly meta?: readonly RouteMetaEntry[]
44
+ /** Guards of the endpoint (HTTP `CanActivate`), after the global `HttpPlugin` guards. */
45
+ readonly guards?: readonly GuardRef[]
46
+ /** Body limit of the endpoint. Default `'100kb'`. */
47
+ readonly bodyLimit?: ByteSize
48
+ /** Queries over `GET`. Default `true`. */
49
+ readonly get?: boolean
50
+ }
51
+
52
+ /** Request limits; `false` disables one. */
53
+ export interface GraphQLLimits {
54
+ /** Tokens of a document (`parse`). Default 2 000. */
55
+ readonly maxTokens?: number | false
56
+ /** Field nesting of an operation (introspection fields exempt). Default 12. */
57
+ readonly maxDepth?: number | false
58
+ /**
59
+ * Aliases of a document, as written and as one operation executes them (a fragment spread several
60
+ * times counts each time). Default 30.
61
+ */
62
+ readonly maxAliases?: number | false
63
+ /** Directives of a document. Default 50. */
64
+ readonly maxDirectives?: number | false
65
+ }
66
+
67
+ /** Subscription options. */
68
+ export interface GraphQLSubscriptionOptions {
69
+ /** Active subscriptions cap (every transport); beyond it, 503. Default 10 000. */
70
+ readonly maxActive?: number
71
+ /** Events buffered per subscriber by the default `MemoryPubSub`. Default 100. */
72
+ readonly bufferSize?: number
73
+ /** SSE heartbeat (ms), `false` for none. Default: the `HttpPlugin` `sse` option. */
74
+ readonly heartbeat?: number | false
75
+ /** SSE maximum duration (ms). Default: none. */
76
+ readonly maxDuration?: number
77
+ }
78
+
79
+ /** Options of `GraphQLPlugin.configure()`. */
80
+ export interface GraphQLPluginOptions {
81
+ /** Path of the endpoint, joined with the `HttpPlugin` prefix. Default `'/graphql'`. */
82
+ readonly path?: string
83
+ /** `false`: no HTTP transport (schema, `execute()` and `subscribe()` only). Default `{}`. */
84
+ readonly http?: GraphQLHttpOptions | false
85
+ /** Global GraphQL guards (classes or instances): root fields, and field resolvers with metadata. */
86
+ readonly guards?: readonly GraphQLGuardRef[]
87
+ /** Error mappers (classes or instances), tried in order. */
88
+ readonly errorMappers?: readonly GraphQLErrorMapperRef[]
89
+ /**
90
+ * Result mappers (classes or instances), tried in order: errors as data (RFC 0008), on the fields
91
+ * whose union holds one of their `types`.
92
+ */
93
+ readonly resultMappers?: readonly GraphQLResultMapperRef[]
94
+ /** Unmasked messages for unexpected errors (development). Default `false`. */
95
+ readonly exposeErrors?: boolean
96
+ /** Introspection; off, suggestions are hidden too. Default `NODE_ENV !== 'production'`. */
97
+ readonly introspection?: boolean
98
+ /** GraphiQL on `GET path` with `accept: text/html`. Default `NODE_ENV === 'development'`. */
99
+ readonly graphiql?: boolean
100
+ readonly limits?: GraphQLLimits
101
+ /** Distinct documents kept parsed and validated (LRU); 0 disables the cache. Default 1 000. */
102
+ readonly documentCache?: number
103
+ /** `false` disables subscriptions over SSE. */
104
+ readonly subscriptions?: GraphQLSubscriptionOptions | false
105
+ /** The `PubSub` provided to the container. Default: a `MemoryPubSub` (closed with the application). */
106
+ readonly pubsub?: AnyPubSub
107
+ /**
108
+ * How the boot check of route decisions on GraphQL fields (RFC 0001, ZNIGHT023) reports problems:
109
+ * `'error'` (default) fails the boot, `'warn'` logs.
110
+ */
111
+ readonly enforcement?: 'error' | 'warn'
112
+ }
113
+
114
+ /** Options with their defaults, validated. */
115
+ export interface ResolvedGraphQLOptions {
116
+ readonly path: string
117
+ readonly http: Required<GraphQLHttpOptions> | false
118
+ readonly guards: readonly GraphQLGuardRef[]
119
+ readonly errorMappers: readonly GraphQLErrorMapperRef[]
120
+ readonly resultMappers: readonly GraphQLResultMapperRef[]
121
+ readonly exposeErrors: boolean
122
+ readonly introspection: boolean
123
+ readonly graphiql: boolean
124
+ readonly limits: {
125
+ readonly maxTokens: number | undefined
126
+ readonly maxDepth: number | undefined
127
+ readonly maxAliases: number | undefined
128
+ readonly maxDirectives: number | undefined
129
+ }
130
+ readonly documentCache: number
131
+ readonly subscriptions:
132
+ | {
133
+ readonly maxActive: number
134
+ readonly bufferSize: number
135
+ readonly heartbeat: number | false | undefined
136
+ readonly maxDuration: number | undefined
137
+ }
138
+ | false
139
+ readonly bufferSize: number
140
+ readonly pubsub: AnyPubSub | undefined
141
+ readonly enforcement: 'error' | 'warn'
142
+ }
143
+
144
+ const KEYS: ReadonlySet<string> = new Set([
145
+ 'path',
146
+ 'http',
147
+ 'guards',
148
+ 'errorMappers',
149
+ 'resultMappers',
150
+ 'exposeErrors',
151
+ 'introspection',
152
+ 'graphiql',
153
+ 'limits',
154
+ 'documentCache',
155
+ 'subscriptions',
156
+ 'pubsub',
157
+ 'enforcement',
158
+ ])
159
+ const PATH = /^\/[A-Za-z0-9\-._~!$&'()*+,;=:@/]*$/
160
+ const LIMIT_DEFAULTS = { maxTokens: 2000, maxDepth: 12, maxAliases: 30, maxDirectives: 50 } as const
161
+
162
+ function show(value: unknown): string {
163
+ if (typeof value === 'function') return 'a function'
164
+ try {
165
+ return JSON.stringify(value) ?? String(value)
166
+ } catch {
167
+ return typeof value
168
+ }
169
+ }
170
+
171
+ /** ZNIGHT066 for an invalid option. */
172
+ export function invalidOption(name: string, expected: string, received: unknown): ZnightError {
173
+ return new ZnightError(
174
+ ErrorCode.InvalidGraphQLSetup,
175
+ `GraphQLPlugin: invalid option ${name}: expected ${expected} (received ${show(received)})`,
176
+ { hints: ['See the options of GraphQLPlugin.configure() (RFC 0005 §5.11)'] },
177
+ )
178
+ }
179
+
180
+ function isRecord(value: unknown): value is Record<string, unknown> {
181
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
182
+ }
183
+
184
+ function boolean(name: string, value: unknown, fallback: boolean): boolean {
185
+ if (value === undefined) return fallback
186
+ if (typeof value !== 'boolean') throw invalidOption(name, 'a boolean', value)
187
+ return value
188
+ }
189
+
190
+ function positiveInteger(name: string, value: unknown, fallback: number): number {
191
+ if (value === undefined) return fallback
192
+ if (!(Number.isSafeInteger(value) && (value as number) >= 1)) throw invalidOption(name, 'a positive integer', value)
193
+ return value as number
194
+ }
195
+
196
+ function limit(name: keyof typeof LIMIT_DEFAULTS, value: unknown): number | undefined {
197
+ if (value === false) return undefined
198
+ return positiveInteger(`limits.${name}`, value, LIMIT_DEFAULTS[name])
199
+ }
200
+
201
+ /** Refs given as classes or instances with `method`. */
202
+ function refs<T>(name: string, value: unknown, method: string): readonly T[] {
203
+ if (value === undefined) return []
204
+ if (!Array.isArray(value))
205
+ throw invalidOption(name, `an array of classes or objects with a ${method}() method`, value)
206
+ for (const ref of value) {
207
+ const ok =
208
+ typeof ref === 'function' ||
209
+ (ref !== null && typeof ref === 'object' && typeof (ref as Record<string, unknown>)[method] === 'function')
210
+ if (!ok) throw invalidOption(name, `classes or objects with a ${method}() method`, ref)
211
+ }
212
+ return value as readonly T[]
213
+ }
214
+
215
+ /** Validates the options and applies the defaults (`env`: `process.env`). */
216
+ export function resolveGraphQLOptions(
217
+ options: unknown,
218
+ env: Readonly<Record<string, string | undefined>> = process.env,
219
+ ): ResolvedGraphQLOptions {
220
+ if (!isRecord(options)) throw invalidOption('options', 'an object', options)
221
+ for (const key of Object.keys(options)) {
222
+ if (!KEYS.has(key)) throw invalidOption(key, `one of ${[...KEYS].join(', ')}`, key)
223
+ }
224
+ const nodeEnv = env['NODE_ENV']
225
+ const path = options['path'] ?? '/graphql'
226
+ if (typeof path !== 'string' || !PATH.test(path)) throw invalidOption('path', "a path starting with '/'", path)
227
+
228
+ const rawHttp = options['http'] ?? {}
229
+ let http: ResolvedGraphQLOptions['http']
230
+ if (rawHttp === false) {
231
+ http = false
232
+ } else {
233
+ if (!isRecord(rawHttp)) throw invalidOption('http', 'false or { meta?, guards?, bodyLimit?, get? }', rawHttp)
234
+ const meta = rawHttp['meta'] ?? []
235
+ if (
236
+ !Array.isArray(meta) ||
237
+ !meta.every(entry => isRecord(entry) && typeof entry['key'] === 'string' && entry['key'] !== '')
238
+ ) {
239
+ throw invalidOption('http.meta', 'an array of route metadata entries (Decorator.meta())', meta)
240
+ }
241
+ const bodyLimit = rawHttp['bodyLimit'] ?? '100kb'
242
+ if (parseByteSize(bodyLimit) === undefined)
243
+ throw invalidOption('http.bodyLimit', "a byte size ('100kb', 1048576)", bodyLimit)
244
+ http = {
245
+ meta: meta as readonly RouteMetaEntry[],
246
+ guards: refs<GuardRef>('http.guards', rawHttp['guards'], 'canActivate'),
247
+ bodyLimit: bodyLimit as ByteSize,
248
+ get: boolean('http.get', rawHttp['get'], true),
249
+ }
250
+ }
251
+
252
+ const rawLimits = options['limits'] ?? {}
253
+ if (!isRecord(rawLimits))
254
+ throw invalidOption('limits', '{ maxTokens?, maxDepth?, maxAliases?, maxDirectives? }', rawLimits)
255
+ for (const key of Object.keys(rawLimits)) {
256
+ if (!(key in LIMIT_DEFAULTS))
257
+ throw invalidOption(`limits.${key}`, 'maxTokens, maxDepth, maxAliases or maxDirectives', key)
258
+ }
259
+
260
+ const documentCache = options['documentCache'] ?? 1000
261
+ if (!(Number.isSafeInteger(documentCache) && (documentCache as number) >= 0)) {
262
+ throw invalidOption('documentCache', 'a non-negative integer', documentCache)
263
+ }
264
+
265
+ const rawSubscriptions = options['subscriptions'] ?? {}
266
+ let subscriptions: ResolvedGraphQLOptions['subscriptions']
267
+ let bufferSize = 100
268
+ if (rawSubscriptions === false) {
269
+ subscriptions = false
270
+ } else {
271
+ if (!isRecord(rawSubscriptions)) {
272
+ throw invalidOption(
273
+ 'subscriptions',
274
+ 'false or { maxActive?, bufferSize?, heartbeat?, maxDuration? }',
275
+ rawSubscriptions,
276
+ )
277
+ }
278
+ bufferSize = positiveInteger('subscriptions.bufferSize', rawSubscriptions['bufferSize'], 100)
279
+ const heartbeat = rawSubscriptions['heartbeat']
280
+ if (heartbeat !== undefined && heartbeat !== false && !(typeof heartbeat === 'number' && heartbeat > 0)) {
281
+ throw invalidOption('subscriptions.heartbeat', 'a positive number of milliseconds or false', heartbeat)
282
+ }
283
+ const maxDuration = rawSubscriptions['maxDuration']
284
+ if (maxDuration !== undefined && !(typeof maxDuration === 'number' && maxDuration > 0)) {
285
+ throw invalidOption('subscriptions.maxDuration', 'a positive number of milliseconds', maxDuration)
286
+ }
287
+ subscriptions = {
288
+ maxActive: positiveInteger('subscriptions.maxActive', rawSubscriptions['maxActive'], 10_000),
289
+ bufferSize,
290
+ heartbeat: heartbeat as number | false | undefined,
291
+ maxDuration: maxDuration as number | undefined,
292
+ }
293
+ }
294
+
295
+ const pubsub = options['pubsub']
296
+ if (
297
+ pubsub !== undefined &&
298
+ !(
299
+ isRecord(pubsub) &&
300
+ typeof (pubsub as { publish?: unknown }).publish === 'function' &&
301
+ typeof (pubsub as { subscribe?: unknown }).subscribe === 'function'
302
+ )
303
+ ) {
304
+ throw invalidOption('pubsub', 'a PubSub (publish() and subscribe() methods)', pubsub)
305
+ }
306
+ const enforcement = options['enforcement'] ?? 'error'
307
+ if (enforcement !== 'error' && enforcement !== 'warn')
308
+ throw invalidOption('enforcement', "'error' or 'warn'", enforcement)
309
+
310
+ return {
311
+ path,
312
+ http,
313
+ guards: refs<GraphQLGuardRef>('guards', options['guards'], 'canResolve'),
314
+ errorMappers: refs<GraphQLErrorMapperRef>('errorMappers', options['errorMappers'], 'map'),
315
+ resultMappers: refs<GraphQLResultMapperRef>('resultMappers', options['resultMappers'], 'toResult'),
316
+ exposeErrors: boolean('exposeErrors', options['exposeErrors'], false),
317
+ introspection: boolean('introspection', options['introspection'], nodeEnv !== 'production'),
318
+ graphiql: boolean('graphiql', options['graphiql'], nodeEnv === 'development'),
319
+ limits: {
320
+ maxTokens: limit('maxTokens', rawLimits['maxTokens']),
321
+ maxDepth: limit('maxDepth', rawLimits['maxDepth']),
322
+ maxAliases: limit('maxAliases', rawLimits['maxAliases']),
323
+ maxDirectives: limit('maxDirectives', rawLimits['maxDirectives']),
324
+ },
325
+ documentCache: documentCache as number,
326
+ subscriptions,
327
+ bufferSize,
328
+ pubsub: pubsub as AnyPubSub | undefined,
329
+ enforcement,
330
+ }
331
+ }
@@ -0,0 +1,338 @@
1
+ /**
2
+ * The resolver pipeline (RFC 0005 §5.9, §7.3, §8.5): the resolve function of every resolver-backed
3
+ * field, decided once at boot.
4
+ *
5
+ * 1. the resolver instance: the singleton resolved at boot, or `container.get(token)` in the
6
+ * current scope (request-scoped and transient resolvers);
7
+ * 2. guards: global ones (on root fields, and on field resolvers carrying their own metadata), class
8
+ * `@UseGuard`, method `@UseGuard`; `false` fails the field with `FORBIDDEN`;
9
+ * 3. null handling of arguments (`nullAsUndefined`), only where a flag is reachable;
10
+ * 4. `invoke(resolver, parent, args, context, info)`.
11
+ *
12
+ * A field without guards nor normaliser calls `invoke` from the closure graphql-js needs anyway:
13
+ * no extra wrapper, no promise, no `await`. Plain property fields keep graphql-js' default resolver.
14
+ * A field that carries results (RFC 0008, `results.ts`) gets one wrapper around the whole pipeline,
15
+ * catching its errors to turn them into values; no other field does.
16
+ */
17
+ import type { Container, Token } from '@znight/core'
18
+ import type { GraphQLFieldResolver, GraphQLResolveInfo } from 'graphql'
19
+ import type { OperationContext } from './context.ts'
20
+ import type { Resolvable } from './format.ts'
21
+ import type {
22
+ GraphQLArgumentDefinition,
23
+ GraphQLFieldDefinition,
24
+ GraphQLInputTypeDefinition,
25
+ GraphQLObjectTypeDefinition,
26
+ GraphQLResolverBinding,
27
+ } from './metadata.ts'
28
+ import type { Recover } from './results.ts'
29
+ import type { FieldFunctions, FieldFunctionsFactory } from './schema.ts'
30
+ import type { GraphQLTypeRef } from './type-ref.ts'
31
+ import type { GraphQLArgs, GraphQLContext, GraphQLGuard, ResolverContext } from './types.ts'
32
+
33
+ import { isThenable } from '@znight/http'
34
+
35
+ import { GraphQLErrorCode, GraphQLException } from './errors.ts'
36
+ import { resolveIn } from './format.ts'
37
+ import { parseTypeRef } from './type-ref.ts'
38
+
39
+ /** What the pipeline needs, resolved at boot. */
40
+ export interface PipelineOptions {
41
+ readonly container: Container
42
+ /** Global guards (plugin option), in order. */
43
+ readonly globalGuards: readonly Resolvable<GraphQLGuard>[]
44
+ /** The guard provider of a `@UseGuard` token (memoised by the caller). */
45
+ readonly guard: (token: Token, coordinate: string) => Resolvable<GraphQLGuard>
46
+ /** The resolver provider of a binding (memoised by the caller). */
47
+ readonly resolver: (binding: GraphQLResolverBinding, coordinate: string) => Resolvable<object>
48
+ /** Input object types by name (null handling of nested input values). */
49
+ readonly inputs: ReadonlyMap<string, GraphQLInputTypeDefinition>
50
+ /** The {@link Recover} function of a field that carries results (RFC 0008), `undefined` for the others. */
51
+ readonly results?: (field: GraphQLFieldDefinition, coordinate: string) => Recover | undefined
52
+ }
53
+
54
+ /** The generated invoker of a binding. */
55
+ type Invoke = GraphQLResolverBinding['invoke']
56
+
57
+ /** Mutates a coerced argument or input value in place: `null` → `undefined` where flagged. */
58
+ type Normalize = (value: unknown) => void
59
+
60
+ /** Whether global guards run on a field: root fields always, field resolvers when they carry metadata (§5.9). */
61
+ export function globalGuardsApply(binding: GraphQLResolverBinding): boolean {
62
+ return binding.operation !== 'field' || Object.keys(binding.meta).length > 0
63
+ }
64
+
65
+ /** The {@link FieldFunctionsFactory} of the plugin. */
66
+ export function pipelineFactory(options: PipelineOptions): FieldFunctionsFactory {
67
+ const normalizers = new NormalizerBuilder(options.inputs)
68
+ return (binding, coordinate, field, parent) =>
69
+ fieldFunctions(options, normalizers, binding, coordinate, field, parent)
70
+ }
71
+
72
+ function fieldFunctions(
73
+ options: PipelineOptions,
74
+ normalizers: NormalizerBuilder,
75
+ binding: GraphQLResolverBinding,
76
+ coordinate: string,
77
+ field: GraphQLFieldDefinition,
78
+ parent: GraphQLObjectTypeDefinition,
79
+ ): FieldFunctions {
80
+ const container = options.container
81
+ const invoke: Invoke = binding.invoke
82
+ const target = options.resolver(binding, coordinate)
83
+ const guards: Resolvable<GraphQLGuard>[] = [
84
+ ...(globalGuardsApply(binding) ? options.globalGuards : []),
85
+ ...binding.guards.map(token => options.guard(token, coordinate)),
86
+ ]
87
+ const normalize = field.args === undefined ? undefined : normalizers.args(field.args)
88
+ const recover = options.results?.(field, coordinate)
89
+ const route =
90
+ guards.length === 0 && recover === undefined ? undefined : Object.freeze({ meta: binding.meta, coordinate })
91
+ const parentType = parent.name
92
+ const fieldName = field.name
93
+
94
+ let resolve: GraphQLFieldResolver<unknown, unknown>
95
+ if (guards.length === 0) {
96
+ // the common case: one closure, invoke called directly (no promise, no await)
97
+ if (target.token === undefined) {
98
+ const instance = target.instance
99
+ resolve =
100
+ normalize === undefined
101
+ ? (p, a, c, i) => invoke(instance, p, a as GraphQLArgs, c as GraphQLContext, i)
102
+ : (p, a, c, i) => {
103
+ normalize(a)
104
+ return invoke(instance, p, a as GraphQLArgs, c as GraphQLContext, i)
105
+ }
106
+ } else {
107
+ const token = target.token
108
+ resolve =
109
+ normalize === undefined
110
+ ? (p, a, c, i) => invoke(container.get(token), p, a as GraphQLArgs, c as GraphQLContext, i)
111
+ : (p, a, c, i) => {
112
+ normalize(a)
113
+ return invoke(container.get(token), p, a as GraphQLArgs, c as GraphQLContext, i)
114
+ }
115
+ }
116
+ } else {
117
+ const forbidden = (): never => {
118
+ throw new GraphQLException(`Not allowed to resolve ${coordinate}`, { code: GraphQLErrorCode.Forbidden })
119
+ }
120
+ resolve = (p, a, c, i) => {
121
+ const operation = c as OperationContext
122
+ const args = a as GraphQLArgs
123
+ const allowed = runGuards(
124
+ container,
125
+ guards,
126
+ new FieldContext(route as ResolverContext['route'], operation, parentType, fieldName, args, p, i),
127
+ )
128
+ // §8.5: guards (they see the coerced arguments), then null handling, then invoke
129
+ const call = (): unknown => {
130
+ if (normalize !== undefined) normalize(args)
131
+ return invoke(resolveIn(container, target), p, args, operation, i)
132
+ }
133
+ if (allowed === true) return call()
134
+ if (allowed === false) return forbidden()
135
+ return allowed.then(ok => (ok ? call() : forbidden()))
136
+ }
137
+ }
138
+ const subscription = binding.operation === 'subscription'
139
+ if (recover !== undefined) {
140
+ resolve = withResults(resolve, recover, route as ResolverContext['route'], parentType, fieldName, subscription)
141
+ }
142
+ if (subscription) {
143
+ // the method gives the source event stream; each event is the field value
144
+ return { subscribe: resolve, resolve: identity }
145
+ }
146
+ return { resolve }
147
+ }
148
+
149
+ function identity(payload: unknown): unknown {
150
+ return payload
151
+ }
152
+
153
+ /**
154
+ * The resolve function of a field that carries results (RFC 0008): errors of the whole pipeline
155
+ * (guards, null handling, `invoke`), thrown or rejected, go through `recover`. Synchronous results
156
+ * stay synchronous; a promise gets one rejection handler. For a subscription, a value is a source
157
+ * stream of that single event.
158
+ */
159
+ function withResults(
160
+ resolve: GraphQLFieldResolver<unknown, unknown>,
161
+ recover: Recover,
162
+ route: ResolverContext['route'],
163
+ parentType: string,
164
+ fieldName: string,
165
+ subscription: boolean,
166
+ ): GraphQLFieldResolver<unknown, unknown> {
167
+ const fail = (error: unknown, p: unknown, a: unknown, c: unknown, i: GraphQLResolveInfo): unknown => {
168
+ const value = recover(
169
+ error,
170
+ new FieldContext(route, c as OperationContext, parentType, fieldName, a as GraphQLArgs, p, i),
171
+ )
172
+ return subscription ? once(value) : value
173
+ }
174
+ // captures nothing per call: the rejection handler is created in recoverAsync, on the promise path only
175
+ return (p, a, c, i) => {
176
+ let result: unknown
177
+ try {
178
+ result = resolve(p, a, c, i)
179
+ } catch (error) {
180
+ return fail(error, p, a, c, i)
181
+ }
182
+ return isThenable(result) ? recoverAsync(result, fail, p, a, c, i) : result
183
+ }
184
+ }
185
+
186
+ function recoverAsync(
187
+ result: PromiseLike<unknown>,
188
+ fail: (error: unknown, p: unknown, a: unknown, c: unknown, i: GraphQLResolveInfo) => unknown,
189
+ p: unknown,
190
+ a: unknown,
191
+ c: unknown,
192
+ i: GraphQLResolveInfo,
193
+ ): Promise<unknown> {
194
+ return Promise.resolve(result).then(undefined, error => fail(error, p, a, c, i))
195
+ }
196
+
197
+ /** A source event stream of one event. */
198
+ // biome-ignore lint/suspicious/useAwait: an async generator is the shortest AsyncIterable of one value
199
+ async function* once(value: unknown): AsyncGenerator<unknown> {
200
+ yield value
201
+ }
202
+
203
+ /** Runs guards in order, synchronously until one returns a promise. */
204
+ function runGuards(
205
+ container: Container,
206
+ guards: readonly Resolvable<GraphQLGuard>[],
207
+ ctx: ResolverContext,
208
+ start = 0,
209
+ ): boolean | Promise<boolean> {
210
+ for (let index = start; index < guards.length; index++) {
211
+ const result = resolveIn(container, guards[index] as Resolvable<GraphQLGuard>).canResolve(ctx)
212
+ if (isThenable(result)) {
213
+ return Promise.resolve(result).then(ok => (ok ? runGuards(container, guards, ctx, index + 1) : false))
214
+ }
215
+ if (!result) return false
216
+ }
217
+ return true
218
+ }
219
+
220
+ /** What a guard receives (§5.9): a `RouteMetaCarrier` with the field's effective metadata. */
221
+ class FieldContext implements ResolverContext {
222
+ readonly route: ResolverContext['route']
223
+ readonly operation: OperationContext
224
+ readonly parentType: string
225
+ readonly field: string
226
+ readonly args: GraphQLArgs
227
+ readonly parent: unknown
228
+ readonly info: GraphQLResolveInfo
229
+
230
+ constructor(
231
+ route: ResolverContext['route'],
232
+ operation: OperationContext,
233
+ parentType: string,
234
+ field: string,
235
+ args: GraphQLArgs,
236
+ parent: unknown,
237
+ info: GraphQLResolveInfo,
238
+ ) {
239
+ this.route = route
240
+ this.operation = operation
241
+ this.parentType = parentType
242
+ this.field = field
243
+ this.args = args
244
+ this.parent = parent
245
+ this.info = info
246
+ }
247
+
248
+ get http(): ResolverContext['http'] {
249
+ return this.operation.http
250
+ }
251
+ }
252
+
253
+ /** One step of a normaliser: an argument or input field. */
254
+ interface Step {
255
+ readonly name: string
256
+ /** The value itself: `null` → `undefined`. */
257
+ readonly own: boolean
258
+ /** Nested input values. */
259
+ readonly nested: Normalize | undefined
260
+ }
261
+
262
+ /**
263
+ * Builds normalisers (§7.3) at boot. Values are mutated in place: graphql-js creates the argument
264
+ * object of every call and the coerced input objects, and the flags of an input type are the same
265
+ * wherever it is used, so a value shared through a variable is normalised the same way twice.
266
+ */
267
+ class NormalizerBuilder {
268
+ readonly #inputs: ReadonlyMap<string, GraphQLInputTypeDefinition>
269
+ /** Input type → its normaliser (`null`: nothing to do), `PENDING` while being built (cycles). */
270
+ readonly #types = new Map<string, Normalize | null | typeof PENDING>()
271
+
272
+ constructor(inputs: ReadonlyMap<string, GraphQLInputTypeDefinition>) {
273
+ this.#inputs = inputs
274
+ }
275
+
276
+ /** The normaliser of a field's arguments, `undefined` when no flag is reachable. */
277
+ args(args: readonly GraphQLArgumentDefinition[]): ((args: unknown) => void) | undefined {
278
+ const steps = this.#steps(args)
279
+ return steps === undefined ? undefined : record => applySteps(record, steps)
280
+ }
281
+
282
+ #steps(values: readonly GraphQLArgumentDefinition[]): readonly Step[] | undefined {
283
+ const steps: Step[] = []
284
+ for (const value of values) {
285
+ const own = value.nullAsUndefined === true
286
+ const ref = parseTypeRef(value.type)
287
+ const nested = ref === undefined ? undefined : this.#value(ref)
288
+ if (own || nested !== undefined) steps.push({ name: value.name, own, nested })
289
+ }
290
+ return steps.length === 0 ? undefined : steps
291
+ }
292
+
293
+ #value(ref: GraphQLTypeRef): Normalize | undefined {
294
+ if (ref.kind === 'list') {
295
+ const item = this.#value(ref.of)
296
+ return item === undefined
297
+ ? undefined
298
+ : value => {
299
+ if (Array.isArray(value)) for (const entry of value) item(entry)
300
+ }
301
+ }
302
+ return this.#input(ref.name)
303
+ }
304
+
305
+ #input(name: string): Normalize | undefined {
306
+ const known = this.#types.get(name)
307
+ if (known === PENDING) {
308
+ // a cycle through this type: delegate to its normaliser once built
309
+ return value => {
310
+ const normalize = this.#types.get(name)
311
+ if (typeof normalize === 'function') normalize(value)
312
+ }
313
+ }
314
+ if (known !== undefined) return known ?? undefined
315
+ const type = this.#inputs.get(name)
316
+ if (type === undefined) return undefined
317
+ this.#types.set(name, PENDING)
318
+ const steps = this.#steps(type.fields)
319
+ const normalize: Normalize | null = steps === undefined ? null : value => applySteps(value, steps)
320
+ this.#types.set(name, normalize)
321
+ return normalize ?? undefined
322
+ }
323
+ }
324
+
325
+ const PENDING: unique symbol = Symbol('pending')
326
+
327
+ function applySteps(value: unknown, steps: readonly Step[]): void {
328
+ if (value === null || typeof value !== 'object') return
329
+ const record = value as Record<string, unknown>
330
+ for (const step of steps) {
331
+ const entry = record[step.name]
332
+ if (entry === null) {
333
+ if (step.own) record[step.name] = undefined
334
+ } else if (entry !== undefined && step.nested !== undefined) {
335
+ step.nested(entry)
336
+ }
337
+ }
338
+ }