@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 CHANGED
@@ -540,6 +540,32 @@ Built-in scalars appear in the schema only when used: `String`, `Float`, `Int`,
540
540
  `plugin.execute()` and `plugin.subscribe()` run in the current scope when one is active (a
541
541
  controller calling GraphQL), otherwise in a scope of their own.
542
542
 
543
+ ### Custom parameter decorators
544
+
545
+ A decorator defined with `createParamDecorator` (`@znight/core`, RFC 0010) works on resolver
546
+ methods when it has a `graphql` resolver, which receives the field's `ResolverContext` (`parent`,
547
+ `args`, `info`, `operation`, `http`, `route.meta`). One definition serves HTTP routes and GraphQL
548
+ fields:
549
+
550
+ ```ts
551
+ import { createParamDecorator } from '@znight/core'
552
+ import type { User } from './user.ts'
553
+
554
+ export const CurrentUser = createParamDecorator<User>({
555
+ http: ctx => ctx.state.get('user') as User,
556
+ graphql: ctx => ctx.operation.state.get('user') as User,
557
+ })
558
+
559
+ // @Query() me(@CurrentUser() user: User): User { return user }
560
+ ```
561
+
562
+ The resolver runs after the guards (it sees what they stored) and before the method; what it
563
+ throws fails the field like the method would (`errors[]`, or a result mapper). A custom parameter
564
+ is not a GraphQL argument: it never appears in the schema. Resolver classes implementing
565
+ `ParamResolver` are resolved in the operation scope. Custom parameters belong to the field
566
+ resolvers of the schema, so they work the same through the built-in transport, `plugin.execute()`
567
+ and a driver such as `yogaDriver()` (RFC 0009).
568
+
543
569
  ## Authorization
544
570
 
