@btravstack/graphql-server 0.25.0 → 0.27.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 +4 -1
- package/dist/index.cjs +46 -1
- package/dist/index.d.cts +63 -7
- package/dist/index.d.mts +63 -7
- package/dist/index.mjs +46 -2
- package/package.json +9 -8
package/README.md
CHANGED
|
@@ -10,7 +10,10 @@ Pass `units` with `unit` to check each injected port against the module the
|
|
|
10
10
|
selected request kind actually forks. Pass the same `units` record to
|
|
11
11
|
`httpServer({ unit: units })` so the runtime can apply unit test overrides.
|
|
12
12
|
Yoga plugins and CORS policy are explicit
|
|
13
|
-
options; the wildcard CORS default and Yoga console logging are disabled
|
|
13
|
+
options; the wildcard CORS default and Yoga console logging are disabled, and
|
|
14
|
+
a masked defect is reported to `Observers` instead. `fieldResult(result)`
|
|
15
|
+
answers a resolver from an `AsyncResult`, with a refusal on its own field's
|
|
16
|
+
path.
|
|
14
17
|
Preflights use Yoga's CORS policy before authentication, and Yoga plugins are
|
|
15
18
|
disposed when the application stops.
|
|
16
19
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let graphql_yoga = require("graphql-yoga");
|
|
3
|
+
let _btravstack_core = require("@btravstack/core");
|
|
2
4
|
let _btravstack_di = require("@btravstack/di");
|
|
3
5
|
let _btravstack_http_server_internal = require("@btravstack/http-server/internal");
|
|
4
|
-
let graphql_yoga = require("graphql-yoga");
|
|
5
6
|
let unthrown = require("unthrown");
|
|
7
|
+
//#region src/field.ts
|
|
8
|
+
/**
|
|
9
|
+
* A resolver's answer from a `Result`, so a modeled failure is never thrown by
|
|
10
|
+
* hand. `Ok` is the field's value. `Err` is the `GraphQLError` the resolver's
|
|
11
|
+
* own triage minted from its `E` — an exhaustive `mapErrCases`, or
|
|
12
|
+
* `flatMapErrCases` where one refusal is `Ok(null)`, so widening `E` fails
|
|
13
|
+
* every resolver that folds it — and GraphQL reports it on this
|
|
14
|
+
* field's path, aliases and list indexes included, while its siblings resolve:
|
|
15
|
+
* its message and `extensions` reach the client, its `originalError` does not.
|
|
16
|
+
* A defect is masked to `Unexpected error.` whatever its cause, and `graphql()`
|
|
17
|
+
* reports it to `Observers`.
|
|
18
|
+
*
|
|
19
|
+
* **It answers a `Promise`, and that is the boundary**: a GraphQL resolver
|
|
20
|
+
* returns a value or a promise and reports a field's error by rejecting, so
|
|
21
|
+
* this is where a `Result` leaves for GraphQL's own channel — the one place a
|
|
22
|
+
* resolver needs, as `runMain` is for a process.
|
|
23
|
+
*/
|
|
24
|
+
const fieldResult = async (result) => {
|
|
25
|
+
const settled = await result;
|
|
26
|
+
if (settled.isDefect()) throw new Error("fieldResult: the field's result was a defect", { cause: settled.cause });
|
|
27
|
+
if (settled.isErr()) throw (0, graphql_yoga.createGraphQLError)(settled.error.message, { extensions: settled.error.extensions });
|
|
28
|
+
return settled.value;
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
6
31
|
//#region src/graphql.ts
|
|
7
32
|
const dispose = Symbol("graphql.dispose");
|
|
33
|
+
/** What failed: GraphQL locates a resolver's error by wrapping it, and a log line walks `cause`, not `originalError`. */
|
|
34
|
+
const causeOf = (error) => typeof error === "object" && error !== null && "originalError" in error && error.originalError ? error.originalError : error;
|
|
35
|
+
const pathOf = (error) => typeof error === "object" && error !== null && "path" in error && Array.isArray(error.path) ? error.path.join(".") : void 0;
|
|
8
36
|
const graphql = (api, options) => {
|
|
9
37
|
const mount = options.prefix ?? "/graphql";
|
|
10
38
|
let end = mount.length;
|
|
@@ -16,11 +44,13 @@ const graphql = (api, options) => {
|
|
|
16
44
|
inject: {
|
|
17
45
|
config: _btravstack_http_server_internal.HttpConfig,
|
|
18
46
|
units: _btravstack_http_server_internal.HttpUnit,
|
|
47
|
+
observers: _btravstack_core.Observers,
|
|
19
48
|
...(0, _btravstack_http_server_internal.schemeDeps)(schemes)
|
|
20
49
|
},
|
|
21
50
|
sync: (services) => {
|
|
22
51
|
const config = services.config;
|
|
23
52
|
const units = services.units;
|
|
53
|
+
const observers = services.observers;
|
|
24
54
|
if (options.units !== void 0) {
|
|
25
55
|
for (const kind of requirements === void 0 ? ["anonymous"] : schemes) if ((options.units[kind] ?? options.units["anonymous"]) !== (units[kind] ?? units["anonymous"])) throw new Error(`[graphql-server] unit kind ${JSON.stringify(kind)} differs from HttpUnit`);
|
|
26
56
|
}
|
|
@@ -32,6 +62,20 @@ const graphql = (api, options) => {
|
|
|
32
62
|
plugins: [{ onRequest: ({ serverContext, fetchAPI, endResponse }) => {
|
|
33
63
|
if (serverContext.refusalStatus !== void 0) endResponse(new fetchAPI.Response(null, { status: serverContext.refusalStatus }));
|
|
34
64
|
} }, ...options.plugins ?? []],
|
|
65
|
+
maskedErrors: { maskError: (error, message, isDev) => {
|
|
66
|
+
const masked = (0, graphql_yoga.maskError)(error, message, isDev);
|
|
67
|
+
if (masked !== error) (0, _btravstack_core.observe)(observers, {
|
|
68
|
+
component: "graphql",
|
|
69
|
+
name: "defect",
|
|
70
|
+
attributes: {},
|
|
71
|
+
details: { "graphql.path": pathOf(error) },
|
|
72
|
+
traced: false
|
|
73
|
+
})({
|
|
74
|
+
outcome: "error",
|
|
75
|
+
cause: causeOf(error)
|
|
76
|
+
});
|
|
77
|
+
return masked;
|
|
78
|
+
} },
|
|
35
79
|
logging: false,
|
|
36
80
|
graphiql: false,
|
|
37
81
|
landingPage: false,
|
|
@@ -84,4 +128,5 @@ const graphql = (api, options) => {
|
|
|
84
128
|
return Object.assign(typed, { authenticators: api.providers });
|
|
85
129
|
};
|
|
86
130
|
//#endregion
|
|
131
|
+
exports.fieldResult = fieldResult;
|
|
87
132
|
exports.graphql = graphql;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,25 +1,81 @@
|
|
|
1
|
+
import { GraphQLError, GraphQLSchema } from "graphql";
|
|
2
|
+
import { AsyncResult, Result } from "unthrown";
|
|
3
|
+
import { IncomingMessage } from "node:http";
|
|
1
4
|
import { OneScheme, Requirements } from "@btravstack/contract";
|
|
5
|
+
import { Observers } from "@btravstack/core";
|
|
2
6
|
import { AnyPort, Provider, Scope } from "@btravstack/di";
|
|
3
|
-
import { Authenticators, HttpAuth, HttpConfig, HttpHandler, HttpUnit, KindOf, Kinds, RequiresGate, SchemePortsOf, UnitFor, UnitsNeedsOf, UnitsOf } from "@btravstack/http-server/internal";
|
|
4
|
-
import {
|
|
5
|
-
|
|
7
|
+
import { Authenticators, HttpAuth, HttpConfig, HttpHandler, HttpUnit, KindOf, Kinds, Principal, RequiresGate, SchemePortsOf, SchemesFrom, SchemesOf, UnitFor, UnitsNeedsOf, UnitsOf } from "@btravstack/http-server/internal";
|
|
8
|
+
import { Plugin, YogaInitialContext, YogaServerOptions } from "graphql-yoga";
|
|
9
|
+
//#region src/field.d.ts
|
|
10
|
+
/**
|
|
11
|
+
* A resolver's answer from a `Result`, so a modeled failure is never thrown by
|
|
12
|
+
* hand. `Ok` is the field's value. `Err` is the `GraphQLError` the resolver's
|
|
13
|
+
* own triage minted from its `E` — an exhaustive `mapErrCases`, or
|
|
14
|
+
* `flatMapErrCases` where one refusal is `Ok(null)`, so widening `E` fails
|
|
15
|
+
* every resolver that folds it — and GraphQL reports it on this
|
|
16
|
+
* field's path, aliases and list indexes included, while its siblings resolve:
|
|
17
|
+
* its message and `extensions` reach the client, its `originalError` does not.
|
|
18
|
+
* A defect is masked to `Unexpected error.` whatever its cause, and `graphql()`
|
|
19
|
+
* reports it to `Observers`.
|
|
20
|
+
*
|
|
21
|
+
* **It answers a `Promise`, and that is the boundary**: a GraphQL resolver
|
|
22
|
+
* returns a value or a promise and reports a field's error by rejecting, so
|
|
23
|
+
* this is where a `Result` leaves for GraphQL's own channel — the one place a
|
|
24
|
+
* resolver needs, as `runMain` is for a process.
|
|
25
|
+
*/
|
|
26
|
+
export declare const fieldResult: <T>(result: AsyncResult<T, GraphQLError> | Result<T, GraphQLError>) => Promise<T>;
|
|
27
|
+
//#endregion
|
|
6
28
|
//#region src/graphql.d.ts
|
|
7
29
|
type YogaCors = YogaServerOptions<Record<string, unknown>, Record<string, unknown>>["cors"];
|
|
8
30
|
type MissingUnitPorts<U extends Readonly<Record<string, AnyPort>>, Units, K extends string> = Exclude<keyof U, keyof UnitFor<U, Units, K>>;
|
|
9
31
|
type UnitGate<U extends Readonly<Record<string, AnyPort>>, Units, K extends string> = [MissingUnitPorts<U, Units, K>] extends [never] ? unknown : {
|
|
10
32
|
readonly "UNIT PORT MISSING — the selected kind does not export": MissingUnitPorts<U, Units, K>;
|
|
11
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* The context `graphql()` hands Yoga for an operation: what a resolver and a
|
|
36
|
+
* plugin's operation hooks (`onParse`, `onValidate`, `onContextBuilding`,
|
|
37
|
+
* `onExecute`, `onSubscribe`) read beside Yoga's own `request`. `principal` is
|
|
38
|
+
* the caller `requires` resolved, `undefined` without `requires`; `unit` is the
|
|
39
|
+
* `unit` record read off the forked kind; `incoming` is the Node request.
|
|
40
|
+
*
|
|
41
|
+
* Those hooks run only once the caller is authenticated. A preflight and a
|
|
42
|
+
* refusal end in Yoga's server hooks (`onRequest`, `onResponse`), whose
|
|
43
|
+
* context is not this type, so a plugin cannot claim a principal there.
|
|
44
|
+
*/
|
|
45
|
+
type GraphqlContext<Principal = unknown, Unit = Readonly<Record<string, unknown>>> = {
|
|
46
|
+
readonly principal: Principal;
|
|
47
|
+
readonly incoming: IncomingMessage;
|
|
48
|
+
readonly unit: Unit;
|
|
49
|
+
readonly signal: AbortSignal;
|
|
50
|
+
};
|
|
51
|
+
/** The {@link GraphqlContext} one `graphql()` call builds, from its own options. */
|
|
52
|
+
type ContextOf<A extends Authenticators, Units, R extends Requirements, U extends Readonly<Record<string, AnyPort>>> = GraphqlContext<[R] extends [never] ? undefined : Principal<SchemesOf<R>, SchemesFrom<A>>, UnitFor<U, Units, KindOf<R>>>;
|
|
53
|
+
/**
|
|
54
|
+
* Each plugin, unless the context this call builds lacks something it reads, or
|
|
55
|
+
* the entry is not a plugin at all.
|
|
56
|
+
* Per element, because Yoga's `Plugin<C>` is invariant in `C`: a plugin typed
|
|
57
|
+
* for the real context is not a `Plugin`, so none could be widened to one.
|
|
58
|
+
*/
|
|
59
|
+
type PluginsGate<PS extends readonly unknown[], C> = { readonly [I in keyof PS]: PS[I] extends Plugin<infer X> ? YogaInitialContext & C extends X ? PS[I] : "PLUGIN CONTEXT MISMATCH — this plugin reads what this graphql() call does not put in its context" : {
|
|
60
|
+
readonly "NOT A PLUGIN — this entry is not a Yoga plugin": PS[I];
|
|
61
|
+
}; };
|
|
12
62
|
/** Serve a GraphQL schema as one answerer under the existing HTTP runtime. */
|
|
13
|
-
type GraphqlOptions<A extends Authenticators, Units extends UnitsOf<A>, R extends Requirements & { readonly [I in keyof R]: OneScheme<R[I]>; }, U extends Readonly<Record<string, AnyPort
|
|
63
|
+
type GraphqlOptions<A extends Authenticators, Units extends UnitsOf<A>, R extends Requirements & { readonly [I in keyof R]: OneScheme<R[I]>; }, U extends Readonly<Record<string, AnyPort>>, PS extends readonly unknown[] = readonly Plugin[]> = {
|
|
14
64
|
readonly schema: GraphQLSchema;
|
|
15
65
|
readonly prefix?: `/${string}`;
|
|
16
66
|
readonly cors?: YogaCors;
|
|
17
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Yoga plugins. Each may be typed by the context it reads — a
|
|
69
|
+
* `Plugin<{ unit: … }>` — and is checked against the {@link GraphqlContext}
|
|
70
|
+
* this call builds.
|
|
71
|
+
*/
|
|
72
|
+
readonly plugins?: (PS & PluginsGate<PS, ContextOf<A, Units, R, U>>) | readonly Plugin<YogaInitialContext & ContextOf<A, Units, R, U>>[];
|
|
18
73
|
readonly units?: Units & { readonly [K in Exclude<keyof Units, Kinds<A>>]: never; };
|
|
19
74
|
readonly unit?: U & UnitGate<U, Units, KindOf<R>>;
|
|
20
75
|
readonly requires?: R & RequiresGate<R, { readonly [K in keyof A]: A[K]["scope"]; }>;
|
|
21
76
|
};
|
|
22
|
-
export declare const graphql: <A extends Authenticators, Units extends UnitsOf<A> = Record<never, never>, const R extends Requirements & { readonly [I in keyof R]: OneScheme<R[I]>; } = never, U extends Readonly<Record<string, AnyPort>> = Record<never, never
|
|
77
|
+
export declare const graphql: <A extends Authenticators, Units extends UnitsOf<A> = Record<never, never>, const R extends Requirements & { readonly [I in keyof R]: OneScheme<R[I]>; } = never, U extends Readonly<Record<string, AnyPort>> = Record<never, never>, const PS extends readonly unknown[] = []>(api: HttpAuth<A>, options: GraphqlOptions<A, Units, R, U, PS>) => Provider<HttpHandler, never, HttpConfig | HttpUnit | Observers | Scope | SchemePortsOf<R> | UnitsNeedsOf<Units>> & {
|
|
23
78
|
authenticators: readonly import("@btravstack/http-server/internal").SchemeProviders<A>[];
|
|
24
79
|
};
|
|
25
|
-
//#endregion
|
|
80
|
+
//#endregion
|
|
81
|
+
export type { GraphqlContext, GraphqlOptions };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,25 +1,81 @@
|
|
|
1
|
+
import { Plugin, YogaInitialContext, YogaServerOptions } from "graphql-yoga";
|
|
2
|
+
import { Observers } from "@btravstack/core";
|
|
1
3
|
import { AnyPort, Provider, Scope } from "@btravstack/di";
|
|
2
|
-
import { Authenticators, HttpAuth, HttpConfig, HttpHandler, HttpUnit, KindOf, Kinds, RequiresGate, SchemePortsOf, UnitFor, UnitsNeedsOf, UnitsOf } from "@btravstack/http-server/internal";
|
|
3
|
-
import {
|
|
4
|
+
import { Authenticators, HttpAuth, HttpConfig, HttpHandler, HttpUnit, KindOf, Kinds, Principal, RequiresGate, SchemePortsOf, SchemesFrom, SchemesOf, UnitFor, UnitsNeedsOf, UnitsOf } from "@btravstack/http-server/internal";
|
|
5
|
+
import { AsyncResult, Result } from "unthrown";
|
|
6
|
+
import { GraphQLError, GraphQLSchema } from "graphql";
|
|
7
|
+
import { IncomingMessage } from "node:http";
|
|
4
8
|
import { OneScheme, Requirements } from "@btravstack/contract";
|
|
5
|
-
|
|
9
|
+
//#region src/field.d.ts
|
|
10
|
+
/**
|
|
11
|
+
* A resolver's answer from a `Result`, so a modeled failure is never thrown by
|
|
12
|
+
* hand. `Ok` is the field's value. `Err` is the `GraphQLError` the resolver's
|
|
13
|
+
* own triage minted from its `E` — an exhaustive `mapErrCases`, or
|
|
14
|
+
* `flatMapErrCases` where one refusal is `Ok(null)`, so widening `E` fails
|
|
15
|
+
* every resolver that folds it — and GraphQL reports it on this
|
|
16
|
+
* field's path, aliases and list indexes included, while its siblings resolve:
|
|
17
|
+
* its message and `extensions` reach the client, its `originalError` does not.
|
|
18
|
+
* A defect is masked to `Unexpected error.` whatever its cause, and `graphql()`
|
|
19
|
+
* reports it to `Observers`.
|
|
20
|
+
*
|
|
21
|
+
* **It answers a `Promise`, and that is the boundary**: a GraphQL resolver
|
|
22
|
+
* returns a value or a promise and reports a field's error by rejecting, so
|
|
23
|
+
* this is where a `Result` leaves for GraphQL's own channel — the one place a
|
|
24
|
+
* resolver needs, as `runMain` is for a process.
|
|
25
|
+
*/
|
|
26
|
+
export declare const fieldResult: <T>(result: AsyncResult<T, GraphQLError> | Result<T, GraphQLError>) => Promise<T>;
|
|
27
|
+
//#endregion
|
|
6
28
|
//#region src/graphql.d.ts
|
|
7
29
|
type YogaCors = YogaServerOptions<Record<string, unknown>, Record<string, unknown>>["cors"];
|
|
8
30
|
type MissingUnitPorts<U extends Readonly<Record<string, AnyPort>>, Units, K extends string> = Exclude<keyof U, keyof UnitFor<U, Units, K>>;
|
|
9
31
|
type UnitGate<U extends Readonly<Record<string, AnyPort>>, Units, K extends string> = [MissingUnitPorts<U, Units, K>] extends [never] ? unknown : {
|
|
10
32
|
readonly "UNIT PORT MISSING — the selected kind does not export": MissingUnitPorts<U, Units, K>;
|
|
11
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* The context `graphql()` hands Yoga for an operation: what a resolver and a
|
|
36
|
+
* plugin's operation hooks (`onParse`, `onValidate`, `onContextBuilding`,
|
|
37
|
+
* `onExecute`, `onSubscribe`) read beside Yoga's own `request`. `principal` is
|
|
38
|
+
* the caller `requires` resolved, `undefined` without `requires`; `unit` is the
|
|
39
|
+
* `unit` record read off the forked kind; `incoming` is the Node request.
|
|
40
|
+
*
|
|
41
|
+
* Those hooks run only once the caller is authenticated. A preflight and a
|
|
42
|
+
* refusal end in Yoga's server hooks (`onRequest`, `onResponse`), whose
|
|
43
|
+
* context is not this type, so a plugin cannot claim a principal there.
|
|
44
|
+
*/
|
|
45
|
+
type GraphqlContext<Principal = unknown, Unit = Readonly<Record<string, unknown>>> = {
|
|
46
|
+
readonly principal: Principal;
|
|
47
|
+
readonly incoming: IncomingMessage;
|
|
48
|
+
readonly unit: Unit;
|
|
49
|
+
readonly signal: AbortSignal;
|
|
50
|
+
};
|
|
51
|
+
/** The {@link GraphqlContext} one `graphql()` call builds, from its own options. */
|
|
52
|
+
type ContextOf<A extends Authenticators, Units, R extends Requirements, U extends Readonly<Record<string, AnyPort>>> = GraphqlContext<[R] extends [never] ? undefined : Principal<SchemesOf<R>, SchemesFrom<A>>, UnitFor<U, Units, KindOf<R>>>;
|
|
53
|
+
/**
|
|
54
|
+
* Each plugin, unless the context this call builds lacks something it reads, or
|
|
55
|
+
* the entry is not a plugin at all.
|
|
56
|
+
* Per element, because Yoga's `Plugin<C>` is invariant in `C`: a plugin typed
|
|
57
|
+
* for the real context is not a `Plugin`, so none could be widened to one.
|
|
58
|
+
*/
|
|
59
|
+
type PluginsGate<PS extends readonly unknown[], C> = { readonly [I in keyof PS]: PS[I] extends Plugin<infer X> ? YogaInitialContext & C extends X ? PS[I] : "PLUGIN CONTEXT MISMATCH — this plugin reads what this graphql() call does not put in its context" : {
|
|
60
|
+
readonly "NOT A PLUGIN — this entry is not a Yoga plugin": PS[I];
|
|
61
|
+
}; };
|
|
12
62
|
/** Serve a GraphQL schema as one answerer under the existing HTTP runtime. */
|
|
13
|
-
type GraphqlOptions<A extends Authenticators, Units extends UnitsOf<A>, R extends Requirements & { readonly [I in keyof R]: OneScheme<R[I]>; }, U extends Readonly<Record<string, AnyPort
|
|
63
|
+
type GraphqlOptions<A extends Authenticators, Units extends UnitsOf<A>, R extends Requirements & { readonly [I in keyof R]: OneScheme<R[I]>; }, U extends Readonly<Record<string, AnyPort>>, PS extends readonly unknown[] = readonly Plugin[]> = {
|
|
14
64
|
readonly schema: GraphQLSchema;
|
|
15
65
|
readonly prefix?: `/${string}`;
|
|
16
66
|
readonly cors?: YogaCors;
|
|
17
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Yoga plugins. Each may be typed by the context it reads — a
|
|
69
|
+
* `Plugin<{ unit: … }>` — and is checked against the {@link GraphqlContext}
|
|
70
|
+
* this call builds.
|
|
71
|
+
*/
|
|
72
|
+
readonly plugins?: (PS & PluginsGate<PS, ContextOf<A, Units, R, U>>) | readonly Plugin<YogaInitialContext & ContextOf<A, Units, R, U>>[];
|
|
18
73
|
readonly units?: Units & { readonly [K in Exclude<keyof Units, Kinds<A>>]: never; };
|
|
19
74
|
readonly unit?: U & UnitGate<U, Units, KindOf<R>>;
|
|
20
75
|
readonly requires?: R & RequiresGate<R, { readonly [K in keyof A]: A[K]["scope"]; }>;
|
|
21
76
|
};
|
|
22
|
-
export declare const graphql: <A extends Authenticators, Units extends UnitsOf<A> = Record<never, never>, const R extends Requirements & { readonly [I in keyof R]: OneScheme<R[I]>; } = never, U extends Readonly<Record<string, AnyPort>> = Record<never, never
|
|
77
|
+
export declare const graphql: <A extends Authenticators, Units extends UnitsOf<A> = Record<never, never>, const R extends Requirements & { readonly [I in keyof R]: OneScheme<R[I]>; } = never, U extends Readonly<Record<string, AnyPort>> = Record<never, never>, const PS extends readonly unknown[] = []>(api: HttpAuth<A>, options: GraphqlOptions<A, Units, R, U, PS>) => Provider<HttpHandler, never, HttpConfig | HttpUnit | Observers | Scope | SchemePortsOf<R> | UnitsNeedsOf<Units>> & {
|
|
23
78
|
authenticators: readonly import("@btravstack/http-server/internal").SchemeProviders<A>[];
|
|
24
79
|
};
|
|
25
|
-
//#endregion
|
|
80
|
+
//#endregion
|
|
81
|
+
export type { GraphqlContext, GraphqlOptions };
|
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,37 @@
|
|
|
1
|
+
import { createGraphQLError, createYoga, maskError } from "graphql-yoga";
|
|
2
|
+
import { Observers, observe } from "@btravstack/core";
|
|
1
3
|
import { Provider } from "@btravstack/di";
|
|
2
4
|
import { HttpConfig, HttpHandler, HttpUnit, forkUnit, principalOf, resolveScheme, schemeDeps, schemeServices } from "@btravstack/http-server/internal";
|
|
3
|
-
import { createYoga } from "graphql-yoga";
|
|
4
5
|
import { P } from "unthrown";
|
|
6
|
+
//#region src/field.ts
|
|
7
|
+
/**
|
|
8
|
+
* A resolver's answer from a `Result`, so a modeled failure is never thrown by
|
|
9
|
+
* hand. `Ok` is the field's value. `Err` is the `GraphQLError` the resolver's
|
|
10
|
+
* own triage minted from its `E` — an exhaustive `mapErrCases`, or
|
|
11
|
+
* `flatMapErrCases` where one refusal is `Ok(null)`, so widening `E` fails
|
|
12
|
+
* every resolver that folds it — and GraphQL reports it on this
|
|
13
|
+
* field's path, aliases and list indexes included, while its siblings resolve:
|
|
14
|
+
* its message and `extensions` reach the client, its `originalError` does not.
|
|
15
|
+
* A defect is masked to `Unexpected error.` whatever its cause, and `graphql()`
|
|
16
|
+
* reports it to `Observers`.
|
|
17
|
+
*
|
|
18
|
+
* **It answers a `Promise`, and that is the boundary**: a GraphQL resolver
|
|
19
|
+
* returns a value or a promise and reports a field's error by rejecting, so
|
|
20
|
+
* this is where a `Result` leaves for GraphQL's own channel — the one place a
|
|
21
|
+
* resolver needs, as `runMain` is for a process.
|
|
22
|
+
*/
|
|
23
|
+
const fieldResult = async (result) => {
|
|
24
|
+
const settled = await result;
|
|
25
|
+
if (settled.isDefect()) throw new Error("fieldResult: the field's result was a defect", { cause: settled.cause });
|
|
26
|
+
if (settled.isErr()) throw createGraphQLError(settled.error.message, { extensions: settled.error.extensions });
|
|
27
|
+
return settled.value;
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
5
30
|
//#region src/graphql.ts
|
|
6
31
|
const dispose = Symbol("graphql.dispose");
|
|
32
|
+
/** What failed: GraphQL locates a resolver's error by wrapping it, and a log line walks `cause`, not `originalError`. */
|
|
33
|
+
const causeOf = (error) => typeof error === "object" && error !== null && "originalError" in error && error.originalError ? error.originalError : error;
|
|
34
|
+
const pathOf = (error) => typeof error === "object" && error !== null && "path" in error && Array.isArray(error.path) ? error.path.join(".") : void 0;
|
|
7
35
|
const graphql = (api, options) => {
|
|
8
36
|
const mount = options.prefix ?? "/graphql";
|
|
9
37
|
let end = mount.length;
|
|
@@ -15,11 +43,13 @@ const graphql = (api, options) => {
|
|
|
15
43
|
inject: {
|
|
16
44
|
config: HttpConfig,
|
|
17
45
|
units: HttpUnit,
|
|
46
|
+
observers: Observers,
|
|
18
47
|
...schemeDeps(schemes)
|
|
19
48
|
},
|
|
20
49
|
sync: (services) => {
|
|
21
50
|
const config = services.config;
|
|
22
51
|
const units = services.units;
|
|
52
|
+
const observers = services.observers;
|
|
23
53
|
if (options.units !== void 0) {
|
|
24
54
|
for (const kind of requirements === void 0 ? ["anonymous"] : schemes) if ((options.units[kind] ?? options.units["anonymous"]) !== (units[kind] ?? units["anonymous"])) throw new Error(`[graphql-server] unit kind ${JSON.stringify(kind)} differs from HttpUnit`);
|
|
25
55
|
}
|
|
@@ -31,6 +61,20 @@ const graphql = (api, options) => {
|
|
|
31
61
|
plugins: [{ onRequest: ({ serverContext, fetchAPI, endResponse }) => {
|
|
32
62
|
if (serverContext.refusalStatus !== void 0) endResponse(new fetchAPI.Response(null, { status: serverContext.refusalStatus }));
|
|
33
63
|
} }, ...options.plugins ?? []],
|
|
64
|
+
maskedErrors: { maskError: (error, message, isDev) => {
|
|
65
|
+
const masked = maskError(error, message, isDev);
|
|
66
|
+
if (masked !== error) observe(observers, {
|
|
67
|
+
component: "graphql",
|
|
68
|
+
name: "defect",
|
|
69
|
+
attributes: {},
|
|
70
|
+
details: { "graphql.path": pathOf(error) },
|
|
71
|
+
traced: false
|
|
72
|
+
})({
|
|
73
|
+
outcome: "error",
|
|
74
|
+
cause: causeOf(error)
|
|
75
|
+
});
|
|
76
|
+
return masked;
|
|
77
|
+
} },
|
|
34
78
|
logging: false,
|
|
35
79
|
graphiql: false,
|
|
36
80
|
landingPage: false,
|
|
@@ -83,4 +127,4 @@ const graphql = (api, options) => {
|
|
|
83
127
|
return Object.assign(typed, { authenticators: api.providers });
|
|
84
128
|
};
|
|
85
129
|
//#endregion
|
|
86
|
-
export { graphql };
|
|
130
|
+
export { fieldResult, graphql };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btravstack/graphql-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "The GraphQL Yoga answerer for @btravstack/http-server",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"graphql",
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
"./package.json": "./package.json"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@btravstack/contract": "0.
|
|
45
|
-
"@btravstack/core": "0.
|
|
46
|
-
"@btravstack/di": "0.
|
|
47
|
-
"@btravstack/http-server": "0.
|
|
44
|
+
"@btravstack/contract": "0.27.0",
|
|
45
|
+
"@btravstack/core": "0.27.0",
|
|
46
|
+
"@btravstack/di": "0.27.0",
|
|
47
|
+
"@btravstack/http-server": "0.27.0",
|
|
48
48
|
"@btravstack/tsconfig": "0.4.0",
|
|
49
49
|
"@types/node": "26.6.4",
|
|
50
50
|
"@unthrown/vitest": "5.12.0",
|
|
@@ -58,9 +58,10 @@
|
|
|
58
58
|
"vitest": "5.0.3"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@btravstack/contract": "^0.
|
|
62
|
-
"@btravstack/
|
|
63
|
-
"@btravstack/
|
|
61
|
+
"@btravstack/contract": "^0.27.0",
|
|
62
|
+
"@btravstack/core": "^0.27.0",
|
|
63
|
+
"@btravstack/di": "^0.27.0",
|
|
64
|
+
"@btravstack/http-server": "^0.27.0",
|
|
64
65
|
"graphql": "^16.0.0 || ^17.0.0",
|
|
65
66
|
"graphql-yoga": "^5.24.1",
|
|
66
67
|
"unthrown": "^5.7.0"
|