@nifrajs/graphql 3.2.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 ADDED
@@ -0,0 +1,119 @@
1
+ # @nifrajs/graphql
2
+
3
+ Mount a GraphQL endpoint on a nifra app. A spec-compliant [GraphQL-over-HTTP](https://graphql.github.io/graphql-over-http/draft/) handler, `graphql-transport-ws` subscriptions over nifra's native WebSocket lane, and your typed nifra context piped straight into resolvers.
4
+
5
+ It executes with the `graphql` package's own `parse`/`validate`/`execute`/`subscribe` - no HTTP framework is re-bundled, the request body reuses core's single bounded, prototype-guarded trust boundary, and the query text is never logged.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bun add @nifrajs/graphql graphql
11
+ # subscriptions also need:
12
+ bun add graphql-ws
13
+ ```
14
+
15
+ `graphql` is a required peer; `graphql-ws` is optional (only for subscriptions).
16
+
17
+ ## Queries and mutations
18
+
19
+ ```ts
20
+ import { server } from "@nifrajs/core"
21
+ import { mountGraphql } from "@nifrajs/graphql"
22
+ import { buildSchema } from "graphql"
23
+
24
+ const schema = buildSchema(`
25
+ type Query { hello: String }
26
+ type Mutation { echo(msg: String!): String }
27
+ `)
28
+
29
+ const rootValue = {
30
+ hello: () => "world",
31
+ echo: ({ msg }: { msg: string }) => msg,
32
+ }
33
+
34
+ const app = server()
35
+ mountGraphql(app, { schema, rootValue })
36
+ // POST /graphql - queries + mutations
37
+ // GET /graphql - queries only (idempotent, cacheable)
38
+ ```
39
+
40
+ Prefer the handler directly? Mount it yourself:
41
+
42
+ ```ts
43
+ import { respondGraphql } from "@nifrajs/graphql"
44
+
45
+ app.post("/graphql", (c) => respondGraphql(c.req, { schema, rootValue }))
46
+ ```
47
+
48
+ ## Context injection
49
+
50
+ The one thing a plain third-party mount cannot do: derive the resolver `contextValue` from what your nifra handler already resolved (auth subject, env, params).
51
+
52
+ ```ts
53
+ mountGraphql(app, {
54
+ schema,
55
+ context: ({ request, nifra }) => ({
56
+ user: nifra?.env, // nifra route context, when mounted via mountGraphql
57
+ ip: request.headers.get("x-forwarded-for"),
58
+ }),
59
+ })
60
+ ```
61
+
62
+ Resolvers read it as their third argument.
63
+
64
+ ## Subscriptions
65
+
66
+ Subscriptions run over the same route via `graphql-transport-ws`. Bring a subscription source - the bundled in-memory `createPubSub` is the reference implementation; swap in a durable bus (Redis, a Durable Object, NATS) that exposes the same `{ publish, subscribe }` shape for multi-instance deploys.
67
+
68
+ ```ts
69
+ import { server } from "@nifrajs/core"
70
+ import { websocket } from "@nifrajs/core/ws"
71
+ import { mountGraphql, createPubSub } from "@nifrajs/graphql"
72
+
73
+ const pubsub = createPubSub<number>()
74
+
75
+ const app = server().use(websocket())
76
+ mountGraphql(app, {
77
+ schema,
78
+ context: () => ({ pubsub }),
79
+ subscriptions: true,
80
+ })
81
+
82
+ // A resolver:
83
+ // count: { subscribe: (_, __, ctx) => ctx.pubsub.subscribe("count"), resolve: (n) => n }
84
+ // Push an event from anywhere:
85
+ pubsub.publish("count", 1)
86
+ ```
87
+
88
+ The route must negotiate the `graphql-transport-ws` subprotocol at the WebSocket layer for a browser `graphql-ws` client to connect.
89
+
90
+ ## Options
91
+
92
+ | Option | Default | Purpose |
93
+ | --- | --- | --- |
94
+ | `schema` | - | Executable `GraphQLSchema` (required). |
95
+ | `context` | `{}` | Build the resolver `contextValue` per operation. |
96
+ | `rootValue` | - | Root value passed to the executor. |
97
+ | `maxBodyBytes` | `1_000_000` | Request body cap; larger bodies answer 413. |
98
+ | `protoPoisoning` | `"reject"` | Prototype-pollution policy for the JSON body. |
99
+ | `allowedOrigins` | reflect `*` | CORS origin allowlist. |
100
+ | `authorize` | - | Per-request guard; return `false` for 401. |
101
+ | `enableGet` | `true` | Serve `GET /graphql` (queries only). |
102
+ | `subscriptions` | `false` | `true` (reuse schema/context) or explicit WS options. |
103
+ | `legacyJsonResponse` | `false` | Old always-200 `application/json` shape. |
104
+
105
+ ## Responses
106
+
107
+ By default responses use the `application/graphql-response+json` media type with spec status codes: `200` for an executed result (including field errors in `errors`), `400` for a parse or validation failure, `401` for a rejected `authorize`, `405` for a mutation attempted over GET. Set `legacyJsonResponse: true` for the older always-`200` `application/json` behavior some clients expect.
108
+
109
+ ## License
110
+
111
+ MIT
112
+
113
+ ## For AI agents
114
+
115
+ Start with [`LLM.md`](./LLM.md) - this package's contract card (the exports you call + its footguns),
116
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
117
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
118
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
119
+ done-gate, or `nifra mcp` to give the agent live project tools.
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The context-injection seam - the one thing a hand-rolled `app.all('/graphql', yoga.fetch)` mount
3
+ * cannot do. A GraphQL executor builds an opaque `contextValue` from the raw `Request`; it never sees
4
+ * the nifra request context (auth subject, params, env, `waitUntil`). This contract lets a resolver's
5
+ * `contextValue` be derived from whatever the surrounding nifra handler already resolved.
6
+ *
7
+ * The builder is called once per operation (per HTTP request, or once per WS `Subscribe` message),
8
+ * before `execute`/`subscribe`. It receives the raw `Request` and, on the HTTP path, the nifra route
9
+ * context `c` when the endpoint is mounted with `mountGraphql` (which has `c` in scope); on the WS path
10
+ * it receives the upgrade request. Return the object your resolvers read as their third argument.
11
+ *
12
+ * MOAT NOTE: this seam only *passes through* context the host already owns. It never persists it. A
13
+ * tenant-aware or durable resolver-context store is an operated concern and lives outside this package.
14
+ */
15
+ /** What the builder is handed. `request` is always present; `nifra` is the route context when known. */
16
+ export interface GraphqlContextInput<Env = unknown> {
17
+ readonly request: Request;
18
+ /**
19
+ * The nifra route context (`c`) when the endpoint was mounted via `mountGraphql`, otherwise
20
+ * `undefined` (e.g. `respondGraphql` called standalone, or the WS transport before a route context
21
+ * exists). Typed loosely on purpose so this package never imports core's context type at the value
22
+ * level - keeping the peer dependency truly optional.
23
+ */
24
+ readonly nifra?: NifraContextLike<Env>;
25
+ }
26
+ /**
27
+ * The subset of a nifra route context this package surfaces to a resolver-context builder. Structural,
28
+ * so the real `c` satisfies it without a nominal import. Kept to the non-body accessors a resolver
29
+ * legitimately needs; the raw body belongs to the GraphQL executor, not the builder.
30
+ */
31
+ export interface NifraContextLike<Env = unknown> {
32
+ readonly req: Request;
33
+ readonly params: Record<string, string>;
34
+ readonly env: Env;
35
+ readonly signal: AbortSignal;
36
+ readonly waitUntil: (promise: Promise<unknown>) => void;
37
+ }
38
+ /**
39
+ * Build the resolver `contextValue` for one operation. Sync or async; a throw is reported as a GraphQL
40
+ * error envelope rather than crashing the transport. Omit it and resolvers get `contextValue: {}`.
41
+ */
42
+ export type GraphqlContextBuilder<Context = unknown, Env = unknown> = (input: GraphqlContextInput<Env>) => Context | Promise<Context>;
43
+ /** Resolve a context builder to a concrete value, defaulting to an empty object. */
44
+ export declare function buildContext<Context, Env>(builder: GraphqlContextBuilder<Context, Env> | undefined, input: GraphqlContextInput<Env>): Promise<Context>;
45
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,wGAAwG;AACxG,MAAM,WAAW,mBAAmB,CAAC,GAAG,GAAG,OAAO;IAChD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAA;CACvC;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB,CAAC,GAAG,GAAG,OAAO;IAC7C,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACvC,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAA;IACjB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;IAC5B,QAAQ,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAA;CACxD;AAED;;;GAGG;AACH,MAAM,MAAM,qBAAqB,CAAC,OAAO,GAAG,OAAO,EAAE,GAAG,GAAG,OAAO,IAAI,CACpE,KAAK,EAAE,mBAAmB,CAAC,GAAG,CAAC,KAC5B,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;AAE/B,oFAAoF;AACpF,wBAAsB,YAAY,CAAC,OAAO,EAAE,GAAG,EAC7C,OAAO,EAAE,qBAAqB,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,SAAS,EACxD,KAAK,EAAE,mBAAmB,CAAC,GAAG,CAAC,GAC9B,OAAO,CAAC,OAAO,CAAC,CAGlB"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The context-injection seam - the one thing a hand-rolled `app.all('/graphql', yoga.fetch)` mount
3
+ * cannot do. A GraphQL executor builds an opaque `contextValue` from the raw `Request`; it never sees
4
+ * the nifra request context (auth subject, params, env, `waitUntil`). This contract lets a resolver's
5
+ * `contextValue` be derived from whatever the surrounding nifra handler already resolved.
6
+ *
7
+ * The builder is called once per operation (per HTTP request, or once per WS `Subscribe` message),
8
+ * before `execute`/`subscribe`. It receives the raw `Request` and, on the HTTP path, the nifra route
9
+ * context `c` when the endpoint is mounted with `mountGraphql` (which has `c` in scope); on the WS path
10
+ * it receives the upgrade request. Return the object your resolvers read as their third argument.
11
+ *
12
+ * MOAT NOTE: this seam only *passes through* context the host already owns. It never persists it. A
13
+ * tenant-aware or durable resolver-context store is an operated concern and lives outside this package.
14
+ */
15
+ /** Resolve a context builder to a concrete value, defaulting to an empty object. */
16
+ export async function buildContext(builder, input) {
17
+ if (builder === undefined)
18
+ return {};
19
+ return await builder(input);
20
+ }
21
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAmCH,oFAAoF;AACpF,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,OAAwD,EACxD,KAA+B;IAE/B,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,EAAa,CAAA;IAC/C,OAAO,MAAM,OAAO,CAAC,KAAK,CAAC,CAAA;AAC7B,CAAC"}
package/dist/http.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The GraphQL-over-HTTP transport - a plain Web `fetch` handler you mount at `POST /graphql` (and
3
+ * optionally `GET /graphql`). It parses, validates, and executes against your `GraphQLSchema` using
4
+ * `graphql`'s own `parse`/`validate`/`execute`; no server framework is re-bundled and no second HTTP
5
+ * trust boundary is created - the request body is read through core's one bounded/proto-guarded lane
6
+ * (`@nifrajs/core/edge-kit`'s {@link readBodyFramed}).
7
+ *
8
+ * Spec: https://graphql.github.io/graphql-over-http/draft/. POST carries `{query, operationName?,
9
+ * variables?, extensions?}` as `application/json`; GET carries them as query-string params and may run
10
+ * **queries only** (never a mutation) so it stays a safe, cacheable idempotent method. Responses use the
11
+ * `application/graphql-response+json` media type with spec status codes by default; set
12
+ * `legacyJsonResponse` for the old always-200 `application/json` shape older clients expect.
13
+ *
14
+ * PRIVACY: the handler never logs the query text, variables, or the request body. GraphQL documents are
15
+ * user payloads; keeping them out of this package's output is the guardrail (a payload sink is an
16
+ * operated concern, not a public-package one).
17
+ */
18
+ import { type ProtoPoisoning } from "@nifrajs/core/edge-kit";
19
+ import { type GraphQLSchema } from "graphql";
20
+ import { type GraphqlContextBuilder, type NifraContextLike } from "./context.js";
21
+ export interface GraphqlHttpOptions<Context = unknown, Env = unknown> {
22
+ /** The executable schema (types + resolvers), e.g. from `graphql`'s `buildSchema` or a schema builder. */
23
+ readonly schema: GraphQLSchema;
24
+ /** Build the resolver `contextValue` from the request (and nifra context when mounted). */
25
+ readonly context?: GraphqlContextBuilder<Context, Env>;
26
+ /** The nifra route context, when the caller has one (supplied by `mountGraphql`). */
27
+ readonly nifra?: NifraContextLike<Env>;
28
+ /** Maximum request body size in bytes. Default 1 MB. Rejected bodies answer 413. */
29
+ readonly maxBodyBytes?: number;
30
+ /** Prototype-pollution policy for the parsed JSON body. Default `"reject"`. */
31
+ readonly protoPoisoning?: ProtoPoisoning;
32
+ /** Optional root value passed to the executor. */
33
+ readonly rootValue?: unknown;
34
+ /**
35
+ * Origin allowlist for the CORS `access-control-allow-origin` header. Omit to reflect any origin
36
+ * (`*`) - correct for a public API; set it to lock the browser callers down.
37
+ */
38
+ readonly allowedOrigins?: readonly string[];
39
+ /**
40
+ * Per-request authorization, run after the body parses and before execution. Return `false` to answer
41
+ * `401` with a GraphQL error envelope and skip execution entirely.
42
+ */
43
+ readonly authorize?: (request: Request) => boolean | Promise<boolean>;
44
+ /** Emit the legacy always-200 `application/json` response shape instead of `application/graphql-response+json`. */
45
+ readonly legacyJsonResponse?: boolean;
46
+ }
47
+ /**
48
+ * Handle one GraphQL HTTP request. Never throws - a malformed request becomes a GraphQL error envelope.
49
+ * Mount it directly (`app.all('/graphql', (c) => respondGraphql(c.req, { schema }))`) or via
50
+ * {@link mountGraphql}, which also wires the nifra context in.
51
+ */
52
+ export declare function respondGraphql<Context = unknown, Env = unknown>(request: Request, options: GraphqlHttpOptions<Context, Env>): Promise<Response>;
53
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAEL,KAAK,cAAc,EAOpB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAKL,KAAK,aAAa,EAMnB,MAAM,SAAS,CAAA;AAChB,OAAO,EAEL,KAAK,qBAAqB,EAE1B,KAAK,gBAAgB,EACtB,MAAM,cAAc,CAAA;AAYrB,MAAM,WAAW,kBAAkB,CAAC,OAAO,GAAG,OAAO,EAAE,GAAG,GAAG,OAAO;IAClE,0GAA0G;IAC1G,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;IAC9B,2FAA2F;IAC3F,QAAQ,CAAC,OAAO,CAAC,EAAE,qBAAqB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;IACtD,qFAAqF;IACrF,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAA;IACtC,oFAAoF;IACpF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,+EAA+E;IAC/E,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAA;IACxC,kDAAkD;IAClD,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3C;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACrE,mHAAmH;IACnH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;CACtC;AAqKD;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,OAAO,GAAG,OAAO,EAAE,GAAG,GAAG,OAAO,EACnE,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,kBAAkB,CAAC,OAAO,EAAE,GAAG,CAAC,GACxC,OAAO,CAAC,QAAQ,CAAC,CA+DnB"}
package/dist/http.js ADDED
@@ -0,0 +1,221 @@
1
+ /**
2
+ * The GraphQL-over-HTTP transport - a plain Web `fetch` handler you mount at `POST /graphql` (and
3
+ * optionally `GET /graphql`). It parses, validates, and executes against your `GraphQLSchema` using
4
+ * `graphql`'s own `parse`/`validate`/`execute`; no server framework is re-bundled and no second HTTP
5
+ * trust boundary is created - the request body is read through core's one bounded/proto-guarded lane
6
+ * (`@nifrajs/core/edge-kit`'s {@link readBodyFramed}).
7
+ *
8
+ * Spec: https://graphql.github.io/graphql-over-http/draft/. POST carries `{query, operationName?,
9
+ * variables?, extensions?}` as `application/json`; GET carries them as query-string params and may run
10
+ * **queries only** (never a mutation) so it stays a safe, cacheable idempotent method. Responses use the
11
+ * `application/graphql-response+json` media type with spec status codes by default; set
12
+ * `legacyJsonResponse` for the old always-200 `application/json` shape older clients expect.
13
+ *
14
+ * PRIVACY: the handler never logs the query text, variables, or the request body. GraphQL documents are
15
+ * user payloads; keeping them out of this package's output is the guardrail (a payload sink is an
16
+ * operated concern, not a public-package one).
17
+ */
18
+ import { EMPTY_RESPONSE_CONTROLS, plainError, queryObjectOf, readBodyFramed, searchOf, toResponse, } from "@nifrajs/core/edge-kit";
19
+ import { execute, GraphQLError, getOperationAST, parse, Source, specifiedRules, validate, } from "graphql";
20
+ import { buildContext, } from "./context.js";
21
+ const DEFAULT_MAX_BODY_BYTES = 1_000_000;
22
+ const GRAPHQL_RESPONSE_JSON = "application/graphql-response+json; charset=utf-8";
23
+ const JSON_UTF8 = "application/json; charset=utf-8";
24
+ const CORS_BASE = {
25
+ "access-control-allow-methods": "GET, POST, OPTIONS",
26
+ "access-control-allow-headers": "content-type, authorization",
27
+ "access-control-max-age": "86400",
28
+ };
29
+ function assertByteLimit(value) {
30
+ if (!Number.isSafeInteger(value) || value < 0) {
31
+ throw new RangeError("GraphQL maxBodyBytes must be a non-negative safe integer");
32
+ }
33
+ }
34
+ function corsFor(request, allowedOrigins) {
35
+ if (allowedOrigins === undefined)
36
+ return { ...CORS_BASE, "access-control-allow-origin": "*" };
37
+ const origin = request.headers.get("origin");
38
+ if (origin === null)
39
+ return { ...CORS_BASE, vary: "Origin" };
40
+ if (allowedOrigins.includes(origin)) {
41
+ return { ...CORS_BASE, "access-control-allow-origin": origin, vary: "Origin" };
42
+ }
43
+ return null;
44
+ }
45
+ /** Render a rejection from readBodyFramed - a `Response` passes through, a `ResponseResult` is rendered
46
+ * through the same core renderer the full Server uses, so the wire bytes match. */
47
+ function render(r) {
48
+ return r instanceof Response ? r : toResponse(r, EMPTY_RESPONSE_CONTROLS);
49
+ }
50
+ /** Serialize an execution/error payload to a JSON `Response` with the negotiated media type + status.
51
+ * `payload` is any JSON-serializable GraphQL result (`{data?, errors?}`); GraphQLError values serialize
52
+ * via their own `toJSON`. */
53
+ function graphqlResponse(payload, status, legacy, cors) {
54
+ return new Response(JSON.stringify(payload), {
55
+ status: legacy ? 200 : status,
56
+ headers: { "content-type": legacy ? JSON_UTF8 : GRAPHQL_RESPONSE_JSON, ...cors },
57
+ });
58
+ }
59
+ /** A request-level (parse/validate/auth/method) failure: `{ errors: [...] }`, no `data` key. */
60
+ function requestError(message, status, legacy, cors, errors) {
61
+ const payload = errors ?? [new GraphQLError(message)];
62
+ return graphqlResponse({ errors: payload.map((e) => e.toJSON()) }, status, legacy, cors);
63
+ }
64
+ /** Coerce a `?variables=<json>` string (GET) into an object, or throw a GraphQLError on bad JSON. */
65
+ function parseVariables(raw) {
66
+ if (raw === undefined || raw === null || raw === "")
67
+ return null;
68
+ if (typeof raw === "object")
69
+ return raw;
70
+ if (typeof raw !== "string")
71
+ throw new GraphQLError("Variables must be a JSON object.");
72
+ let parsed;
73
+ try {
74
+ parsed = JSON.parse(raw);
75
+ }
76
+ catch {
77
+ throw new GraphQLError("Variables are invalid JSON.");
78
+ }
79
+ if (parsed === null)
80
+ return null;
81
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
82
+ throw new GraphQLError("Variables must be a JSON object.");
83
+ }
84
+ return parsed;
85
+ }
86
+ /** Read `{query, operationName, variables}` from a parsed POST body. Throws GraphQLError when malformed. */
87
+ function paramsFromBody(body) {
88
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
89
+ throw new GraphQLError("POST body must be a JSON object.");
90
+ }
91
+ const b = body;
92
+ if (typeof b.query !== "string" || b.query.length === 0) {
93
+ throw new GraphQLError("A `query` string is required.");
94
+ }
95
+ const operationName = typeof b.operationName === "string" ? b.operationName : null;
96
+ return { query: b.query, operationName, variables: parseVariables(b.variables) };
97
+ }
98
+ /** Read the same shape from a GET query string. */
99
+ function paramsFromQuery(query) {
100
+ const q = query.query;
101
+ if (typeof q !== "string" || q.length === 0)
102
+ throw new GraphQLError("A `query` parameter is required.");
103
+ const operationName = typeof query.operationName === "string" ? query.operationName : null;
104
+ return { query: q, operationName, variables: parseVariables(query.variables) };
105
+ }
106
+ /** Parse, validate, and execute one already-extracted GraphQL request. */
107
+ async function run(params, request, opts, isGet, legacy, cors) {
108
+ let document;
109
+ try {
110
+ document = parse(new Source(params.query, "GraphQL request"));
111
+ }
112
+ catch (err) {
113
+ const gqlErr = err instanceof GraphQLError ? err : new GraphQLError(String(err));
114
+ return requestError("", 400, legacy, cors, [gqlErr]);
115
+ }
116
+ const validationErrors = validate(opts.schema, document, specifiedRules);
117
+ if (validationErrors.length > 0) {
118
+ return requestError("", 400, legacy, cors, validationErrors);
119
+ }
120
+ // GET is idempotent and cacheable, so per the GraphQL-over-HTTP spec it may run queries only.
121
+ const operationAst = getOperationAST(document, params.operationName ?? undefined);
122
+ if (isGet && operationAst != null && operationAst.operation !== "query") {
123
+ return requestError("Only query operations may run over GET.", 405, legacy, { ...cors, allow: "POST" }, [new GraphQLError("Only query operations are allowed over GET.")]);
124
+ }
125
+ const contextInput = opts.nifra !== undefined ? { request, nifra: opts.nifra } : { request };
126
+ let contextValue;
127
+ try {
128
+ contextValue = await buildContext(opts.context, contextInput);
129
+ }
130
+ catch (err) {
131
+ const gqlErr = err instanceof GraphQLError ? err : new GraphQLError("Failed to build request context.");
132
+ return graphqlResponse({ errors: [gqlErr.toJSON()] }, 500, legacy, cors);
133
+ }
134
+ let result;
135
+ try {
136
+ result = (await execute({
137
+ schema: opts.schema,
138
+ document,
139
+ rootValue: opts.rootValue,
140
+ contextValue,
141
+ variableValues: params.variables ?? undefined,
142
+ operationName: params.operationName ?? undefined,
143
+ }));
144
+ }
145
+ catch (err) {
146
+ const gqlErr = err instanceof GraphQLError ? err : new GraphQLError("Execution failed.");
147
+ return graphqlResponse({ errors: [gqlErr.toJSON()] }, 400, legacy, cors);
148
+ }
149
+ // Field/execution errors are a normal 200 result with both `data` and `errors`.
150
+ return graphqlResponse(result, 200, legacy, cors);
151
+ }
152
+ /**
153
+ * Handle one GraphQL HTTP request. Never throws - a malformed request becomes a GraphQL error envelope.
154
+ * Mount it directly (`app.all('/graphql', (c) => respondGraphql(c.req, { schema }))`) or via
155
+ * {@link mountGraphql}, which also wires the nifra context in.
156
+ */
157
+ export async function respondGraphql(request, options) {
158
+ const maxBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
159
+ assertByteLimit(maxBytes);
160
+ const legacy = options.legacyJsonResponse === true;
161
+ const cors = corsFor(request, options.allowedOrigins);
162
+ if (cors === null)
163
+ return new Response(null, { status: 403 });
164
+ if (request.method === "OPTIONS")
165
+ return new Response(null, { status: 204, headers: cors });
166
+ if (options.authorize !== undefined) {
167
+ const ok = await options.authorize(request);
168
+ if (!ok) {
169
+ return requestError("Unauthorized.", 401, legacy, cors, [new GraphQLError("Unauthorized.")]);
170
+ }
171
+ }
172
+ if (request.method === "GET") {
173
+ let params;
174
+ try {
175
+ params = paramsFromQuery(queryObjectOf(searchOf(request.url)));
176
+ }
177
+ catch (err) {
178
+ const gqlErr = err instanceof GraphQLError ? err : new GraphQLError(String(err));
179
+ return requestError("", 400, legacy, cors, [gqlErr]);
180
+ }
181
+ return run(params, request, options, true, legacy, cors);
182
+ }
183
+ if (request.method !== "POST") {
184
+ return requestError("Method not allowed.", 405, legacy, { ...cors, allow: "GET, POST" }, [
185
+ new GraphQLError("Only GET and POST are supported."),
186
+ ]);
187
+ }
188
+ // Read the JSON body through core's single bounded + proto-guarded framing lane.
189
+ return readBodyFramed(request, maxBytes, options.protoPoisoning ?? "reject", async (parsed) => {
190
+ let params;
191
+ try {
192
+ params = paramsFromBody(parsed);
193
+ }
194
+ catch (err) {
195
+ const gqlErr = err instanceof GraphQLError ? err : new GraphQLError(String(err));
196
+ return requestError("", 400, legacy, cors, [gqlErr]);
197
+ }
198
+ return run(params, request, options, false, legacy, cors);
199
+ }, (response) => {
200
+ // readBodyFramed's own rejections (413 too-large, 415, proto-guard) - render + attach CORS.
201
+ const res = render(response);
202
+ for (const [k, v] of Object.entries(cors))
203
+ res.headers.set(k, v);
204
+ return res;
205
+ }, (err) => {
206
+ // A body that failed to read (bad JSON / proto-guard) is a request error, not a crash.
207
+ if (err instanceof Response)
208
+ return err;
209
+ return requestError("Invalid request body.", 400, legacy, cors, [
210
+ new GraphQLError("Request body could not be read as JSON."),
211
+ ]);
212
+ }).catch(() => plainErrorResponse(cors));
213
+ }
214
+ /** Last-resort framing fallback so the handler contract ("never throws") holds even on an edge-kit change. */
215
+ function plainErrorResponse(cors) {
216
+ const res = render(plainError(400, "bad_request"));
217
+ for (const [k, v] of Object.entries(cors))
218
+ res.headers.set(k, v);
219
+ return res;
220
+ }
221
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `@nifrajs/graphql` - mount a GraphQL endpoint on a nifra app.
3
+ *
4
+ * - {@link respondGraphql} (`./http.ts`) - a spec-compliant GraphQL-over-HTTP `fetch` handler.
5
+ * - {@link graphqlWebSocket} (`./ws.ts`) - `graphql-transport-ws` subscriptions over `app.ws()`.
6
+ * - {@link createPubSub} (`./pubsub.ts`) - an in-memory subscription source; swap for a durable bus.
7
+ * - {@link mountGraphql} - the one-call sugar that wires POST/GET (and, optionally, subscriptions).
8
+ *
9
+ * It executes with `graphql`'s own `parse`/`validate`/`execute`/`subscribe`; no HTTP framework is
10
+ * re-bundled, the request body reuses core's single bounded trust boundary, and the query text is never
11
+ * logged. `graphql` is a required peer; `graphql-ws` is optional (needed only for subscriptions).
12
+ */
13
+ import type { NifraContextLike } from "./context.js";
14
+ import { type GraphqlHttpOptions } from "./http.js";
15
+ import { type GraphqlWsOptions } from "./ws.js";
16
+ export type { GraphqlContextBuilder, GraphqlContextInput, NifraContextLike, } from "./context.js";
17
+ export { buildContext } from "./context.js";
18
+ export { type GraphqlHttpOptions, respondGraphql } from "./http.js";
19
+ export { createPubSub, type GraphqlPubSub } from "./pubsub.js";
20
+ export { type GraphqlWsOptions, graphqlWebSocket } from "./ws.js";
21
+ /** The structural slice of a nifra server `mountGraphql` needs. Kept loose so core stays a peer dep. */
22
+ interface RouteContextLike<Env = unknown> extends NifraContextLike<Env> {
23
+ readonly req: Request;
24
+ }
25
+ interface MountableApp<Env = unknown> {
26
+ get(path: string, handler: (c: RouteContextLike<Env>) => Response | Promise<Response>): unknown;
27
+ post(path: string, handler: (c: RouteContextLike<Env>) => Response | Promise<Response>): unknown;
28
+ ws?(path: string, handler: unknown): unknown;
29
+ }
30
+ export interface MountGraphqlOptions<Context = unknown, Env = unknown> extends Omit<GraphqlHttpOptions<Context, Env>, "nifra"> {
31
+ /** Endpoint path. Default `/graphql`. */
32
+ readonly path?: string;
33
+ /** Enable `GET /graphql` (queries only). Default `true`. */
34
+ readonly enableGet?: boolean;
35
+ /**
36
+ * Enable `graphql-transport-ws` subscriptions on the same path. Pass the WS options (or `true` to
37
+ * reuse `schema`/`context`). Requires the app to have `.use(websocket())` and `graphql-ws` installed.
38
+ */
39
+ readonly subscriptions?: boolean | GraphqlWsOptions<Record<string, unknown>>;
40
+ }
41
+ /**
42
+ * Mount a GraphQL endpoint. Wires `POST` (and `GET`, unless disabled) on `path`, injecting the nifra
43
+ * route context into your resolver context, and - when `subscriptions` is set - a `graphql-ws` WebSocket
44
+ * on the same path.
45
+ *
46
+ * ```ts
47
+ * import { server } from "@nifrajs/core"
48
+ * import { websocket } from "@nifrajs/core/ws"
49
+ * import { mountGraphql, createPubSub } from "@nifrajs/graphql"
50
+ *
51
+ * const pubsub = createPubSub()
52
+ * const app = server().use(websocket())
53
+ * mountGraphql(app, {
54
+ * schema,
55
+ * context: ({ nifra }) => ({ user: nifra?.env, pubsub }),
56
+ * subscriptions: true,
57
+ * })
58
+ * ```
59
+ */
60
+ export declare function mountGraphql<Context = unknown, Env = unknown>(app: MountableApp<Env>, options: MountGraphqlOptions<Context, Env>): void;
61
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAyB,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAC3E,OAAO,EAAE,KAAK,kBAAkB,EAAkB,MAAM,WAAW,CAAA;AACnE,OAAO,EAAE,KAAK,gBAAgB,EAAoB,MAAM,SAAS,CAAA;AAEjE,YAAY,EACV,qBAAqB,EACrB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,KAAK,kBAAkB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AACnE,OAAO,EAAE,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAA;AAC9D,OAAO,EAAE,KAAK,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAEjE,wGAAwG;AACxG,UAAU,gBAAgB,CAAC,GAAG,GAAG,OAAO,CAAE,SAAQ,gBAAgB,CAAC,GAAG,CAAC;IACrE,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;CACtB;AAED,UAAU,YAAY,CAAC,GAAG,GAAG,OAAO;IAClC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,EAAE,gBAAgB,CAAC,GAAG,CAAC,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAA;IAC/F,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,EAAE,gBAAgB,CAAC,GAAG,CAAC,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAA;IAChG,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAA;CAC7C;AAED,MAAM,WAAW,mBAAmB,CAAC,OAAO,GAAG,OAAO,EAAE,GAAG,GAAG,OAAO,CACnE,SAAQ,IAAI,CAAC,kBAAkB,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC;IACvD,yCAAyC;IACzC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CAC7E;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAG,OAAO,EAAE,GAAG,GAAG,OAAO,EAC3D,GAAG,EAAE,YAAY,CAAC,GAAG,CAAC,EACtB,OAAO,EAAE,mBAAmB,CAAC,OAAO,EAAE,GAAG,CAAC,GACzC,IAAI,CA0BN"}
package/dist/index.js ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `@nifrajs/graphql` - mount a GraphQL endpoint on a nifra app.
3
+ *
4
+ * - {@link respondGraphql} (`./http.ts`) - a spec-compliant GraphQL-over-HTTP `fetch` handler.
5
+ * - {@link graphqlWebSocket} (`./ws.ts`) - `graphql-transport-ws` subscriptions over `app.ws()`.
6
+ * - {@link createPubSub} (`./pubsub.ts`) - an in-memory subscription source; swap for a durable bus.
7
+ * - {@link mountGraphql} - the one-call sugar that wires POST/GET (and, optionally, subscriptions).
8
+ *
9
+ * It executes with `graphql`'s own `parse`/`validate`/`execute`/`subscribe`; no HTTP framework is
10
+ * re-bundled, the request body reuses core's single bounded trust boundary, and the query text is never
11
+ * logged. `graphql` is a required peer; `graphql-ws` is optional (needed only for subscriptions).
12
+ */
13
+ import { respondGraphql } from "./http.js";
14
+ import { graphqlWebSocket } from "./ws.js";
15
+ export { buildContext } from "./context.js";
16
+ export { respondGraphql } from "./http.js";
17
+ export { createPubSub } from "./pubsub.js";
18
+ export { graphqlWebSocket } from "./ws.js";
19
+ /**
20
+ * Mount a GraphQL endpoint. Wires `POST` (and `GET`, unless disabled) on `path`, injecting the nifra
21
+ * route context into your resolver context, and - when `subscriptions` is set - a `graphql-ws` WebSocket
22
+ * on the same path.
23
+ *
24
+ * ```ts
25
+ * import { server } from "@nifrajs/core"
26
+ * import { websocket } from "@nifrajs/core/ws"
27
+ * import { mountGraphql, createPubSub } from "@nifrajs/graphql"
28
+ *
29
+ * const pubsub = createPubSub()
30
+ * const app = server().use(websocket())
31
+ * mountGraphql(app, {
32
+ * schema,
33
+ * context: ({ nifra }) => ({ user: nifra?.env, pubsub }),
34
+ * subscriptions: true,
35
+ * })
36
+ * ```
37
+ */
38
+ export function mountGraphql(app, options) {
39
+ const path = options.path ?? "/graphql";
40
+ const enableGet = options.enableGet !== false;
41
+ const handle = (c) => respondGraphql(c.req, { ...options, nifra: c });
42
+ app.post(path, handle);
43
+ if (enableGet)
44
+ app.get(path, handle);
45
+ if (options.subscriptions) {
46
+ if (typeof app.ws !== "function") {
47
+ throw new Error("mountGraphql: subscriptions require a WebSocket-enabled app - call `.use(websocket())` first.");
48
+ }
49
+ let wsOptions;
50
+ if (options.subscriptions === true) {
51
+ const ctx = options.context;
52
+ wsOptions =
53
+ ctx !== undefined ? { schema: options.schema, context: ctx } : { schema: options.schema };
54
+ }
55
+ else {
56
+ wsOptions = options.subscriptions;
57
+ }
58
+ app.ws(path, graphqlWebSocket(wsOptions));
59
+ }
60
+ }
61
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAA2B,cAAc,EAAE,MAAM,WAAW,CAAA;AACnE,OAAO,EAAyB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAOjE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAA2B,cAAc,EAAE,MAAM,WAAW,CAAA;AACnE,OAAO,EAAE,YAAY,EAAsB,MAAM,aAAa,CAAA;AAC9D,OAAO,EAAyB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AA0BjE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,YAAY,CAC1B,GAAsB,EACtB,OAA0C;IAE1C,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,UAAU,CAAA;IACvC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,KAAK,KAAK,CAAA;IAE7C,MAAM,MAAM,GAAG,CAAC,CAAwB,EAAqB,EAAE,CAC7D,cAAc,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAA;IAEjD,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACtB,IAAI,SAAS;QAAE,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IAEpC,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;QAC1B,IAAI,OAAO,GAAG,CAAC,EAAE,KAAK,UAAU,EAAE,CAAC;YACjC,MAAM,IAAI,KAAK,CACb,+FAA+F,CAChG,CAAA;QACH,CAAC;QACD,IAAI,SAAoD,CAAA;QACxD,IAAI,OAAO,CAAC,aAAa,KAAK,IAAI,EAAE,CAAC;YACnC,MAAM,GAAG,GAAG,OAAO,CAAC,OAAqE,CAAA;YACzF,SAAS;gBACP,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAA;QAC7F,CAAC;aAAM,CAAC;YACN,SAAS,GAAG,OAAO,CAAC,aAAa,CAAA;QACnC,CAAC;QACD,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAA;IAC3C,CAAC;AACH,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * A trivial in-memory pub/sub for `subscription` resolvers - the reference implementation of the
3
+ * subscription source seam. A resolver's `subscribe` returns `pubsub.subscribe(topic)` (an async
4
+ * iterator); anything in the process calls `pubsub.publish(topic, payload)` to push an event.
5
+ *
6
+ * This is deliberately in-process and unbounded-fan-in only within one server instance - the same
7
+ * single-instance contract as core's `TopicRegistry`. A durable, multi-instance, or tenant-scoped
8
+ * pub/sub (Redis, a Durable Object, NATS) is an operated concern: implement the same
9
+ * `{ publish, subscribe }` shape over your bus and pass it in. Keeping only the in-memory impl here is
10
+ * the guardrail - a durable payload-carrying sink does not belong in a public package.
11
+ */
12
+ /** The subscription source shape a resolver's `subscribe` consumes. Swap the impl for a durable bus. */
13
+ export interface GraphqlPubSub<Event = unknown> {
14
+ /** Push `payload` to every open iterator on `topic`. */
15
+ publish(topic: string, payload: Event): void;
16
+ /** An async iterator that yields each future `publish(topic, ...)` until it is returned/closed. */
17
+ subscribe(topic: string): AsyncIterableIterator<Event>;
18
+ }
19
+ /** Create an in-memory {@link GraphqlPubSub}. */
20
+ export declare function createPubSub<Event = unknown>(): GraphqlPubSub<Event>;
21
+ //# sourceMappingURL=pubsub.d.ts.map