@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.
- package/README.md +40 -2
- package/dist/compiler.d.mts +21 -1
- package/dist/compiler.mjs +116 -15
- package/dist/compiler.mjs.map +1 -1
- package/dist/{driver-Cnze8Oyr.d.mts → driver-DgbpFj4-.d.mts} +10 -3
- package/dist/index.d.mts +16 -3
- package/dist/index.mjs +1 -1
- package/dist/names-BlzDM5cF.mjs.map +1 -1
- package/dist/{plugin-q4oz7d6s.mjs → plugin-BssB7wtn.mjs} +29 -5
- package/dist/plugin-BssB7wtn.mjs.map +1 -0
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/dist/yoga.d.mts +1 -1
- package/package.json +8 -8
- package/src/compiler/analyze.ts +27 -0
- package/src/compiler/data.ts +20 -1
- package/src/compiler/generate.ts +137 -19
- package/src/metadata.ts +20 -4
- package/src/pipeline.ts +72 -9
- package/src/plugin.ts +2 -0
- package/src/types.ts +10 -2
- package/dist/plugin-q4oz7d6s.mjs.map +0 -1
package/src/compiler/generate.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
226
|
-
function
|
|
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(
|
|
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 = [
|
|
259
|
-
|
|
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
|
|
265
|
-
|
|
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 (
|
|
271
|
-
return validatingBlock(ctx, imports, r, scope, field,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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)
|
|
136
|
-
* `RouteMetaCarrier` of `@znight/http`, so route
|
|
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. */
|