@znight/graphql 0.3.0 → 0.5.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.
@@ -13,7 +13,7 @@
13
13
  * ```
14
14
  */
15
15
 
16
- import type { GenerateContext, GenInput, GenNode, GenParam } from '@znight/compiler'
16
+ import type { ClassRef, GenerateContext, GenInput, GenNode, GenParam, StaticValue, ValueRef } from '@znight/compiler'
17
17
  import type { Token } from '@znight/core'
18
18
  import type { MetaEntry } from '@znight/http/compiler'
19
19
  import type { FieldData, GraphQLData, ParamData, ResolverData } from './data.ts'
@@ -34,6 +34,8 @@ interface Imports {
34
34
  argsType(): GenNode
35
35
  contextType(): GenNode
36
36
  infoType(): GenNode
37
+ resolverContextType(): GenNode
38
+ paramFunctionOf(): GenNode
37
39
  validateArgs(): GenNode
38
40
  isThenable(): GenNode
39
41
  hostType(identity: string): GenNode
@@ -63,6 +65,8 @@ function createImports(ctx: Context): Imports {
63
65
  let args: GenNode | undefined
64
66
  let context: GenNode | undefined
65
67
  let info: GenNode | undefined
68
+ let resolverContext: GenNode | undefined
69
+ let paramFunctionOf: GenNode | undefined
66
70
  let validateArgs: GenNode | undefined
67
71
  let isThenable: GenNode | undefined
68
72
  const hosts = new Map<string, GenNode>()
@@ -79,6 +83,14 @@ function createImports(ctx: Context): Imports {
79
83
  info ??= ctx.importFrom('graphql', 'GraphQLResolveInfo', { type: true })
80
84
  return info
81
85
  },
86
+ resolverContextType: () => {
87
+ resolverContext ??= ctx.importFrom(RUNTIME_MODULE, 'ResolverContext', { type: true })
88
+ return resolverContext
89
+ },
90
+ paramFunctionOf: () => {
91
+ paramFunctionOf ??= ctx.importFrom('@znight/core', 'ParamFunctionOf', { type: true })
92
+ return paramFunctionOf
93
+ },
82
94
  validateArgs: () => {
83
95
  validateArgs ??= ctx.importFrom(RUNTIME_MODULE, 'validateArgs')
84
96
  return validateArgs
@@ -208,7 +220,13 @@ function resolverNode(ctx: Context, imports: Imports, resolver: ResolverData, fi
208
220
  ]
209
221
  const loaders = field.params.flatMap(param => (param?.kind === 'loader' ? [loaderToken(ctx, param)] : []))
210
222
  if (loaders.length > 0) entries.push(['loaders', ctx.lit(loaders)])
211
- entries.push(['invoke', invokeNode(ctx, imports, resolver, field)])
223
+ // custom parameters (RFC 0010): the pipeline builds the ResolverContext and resolves the class resolvers
224
+ const resolvers = paramResolvers(field)
225
+ if (field.params.some(param => param?.kind === 'custom')) entries.push(['context', ctx.lit(true)])
226
+ if (resolvers.length > 0) {
227
+ entries.push(['paramResolvers', ctx.lit(resolvers.map(ref => ctx.tokenOf(ref) ?? ref.token))])
228
+ }
229
+ entries.push(['invoke', invokeNode(ctx, imports, resolver, field, resolvers)])
212
230
  return ctx.obj(entries)
213
231
  }
214
232
 
@@ -222,14 +240,31 @@ function loaderToken(ctx: Context, param: Extract<ParamData, { kind: 'loader' }>
222
240
 
223
241
  // ------------------------------------------------------------------------------------------- invoke
224
242
 
225
- /** Which `invoke` parameters a field uses: `(r, p, a, c, i)`. */
226
- function usedParameters(field: FieldData): readonly [p: boolean, a: boolean, c: boolean, i: boolean] {
243
+ /** The provider classes resolving custom parameters, in order of first use (`x[i]`). */
244
+ function paramResolvers(field: FieldData): ClassRef[] {
245
+ const found = new Map<string, ClassRef>()
246
+ for (const param of field.params) {
247
+ const resolver = param?.kind === 'custom' ? param.resolver : undefined
248
+ if (resolver !== undefined && !found.has(resolver.token)) found.set(resolver.token, resolver)
249
+ }
250
+ return [...found.values()]
251
+ }
252
+
253
+ /** Which `invoke` parameters a field uses: `(r, p, a, c, i, f, x)`. */
254
+ function usedParameters(field: FieldData): readonly boolean[] {
227
255
  let p = false
228
256
  let a = false
229
257
  let c = false
230
258
  let i = false
259
+ let f = false
260
+ let x = false
231
261
  for (const param of field.params) {
232
262
  switch (param?.kind) {
263
+ case 'custom':
264
+ // a class resolver whose `resolve()` takes no parameter does not read `f`
265
+ f ||= param.resolver === undefined || param.arity !== 0
266
+ x ||= param.resolver !== undefined
267
+ break
233
268
  case 'parent':
234
269
  p = true
235
270
  break
@@ -245,39 +280,73 @@ function usedParameters(field: FieldData): readonly [p: boolean, a: boolean, c:
245
280
  break
246
281
  }
247
282
  }
248
- return [p, a, c, i]
283
+ return [p, a, c, i, f, x]
249
284
  }
250
285
 
251
286
  /**
252
287
  * `(r: Resolver, p: unknown, a: GraphQLArgs, c: GraphQLContext, i: GraphQLResolveInfo) => r.m(...)`:
253
288
  * unused middle parameters are `_`-prefixed, trailing ones omitted (`noUnusedParameters`).
254
289
  */
255
- function invokeNode(ctx: Context, imports: Imports, resolver: ResolverData, field: FieldData): GenNode {
290
+ function invokeNode(
291
+ ctx: Context,
292
+ imports: Imports,
293
+ resolver: ResolverData,
294
+ field: FieldData,
295
+ resolvers: readonly ClassRef[],
296
+ ): GenNode {
256
297
  const used = usedParameters(field)
257
298
  const last = used.lastIndexOf(true)
258
- const types = [() => ctx.code`unknown`, imports.argsType, imports.contextType, imports.infoType]
259
- const names = ['p', 'a', 'c', 'i']
299
+ const types = [
300
+ () => ctx.code`unknown`,
301
+ imports.argsType,
302
+ imports.contextType,
303
+ imports.infoType,
304
+ imports.resolverContextType,
305
+ () =>
306
+ tupleType(
307
+ ctx,
308
+ resolvers.map(ref => ctx.importType(ref)),
309
+ ),
310
+ ]
311
+ const names = ['p', 'a', 'c', 'i', 'f', 'x']
260
312
  const params: [string, GenNode][] = [['r', imports.hostType(resolver.host.identity)]]
261
313
  for (let index = 0; index <= last; index++) {
262
314
  params.push([`${used[index] ? '' : '_'}${names[index] as string}`, (types[index] as () => GenNode)()])
263
315
  }
264
- const validated = field.params.flatMap((param, index) =>
265
- param?.kind === 'args' && param.schema !== undefined ? [index] : [],
266
- )
316
+ const deferred = field.params.flatMap((param, index) => (param !== undefined && isDeferred(param) ? [index] : []))
317
+ const positions = new Map(resolvers.map((ref, position) => [ref.token, position]))
267
318
  return ctx.arrow(params, (r, ...rest) => {
268
- const [p, a, c, i] = rest as (GenParam | undefined)[]
269
- const scope: Scope = { p, a, c, i }
270
- if (validated.length === 0) return call(ctx, r, field, index => argument(ctx, scope, field, index))
271
- return validatingBlock(ctx, imports, r, scope, field, validated)
319
+ const [p, a, c, i, f, x] = rest as (GenParam | undefined)[]
320
+ const scope: Scope = { p, a, c, i, f, x, resolvers: positions, imports }
321
+ if (deferred.length === 0) return call(ctx, r, field, index => argument(ctx, scope, field, index))
322
+ return validatingBlock(ctx, imports, r, scope, field, deferred)
272
323
  })
273
324
  }
274
325
 
326
+ /** `readonly [A, B]`. */
327
+ function tupleType(ctx: Context, nodes: readonly GenNode[]): GenNode {
328
+ const joined = nodes.slice(1).reduce((all, node) => ctx.code`${all}, ${node}`, nodes[0] as GenNode)
329
+ return ctx.code`readonly [${joined}]`
330
+ }
331
+
332
+ /** An argument whose expression may return a thenable: a validated `@Args`, or a custom parameter. */
333
+ function isDeferred(param: ParamData): boolean {
334
+ return (param.kind === 'args' && param.schema !== undefined) || param.kind === 'custom'
335
+ }
336
+
275
337
  /** The parameters of the `invoke` arrow (absent when not declared). */
276
338
  interface Scope {
277
339
  readonly p: GenNode | undefined
278
340
  readonly a: GenNode | undefined
279
341
  readonly c: GenNode | undefined
280
342
  readonly i: GenNode | undefined
343
+ /** The `ResolverContext` of custom parameters. */
344
+ readonly f: GenNode | undefined
345
+ /** The class resolver instances (`x[i]`). */
346
+ readonly x: GenNode | undefined
347
+ /** Position of each class resolver token in `x`. */
348
+ readonly resolvers: ReadonlyMap<string, number>
349
+ readonly imports: Imports
281
350
  }
282
351
 
283
352
  /** `r.method(args)`. */
@@ -302,11 +371,62 @@ function argument(ctx: Context, scope: Scope, field: FieldData, index: number):
302
371
  return ctx.code`${ctx.call(ctx.member(scope.c as GenNode, 'loader'), [loaderToken(ctx, param)])} as never`
303
372
  case 'args':
304
373
  return ctx.code`${argsValue(ctx, scope, param.name)} as never`
374
+ case 'custom':
375
+ return customArgument(ctx, scope, param)
305
376
  default:
306
377
  return ctx.lit(undefined)
307
378
  }
308
379
  }
309
380
 
381
+ /**
382
+ * A custom parameter (RFC 0010): `(D.resolvers.graphql as ParamFunctionOf<typeof D, 'graphql'>)(f, data)`
383
+ * or `x[i].resolve(f, data)`, cast with `as never` when the value type could not be checked.
384
+ */
385
+ function customArgument(ctx: Context, scope: Scope, param: Extract<ParamData, { kind: 'custom' }>): GenNode {
386
+ const f = scope.f as GenNode
387
+ const data = param.data === undefined ? ctx.lit(undefined) : dataNode(ctx, param.data.value)
388
+ let node: GenNode
389
+ if (param.resolver === undefined) {
390
+ const decorator = ctx.importValue(param.identity)
391
+ const type = ctx.code`${scope.imports.paramFunctionOf()}<typeof ${decorator}, 'graphql'>`
392
+ node = ctx.call(ctx.code`${ctx.member(decorator, 'resolvers', 'graphql')} as ${type}`, [f, data])
393
+ } else {
394
+ const position = scope.resolvers.get(param.resolver.token) as number
395
+ if (param.arity === undefined) {
396
+ // a `resolve` the compiler cannot read (inherited from a mixin...): the instance is cast to a
397
+ // method taking both arguments (method parameters are bivariant), the value with `as never`
398
+ return ctx.code`(${scope.x as GenNode}[${position}] as { resolve(ctx: unknown, data: unknown): unknown }).resolve(${f}, ${data}) as never`
399
+ }
400
+ // a `resolve(ctx)` that ignores the data gets no data argument (TypeScript checks the arity)
401
+ const arity = Math.min(param.arity, 2)
402
+ let args = ctx.code``
403
+ if (arity === 1) args = f
404
+ else if (arity === 2) args = ctx.code`${f}, ${data}`
405
+ node = ctx.code`${scope.x as GenNode}[${position}].resolve(${args})`
406
+ }
407
+ return param.cast ? ctx.code`${node} as never` : node
408
+ }
409
+
410
+ /** The data argument of a custom parameter: a literal (references imported) or a reference. */
411
+ function dataNode(ctx: Context, value: StaticValue): GenNode {
412
+ return isValueRef(value) ? ctx.ref(value as ValueRef) : ctx.lit(value as never)
413
+ }
414
+
415
+ /** `true` for a {@link ValueRef} (the only non-plain object a static value can hold). */
416
+ function isValueRef(value: unknown): boolean {
417
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
418
+ const prototype = Object.getPrototypeOf(value)
419
+ return prototype !== Object.prototype && prototype !== null && (value as { kind?: unknown }).kind === 'ref'
420
+ }
421
+
422
+ /** The expression of a deferred parameter (validation or custom resolver), before any `await`. */
423
+ function deferredValue(ctx: Context, imports: Imports, scope: Scope, field: FieldData, index: number): GenNode {
424
+ const param = field.params[index] as ParamData
425
+ return param.kind === 'custom'
426
+ ? customArgument(ctx, scope, param)
427
+ : validation(ctx, imports, scope, param as Extract<ParamData, { kind: 'args' }>)
428
+ }
429
+
310
430
  /** `a['name']`, or `a` for `@Args()`. */
311
431
  function argsValue(ctx: Context, scope: Scope, name: string | undefined): GenNode {
312
432
  const a = scope.a as GenNode
@@ -356,8 +476,7 @@ function validatingBlock(
356
476
  const statements: GenNode[] = []
357
477
  for (const index of validated) {
358
478
  const name = local.get(index) as GenNode
359
- const param = field.params[index] as Extract<ParamData, { kind: 'args' }>
360
- statements.push(ctx.code`const ${name} = ${validation(ctx, imports, scope, param)}`)
479
+ statements.push(ctx.code`const ${name} = ${deferredValue(ctx, imports, scope, field, index)}`)
361
480
  const resume = ctx.arrow(
362
481
  [],
363
482
  () =>
@@ -366,8 +485,7 @@ function validatingBlock(
366
485
  if (value === undefined) return argument(ctx, scope, field, position)
367
486
  if (position < index) return ctx.code`${value} as never`
368
487
  if (position === index) return ctx.code`await ${value} as never`
369
- const later = field.params[position] as Extract<ParamData, { kind: 'args' }>
370
- return ctx.code`await ${validation(ctx, imports, scope, later)} as never`
488
+ return ctx.code`await ${deferredValue(ctx, imports, scope, field, position)} as never`
371
489
  }),
372
490
  { async: true },
373
491
  )
package/src/metadata.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  import type { Token } from '@znight/core'
13
13
  import type { HttpRouteGroupDefinition } from '@znight/http'
14
14
  import type { GraphQLResolveInfo, GraphQLScalarType } from 'graphql'
15
- import type { GraphQLArgs, GraphQLContext, ScalarDefinition } from './types.ts'
15
+ import type { GraphQLArgs, GraphQLContext, ResolverContext, ScalarDefinition } from './types.ts'
16
16
 
17
17
  /** Format version of `metadata.extensions.graphql` (another version is ZNIGHT066 at boot). */
18
18
  export const GRAPHQL_METADATA_VERSION = 1
@@ -201,18 +201,34 @@ export interface GraphQLResolverBinding {
201
201
  readonly meta: Readonly<Record<string, unknown>>
202
202
  /** Informational: loader provider tokens the method injects. */
203
203
  readonly loaders?: readonly Token[]
204
+ /**
205
+ * The method has custom parameters (`createParamDecorator`, RFC 0010): the pipeline passes the
206
+ * field's `ResolverContext` to `invoke` (`f`). Absent otherwise.
207
+ */
208
+ readonly context?: true
209
+ /**
210
+ * Provider classes resolving custom parameters (`ParamResolver`), in order of first use: the
211
+ * pipeline resolves them in the operation scope and passes the instances to `invoke` (`x`).
212
+ * Absent when there is none.
213
+ */
214
+ readonly paramResolvers?: readonly Token[]
204
215
  /**
205
216
  * Calls the method: maps the graphql-js arguments to the method parameters (§7.3). Synchronous
206
- * unless an argument is validated with a Standard Schema.
217
+ * unless an argument is validated with a Standard Schema or a custom parameter resolver returns a
218
+ * promise. `field` and `resolvers` are passed only to methods with custom parameters (declared as
219
+ * a method so that a generated invoke requiring them is accepted).
207
220
  */
208
- readonly invoke: (
221
+ invoke(
209
222
  // biome-ignore lint/suspicious/noExplicitAny: generated invokers receive the concrete resolver class
210
223
  resolver: any,
211
224
  parent: unknown,
212
225
  args: GraphQLArgs,
213
226
  context: GraphQLContext,
214
227
  info: GraphQLResolveInfo,
215
- ) => unknown
228
+ field?: ResolverContext,
229
+ // biome-ignore lint/suspicious/noExplicitAny: generated invokers receive the tuple of resolver classes
230
+ resolvers?: any,
231
+ ): unknown
216
232
  }
217
233
 
218
234
  declare module '@znight/core' {
package/src/pipeline.ts CHANGED
@@ -45,6 +45,8 @@ export interface PipelineOptions {
45
45
  readonly guard: (token: Token, coordinate: string) => Resolvable<GraphQLGuard>
46
46
  /** The resolver provider of a binding (memoised by the caller). */
47
47
  readonly resolver: (binding: GraphQLResolverBinding, coordinate: string) => Resolvable<object>
48
+ /** The provider of a custom parameter resolver class (RFC 0010; memoised by the caller). */
49
+ readonly paramResolver?: (token: Token, coordinate: string) => Resolvable<object>
48
50
  /** Input object types by name (null handling of nested input values). */
49
51
  readonly inputs: ReadonlyMap<string, GraphQLInputTypeDefinition>
50
52
  /** The {@link Recover} function of a field that carries results (RFC 0008), `undefined` for the others. */
@@ -52,7 +54,15 @@ export interface PipelineOptions {
52
54
  }
53
55
 
54
56
  /** The generated invoker of a binding. */
55
- type Invoke = GraphQLResolverBinding['invoke']
57
+ type Invoke = (
58
+ resolver: object,
59
+ parent: unknown,
60
+ args: GraphQLArgs,
61
+ context: GraphQLContext,
62
+ info: GraphQLResolveInfo,
63
+ field?: ResolverContext,
64
+ resolvers?: readonly object[],
65
+ ) => unknown
56
66
 
57
67
  /** Mutates a coerced argument or input value in place: `null` → `undefined` where flagged. */
58
68
  type Normalize = (value: unknown) => void
@@ -78,7 +88,6 @@ function fieldFunctions(
78
88
  parent: GraphQLObjectTypeDefinition,
79
89
  ): FieldFunctions {
80
90
  const container = options.container
81
- const invoke: Invoke = binding.invoke
82
91
  const target = options.resolver(binding, coordinate)
83
92
  const guards: Resolvable<GraphQLGuard>[] = [
84
93
  ...(globalGuardsApply(binding) ? options.globalGuards : []),
@@ -86,10 +95,16 @@ function fieldFunctions(
86
95
  ]
87
96
  const normalize = field.args === undefined ? undefined : normalizers.args(field.args)
88
97
  const recover = options.results?.(field, coordinate)
98
+ const custom = binding.context === true
89
99
  const route =
90
- guards.length === 0 && recover === undefined ? undefined : Object.freeze({ meta: binding.meta, coordinate })
100
+ guards.length === 0 && recover === undefined && !custom
101
+ ? undefined
102
+ : Object.freeze({ meta: binding.meta, coordinate })
91
103
  const parentType = parent.name
92
104
  const fieldName = field.name
105
+ const invoke: Invoke = custom
106
+ ? customInvoke(options, binding, coordinate, route as ResolverContext['route'], parentType, fieldName)
107
+ : (binding.invoke as Invoke)
93
108
 
94
109
  let resolve: GraphQLFieldResolver<unknown, unknown>
95
110
  if (guards.length === 0) {
@@ -120,15 +135,21 @@ function fieldFunctions(
120
135
  resolve = (p, a, c, i) => {
121
136
  const operation = c as OperationContext
122
137
  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),
138
+ const fieldContext = new FieldContext(
139
+ route as ResolverContext['route'],
140
+ operation,
141
+ parentType,
142
+ fieldName,
143
+ args,
144
+ p,
145
+ i,
127
146
  )
128
- // §8.5: guards (they see the coerced arguments), then null handling, then invoke
147
+ const allowed = runGuards(container, guards, fieldContext)
148
+ // §8.5: guards (they see the coerced arguments), then null handling, then invoke (custom
149
+ // parameters share the guards' ResolverContext)
129
150
  const call = (): unknown => {
130
151
  if (normalize !== undefined) normalize(args)
131
- return invoke(resolveIn(container, target), p, args, operation, i)
152
+ return invoke(resolveIn(container, target), p, args, operation, i, fieldContext)
132
153
  }
133
154
  if (allowed === true) return call()
134
155
  if (allowed === false) return forbidden()
@@ -150,6 +171,48 @@ function identity(payload: unknown): unknown {
150
171
  return payload
151
172
  }
152
173
 
174
+ /**
175
+ * The invoke of a method with custom parameters (RFC 0010): the generated `invoke` receives the
176
+ * field's `ResolverContext` (the guards' one when the field has guards, else a new one) and the
177
+ * instances of its parameter resolvers: one frozen array resolved at boot when they are all
178
+ * singletons, otherwise resolved in the operation scope on every call.
179
+ */
180
+ function customInvoke(
181
+ options: PipelineOptions,
182
+ binding: GraphQLResolverBinding,
183
+ coordinate: string,
184
+ route: ResolverContext['route'],
185
+ parentType: string,
186
+ fieldName: string,
187
+ ): Invoke {
188
+ const generated = binding.invoke as Invoke
189
+ const container = options.container
190
+ const tokens = binding.paramResolvers ?? []
191
+ const resolvers = tokens.map(token => {
192
+ if (options.paramResolver === undefined) throw new TypeError(`No parameter resolver for ${coordinate}`)
193
+ return options.paramResolver(token, coordinate)
194
+ })
195
+ const context = (p: unknown, a: GraphQLArgs, c: GraphQLContext, i: GraphQLResolveInfo, f?: ResolverContext) =>
196
+ f ?? new FieldContext(route, c as OperationContext, parentType, fieldName, a, p, i)
197
+ if (resolvers.length === 0) {
198
+ return (r, p, a, c, i, f) => generated(r, p, a, c, i, context(p, a, c, i, f))
199
+ }
200
+ if (resolvers.every(resolver => resolver.token === undefined)) {
201
+ const instances = Object.freeze(resolvers.map(resolver => resolveIn(container, resolver)))
202
+ return (r, p, a, c, i, f) => generated(r, p, a, c, i, context(p, a, c, i, f), instances)
203
+ }
204
+ return (r, p, a, c, i, f) =>
205
+ generated(
206
+ r,
207
+ p,
208
+ a,
209
+ c,
210
+ i,
211
+ context(p, a, c, i, f),
212
+ resolvers.map(resolver => resolveIn(container, resolver)),
213
+ )
214
+ }
215
+
153
216
  /**
154
217
  * The resolve function of a field that carries results (RFC 0008): errors of the whole pipeline
155
218
  * (guards, null handling, `invoke`), thrown or rejected, go through `recover`. Synchronous results
package/src/plugin.ts CHANGED
@@ -493,6 +493,8 @@ export class GraphQLPlugin implements Plugin, PluginHttpRoutes {
493
493
  guard: (token, coordinate) => providers.token<GraphQLGuard>(token, `guard of ${coordinate}`, 'canResolve'),
494
494
  resolver: (binding, coordinate) =>
495
495
  providers.token<object>(binding.token, `resolver of ${coordinate}`, binding.method),
496
+ paramResolver: (token, coordinate) =>
497
+ providers.token<object>(token, `parameter resolver of ${coordinate}`, 'resolve'),
496
498
  inputs,
497
499
  results: (field, coordinate) => results.recover(field, coordinate),
498
500
  }),
package/src/types.ts CHANGED
@@ -131,9 +131,17 @@ export interface GraphQLContext {
131
131
  loader(token: Token): BatchLoader<unknown, unknown>
132
132
  }
133
133
 
134
+ declare module '@znight/core' {
135
+ /** Custom parameter decorators (`createParamDecorator`) resolve GraphQL parameters from the {@link ResolverContext}. */
136
+ interface ParamContexts {
137
+ graphql: ResolverContext
138
+ }
139
+ }
140
+
134
141
  /**
135
- * What a {@link GraphQLGuard} receives for one resolver call (§5.9). It satisfies the
136
- * `RouteMetaCarrier` of `@znight/http`, so route decorators read it: `Roles.get(ctx)`.
142
+ * What a {@link GraphQLGuard} receives for one resolver call (§5.9), and the context of custom
143
+ * parameter resolvers (RFC 0010). It satisfies the `RouteMetaCarrier` of `@znight/http`, so route
144
+ * decorators read it: `Roles.get(ctx)`.
137
145
  */
138
146
  export interface ResolverContext<TParent = unknown, TArgs extends GraphQLArgs = GraphQLArgs> extends RouteMetaCarrier {
139
147
  /** Effective metadata of the field (method decisions over class decisions) and its schema coordinate. */