@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/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
|
-
|
|
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)
|
|
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
|
package/dist/compiler.d.mts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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 (
|
|
2804
|
-
return validatingBlock(ctx, imports, r, scope, field,
|
|
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
|
|
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
|
-
|
|
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
|
}
|