@btravstack/graphql-server 0.28.0 → 0.30.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 +5 -3
- package/dist/index.cjs +25 -3
- package/dist/index.d.cts +11 -4
- package/dist/index.d.mts +11 -4
- package/dist/index.mjs +26 -4
- package/package.json +12 -12
package/README.md
CHANGED
|
@@ -11,9 +11,11 @@ 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
13
|
options; the wildcard CORS default and Yoga console logging are disabled, and
|
|
14
|
-
a masked defect is reported to `Observers` instead. GraphiQL
|
|
15
|
-
introspection stay off unless
|
|
16
|
-
`developerTools: true`) turns
|
|
14
|
+
a masked defect is reported to `Observers` instead. GraphiQL,
|
|
15
|
+
introspection and "Did you mean …" suggestions stay off unless
|
|
16
|
+
`GRAPHQL_DEVELOPER_TOOLS=true` (or `developerTools: true`) turns them on. A
|
|
17
|
+
document over `GRAPHQL_MAX_TOKENS` (default `1000`, pinned by `maxTokens`) is
|
|
18
|
+
refused by the parser, before validation. `fieldResult(result)`
|
|
17
19
|
answers a resolver from an `AsyncResult`, with a refusal on its own field's
|
|
18
20
|
path.
|
|
19
21
|
Preflights use Yoga's CORS policy before authentication, and Yoga plugins are
|
package/dist/index.cjs
CHANGED
|
@@ -38,6 +38,21 @@ const noIntrospection = (context) => ({ Field: (node) => {
|
|
|
38
38
|
if (node.name.value === "__schema" || node.name.value === "__type") context.reportError((0, graphql_yoga.createGraphQLError)(`GraphQL introspection is not allowed, but the query contained ${node.name.value}`, { nodes: [node] }));
|
|
39
39
|
} });
|
|
40
40
|
const withoutIntrospection = { onValidate: ({ addValidationRule }) => addValidationRule(noIntrospection) };
|
|
41
|
+
const SUGGESTION = / Did you mean [^?]*\?/g;
|
|
42
|
+
const hideSuggestion = (error) => {
|
|
43
|
+
if (error instanceof Error) error.message = error.message.replace(SUGGESTION, "");
|
|
44
|
+
};
|
|
45
|
+
const hideSuggestionsIn = (payload) => (0, graphql_yoga.handleStreamOrSingleExecutionResult)(payload, ({ result }) => result.errors?.forEach(hideSuggestion));
|
|
46
|
+
const withoutSuggestions = {
|
|
47
|
+
onValidate: () => ({ result }) => result.forEach(hideSuggestion),
|
|
48
|
+
onExecute: () => ({ onExecuteDone: hideSuggestionsIn }),
|
|
49
|
+
onSubscribe: () => ({ onSubscribeResult: hideSuggestionsIn })
|
|
50
|
+
};
|
|
51
|
+
const DEFAULT_MAX_TOKENS = 1e3;
|
|
52
|
+
const tokenLimit = (maxTokens) => ({ onParse: ({ parseFn, setParseFn }) => setParseFn((source, options) => parseFn(source, {
|
|
53
|
+
...options,
|
|
54
|
+
maxTokens
|
|
55
|
+
})) });
|
|
41
56
|
const graphql = (api, options) => {
|
|
42
57
|
const mount = options.prefix ?? "/graphql";
|
|
43
58
|
let end = mount.length;
|
|
@@ -45,7 +60,13 @@ const graphql = (api, options) => {
|
|
|
45
60
|
const prefix = mount.slice(0, end);
|
|
46
61
|
const requirements = options.requires;
|
|
47
62
|
const schemes = [...new Set(requirements?.flatMap((requirement) => Object.keys(requirement)) ?? [])];
|
|
48
|
-
const
|
|
63
|
+
const configSchema = _btravstack_config.Config.object({
|
|
64
|
+
developerTools: _btravstack_config.Config.pinned(options.developerTools, _btravstack_config.Config.boolean("GRAPHQL_DEVELOPER_TOOLS", { default: false })),
|
|
65
|
+
maxTokens: _btravstack_config.Config.pinned(options.maxTokens === false ? 0 : options.maxTokens, _btravstack_config.Config.integer("GRAPHQL_MAX_TOKENS", {
|
|
66
|
+
default: DEFAULT_MAX_TOKENS,
|
|
67
|
+
min: 0
|
|
68
|
+
}))
|
|
69
|
+
});
|
|
49
70
|
const typed = _btravstack_di.Provider.member(_btravstack_http_server_internal.HttpHandler)({
|
|
50
71
|
inject: {
|
|
51
72
|
env: _btravstack_config.Env,
|
|
@@ -54,7 +75,7 @@ const graphql = (api, options) => {
|
|
|
54
75
|
observers: _btravstack_core.Observers,
|
|
55
76
|
...(0, _btravstack_http_server_internal.schemeDeps)(schemes)
|
|
56
77
|
},
|
|
57
|
-
make: (services) => _btravstack_config.Config.parse("GraphqlConfig",
|
|
78
|
+
make: (services) => _btravstack_config.Config.parse("GraphqlConfig", configSchema)(services.env).map(({ developerTools, maxTokens }) => {
|
|
58
79
|
const config = services.config;
|
|
59
80
|
const units = services.units;
|
|
60
81
|
const observers = services.observers;
|
|
@@ -70,7 +91,8 @@ const graphql = (api, options) => {
|
|
|
70
91
|
{ onRequest: ({ serverContext, fetchAPI, endResponse }) => {
|
|
71
92
|
if (serverContext.refusalStatus !== void 0) endResponse(new fetchAPI.Response(null, { status: serverContext.refusalStatus }));
|
|
72
93
|
} },
|
|
73
|
-
...developerTools ? [] : [withoutIntrospection],
|
|
94
|
+
...developerTools ? [] : [withoutIntrospection, withoutSuggestions],
|
|
95
|
+
...maxTokens === 0 ? [] : [tokenLimit(maxTokens)],
|
|
74
96
|
...options.plugins ?? []
|
|
75
97
|
],
|
|
76
98
|
maskedErrors: { maskError: (error, message, isDev) => {
|
package/dist/index.d.cts
CHANGED
|
@@ -66,11 +66,18 @@ type GraphqlOptions<A extends Authenticators, Units extends UnitsOf<A>, R extend
|
|
|
66
66
|
readonly prefix?: `/${string}`;
|
|
67
67
|
readonly cors?: YogaCors;
|
|
68
68
|
/**
|
|
69
|
-
* GraphiQL and
|
|
70
|
-
* `GRAPHQL_DEVELOPER_TOOLS`, which defaults to `false`, so
|
|
71
|
-
* exposes
|
|
69
|
+
* GraphiQL, introspection and the "Did you mean …" suggestions in an error,
|
|
70
|
+
* all or none — pins `GRAPHQL_DEVELOPER_TOOLS`, which defaults to `false`, so
|
|
71
|
+
* a deployment exposes none of them unless it says so.
|
|
72
72
|
*/
|
|
73
73
|
readonly developerTools?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Pins `GRAPHQL_MAX_TOKENS` (default `1000`) — the most tokens graphql-js
|
|
76
|
+
* parses in one document before refusing it, so an oversized document is
|
|
77
|
+
* refused before any validation rule runs on it. `false` pins the unbounded
|
|
78
|
+
* `0`.
|
|
79
|
+
*/
|
|
80
|
+
readonly maxTokens?: number | false;
|
|
74
81
|
/**
|
|
75
82
|
* Yoga plugins. Each may be typed by the context it reads — a
|
|
76
83
|
* `Plugin<{ unit: … }>` — and is checked against the {@link GraphqlContext}
|
|
@@ -81,7 +88,7 @@ type GraphqlOptions<A extends Authenticators, Units extends UnitsOf<A>, R extend
|
|
|
81
88
|
readonly unit?: U & UnitGate<U, Units, KindOf<R>>;
|
|
82
89
|
readonly requires?: R & RequiresGate<R, { readonly [K in keyof A]: A[K]["scope"]; }>;
|
|
83
90
|
};
|
|
84
|
-
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, ConfigInvalid, HttpConfig | HttpUnit | Observers | Scope | SchemePortsOf<R> | UnitsNeedsOf<Units> | EnvReading<never, "GRAPHQL_DEVELOPER_TOOLS">> & {
|
|
91
|
+
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, ConfigInvalid, HttpConfig | HttpUnit | Observers | Scope | SchemePortsOf<R> | UnitsNeedsOf<Units> | EnvReading<never, "GRAPHQL_DEVELOPER_TOOLS" | "GRAPHQL_MAX_TOKENS">> & {
|
|
85
92
|
authenticators: readonly import("@btravstack/http-server/internal").SchemeProviders<A>[];
|
|
86
93
|
};
|
|
87
94
|
//#endregion
|
package/dist/index.d.mts
CHANGED
|
@@ -66,11 +66,18 @@ type GraphqlOptions<A extends Authenticators, Units extends UnitsOf<A>, R extend
|
|
|
66
66
|
readonly prefix?: `/${string}`;
|
|
67
67
|
readonly cors?: YogaCors;
|
|
68
68
|
/**
|
|
69
|
-
* GraphiQL and
|
|
70
|
-
* `GRAPHQL_DEVELOPER_TOOLS`, which defaults to `false`, so
|
|
71
|
-
* exposes
|
|
69
|
+
* GraphiQL, introspection and the "Did you mean …" suggestions in an error,
|
|
70
|
+
* all or none — pins `GRAPHQL_DEVELOPER_TOOLS`, which defaults to `false`, so
|
|
71
|
+
* a deployment exposes none of them unless it says so.
|
|
72
72
|
*/
|
|
73
73
|
readonly developerTools?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Pins `GRAPHQL_MAX_TOKENS` (default `1000`) — the most tokens graphql-js
|
|
76
|
+
* parses in one document before refusing it, so an oversized document is
|
|
77
|
+
* refused before any validation rule runs on it. `false` pins the unbounded
|
|
78
|
+
* `0`.
|
|
79
|
+
*/
|
|
80
|
+
readonly maxTokens?: number | false;
|
|
74
81
|
/**
|
|
75
82
|
* Yoga plugins. Each may be typed by the context it reads — a
|
|
76
83
|
* `Plugin<{ unit: … }>` — and is checked against the {@link GraphqlContext}
|
|
@@ -81,7 +88,7 @@ type GraphqlOptions<A extends Authenticators, Units extends UnitsOf<A>, R extend
|
|
|
81
88
|
readonly unit?: U & UnitGate<U, Units, KindOf<R>>;
|
|
82
89
|
readonly requires?: R & RequiresGate<R, { readonly [K in keyof A]: A[K]["scope"]; }>;
|
|
83
90
|
};
|
|
84
|
-
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, ConfigInvalid, HttpConfig | HttpUnit | Observers | Scope | SchemePortsOf<R> | UnitsNeedsOf<Units> | EnvReading<never, "GRAPHQL_DEVELOPER_TOOLS">> & {
|
|
91
|
+
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, ConfigInvalid, HttpConfig | HttpUnit | Observers | Scope | SchemePortsOf<R> | UnitsNeedsOf<Units> | EnvReading<never, "GRAPHQL_DEVELOPER_TOOLS" | "GRAPHQL_MAX_TOKENS">> & {
|
|
85
92
|
authenticators: readonly import("@btravstack/http-server/internal").SchemeProviders<A>[];
|
|
86
93
|
};
|
|
87
94
|
//#endregion
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createGraphQLError, createYoga, maskError } from "graphql-yoga";
|
|
1
|
+
import { createGraphQLError, createYoga, handleStreamOrSingleExecutionResult, maskError } from "graphql-yoga";
|
|
2
2
|
import { Config, Env } from "@btravstack/config";
|
|
3
3
|
import { Observers, observe } from "@btravstack/core";
|
|
4
4
|
import { Provider } from "@btravstack/di";
|
|
@@ -37,6 +37,21 @@ const noIntrospection = (context) => ({ Field: (node) => {
|
|
|
37
37
|
if (node.name.value === "__schema" || node.name.value === "__type") context.reportError(createGraphQLError(`GraphQL introspection is not allowed, but the query contained ${node.name.value}`, { nodes: [node] }));
|
|
38
38
|
} });
|
|
39
39
|
const withoutIntrospection = { onValidate: ({ addValidationRule }) => addValidationRule(noIntrospection) };
|
|
40
|
+
const SUGGESTION = / Did you mean [^?]*\?/g;
|
|
41
|
+
const hideSuggestion = (error) => {
|
|
42
|
+
if (error instanceof Error) error.message = error.message.replace(SUGGESTION, "");
|
|
43
|
+
};
|
|
44
|
+
const hideSuggestionsIn = (payload) => handleStreamOrSingleExecutionResult(payload, ({ result }) => result.errors?.forEach(hideSuggestion));
|
|
45
|
+
const withoutSuggestions = {
|
|
46
|
+
onValidate: () => ({ result }) => result.forEach(hideSuggestion),
|
|
47
|
+
onExecute: () => ({ onExecuteDone: hideSuggestionsIn }),
|
|
48
|
+
onSubscribe: () => ({ onSubscribeResult: hideSuggestionsIn })
|
|
49
|
+
};
|
|
50
|
+
const DEFAULT_MAX_TOKENS = 1e3;
|
|
51
|
+
const tokenLimit = (maxTokens) => ({ onParse: ({ parseFn, setParseFn }) => setParseFn((source, options) => parseFn(source, {
|
|
52
|
+
...options,
|
|
53
|
+
maxTokens
|
|
54
|
+
})) });
|
|
40
55
|
const graphql = (api, options) => {
|
|
41
56
|
const mount = options.prefix ?? "/graphql";
|
|
42
57
|
let end = mount.length;
|
|
@@ -44,7 +59,13 @@ const graphql = (api, options) => {
|
|
|
44
59
|
const prefix = mount.slice(0, end);
|
|
45
60
|
const requirements = options.requires;
|
|
46
61
|
const schemes = [...new Set(requirements?.flatMap((requirement) => Object.keys(requirement)) ?? [])];
|
|
47
|
-
const
|
|
62
|
+
const configSchema = Config.object({
|
|
63
|
+
developerTools: Config.pinned(options.developerTools, Config.boolean("GRAPHQL_DEVELOPER_TOOLS", { default: false })),
|
|
64
|
+
maxTokens: Config.pinned(options.maxTokens === false ? 0 : options.maxTokens, Config.integer("GRAPHQL_MAX_TOKENS", {
|
|
65
|
+
default: DEFAULT_MAX_TOKENS,
|
|
66
|
+
min: 0
|
|
67
|
+
}))
|
|
68
|
+
});
|
|
48
69
|
const typed = Provider.member(HttpHandler)({
|
|
49
70
|
inject: {
|
|
50
71
|
env: Env,
|
|
@@ -53,7 +74,7 @@ const graphql = (api, options) => {
|
|
|
53
74
|
observers: Observers,
|
|
54
75
|
...schemeDeps(schemes)
|
|
55
76
|
},
|
|
56
|
-
make: (services) => Config.parse("GraphqlConfig",
|
|
77
|
+
make: (services) => Config.parse("GraphqlConfig", configSchema)(services.env).map(({ developerTools, maxTokens }) => {
|
|
57
78
|
const config = services.config;
|
|
58
79
|
const units = services.units;
|
|
59
80
|
const observers = services.observers;
|
|
@@ -69,7 +90,8 @@ const graphql = (api, options) => {
|
|
|
69
90
|
{ onRequest: ({ serverContext, fetchAPI, endResponse }) => {
|
|
70
91
|
if (serverContext.refusalStatus !== void 0) endResponse(new fetchAPI.Response(null, { status: serverContext.refusalStatus }));
|
|
71
92
|
} },
|
|
72
|
-
...developerTools ? [] : [withoutIntrospection],
|
|
93
|
+
...developerTools ? [] : [withoutIntrospection, withoutSuggestions],
|
|
94
|
+
...maxTokens === 0 ? [] : [tokenLimit(maxTokens)],
|
|
73
95
|
...options.plugins ?? []
|
|
74
96
|
],
|
|
75
97
|
maskedErrors: { maskError: (error, message, isDev) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btravstack/graphql-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "The GraphQL Yoga answerer for @btravstack/http-server",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"graphql",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"./package.json": "./package.json"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@btravstack/config": "0.
|
|
45
|
-
"@btravstack/contract": "0.
|
|
46
|
-
"@btravstack/core": "0.
|
|
47
|
-
"@btravstack/di": "0.
|
|
48
|
-
"@btravstack/http-server": "0.
|
|
44
|
+
"@btravstack/config": "0.30.0",
|
|
45
|
+
"@btravstack/contract": "0.30.0",
|
|
46
|
+
"@btravstack/core": "0.30.0",
|
|
47
|
+
"@btravstack/di": "0.30.0",
|
|
48
|
+
"@btravstack/http-server": "0.30.0",
|
|
49
49
|
"@btravstack/tsconfig": "0.4.0",
|
|
50
50
|
"@types/node": "26.6.4",
|
|
51
51
|
"@unthrown/vitest": "5.12.0",
|
|
@@ -59,12 +59,12 @@
|
|
|
59
59
|
"vitest": "5.0.3"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
|
-
"@btravstack/config": "^0.
|
|
63
|
-
"@btravstack/contract": "^0.
|
|
64
|
-
"@btravstack/core": "^0.
|
|
65
|
-
"@btravstack/di": "^0.
|
|
66
|
-
"@btravstack/http-server": "^0.
|
|
67
|
-
"graphql": "^16.
|
|
62
|
+
"@btravstack/config": "^0.30.0",
|
|
63
|
+
"@btravstack/contract": "^0.30.0",
|
|
64
|
+
"@btravstack/core": "^0.30.0",
|
|
65
|
+
"@btravstack/di": "^0.30.0",
|
|
66
|
+
"@btravstack/http-server": "^0.30.0",
|
|
67
|
+
"graphql": "^16.6.0 || ^17.0.0",
|
|
68
68
|
"graphql-yoga": "^5.24.1",
|
|
69
69
|
"unthrown": "^5.7.0"
|
|
70
70
|
},
|