@znight/graphql 0.2.2 → 0.4.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
  )