@btravstack/graphql-server 0.0.0-stage → 0.21.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Benoit TRAVERS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,18 @@
1
- # Temporary Holding Version
1
+ # @btravstack/graphql-server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A GraphQL Yoga answerer for the protocol-neutral
4
+ [`@btravstack/http-server`](../http-server) runtime. Supply any
5
+ `GraphQLSchema`; the package has required GraphQL and Yoga peers and no oRPC
6
+ server dependency. `graphql(defineAuth(), { schema })` contributes a
7
+ `HttpHandler` member. A request-scoped unit and the Node incoming request are
8
+ available to resolvers.
9
+ Pass `units` with `unit` to check each injected port against the module the
10
+ selected request kind actually forks. Pass the same `units` record to
11
+ `httpServer({ unit: units })` so the runtime can apply unit test overrides.
12
+ Yoga plugins and CORS policy are explicit
13
+ options; the wildcard CORS default and Yoga console logging are disabled.
14
+ Preflights use Yoga's CORS policy before authentication, and Yoga plugins are
15
+ disposed when the application stops.
16
+
17
+ See [Serve GraphQL](https://btravstack.github.io/btravstack/how-to/serve-graphql)
18
+ and the [separate GraphQL gateway](../../examples/order-graphql-api).
package/dist/index.cjs ADDED
@@ -0,0 +1,87 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _btravstack_di = require("@btravstack/di");
3
+ let _btravstack_http_server_internal = require("@btravstack/http-server/internal");
4
+ let graphql_yoga = require("graphql-yoga");
5
+ let unthrown = require("unthrown");
6
+ //#region src/graphql.ts
7
+ const dispose = Symbol("graphql.dispose");
8
+ const graphql = (api, options) => {
9
+ const mount = options.prefix ?? "/graphql";
10
+ let end = mount.length;
11
+ while (end > 1 && mount.charCodeAt(end - 1) === 47) end--;
12
+ const prefix = mount.slice(0, end);
13
+ const requirements = options.requires;
14
+ const schemes = [...new Set(requirements?.flatMap((requirement) => Object.keys(requirement)) ?? [])];
15
+ const typed = _btravstack_di.Provider.member(_btravstack_http_server_internal.HttpHandler)({
16
+ inject: {
17
+ config: _btravstack_http_server_internal.HttpConfig,
18
+ units: _btravstack_http_server_internal.HttpUnit,
19
+ ...(0, _btravstack_http_server_internal.schemeDeps)(schemes)
20
+ },
21
+ sync: (services) => {
22
+ const config = services.config;
23
+ const units = services.units;
24
+ if (options.units !== void 0) {
25
+ 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
+ }
27
+ const authenticators = (0, _btravstack_http_server_internal.schemeServices)(schemes, services);
28
+ const yoga = (0, graphql_yoga.createYoga)({
29
+ schema: options.schema,
30
+ graphqlEndpoint: prefix,
31
+ cors: options.cors ?? (config.corsOrigin === "" ? false : { origin: config.corsOrigin.split(",").map((origin) => origin.trim()) }),
32
+ plugins: [{ onRequest: ({ serverContext, fetchAPI, endResponse }) => {
33
+ if (serverContext.refusalStatus !== void 0) endResponse(new fetchAPI.Response(null, { status: serverContext.refusalStatus }));
34
+ } }, ...options.plugins ?? []],
35
+ logging: false,
36
+ graphiql: false,
37
+ landingPage: false,
38
+ multipart: false,
39
+ maxRequestBodySize: config.bodyLimit === 0 ? false : config.bodyLimit,
40
+ disposeOnProcessTerminate: false
41
+ });
42
+ return {
43
+ prefix,
44
+ handle: async (request, response, signal, host) => {
45
+ if (request.method === "OPTIONS" && request.headers["access-control-request-method"] !== void 0) {
46
+ await yoga.handle(request, response, {
47
+ principal: void 0,
48
+ incoming: request,
49
+ unit: {},
50
+ signal
51
+ });
52
+ return;
53
+ }
54
+ let resolved;
55
+ if (requirements !== void 0) {
56
+ const result = await (0, _btravstack_http_server_internal.resolveScheme)(requirements, authenticators, request.headers).mapErrCases((matcher) => matcher.with(unthrown.P.tag("Unauthenticated"), () => 401).with(unthrown.P.tag("UnderScoped"), () => 403));
57
+ if (result.isDefect()) throw result.cause;
58
+ if (result.isErr()) {
59
+ await yoga.handle(request, response, {
60
+ principal: void 0,
61
+ incoming: request,
62
+ unit: {},
63
+ signal,
64
+ refusalStatus: result.error
65
+ });
66
+ return;
67
+ }
68
+ resolved = result.value;
69
+ }
70
+ const forked = await (0, _btravstack_http_server_internal.forkUnit)(host, units, api.principals, resolved, options.unit ?? {});
71
+ if (forked.isDefect()) throw forked.cause;
72
+ await yoga.handle(request, response, {
73
+ principal: requirements === void 0 ? void 0 : (0, _btravstack_http_server_internal.principalOf)(requirements, resolved),
74
+ incoming: request,
75
+ unit: forked.get(),
76
+ signal
77
+ });
78
+ },
79
+ [dispose]: () => yoga.dispose()
80
+ };
81
+ },
82
+ onStop: (answerer) => answerer[dispose]()
83
+ });
84
+ return Object.assign(typed, { authenticators: api.providers });
85
+ };
86
+ //#endregion
87
+ exports.graphql = graphql;
@@ -0,0 +1,25 @@
1
+ import { OneScheme, Requirements } from "@btravstack/contract";
2
+ 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 { GraphQLSchema } from "graphql";
5
+ import { Plugin, YogaServerOptions } from "graphql-yoga";
6
+ //#region src/graphql.d.ts
7
+ type YogaCors = YogaServerOptions<Record<string, unknown>, Record<string, unknown>>["cors"];
8
+ type MissingUnitPorts<U extends Readonly<Record<string, AnyPort>>, Units, K extends string> = Exclude<keyof U, keyof UnitFor<U, Units, K>>;
9
+ type UnitGate<U extends Readonly<Record<string, AnyPort>>, Units, K extends string> = [MissingUnitPorts<U, Units, K>] extends [never] ? unknown : {
10
+ readonly "UNIT PORT MISSING — the selected kind does not export": MissingUnitPorts<U, Units, K>;
11
+ };
12
+ /** 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>>> = {
14
+ readonly schema: GraphQLSchema;
15
+ readonly prefix?: `/${string}`;
16
+ readonly cors?: YogaCors;
17
+ readonly plugins?: readonly Plugin[];
18
+ readonly units?: Units & { readonly [K in Exclude<keyof Units, Kinds<A>>]: never; };
19
+ readonly unit?: U & UnitGate<U, Units, KindOf<R>>;
20
+ readonly requires?: R & RequiresGate<R, { readonly [K in keyof A]: A[K]["scope"]; }>;
21
+ };
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>>(api: HttpAuth<A>, options: GraphqlOptions<A, Units, R, U>) => Provider<HttpHandler, never, HttpConfig | HttpUnit | Scope | SchemePortsOf<R> | UnitsNeedsOf<Units>> & {
23
+ authenticators: readonly import("@btravstack/http-server/internal").SchemeProviders<A>[];
24
+ };
25
+ //#endregion
@@ -0,0 +1,25 @@
1
+ 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 { Plugin, YogaServerOptions } from "graphql-yoga";
4
+ import { OneScheme, Requirements } from "@btravstack/contract";
5
+ import { GraphQLSchema } from "graphql";
6
+ //#region src/graphql.d.ts
7
+ type YogaCors = YogaServerOptions<Record<string, unknown>, Record<string, unknown>>["cors"];
8
+ type MissingUnitPorts<U extends Readonly<Record<string, AnyPort>>, Units, K extends string> = Exclude<keyof U, keyof UnitFor<U, Units, K>>;
9
+ type UnitGate<U extends Readonly<Record<string, AnyPort>>, Units, K extends string> = [MissingUnitPorts<U, Units, K>] extends [never] ? unknown : {
10
+ readonly "UNIT PORT MISSING — the selected kind does not export": MissingUnitPorts<U, Units, K>;
11
+ };
12
+ /** 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>>> = {
14
+ readonly schema: GraphQLSchema;
15
+ readonly prefix?: `/${string}`;
16
+ readonly cors?: YogaCors;
17
+ readonly plugins?: readonly Plugin[];
18
+ readonly units?: Units & { readonly [K in Exclude<keyof Units, Kinds<A>>]: never; };
19
+ readonly unit?: U & UnitGate<U, Units, KindOf<R>>;
20
+ readonly requires?: R & RequiresGate<R, { readonly [K in keyof A]: A[K]["scope"]; }>;
21
+ };
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>>(api: HttpAuth<A>, options: GraphqlOptions<A, Units, R, U>) => Provider<HttpHandler, never, HttpConfig | HttpUnit | Scope | SchemePortsOf<R> | UnitsNeedsOf<Units>> & {
23
+ authenticators: readonly import("@btravstack/http-server/internal").SchemeProviders<A>[];
24
+ };
25
+ //#endregion
package/dist/index.mjs ADDED
@@ -0,0 +1,86 @@
1
+ import { Provider } from "@btravstack/di";
2
+ import { HttpConfig, HttpHandler, HttpUnit, forkUnit, principalOf, resolveScheme, schemeDeps, schemeServices } from "@btravstack/http-server/internal";
3
+ import { createYoga } from "graphql-yoga";
4
+ import { P } from "unthrown";
5
+ //#region src/graphql.ts
6
+ const dispose = Symbol("graphql.dispose");
7
+ const graphql = (api, options) => {
8
+ const mount = options.prefix ?? "/graphql";
9
+ let end = mount.length;
10
+ while (end > 1 && mount.charCodeAt(end - 1) === 47) end--;
11
+ const prefix = mount.slice(0, end);
12
+ const requirements = options.requires;
13
+ const schemes = [...new Set(requirements?.flatMap((requirement) => Object.keys(requirement)) ?? [])];
14
+ const typed = Provider.member(HttpHandler)({
15
+ inject: {
16
+ config: HttpConfig,
17
+ units: HttpUnit,
18
+ ...schemeDeps(schemes)
19
+ },
20
+ sync: (services) => {
21
+ const config = services.config;
22
+ const units = services.units;
23
+ if (options.units !== void 0) {
24
+ 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
+ }
26
+ const authenticators = schemeServices(schemes, services);
27
+ const yoga = createYoga({
28
+ schema: options.schema,
29
+ graphqlEndpoint: prefix,
30
+ cors: options.cors ?? (config.corsOrigin === "" ? false : { origin: config.corsOrigin.split(",").map((origin) => origin.trim()) }),
31
+ plugins: [{ onRequest: ({ serverContext, fetchAPI, endResponse }) => {
32
+ if (serverContext.refusalStatus !== void 0) endResponse(new fetchAPI.Response(null, { status: serverContext.refusalStatus }));
33
+ } }, ...options.plugins ?? []],
34
+ logging: false,
35
+ graphiql: false,
36
+ landingPage: false,
37
+ multipart: false,
38
+ maxRequestBodySize: config.bodyLimit === 0 ? false : config.bodyLimit,
39
+ disposeOnProcessTerminate: false
40
+ });
41
+ return {
42
+ prefix,
43
+ handle: async (request, response, signal, host) => {
44
+ if (request.method === "OPTIONS" && request.headers["access-control-request-method"] !== void 0) {
45
+ await yoga.handle(request, response, {
46
+ principal: void 0,
47
+ incoming: request,
48
+ unit: {},
49
+ signal
50
+ });
51
+ return;
52
+ }
53
+ let resolved;
54
+ if (requirements !== void 0) {
55
+ const result = await resolveScheme(requirements, authenticators, request.headers).mapErrCases((matcher) => matcher.with(P.tag("Unauthenticated"), () => 401).with(P.tag("UnderScoped"), () => 403));
56
+ if (result.isDefect()) throw result.cause;
57
+ if (result.isErr()) {
58
+ await yoga.handle(request, response, {
59
+ principal: void 0,
60
+ incoming: request,
61
+ unit: {},
62
+ signal,
63
+ refusalStatus: result.error
64
+ });
65
+ return;
66
+ }
67
+ resolved = result.value;
68
+ }
69
+ const forked = await forkUnit(host, units, api.principals, resolved, options.unit ?? {});
70
+ if (forked.isDefect()) throw forked.cause;
71
+ await yoga.handle(request, response, {
72
+ principal: requirements === void 0 ? void 0 : principalOf(requirements, resolved),
73
+ incoming: request,
74
+ unit: forked.get(),
75
+ signal
76
+ });
77
+ },
78
+ [dispose]: () => yoga.dispose()
79
+ };
80
+ },
81
+ onStop: (answerer) => answerer[dispose]()
82
+ });
83
+ return Object.assign(typed, { authenticators: api.providers });
84
+ };
85
+ //#endregion
86
+ export { graphql };
package/package.json CHANGED
@@ -1,6 +1,77 @@
1
1
  {
2
2
  "name": "@btravstack/graphql-server",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.21.0",
4
+ "description": "The GraphQL Yoga answerer for @btravstack/http-server",
5
+ "keywords": [
6
+ "graphql",
7
+ "http",
8
+ "server",
9
+ "typescript"
10
+ ],
11
+ "homepage": "https://github.com/btravstack/btravstack#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/btravstack/btravstack/issues"
14
+ },
15
+ "license": "MIT",
16
+ "author": "Benoit TRAVERS <benoit.travers.fr@gmail.com>",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/btravstack/btravstack.git",
20
+ "directory": "packages/graphql-server"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "main": "./dist/index.cjs",
28
+ "module": "./dist/index.mjs",
29
+ "types": "./dist/index.d.cts",
30
+ "exports": {
31
+ ".": {
32
+ "import": {
33
+ "types": "./dist/index.d.mts",
34
+ "default": "./dist/index.mjs"
35
+ },
36
+ "require": {
37
+ "types": "./dist/index.d.cts",
38
+ "default": "./dist/index.cjs"
39
+ }
40
+ },
41
+ "./package.json": "./package.json"
42
+ },
43
+ "devDependencies": {
44
+ "@btravstack/contract": "0.21.0",
45
+ "@btravstack/core": "0.21.0",
46
+ "@btravstack/di": "0.21.0",
47
+ "@btravstack/http-server": "0.21.0",
48
+ "@btravstack/tsconfig": "0.4.0",
49
+ "@types/node": "26.6.4",
50
+ "@unthrown/vitest": "5.12.0",
51
+ "@vitest/coverage-v8": "5.0.3",
52
+ "graphql": "17.0.2",
53
+ "graphql-yoga": "5.24.1",
54
+ "tsdown": "0.23.0",
55
+ "typescript": "7.0.2",
56
+ "typescript-consumer": "npm:typescript@5.9.3",
57
+ "unthrown": "5.12.0",
58
+ "vitest": "5.0.3"
59
+ },
60
+ "peerDependencies": {
61
+ "@btravstack/contract": "^0.21.0",
62
+ "@btravstack/di": "^0.21.0",
63
+ "@btravstack/http-server": "^0.21.0",
64
+ "graphql": "^16.0.0 || ^17.0.0",
65
+ "graphql-yoga": "^5.24.1",
66
+ "unthrown": "^5.7.0"
67
+ },
68
+ "engines": {
69
+ "node": ">=22.15"
70
+ },
71
+ "scripts": {
72
+ "build": "tsdown src/index.ts --format cjs,esm --dts --clean",
73
+ "dev": "tsdown src/index.ts --format cjs,esm --dts --watch",
74
+ "test": "vitest run --coverage",
75
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test-d.json && node ./node_modules/typescript-consumer/bin/tsc --noEmit -p tsconfig.test-d.json"
76
+ }
6
77
  }