545
571
  The route decorators of `@znight/http` (`createRouteDecorator`, with RFC 0001's groups) apply to
@@ -1486,7 +1512,17 @@ interface GraphQLResolverBinding {
1486
1512
  guards: Token[] // class @UseGuard guards, then method ones
1487
1513
  meta: Record<string, unknown> // effective route metadata (method over class); {}: none
1488
1514
  loaders?: Token[] // informational: loaders the method injects
1489
- invoke: (resolver, parent, args: GraphQLArgs, context: GraphQLContext, info: GraphQLResolveInfo) => unknown
1515
+ context?: true // custom parameters (RFC 0010): the pipeline passes the ResolverContext as `f`
1516
+ paramResolvers?: Token[] // class resolvers of custom parameters, passed as `x`
1517
+ invoke(
1518
+ resolver,
1519
+ parent,
1520
+ args: GraphQLArgs,
1521
+ context: GraphQLContext,
1522
+ info: GraphQLResolveInfo,
1523
+ f?: ResolverContext,
1524
+ x?,
1525
+ ): unknown
1490
1526
  }
1491
1527
  ```
1492
1528
 
@@ -1498,7 +1534,9 @@ Type references are strings in graphql-js' canonical form, parsed by
1498
1534
 
1499
1535
  `invoke` maps the graphql-js arguments to the method parameters: `@Args('x')` → `a['x']`,
1500
1536
  `@Args()` → `a`, `@Parent()` → `p`, `@Ctx()` → `c`, `@Info()` → `i`, `@Loader()` →
1501
- `c.loader(token)`. Unused middle parameters are `_`-prefixed and trailing ones omitted, so the code
1537
+ `c.loader(token)`, a custom parameter decorator (`createParamDecorator`, RFC 0010) →
1538
+ `(D.resolvers.graphql as ParamFunctionOf<typeof D, 'graphql'>)(f, data)` or `x[i].resolve(f, data)`
1539
+ (evaluated like a validated argument: synchronous while the resolvers return values). Unused middle parameters are `_`-prefixed and trailing ones omitted, so the code
1502
1540
  type-checks with `noUnusedParameters`. It calls these runtime helpers, kept stable:
1503
1541
 
1504
1542
  - `validateArgs(schema, value, label)`, exported from `@znight/graphql`: validates an argument with
@@ -1,6 +1,6 @@
1
1
  import { Scope } from "@znight/core";
2
2
  import { MetaEntry } from "@znight/http/compiler";
3
- import { BindingHandle, ClassInfo, ClassRef, CompilerExtension, Location, ValueRef } from "@znight/compiler";
3
+ import { BindingHandle, ClassInfo, ClassRef, CompilerExtension, Location, StaticValue, ValueRef } from "@znight/compiler";
4
4
  /** Kind of field a resolver method provides. */
5
5
  type FieldOperation = "query" | "mutation" | "subscription" | "field";
6
6
  /** A `@Resolver` class. */
@@ -71,6 +71,26 @@ type ParamData = {
71
71
  /** Identity of `L` in `Loader<L>`. */
72
72
  readonly identity: string;
73
73
  readonly location: Location;
74
+ } | {
75
+ /** A custom parameter decorator (`createParamDecorator`, RFC 0010). */
76
+ readonly kind: "custom";
77
+ /** Canonical identity of the decorator `const` (imported by the generated module). */
78
+ readonly identity: string;
79
+ readonly name: string;
80
+ /** The data argument (a value, possibly holding references, or a reference); `undefined` without argument. */
81
+ readonly data: {
82
+ readonly value: StaticValue;
83
+ } | undefined;
84
+ /** The class resolver (`x[i].resolve(f, data)`); `undefined` for a function resolver. */
85
+ readonly resolver: ClassRef | undefined;
86
+ /**
87
+ * Parameters of the class resolver's `resolve` (own, inherited or an arrow function property);
88
+ * `undefined` for a function, or when the compiler cannot read it (the call is cast).
89
+ */
90
+ readonly arity?: number;
91
+ /** The value type could not be checked against the parameter. */
92
+ readonly cast: boolean;
93
+ readonly location: Location;
74
94
  };
75
95
  /** Everything the extension stores. */
76
96
  type GraphQLData = ResolverData;
package/dist/compiler.mjs CHANGED
@@ -417,6 +417,22 @@ function parameterDecorators(param) {
417
417
  }
418
418
  function analyzeParameter(ctx, cls, param, where) {
419
419
  const decorators = parameterDecorators(param);
420
+ const custom = typeof ctx.paramDecorator === "function" ? ctx.paramDecorator(param, "graphql", cls) : void 0;
421
+ if (custom !== void 0) {
422
+ for (const other of decorators) error(ctx, Codes.InvalidDecorator, other.location, `Parameter '${param.name}' of ${where} has several parameter decorators (@${custom.decorator.name} and @${other.name})`, ["Keep a single parameter decorator"]);
423
+ if (!custom.ok || decorators.length > 0) return void 0;
424
+ const data = custom.data;
425
+ return {
426
+ kind: "custom",
427
+ identity: custom.identity,
428
+ name: custom.name,
429
+ data: data === void 0 ? void 0 : { value: data.ref ?? data.value },
430
+ resolver: custom.resolver.kind === "class" ? custom.resolver.ref : void 0,
431
+ ...custom.resolver.kind === "class" && custom.resolver.arity !== void 0 ? { arity: custom.resolver.arity } : {},
432
+ cast: custom.cast,
433
+ location: custom.location
434
+ };
435
+ }
420
436
  const decorator = decorators[0];
421
437
  if (decorator === void 0) {
422
438
  const lookAlike = param.decorators.find((found) => found.identity !== void 0 && HTTP_LOOK_ALIKES.has(found.identity));
@@ -2627,6 +2643,8 @@ function createImports(ctx) {
2627
2643
  let args;
2628
2644
  let context;
2629
2645
  let info;
2646
+ let resolverContext;
2647
+ let paramFunctionOf;
2630
2648
  let validateArgs;
2631
2649
  let isThenable;
2632
2650
  const hosts = /* @__PURE__ */ new Map();
@@ -2643,6 +2661,14 @@ function createImports(ctx) {
2643
2661
  info ??= ctx.importFrom("graphql", "GraphQLResolveInfo", { type: true });
2644
2662
  return info;
2645
2663
  },
2664
+ resolverContextType: () => {
2665
+ resolverContext ??= ctx.importFrom(RUNTIME_MODULE, "ResolverContext", { type: true });
2666
+ return resolverContext;
2667
+ },
2668
+ paramFunctionOf: () => {
2669
+ paramFunctionOf ??= ctx.importFrom("@znight/core", "ParamFunctionOf", { type: true });
2670
+ return paramFunctionOf;
2671
+ },
2646
2672
  validateArgs: () => {
2647
2673
  validateArgs ??= ctx.importFrom(RUNTIME_MODULE, "validateArgs");
2648
2674
  return validateArgs;
@@ -2734,7 +2760,10 @@ function resolverNode(ctx, imports, resolver, field) {
2734
2760
  ];
2735
2761
  const loaders = field.params.flatMap((param) => param?.kind === "loader" ? [loaderToken(ctx, param)] : []);
2736
2762
  if (loaders.length > 0) entries.push(["loaders", ctx.lit(loaders)]);
2737
- entries.push(["invoke", invokeNode(ctx, imports, resolver, field)]);
2763
+ const resolvers = paramResolvers(field);
2764
+ if (field.params.some((param) => param?.kind === "custom")) entries.push(["context", ctx.lit(true)]);
2765
+ if (resolvers.length > 0) entries.push(["paramResolvers", ctx.lit(resolvers.map((ref) => ctx.tokenOf(ref) ?? ref.token))]);
2766
+ entries.push(["invoke", invokeNode(ctx, imports, resolver, field, resolvers)]);
2738
2767
  return ctx.obj(entries);
2739
2768
  }
2740
2769
  /** The provider token a loader parameter is bound to. */
@@ -2744,13 +2773,28 @@ function loaderToken(ctx, param) {
2744
2773
  if (ref !== void 0 && "token" in ref) return ref.token;
2745
2774
  return param.identity;
2746
2775
  }
2747
- /** Which `invoke` parameters a field uses: `(r, p, a, c, i)`. */
2776
+ /** The provider classes resolving custom parameters, in order of first use (`x[i]`). */
2777
+ function paramResolvers(field) {
2778
+ const found = /* @__PURE__ */ new Map();
2779
+ for (const param of field.params) {
2780
+ const resolver = param?.kind === "custom" ? param.resolver : void 0;
2781
+ if (resolver !== void 0 && !found.has(resolver.token)) found.set(resolver.token, resolver);
2782
+ }
2783
+ return [...found.values()];
2784
+ }
2785
+ /** Which `invoke` parameters a field uses: `(r, p, a, c, i, f, x)`. */
2748
2786
  function usedParameters(field) {
2749
2787
  let p = false;
2750
2788
  let a = false;
2751
2789
  let c = false;
2752
2790
  let i = false;
2791
+ let f = false;
2792
+ let x = false;
2753
2793
  for (const param of field.params) switch (param?.kind) {
2794
+ case "custom":
2795
+ f ||= param.resolver === void 0 || param.arity !== 0;
2796
+ x ||= param.resolver !== void 0;
2797
+ break;
2754
2798
  case "parent":
2755
2799
  p = true;
2756
2800
  break;
@@ -2767,43 +2811,63 @@ function usedParameters(field) {
2767
2811
  p,
2768
2812
  a,
2769
2813
  c,
2770
- i
2814
+ i,
2815
+ f,
2816
+ x
2771
2817
  ];
2772
2818
  }
2773
2819
  /**
2774
2820
  * `(r: Resolver, p: unknown, a: GraphQLArgs, c: GraphQLContext, i: GraphQLResolveInfo) => r.m(...)`:
2775
2821
  * unused middle parameters are `_`-prefixed, trailing ones omitted (`noUnusedParameters`).
2776
2822
  */
2777
- function invokeNode(ctx, imports, resolver, field) {
2823
+ function invokeNode(ctx, imports, resolver, field, resolvers) {
2778
2824
  const used = usedParameters(field);
2779
2825
  const last = used.lastIndexOf(true);
2780
2826
  const types = [
2781
2827
  () => ctx.code`unknown`,
2782
2828
  imports.argsType,
2783
2829
  imports.contextType,
2784
- imports.infoType
2830
+ imports.infoType,
2831
+ imports.resolverContextType,
2832
+ () => tupleType(ctx, resolvers.map((ref) => ctx.importType(ref)))
2785
2833
  ];
2786
2834
  const names = [
2787
2835
  "p",
2788
2836
  "a",
2789
2837
  "c",
2790
- "i"
2838
+ "i",
2839
+ "f",
2840
+ "x"
2791
2841
  ];
2792
2842
  const params = [["r", imports.hostType(resolver.host.identity)]];
2793
2843
  for (let index = 0; index <= last; index++) params.push([`${used[index] ? "" : "_"}${names[index]}`, types[index]()]);
2794
- const validated = field.params.flatMap((param, index) => param?.kind === "args" && param.schema !== void 0 ? [index] : []);
2844
+ const deferred = field.params.flatMap((param, index) => param !== void 0 && isDeferred(param) ? [index] : []);
2845
+ const positions = new Map(resolvers.map((ref, position) => [ref.token, position]));
2795
2846
  return ctx.arrow(params, (r, ...rest) => {
2796
- const [p, a, c, i] = rest;
2847
+ const [p, a, c, i, f, x] = rest;
2797
2848
  const scope = {
2798
2849
  p,
2799
2850
  a,
2800
2851
  c,
2801
- i
2852
+ i,
2853
+ f,
2854
+ x,
2855
+ resolvers: positions,
2856
+ imports
2802
2857
  };
2803
- if (validated.length === 0) return call(ctx, r, field, (index) => argument(ctx, scope, field, index));
2804
- return validatingBlock(ctx, imports, r, scope, field, validated);
2858
+ if (deferred.length === 0) return call(ctx, r, field, (index) => argument(ctx, scope, field, index));
2859
+ return validatingBlock(ctx, imports, r, scope, field, deferred);
2805
2860
  });
2806
2861
  }
2862
+ /** `readonly [A, B]`. */
2863
+ function tupleType(ctx, nodes) {
2864
+ const joined = nodes.slice(1).reduce((all, node) => ctx.code`${all}, ${node}`, nodes[0]);
2865
+ return ctx.code`readonly [${joined}]`;
2866
+ }
2867
+ /** An argument whose expression may return a thenable: a validated `@Args`, or a custom parameter. */
2868
+ function isDeferred(param) {
2869
+ return param.kind === "args" && param.schema !== void 0 || param.kind === "custom";
2870
+ }
2807
2871
  /** `r.method(args)`. */
2808
2872
  function call(ctx, r, field, argumentOf) {
2809
2873
  return ctx.call(ctx.member(r, field.method), field.params.map((_, index) => argumentOf(index)));
@@ -2817,9 +2881,48 @@ function argument(ctx, scope, field, index) {
2817
2881
  case "info": return scope.i;
2818
2882
  case "loader": return ctx.code`${ctx.call(ctx.member(scope.c, "loader"), [loaderToken(ctx, param)])} as never`;
2819
2883
  case "args": return ctx.code`${argsValue(ctx, scope, param.name)} as never`;
2884
+ case "custom": return customArgument(ctx, scope, param);
2820
2885
  default: return ctx.lit(void 0);
2821
2886
  }
2822
2887
  }
2888
+ /**
2889
+ * A custom parameter (RFC 0010): `(D.resolvers.graphql as ParamFunctionOf<typeof D, 'graphql'>)(f, data)`
2890
+ * or `x[i].resolve(f, data)`, cast with `as never` when the value type could not be checked.
2891
+ */
2892
+ function customArgument(ctx, scope, param) {
2893
+ const f = scope.f;
2894
+ const data = param.data === void 0 ? ctx.lit(void 0) : dataNode(ctx, param.data.value);
2895
+ let node;
2896
+ if (param.resolver === void 0) {
2897
+ const decorator = ctx.importValue(param.identity);
2898
+ const type = ctx.code`${scope.imports.paramFunctionOf()}<typeof ${decorator}, 'graphql'>`;
2899
+ node = ctx.call(ctx.code`${ctx.member(decorator, "resolvers", "graphql")} as ${type}`, [f, data]);
2900
+ } else {
2901
+ const position = scope.resolvers.get(param.resolver.token);
2902
+ if (param.arity === void 0) return ctx.code`(${scope.x}[${position}] as { resolve(ctx: unknown, data: unknown): unknown }).resolve(${f}, ${data}) as never`;
2903
+ const arity = Math.min(param.arity, 2);
2904
+ let args = ctx.code``;
2905
+ if (arity === 1) args = f;
2906
+ else if (arity === 2) args = ctx.code`${f}, ${data}`;
2907
+ node = ctx.code`${scope.x}[${position}].resolve(${args})`;
2908
+ }
2909
+ return param.cast ? ctx.code`${node} as never` : node;
2910
+ }
2911
+ /** The data argument of a custom parameter: a literal (references imported) or a reference. */
2912
+ function dataNode(ctx, value) {
2913
+ return isValueRef(value) ? ctx.ref(value) : ctx.lit(value);
2914
+ }
2915
+ /** `true` for a {@link ValueRef} (the only non-plain object a static value can hold). */
2916
+ function isValueRef(value) {
2917
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2918
+ const prototype = Object.getPrototypeOf(value);
2919
+ return prototype !== Object.prototype && prototype !== null && value.kind === "ref";
2920
+ }
2921
+ /** The expression of a deferred parameter (validation or custom resolver), before any `await`. */
2922
+ function deferredValue(ctx, imports, scope, field, index) {
2923
+ const param = field.params[index];
2924
+ return param.kind === "custom" ? customArgument(ctx, scope, param) : validation(ctx, imports, scope, param);
2925
+ }
2823
2926
  /** `a['name']`, or `a` for `@Args()`. */
2824
2927
  function argsValue(ctx, scope, name) {
2825
2928
  const a = scope.a;
@@ -2853,15 +2956,13 @@ function validatingBlock(ctx, imports, r, scope, field, validated) {
2853
2956
  const statements = [];
2854
2957
  for (const index of validated) {
2855
2958
  const name = local.get(index);
2856
- const param = field.params[index];
2857
- statements.push(ctx.code`const ${name} = ${validation(ctx, imports, scope, param)}`);
2959
+ statements.push(ctx.code`const ${name} = ${deferredValue(ctx, imports, scope, field, index)}`);
2858
2960
  const resume = ctx.arrow([], () => call(ctx, r, field, (position) => {
2859
2961
  const value = local.get(position);
2860
2962
  if (value === void 0) return argument(ctx, scope, field, position);
2861
2963
  if (position < index) return ctx.code`${value} as never`;
2862
2964
  if (position === index) return ctx.code`await ${value} as never`;
2863
- const later = field.params[position];
2864
- return ctx.code`await ${validation(ctx, imports, scope, later)} as never`;
2965
+ return ctx.code`await ${deferredValue(ctx, imports, scope, field, position)} as never`;
2865
2966
  }), { async: true });
2866
2967
  statements.push(ctx.code`if (${ctx.call(imports.isThenable(), [name])}) return (${resume})()`);
2867
2968
  }