@znight/graphql 0.2.1 → 0.3.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 +238 -18
- package/dist/{types-DQ713UgI.d.mts → driver-Cnze8Oyr.d.mts} +184 -7
- package/dist/http-BWcayUtH.mjs +2165 -0
- package/dist/http-BWcayUtH.mjs.map +1 -0
- package/dist/index.d.mts +37 -49
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -1
- package/dist/{plugin-CR_wVfRU.mjs → plugin-q4oz7d6s.mjs} +214 -1774
- package/dist/plugin-q4oz7d6s.mjs.map +1 -0
- package/dist/testing.d.mts +40 -1
- package/dist/testing.mjs +649 -4
- package/dist/testing.mjs.map +1 -1
- package/dist/yoga.d.mts +64 -0
- package/dist/yoga.mjs +379 -0
- package/dist/yoga.mjs.map +1 -0
- package/package.json +25 -11
- package/src/conformance.ts +700 -0
- package/src/context.ts +13 -0
- package/src/driver.ts +63 -0
- package/src/engine.ts +146 -0
- package/src/execution.ts +602 -67
- package/src/index.ts +7 -0
- package/src/limits.ts +22 -1
- package/src/options.ts +53 -4
- package/src/plugin.ts +170 -29
- package/src/testing.ts +3 -0
- package/src/transport/builtin.ts +57 -0
- package/src/transport/fetch.ts +57 -0
- package/src/transport/http.ts +12 -9
- package/src/types.ts +12 -2
- package/src/yoga.ts +471 -0
- package/dist/plugin-CR_wVfRU.mjs.map +0 -1
package/dist/yoga.mjs
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { a as Media, f as GraphQLErrorCode, i as notJson, o as negotiate, r as isJsonRequest } from "./http-BWcayUtH.mjs";
|
|
2
|
+
import { ErrorCode, ZnightError } from "@znight/core";
|
|
3
|
+
import { parseByteSize } from "@znight/http";
|
|
4
|
+
import { GraphQLError } from "graphql";
|
|
5
|
+
import { createYoga } from "graphql-yoga";
|
|
6
|
+
// src/transport/fetch.ts
|
|
7
|
+
/** The Web `Request` of the route's request, read under `limit` bytes. */
|
|
8
|
+
function fetchRequest(ctx, limit) {
|
|
9
|
+
const native = ctx.request;
|
|
10
|
+
const method = ctx.method;
|
|
11
|
+
const bodyless = method === "GET" || method === "HEAD";
|
|
12
|
+
if (native instanceof Request && !native.bodyUsed) {
|
|
13
|
+
if (bodyless) return native;
|
|
14
|
+
const length = ctx.headers["content-length"];
|
|
15
|
+
if (length !== void 0 && ctx.headers["transfer-encoding"] === void 0) {
|
|
16
|
+
const declared = Number(length);
|
|
17
|
+
if (Number.isSafeInteger(declared) && declared >= 0 && declared <= limit) return native;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const url = requestUrl(ctx);
|
|
21
|
+
const headers = requestHeaders(ctx);
|
|
22
|
+
if (bodyless) return new Request(url, {
|
|
23
|
+
method,
|
|
24
|
+
headers,
|
|
25
|
+
signal: ctx.signal
|
|
26
|
+
});
|
|
27
|
+
return ctx.rawBody().then((body) => new Request(url, {
|
|
28
|
+
method,
|
|
29
|
+
headers,
|
|
30
|
+
body,
|
|
31
|
+
signal: ctx.signal
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
/** The absolute URL of the request: the raw target on the request's protocol and host. */
|
|
35
|
+
function requestUrl(ctx) {
|
|
36
|
+
const host = ctx.headers["host"];
|
|
37
|
+
const target = ctx.url;
|
|
38
|
+
if (host !== void 0 && target.startsWith("/") && !target.startsWith("//")) {
|
|
39
|
+
const url = `${ctx.protocol}://${host}${target}`;
|
|
40
|
+
if (URL.canParse(url)) return url;
|
|
41
|
+
}
|
|
42
|
+
return new URL(target.startsWith("/") ? target.replace(/^\/+/, "/") : `/${target}`, `${ctx.protocol}://localhost`).href;
|
|
43
|
+
}
|
|
44
|
+
function requestHeaders(ctx) {
|
|
45
|
+
const headers = new Headers();
|
|
46
|
+
const source = ctx.headers;
|
|
47
|
+
for (const name in source) {
|
|
48
|
+
const value = source[name];
|
|
49
|
+
if (value !== void 0) headers.append(name, value);
|
|
50
|
+
}
|
|
51
|
+
return headers;
|
|
52
|
+
}
|
|
53
|
+
// src/yoga.ts
|
|
54
|
+
/** Options of Yoga the engine or `GraphQLPlugin` own (RFC 0009 §4.6): refused by {@link yogaDriver}. */
|
|
55
|
+
const OWNED = [
|
|
56
|
+
"schema",
|
|
57
|
+
"context",
|
|
58
|
+
"graphqlEndpoint",
|
|
59
|
+
"maskedErrors",
|
|
60
|
+
"cors",
|
|
61
|
+
"fetchAPI",
|
|
62
|
+
"maxRequestBodySize",
|
|
63
|
+
"parserAndValidationCache",
|
|
64
|
+
"requestId",
|
|
65
|
+
"healthCheckEndpoint",
|
|
66
|
+
"landingPage",
|
|
67
|
+
"disposeOnProcessTerminate",
|
|
68
|
+
"batching",
|
|
69
|
+
"multipart"
|
|
70
|
+
];
|
|
71
|
+
/** Where each owned option comes from, for the ZNIGHT066 hint. */
|
|
72
|
+
const OWNER = {
|
|
73
|
+
schema: "the engine serves the schema GraphQLPlugin built",
|
|
74
|
+
context: "resolvers get GraphQLContext; read Yoga's context with yogaContext(ctx)",
|
|
75
|
+
graphqlEndpoint: "set the path option of GraphQLPlugin",
|
|
76
|
+
maskedErrors: "the engine masks errors once (exposeErrors, errorMappers)",
|
|
77
|
+
cors: "set the cors option of HttpPlugin",
|
|
78
|
+
fetchAPI: "the driver gives Yoga the runtime's own Fetch API",
|
|
79
|
+
maxRequestBodySize: "set http.bodyLimit of GraphQLPlugin",
|
|
80
|
+
parserAndValidationCache: "the engine caches documents (the documentCache option of GraphQLPlugin)",
|
|
81
|
+
requestId: "Yoga uses the request id of HttpPlugin",
|
|
82
|
+
healthCheckEndpoint: "the driver only serves the GraphQL endpoint",
|
|
83
|
+
landingPage: "the driver only serves the GraphQL endpoint",
|
|
84
|
+
disposeOnProcessTerminate: "the application stops Yoga once HttpPlugin drained its requests",
|
|
85
|
+
batching: "Send one operation per request: the operations of a batch would share the request scope and its GraphQLContext",
|
|
86
|
+
multipart: "POST bodies are application/json only: form and multipart bodies skip the CORS preflight (CSRF); upload files through an HTTP controller"
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* The GraphQL Yoga driver (RFC 0009 §5.1): `GraphQLPlugin.configure({ driver: yogaDriver({ plugins }) })`.
|
|
90
|
+
* Yoga handles the protocol and its Envelop plugins; the engine parses, validates and executes
|
|
91
|
+
* (guards, loaders, request scope, masking, the `graphql:operation` hook). Plugins that replace
|
|
92
|
+
* the engine's execution (`useGraphQlJit`) are refused: the operation fails with ZNIGHT066.
|
|
93
|
+
*
|
|
94
|
+
* Like the built-in transport, a `POST` body is `application/json` only (415 otherwise): form,
|
|
95
|
+
* multipart and text bodies, which a cross-site page sends without a CORS preflight, never run
|
|
96
|
+
* an operation (CSRF). Errors thrown outside the engine (by Yoga or an Envelop plugin) are masked
|
|
97
|
+
* like the engine's (`Unexpected error.` unless `exposeErrors`) and logged.
|
|
98
|
+
*
|
|
99
|
+
* ```ts
|
|
100
|
+
* GraphQLPlugin.configure({
|
|
101
|
+
* guards: [AccessGuard],
|
|
102
|
+
* driver: yogaDriver({ plugins: [useResponseCache({ session: () => null })] }),
|
|
103
|
+
* })
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
function yogaDriver(options = {}) {
|
|
107
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) throw invalid("yogaDriver(): expected an options object", ["yogaDriver({ plugins: [...] })"]);
|
|
108
|
+
for (const key of OWNED) if (options[key] !== void 0) throw invalid(`yogaDriver(): the ${key} option of Yoga is not available with Znight`, [OWNER[key]]);
|
|
109
|
+
const graphiql = options.graphiql;
|
|
110
|
+
if (graphiql !== void 0 && (graphiql === null || typeof graphiql !== "object")) throw invalid("yogaDriver(): graphiql takes GraphiQL's options", ["Turn the IDE on or off with the graphiql option of GraphQLPlugin"]);
|
|
111
|
+
if (options.plugins !== void 0 && !Array.isArray(options.plugins)) throw invalid("yogaDriver(): plugins must be an array of Envelop plugins", []);
|
|
112
|
+
return new Driver(options);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Yoga's context of an operation received by the Yoga driver (RFC 0009 Q2): `GraphQLContext.serverContext`,
|
|
116
|
+
* typed. For the rare resolver that needs Yoga's `request`, `params` or the context an Envelop
|
|
117
|
+
* plugin extended; the HTTP request is `ctx.http`, the user a request-scoped provider.
|
|
118
|
+
*
|
|
119
|
+
* @throws ZnightError (ZNIGHT066) when the operation did not come through the Yoga driver.
|
|
120
|
+
*/
|
|
121
|
+
function yogaContext(ctx) {
|
|
122
|
+
const server = ctx?.serverContext;
|
|
123
|
+
if (server === null || typeof server !== "object" || server.znight === void 0 || server.params === void 0) throw invalid("yogaContext(): the operation was not received by the Yoga driver", ["yogaContext() reads GraphQLContext.serverContext of operations served by yogaDriver()"]);
|
|
124
|
+
return server;
|
|
125
|
+
}
|
|
126
|
+
const SSE = Object.freeze({ transport: "sse" });
|
|
127
|
+
const DEFAULT_BODY_LIMIT = 1048576;
|
|
128
|
+
var Driver = class {
|
|
129
|
+
name = "yoga";
|
|
130
|
+
#options;
|
|
131
|
+
#yoga;
|
|
132
|
+
/** Stopped: the application stops, requests that still reach the driver get a 503. */
|
|
133
|
+
#stopped = false;
|
|
134
|
+
#get = true;
|
|
135
|
+
#ide = false;
|
|
136
|
+
#limit = DEFAULT_BODY_LIMIT;
|
|
137
|
+
constructor(options) {
|
|
138
|
+
this.#options = options;
|
|
139
|
+
}
|
|
140
|
+
get yoga() {
|
|
141
|
+
return this.#yoga;
|
|
142
|
+
}
|
|
143
|
+
/** `GET` and `POST` on the path (`GET` answers 405 when neither queries over `GET` nor GraphiQL apply). */
|
|
144
|
+
routes(context) {
|
|
145
|
+
return [{
|
|
146
|
+
method: "GET",
|
|
147
|
+
path: context.path,
|
|
148
|
+
name: "yoga",
|
|
149
|
+
handler: this.#getHandler
|
|
150
|
+
}, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
path: context.path,
|
|
153
|
+
name: "yoga",
|
|
154
|
+
handler: this.#postHandler
|
|
155
|
+
}];
|
|
156
|
+
}
|
|
157
|
+
start(engine, context) {
|
|
158
|
+
if (this.#yoga !== void 0) throw invalid("yogaDriver(): this driver already serves an application", ["Create one driver per application: GraphQLPlugin.configure({ driver: yogaDriver() })"]);
|
|
159
|
+
const settings = engine.settings;
|
|
160
|
+
const http = settings.http;
|
|
161
|
+
this.#get = http !== false && http.get;
|
|
162
|
+
this.#ide = settings.ide;
|
|
163
|
+
this.#limit = http === false ? DEFAULT_BODY_LIMIT : parseByteSize(http.bodyLimit) ?? DEFAULT_BODY_LIMIT;
|
|
164
|
+
const { graphiql, plugins = [], logging = false, ...rest } = this.#options;
|
|
165
|
+
const engineFns = engineFunctions(engine, context.logger);
|
|
166
|
+
this.#yoga = createYoga({
|
|
167
|
+
...rest,
|
|
168
|
+
logging,
|
|
169
|
+
schema: engine.schema,
|
|
170
|
+
graphqlEndpoint: settings.path,
|
|
171
|
+
graphiql: settings.ide ? graphiql ?? true : false,
|
|
172
|
+
landingPage: false,
|
|
173
|
+
healthCheckEndpoint: UNREACHABLE,
|
|
174
|
+
maskedErrors: { maskError: outsideErrors(settings.exposeErrors, context.logger) },
|
|
175
|
+
batching: false,
|
|
176
|
+
multipart: false,
|
|
177
|
+
cors: false,
|
|
178
|
+
maxRequestBodySize: this.#limit,
|
|
179
|
+
parserAndValidationCache: false,
|
|
180
|
+
disposeOnProcessTerminate: false,
|
|
181
|
+
requestId: {
|
|
182
|
+
generateRequestId: requestIdOf,
|
|
183
|
+
trustHeader: false
|
|
184
|
+
},
|
|
185
|
+
fetchAPI: fetchApi(),
|
|
186
|
+
plugins: [
|
|
187
|
+
engineFns.plugin,
|
|
188
|
+
...plugins,
|
|
189
|
+
engineFns.guard
|
|
190
|
+
]
|
|
191
|
+
});
|
|
192
|
+
this.#stopped = false;
|
|
193
|
+
}
|
|
194
|
+
/** Called once `HttpPlugin` drained the requests (`GraphQLPlugin.onDispose`). */
|
|
195
|
+
async stop() {
|
|
196
|
+
const yoga = this.#yoga;
|
|
197
|
+
this.#yoga = void 0;
|
|
198
|
+
this.#stopped = true;
|
|
199
|
+
await yoga?.dispose();
|
|
200
|
+
}
|
|
201
|
+
#handle = (ctx) => {
|
|
202
|
+
const yoga = this.#yoga;
|
|
203
|
+
if (yoga === void 0) {
|
|
204
|
+
if (this.#stopped) return shuttingDown();
|
|
205
|
+
throw notStarted();
|
|
206
|
+
}
|
|
207
|
+
const request = fetchRequest(ctx, this.#limit);
|
|
208
|
+
const server = { znight: ctx };
|
|
209
|
+
const response = request instanceof Request ? yoga.fetch(request, server) : request.then((built) => yoga.fetch(built, server));
|
|
210
|
+
return response instanceof Response ? response : Promise.resolve(response).then(sendable);
|
|
211
|
+
};
|
|
212
|
+
/** `POST`: JSON bodies only (the 415 of the built-in transport otherwise: no CSRF). */
|
|
213
|
+
#postHandler = (ctx) => {
|
|
214
|
+
const headers = ctx.headers;
|
|
215
|
+
if (!isJsonRequest(headers["content-type"])) return notJson(jsonMedia(headers["accept"]));
|
|
216
|
+
return this.#handle(ctx);
|
|
217
|
+
};
|
|
218
|
+
/** `GET`: Yoga's, or 405 when queries over `GET` are off (GraphiQL pages still served). */
|
|
219
|
+
#getHandler = (ctx) => {
|
|
220
|
+
if (this.#get || this.#ide && acceptsHtml(ctx)) return this.#handle(ctx);
|
|
221
|
+
return getNotAllowed();
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
/** A path no request reaches: the driver's routes only serve the GraphQL endpoint. */
|
|
225
|
+
const UNREACHABLE = "/.znight/unreachable";
|
|
226
|
+
/**
|
|
227
|
+
* The Fetch API of the runtime: on Node.js, Yoga would otherwise use the ponyfills of
|
|
228
|
+
* `@whatwg-node/fetch`, whose `Response` objects `HttpPlugin` cannot send.
|
|
229
|
+
*/
|
|
230
|
+
function fetchApi() {
|
|
231
|
+
return {
|
|
232
|
+
fetch: globalThis.fetch,
|
|
233
|
+
Request: globalThis.Request,
|
|
234
|
+
Response: globalThis.Response,
|
|
235
|
+
Headers: globalThis.Headers,
|
|
236
|
+
FormData: globalThis.FormData,
|
|
237
|
+
ReadableStream: globalThis.ReadableStream,
|
|
238
|
+
WritableStream: globalThis.WritableStream,
|
|
239
|
+
TransformStream: globalThis.TransformStream,
|
|
240
|
+
CompressionStream: globalThis.CompressionStream,
|
|
241
|
+
DecompressionStream: globalThis.DecompressionStream,
|
|
242
|
+
TextDecoderStream: globalThis.TextDecoderStream,
|
|
243
|
+
TextEncoderStream: globalThis.TextEncoderStream,
|
|
244
|
+
Blob: globalThis.Blob,
|
|
245
|
+
File: globalThis.File,
|
|
246
|
+
TextEncoder: globalThis.TextEncoder,
|
|
247
|
+
TextDecoder: globalThis.TextDecoder,
|
|
248
|
+
URL: globalThis.URL,
|
|
249
|
+
URLSearchParams: globalThis.URLSearchParams,
|
|
250
|
+
btoa: globalThis.btoa
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* A `Response` `HttpPlugin` recognises: `@hono/node-server` replaces the global `Response` when it
|
|
255
|
+
* starts listening, after Yoga captured the runtime's own; such a response is rebuilt on the
|
|
256
|
+
* global class (same status, headers and body stream).
|
|
257
|
+
*/
|
|
258
|
+
function sendable(response) {
|
|
259
|
+
if (response instanceof Response) return response;
|
|
260
|
+
const foreign = response;
|
|
261
|
+
return new Response(foreign.body, foreign);
|
|
262
|
+
}
|
|
263
|
+
/** The request id of `HttpPlugin` (a UUID for a request handed to `yoga.fetch()` directly). */
|
|
264
|
+
function requestIdOf(payload) {
|
|
265
|
+
return payload.context.znight?.requestId ?? crypto.randomUUID();
|
|
266
|
+
}
|
|
267
|
+
function acceptsHtml(ctx) {
|
|
268
|
+
return ctx.headers["accept"]?.includes("text/html") === true;
|
|
269
|
+
}
|
|
270
|
+
/** The JSON type a refusal is written in, like the built-in transport: `application/json` unless the client prefers `application/graphql-response+json`. */
|
|
271
|
+
function jsonMedia(accept) {
|
|
272
|
+
const media = negotiate(accept, 1 << Media.GraphQLJson | 1 << Media.Json) & 7;
|
|
273
|
+
return media === Media.None ? Media.Json : media;
|
|
274
|
+
}
|
|
275
|
+
/** A request reaching the driver once it stopped (after `HttpPlugin` drained): 503. */
|
|
276
|
+
function shuttingDown() {
|
|
277
|
+
return Response.json({ errors: [{
|
|
278
|
+
message: "The server is shutting down, retry later",
|
|
279
|
+
extensions: { code: GraphQLErrorCode.ServiceUnavailable }
|
|
280
|
+
}] }, { status: 503 });
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Yoga's `maskError`: the errors of the engine (masked already) and the `GraphQLError`s thrown on
|
|
284
|
+
* purpose (by Yoga or a plugin, with a code) are kept; any other error, thrown outside the engine
|
|
285
|
+
* (an Envelop plugin's hook, Yoga itself), is logged and masked like the engine masks it
|
|
286
|
+
* (`Unexpected error.`, its message with `exposeErrors`). Yoga then answers 500 for a request error.
|
|
287
|
+
*/
|
|
288
|
+
function outsideErrors(expose, logger) {
|
|
289
|
+
return (error, message) => {
|
|
290
|
+
if (isOriginalGraphQLError(error)) return error;
|
|
291
|
+
logger.error("GraphQL: an error was thrown outside the engine (by Yoga or an Envelop plugin)", { err: error });
|
|
292
|
+
const original = error instanceof Error ? error : new Error(String(error));
|
|
293
|
+
return new GraphQLError(expose ? original.message : message, {
|
|
294
|
+
originalError: original,
|
|
295
|
+
extensions: { code: GraphQLErrorCode.InternalServerError }
|
|
296
|
+
});
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/** A `GraphQLError` that wraps no other error: the engine's, or thrown on purpose. */
|
|
300
|
+
function isOriginalGraphQLError(error) {
|
|
301
|
+
if (!(error instanceof GraphQLError)) return false;
|
|
302
|
+
const original = error.originalError;
|
|
303
|
+
return original === void 0 || original === null || isOriginalGraphQLError(original);
|
|
304
|
+
}
|
|
305
|
+
function getNotAllowed() {
|
|
306
|
+
return Response.json({ errors: [{
|
|
307
|
+
message: "GraphQL operations are sent with POST on this endpoint",
|
|
308
|
+
extensions: { code: "BAD_REQUEST" }
|
|
309
|
+
}] }, {
|
|
310
|
+
status: 405,
|
|
311
|
+
headers: { allow: "POST" }
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* The Envelop plugins of the engine: one installed before the application's plugins, which makes
|
|
316
|
+
* the engine Yoga's `parse`, `validate`, `execute` and `subscribe` (Envelop's `useEngine`), and a
|
|
317
|
+
* guard installed after them, which refuses an operation that a plugin executed elsewhere
|
|
318
|
+
* (`useGraphQlJit`): its resolvers would not get the `GraphQLContext`. Plugins wrapping the
|
|
319
|
+
* execution (`useResponseCache`) keep working: they call the engine.
|
|
320
|
+
*/
|
|
321
|
+
function engineFunctions(engine, logger) {
|
|
322
|
+
const parse = engine.parse;
|
|
323
|
+
const validate = (schema, document, rules) => engine.validate(schema, document, rules);
|
|
324
|
+
let watching = false;
|
|
325
|
+
const ran = /* @__PURE__ */ new WeakSet();
|
|
326
|
+
const execute = (args) => {
|
|
327
|
+
if (watching) mark(ran, args.contextValue);
|
|
328
|
+
return engine.execute(args);
|
|
329
|
+
};
|
|
330
|
+
const subscribe = (args) => {
|
|
331
|
+
if (watching) mark(ran, args.contextValue);
|
|
332
|
+
return engine.subscribe(args, SSE);
|
|
333
|
+
};
|
|
334
|
+
let reported = false;
|
|
335
|
+
const bypassed = () => {
|
|
336
|
+
const error = invalid("GraphQLPlugin: a Yoga plugin executed the operation instead of the Znight engine (useGraphQlJit?)", ["Remove the plugin: resolvers, guards and loaders only run on the engine"]);
|
|
337
|
+
if (!reported) {
|
|
338
|
+
reported = true;
|
|
339
|
+
logger.error(error.message, { error });
|
|
340
|
+
}
|
|
341
|
+
return { errors: [new GraphQLError(error.message, { extensions: { code: "INTERNAL_SERVER_ERROR" } })] };
|
|
342
|
+
};
|
|
343
|
+
return {
|
|
344
|
+
plugin: {
|
|
345
|
+
onParse: ({ setParseFn }) => setParseFn(parse),
|
|
346
|
+
onValidate: ({ setValidationFn }) => setValidationFn(validate),
|
|
347
|
+
onExecute: ({ setExecuteFn }) => setExecuteFn(execute),
|
|
348
|
+
onSubscribe: ({ setSubscribeFn }) => setSubscribeFn(subscribe)
|
|
349
|
+
},
|
|
350
|
+
guard: {
|
|
351
|
+
onExecute: ({ executeFn, context }) => {
|
|
352
|
+
if (executeFn === execute) return void 0;
|
|
353
|
+
watching = true;
|
|
354
|
+
return { onExecuteDone: ({ setResult }) => {
|
|
355
|
+
if (!ran.has(context)) setResult(bypassed());
|
|
356
|
+
} };
|
|
357
|
+
},
|
|
358
|
+
onSubscribe: ({ subscribeFn, context }) => {
|
|
359
|
+
if (subscribeFn === subscribe) return void 0;
|
|
360
|
+
watching = true;
|
|
361
|
+
return { onSubscribeResult: ({ setResult }) => {
|
|
362
|
+
if (!ran.has(context)) setResult(bypassed());
|
|
363
|
+
} };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function mark(ran, context) {
|
|
369
|
+
if (context !== null && typeof context === "object") ran.add(context);
|
|
370
|
+
}
|
|
371
|
+
function notStarted() {
|
|
372
|
+
return new ZnightError(ErrorCode.LifecycleFailed, "The Yoga driver is not started yet", { hints: ["GraphQLPlugin starts its driver in onBootstrap"] });
|
|
373
|
+
}
|
|
374
|
+
function invalid(message, hints) {
|
|
375
|
+
return new ZnightError(ErrorCode.InvalidGraphQLSetup, message, { hints: [...hints] });
|
|
376
|
+
}
|
|
377
|
+
export { yogaContext, yogaDriver };
|
|
378
|
+
|
|
379
|
+
//# sourceMappingURL=yoga.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"yoga.mjs","names":[],"sources":["../src/transport/fetch.ts","../src/yoga.ts"],"sourcesContent":["/**\n * The Fetch bridge of route-mounted drivers (RFC 0009 §4.4, Q10): the Web `Request` a Fetch server\n * (GraphQL Yoga) handles, built from the `HttpContext` of the route. Internal to `@znight/graphql`\n * (a candidate for `@znight/http` once a second user appears).\n *\n * - On `honoAdapter()` the native request already is a Web `Request`: it is handed over as is when\n * its body was not read and cannot exceed the route's body limit (a `GET`, or a declared\n * `content-length` within the limit), so nothing is copied.\n * - Otherwise (Node-style adapters, chunked bodies) a `Request` is built from the method, the URL,\n * the headers and the body read by `ctx.rawBody()`: the route's body limit then answers with\n * `HttpPlugin`'s 413 on every adapter, before the server sees the request. GraphQL servers buffer\n * the body to parse it anyway, so reading it first costs no extra copy of note.\n */\nimport type { HttpContext } from '@znight/http'\n\n/** The Web `Request` of the route's request, read under `limit` bytes. */\nexport function fetchRequest(ctx: HttpContext, limit: number): Request | Promise<Request> {\n const native = ctx.request\n const method = ctx.method\n const bodyless = method === 'GET' || method === 'HEAD'\n if (native instanceof Request && !native.bodyUsed) {\n if (bodyless) return native\n const length = ctx.headers['content-length']\n if (length !== undefined && ctx.headers['transfer-encoding'] === undefined) {\n // past the limit, ctx.rawBody() below rejects with HttpPlugin's own 413\n const declared = Number(length)\n if (Number.isSafeInteger(declared) && declared >= 0 && declared <= limit) return native\n }\n }\n const url = requestUrl(ctx)\n const headers = requestHeaders(ctx)\n if (bodyless) return new Request(url, { method, headers, signal: ctx.signal })\n return ctx.rawBody().then(body => new Request(url, { method, headers, body, signal: ctx.signal }))\n}\n\n/** The absolute URL of the request: the raw target on the request's protocol and host. */\nfunction requestUrl(ctx: HttpContext): string {\n const host = ctx.headers['host']\n const target = ctx.url\n if (host !== undefined && target.startsWith('/') && !target.startsWith('//')) {\n const url = `${ctx.protocol}://${host}${target}`\n if (URL.canParse(url)) return url\n }\n // no host header, or one a URL cannot hold: the path and query string still route the request\n return new URL(target.startsWith('/') ? target.replace(/^\\/+/, '/') : `/${target}`, `${ctx.protocol}://localhost`)\n .href\n}\n\nfunction requestHeaders(ctx: HttpContext): Headers {\n const headers = new Headers()\n const source = ctx.headers\n for (const name in source) {\n const value = source[name]\n if (value !== undefined) headers.append(name, value)\n }\n return headers\n}\n","/**\n * `@znight/graphql/yoga`: the GraphQL Yoga driver (RFC 0009 §5.1). GraphQL Yoga 6 serves the schema\n * of `GraphQLPlugin` (its protocol, Envelop plugins, GraphiQL, SSE subscriptions) and hands every\n * document to Znight's engine, which parses it through its document cache, validates it with its\n * limits, and executes it with the application's guards, loaders, request scope, error masking and\n * hook. Mounted as plugin routes of `HttpPlugin`: the HTTP pipeline (request scope, hooks, guards,\n * CORS, security headers, drain) wraps every request, on every adapter, on Node.js and Bun.\n *\n * Needs `graphql-yoga` (an optional peer dependency of `@znight/graphql`), only loaded by this entry.\n */\nimport type { CoreLogger } from '@znight/core'\nimport type { HttpContext } from '@znight/http'\nimport type { DocumentNode, ExecutionArgs, ExecutionResult, GraphQLSchema, ValidationRule } from 'graphql'\nimport type {\n GraphiQLOptions,\n YogaInitialContext,\n Plugin as YogaPlugin,\n YogaServerInstance,\n YogaServerOptions,\n} from 'graphql-yoga'\nimport type { GraphQLDriver, GraphQLDriverContext, GraphQLDriverRoute, GraphQLDriverRoutesContext } from './driver.ts'\nimport type { EngineExecuteOptions, GraphQLEngine } from './engine.ts'\nimport type { GraphQLContext } from './types.ts'\n\nimport { ErrorCode, ZnightError } from '@znight/core'\nimport { parseByteSize } from '@znight/http'\nimport { GraphQLError } from 'graphql'\nimport { createYoga } from 'graphql-yoga'\n\nimport { GraphQLErrorCode } from './errors.ts'\nimport { MEDIA, Media, type MediaKind, negotiate } from './transport/accept.ts'\nimport { fetchRequest } from './transport/fetch.ts'\nimport { isJsonRequest, notJson } from './transport/http.ts'\n\n/** The server context the driver gives Yoga: the `HttpContext` of the route, as `znight`. */\nexport interface YogaServerContext {\n readonly znight: HttpContext\n}\n\n/**\n * Yoga's context of an operation: its initial context (`request`, `params`, `log`) and the server\n * context of the driver. What {@link yogaContext} returns.\n */\nexport type YogaContext = YogaInitialContext & YogaServerContext\n\n/** Options of Yoga the engine or `GraphQLPlugin` own (RFC 0009 §4.6): refused by {@link yogaDriver}. */\nconst OWNED = [\n 'schema',\n 'context',\n 'graphqlEndpoint',\n 'maskedErrors',\n 'cors',\n 'fetchAPI',\n 'maxRequestBodySize',\n 'parserAndValidationCache',\n 'requestId',\n 'healthCheckEndpoint',\n 'landingPage',\n 'disposeOnProcessTerminate',\n 'batching',\n 'multipart',\n] as const\n\n/** Where each owned option comes from, for the ZNIGHT066 hint. */\nconst OWNER: Readonly<Record<(typeof OWNED)[number], string>> = {\n schema: 'the engine serves the schema GraphQLPlugin built',\n context: \"resolvers get GraphQLContext; read Yoga's context with yogaContext(ctx)\",\n graphqlEndpoint: 'set the path option of GraphQLPlugin',\n maskedErrors: 'the engine masks errors once (exposeErrors, errorMappers)',\n cors: 'set the cors option of HttpPlugin',\n fetchAPI: \"the driver gives Yoga the runtime's own Fetch API\",\n maxRequestBodySize: 'set http.bodyLimit of GraphQLPlugin',\n parserAndValidationCache: 'the engine caches documents (the documentCache option of GraphQLPlugin)',\n requestId: 'Yoga uses the request id of HttpPlugin',\n healthCheckEndpoint: 'the driver only serves the GraphQL endpoint',\n landingPage: 'the driver only serves the GraphQL endpoint',\n disposeOnProcessTerminate: 'the application stops Yoga once HttpPlugin drained its requests',\n batching:\n 'Send one operation per request: the operations of a batch would share the request scope and its GraphQLContext',\n multipart:\n 'POST bodies are application/json only: form and multipart bodies skip the CORS preflight (CSRF); upload files through an HTTP controller',\n}\n\n/**\n * Options of {@link yogaDriver}: Yoga's own (`plugins`, `logging`, `allowedHeaders`...), except\n * those the engine, `GraphQLPlugin` or `HttpPlugin` own: the schema, the context, the endpoint\n * (`path`), masking (`exposeErrors`), CORS, the body limit (`http.bodyLimit`), the document cache,\n * the request id and the IDE switch (`graphiql`); and those the driver refuses: `batching` (one\n * operation per request scope) and `multipart` (JSON bodies only, see {@link yogaDriver}). Yoga's\n * `logging` is off unless set: requests and operations are logged by Znight.\n */\nexport interface YogaDriverOptions\n extends Omit<YogaServerOptions<YogaServerContext, Record<string, unknown>>, (typeof OWNED)[number] | 'graphiql'> {\n /**\n * GraphiQL's options (title, default query...), when the `graphiql` option of `GraphQLPlugin` is\n * on (it decides whether the IDE is served).\n */\n readonly graphiql?: GraphiQLOptions\n}\n\n/** A GraphQL Yoga driver: `GraphQLPlugin.configure({ driver: yogaDriver() })`. */\nexport interface YogaDriver extends GraphQLDriver {\n readonly name: 'yoga'\n /** The Yoga server, once started (`undefined` before `start()` and after `stop()`). */\n readonly yoga: YogaServerInstance<YogaServerContext, Record<string, unknown>> | undefined\n}\n\n/**\n * The GraphQL Yoga driver (RFC 0009 §5.1): `GraphQLPlugin.configure({ driver: yogaDriver({ plugins }) })`.\n * Yoga handles the protocol and its Envelop plugins; the engine parses, validates and executes\n * (guards, loaders, request scope, masking, the `graphql:operation` hook). Plugins that replace\n * the engine's execution (`useGraphQlJit`) are refused: the operation fails with ZNIGHT066.\n *\n * Like the built-in transport, a `POST` body is `application/json` only (415 otherwise): form,\n * multipart and text bodies, which a cross-site page sends without a CORS preflight, never run\n * an operation (CSRF). Errors thrown outside the engine (by Yoga or an Envelop plugin) are masked\n * like the engine's (`Unexpected error.` unless `exposeErrors`) and logged.\n *\n * ```ts\n * GraphQLPlugin.configure({\n * guards: [AccessGuard],\n * driver: yogaDriver({ plugins: [useResponseCache({ session: () => null })] }),\n * })\n * ```\n */\nexport function yogaDriver(options: YogaDriverOptions = {}): YogaDriver {\n if (options === null || typeof options !== 'object' || Array.isArray(options)) {\n throw invalid('yogaDriver(): expected an options object', ['yogaDriver({ plugins: [...] })'])\n }\n for (const key of OWNED) {\n if ((options as Record<string, unknown>)[key] !== undefined) {\n throw invalid(`yogaDriver(): the ${key} option of Yoga is not available with Znight`, [OWNER[key]])\n }\n }\n const graphiql: unknown = options.graphiql\n if (graphiql !== undefined && (graphiql === null || typeof graphiql !== 'object')) {\n throw invalid(\"yogaDriver(): graphiql takes GraphiQL's options\", [\n 'Turn the IDE on or off with the graphiql option of GraphQLPlugin',\n ])\n }\n if (options.plugins !== undefined && !Array.isArray(options.plugins)) {\n throw invalid('yogaDriver(): plugins must be an array of Envelop plugins', [])\n }\n return new Driver(options)\n}\n\n/**\n * Yoga's context of an operation received by the Yoga driver (RFC 0009 Q2): `GraphQLContext.serverContext`,\n * typed. For the rare resolver that needs Yoga's `request`, `params` or the context an Envelop\n * plugin extended; the HTTP request is `ctx.http`, the user a request-scoped provider.\n *\n * @throws ZnightError (ZNIGHT066) when the operation did not come through the Yoga driver.\n */\nexport function yogaContext<TContext extends Record<string, unknown> = Record<string, unknown>>(\n ctx: GraphQLContext,\n): YogaContext & TContext {\n const server = ctx?.serverContext as Partial<YogaContext> | undefined\n if (server === null || typeof server !== 'object' || server.znight === undefined || server.params === undefined) {\n throw invalid('yogaContext(): the operation was not received by the Yoga driver', [\n 'yogaContext() reads GraphQLContext.serverContext of operations served by yogaDriver()',\n ])\n }\n return server as YogaContext & TContext\n}\n\n// ------------------------------------------------------------------------------------------------\n\ntype Yoga = YogaServerInstance<YogaServerContext, Record<string, unknown>>\n\nconst SSE: EngineExecuteOptions = Object.freeze({ transport: 'sse' })\nconst DEFAULT_BODY_LIMIT = 1024 * 1024\n\nclass Driver implements YogaDriver {\n readonly name = 'yoga' as const\n readonly #options: YogaDriverOptions\n #yoga: Yoga | undefined\n /** Stopped: the application stops, requests that still reach the driver get a 503. */\n #stopped = false\n #get = true\n #ide = false\n #limit = DEFAULT_BODY_LIMIT\n\n constructor(options: YogaDriverOptions) {\n this.#options = options\n }\n\n get yoga(): Yoga | undefined {\n return this.#yoga\n }\n\n /** `GET` and `POST` on the path (`GET` answers 405 when neither queries over `GET` nor GraphiQL apply). */\n routes(context: GraphQLDriverRoutesContext): readonly GraphQLDriverRoute[] {\n return [\n { method: 'GET', path: context.path, name: 'yoga', handler: this.#getHandler },\n { method: 'POST', path: context.path, name: 'yoga', handler: this.#postHandler },\n ]\n }\n\n start(engine: GraphQLEngine, context: GraphQLDriverContext): void {\n if (this.#yoga !== undefined) {\n throw invalid('yogaDriver(): this driver already serves an application', [\n 'Create one driver per application: GraphQLPlugin.configure({ driver: yogaDriver() })',\n ])\n }\n const settings = engine.settings\n const http = settings.http\n this.#get = http !== false && http.get\n this.#ide = settings.ide\n this.#limit = http === false ? DEFAULT_BODY_LIMIT : (parseByteSize(http.bodyLimit) ?? DEFAULT_BODY_LIMIT)\n const { graphiql, plugins = [], logging = false, ...rest } = this.#options\n const engineFns = engineFunctions(engine, context.logger)\n this.#yoga = createYoga<YogaServerContext, Record<string, unknown>>({\n ...rest,\n logging,\n schema: engine.schema,\n graphqlEndpoint: settings.path,\n graphiql: settings.ide ? (graphiql ?? true) : false,\n landingPage: false,\n healthCheckEndpoint: UNREACHABLE,\n // the engine masked its errors (kept as they are); those thrown outside of it are masked here\n maskedErrors: { maskError: outsideErrors(settings.exposeErrors, context.logger) },\n batching: false,\n multipart: false,\n cors: false, // HttpPlugin's CORS\n maxRequestBodySize: this.#limit,\n parserAndValidationCache: false, // the engine's document cache\n disposeOnProcessTerminate: false,\n requestId: { generateRequestId: requestIdOf, trustHeader: false },\n fetchAPI: fetchApi(),\n plugins: [engineFns.plugin, ...plugins, engineFns.guard],\n })\n this.#stopped = false\n }\n\n /** Called once `HttpPlugin` drained the requests (`GraphQLPlugin.onDispose`). */\n async stop(): Promise<void> {\n const yoga = this.#yoga\n this.#yoga = undefined\n this.#stopped = true\n await yoga?.dispose()\n }\n\n readonly #handle = (ctx: HttpContext): Response | Promise<Response> => {\n const yoga = this.#yoga\n if (yoga === undefined) {\n if (this.#stopped) return shuttingDown()\n throw notStarted()\n }\n const request = fetchRequest(ctx, this.#limit)\n const server: YogaServerContext = { znight: ctx }\n const response =\n request instanceof Request ? yoga.fetch(request, server) : request.then(built => yoga.fetch(built, server))\n return response instanceof Response ? response : Promise.resolve(response).then(sendable)\n }\n\n /** `POST`: JSON bodies only (the 415 of the built-in transport otherwise: no CSRF). */\n readonly #postHandler = (ctx: HttpContext): unknown => {\n const headers = ctx.headers\n if (!isJsonRequest(headers['content-type'])) return notJson(jsonMedia(headers['accept']))\n return this.#handle(ctx)\n }\n\n /** `GET`: Yoga's, or 405 when queries over `GET` are off (GraphiQL pages still served). */\n readonly #getHandler = (ctx: HttpContext): Response | Promise<Response> => {\n if (this.#get || (this.#ide && acceptsHtml(ctx))) return this.#handle(ctx)\n return getNotAllowed()\n }\n}\n\n/** A path no request reaches: the driver's routes only serve the GraphQL endpoint. */\nconst UNREACHABLE = '/.znight/unreachable'\n\n/**\n * The Fetch API of the runtime: on Node.js, Yoga would otherwise use the ponyfills of\n * `@whatwg-node/fetch`, whose `Response` objects `HttpPlugin` cannot send.\n */\nfunction fetchApi(): Record<string, unknown> {\n return {\n fetch: globalThis.fetch,\n Request: globalThis.Request,\n Response: globalThis.Response,\n Headers: globalThis.Headers,\n FormData: globalThis.FormData,\n ReadableStream: globalThis.ReadableStream,\n WritableStream: globalThis.WritableStream,\n TransformStream: globalThis.TransformStream,\n CompressionStream: globalThis.CompressionStream,\n DecompressionStream: globalThis.DecompressionStream,\n TextDecoderStream: globalThis.TextDecoderStream,\n TextEncoderStream: globalThis.TextEncoderStream,\n Blob: globalThis.Blob,\n File: globalThis.File,\n TextEncoder: globalThis.TextEncoder,\n TextDecoder: globalThis.TextDecoder,\n URL: globalThis.URL,\n URLSearchParams: globalThis.URLSearchParams,\n btoa: globalThis.btoa,\n }\n}\n\n/**\n * A `Response` `HttpPlugin` recognises: `@hono/node-server` replaces the global `Response` when it\n * starts listening, after Yoga captured the runtime's own; such a response is rebuilt on the\n * global class (same status, headers and body stream).\n */\nfunction sendable(response: Response): Response {\n if (response instanceof Response) return response\n const foreign: { readonly body: ReadableStream<Uint8Array> | null } & ResponseInit = response\n return new Response(foreign.body, foreign)\n}\n\n/** The request id of `HttpPlugin` (a UUID for a request handed to `yoga.fetch()` directly). */\nfunction requestIdOf(payload: { readonly context: Partial<YogaServerContext> }): string {\n return payload.context.znight?.requestId ?? crypto.randomUUID()\n}\n\nfunction acceptsHtml(ctx: HttpContext): boolean {\n return ctx.headers['accept']?.includes('text/html') === true\n}\n\n/** The JSON type a refusal is written in, like the built-in transport: `application/json` unless the client prefers `application/graphql-response+json`. */\nfunction jsonMedia(accept: string | undefined): MediaKind {\n const media = (negotiate(accept, (1 << Media.GraphQLJson) | (1 << Media.Json)) & MEDIA) as MediaKind\n return media === Media.None ? Media.Json : media\n}\n\n/** A request reaching the driver once it stopped (after `HttpPlugin` drained): 503. */\nfunction shuttingDown(): Response {\n return Response.json(\n {\n errors: [\n {\n message: 'The server is shutting down, retry later',\n extensions: { code: GraphQLErrorCode.ServiceUnavailable },\n },\n ],\n },\n { status: 503 },\n )\n}\n\n/**\n * Yoga's `maskError`: the errors of the engine (masked already) and the `GraphQLError`s thrown on\n * purpose (by Yoga or a plugin, with a code) are kept; any other error, thrown outside the engine\n * (an Envelop plugin's hook, Yoga itself), is logged and masked like the engine masks it\n * (`Unexpected error.`, its message with `exposeErrors`). Yoga then answers 500 for a request error.\n */\nfunction outsideErrors(expose: boolean, logger: CoreLogger): (error: unknown, message: string) => Error {\n return (error, message) => {\n if (isOriginalGraphQLError(error)) return error\n logger.error('GraphQL: an error was thrown outside the engine (by Yoga or an Envelop plugin)', { err: error })\n const original = error instanceof Error ? error : new Error(String(error))\n return new GraphQLError(expose ? original.message : message, {\n originalError: original,\n extensions: { code: GraphQLErrorCode.InternalServerError },\n })\n }\n}\n\n/** A `GraphQLError` that wraps no other error: the engine's, or thrown on purpose. */\nfunction isOriginalGraphQLError(error: unknown): error is GraphQLError {\n if (!(error instanceof GraphQLError)) return false\n const original = error.originalError\n return original === undefined || original === null || isOriginalGraphQLError(original)\n}\n\nfunction getNotAllowed(): Response {\n return Response.json(\n {\n errors: [\n { message: 'GraphQL operations are sent with POST on this endpoint', extensions: { code: 'BAD_REQUEST' } },\n ],\n },\n { status: 405, headers: { allow: 'POST' } },\n )\n}\n\n/** Envelop's onExecute payload (the part the driver reads). */\ninterface ExecutePayload {\n readonly executeFn: unknown\n readonly context: unknown\n setExecuteFn(fn: (args: ExecutionArgs) => unknown): void\n}\n\ninterface SubscribePayload {\n readonly subscribeFn: unknown\n readonly context: unknown\n setSubscribeFn(fn: (args: ExecutionArgs) => unknown): void\n}\n\ninterface ResultPayload {\n readonly result: unknown\n setResult(result: ExecutionResult): void\n}\n\n/**\n * The Envelop plugins of the engine: one installed before the application's plugins, which makes\n * the engine Yoga's `parse`, `validate`, `execute` and `subscribe` (Envelop's `useEngine`), and a\n * guard installed after them, which refuses an operation that a plugin executed elsewhere\n * (`useGraphQlJit`): its resolvers would not get the `GraphQLContext`. Plugins wrapping the\n * execution (`useResponseCache`) keep working: they call the engine.\n */\nfunction engineFunctions(engine: GraphQLEngine, logger: CoreLogger): { plugin: YogaPlugin; guard: YogaPlugin } {\n const parse = engine.parse\n const validate = (schema: GraphQLSchema, document: DocumentNode, rules?: readonly ValidationRule[]) =>\n engine.validate(schema, document, rules)\n // marks the contexts the engine ran, once a plugin wrapped or replaced the execution\n let watching = false\n const ran = new WeakSet<object>()\n const execute = (args: ExecutionArgs): unknown => {\n if (watching) mark(ran, args.contextValue)\n return engine.execute(args)\n }\n const subscribe = (args: ExecutionArgs): unknown => {\n if (watching) mark(ran, args.contextValue)\n return engine.subscribe(args, SSE)\n }\n let reported = false\n const bypassed = (): ExecutionResult => {\n const error = invalid(\n 'GraphQLPlugin: a Yoga plugin executed the operation instead of the Znight engine (useGraphQlJit?)',\n ['Remove the plugin: resolvers, guards and loaders only run on the engine'],\n )\n if (!reported) {\n reported = true\n logger.error(error.message, { error })\n }\n return { errors: [new GraphQLError(error.message, { extensions: { code: 'INTERNAL_SERVER_ERROR' } })] }\n }\n const plugin = {\n onParse: ({ setParseFn }: { setParseFn(fn: unknown): void }) => setParseFn(parse),\n onValidate: ({ setValidationFn }: { setValidationFn(fn: unknown): void }) => setValidationFn(validate),\n onExecute: ({ setExecuteFn }: ExecutePayload) => setExecuteFn(execute),\n onSubscribe: ({ setSubscribeFn }: SubscribePayload) => setSubscribeFn(subscribe),\n }\n const guard = {\n onExecute: ({ executeFn, context }: ExecutePayload) => {\n if (executeFn === execute) return undefined\n watching = true\n return {\n onExecuteDone: ({ setResult }: ResultPayload) => {\n if (!ran.has(context as object)) setResult(bypassed())\n },\n }\n },\n onSubscribe: ({ subscribeFn, context }: SubscribePayload) => {\n if (subscribeFn === subscribe) return undefined\n watching = true\n return {\n onSubscribeResult: ({ setResult }: ResultPayload) => {\n if (!ran.has(context as object)) setResult(bypassed())\n },\n }\n },\n }\n return { plugin: plugin as YogaPlugin, guard: guard as YogaPlugin }\n}\n\nfunction mark(ran: WeakSet<object>, context: unknown): void {\n if (context !== null && typeof context === 'object') ran.add(context)\n}\n\nfunction notStarted(): ZnightError {\n return new ZnightError(ErrorCode.LifecycleFailed, 'The Yoga driver is not started yet', {\n hints: ['GraphQLPlugin starts its driver in onBootstrap'],\n })\n}\n\nfunction invalid(message: string, hints: readonly string[]): ZnightError {\n return new ZnightError(ErrorCode.InvalidGraphQLSetup, message, { hints: [...hints] })\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@znight/graphql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Znight GraphQL: code-first GraphQL whose schema is derived from TypeScript types at build time, on graphql-js 17",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ugho STEPHAN",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"directory": "packages/graphql"
|
|
12
12
|
},
|
|
13
13
|
"bugs": "https://github.com/znight-org/znight/issues",
|
|
14
|
-
"keywords": ["znight", "graphql", "code-first", "dataloader", "subscriptions", "sse"],
|
|
14
|
+
"keywords": ["znight", "graphql", "code-first", "dataloader", "subscriptions", "sse", "yoga"],
|
|
15
15
|
"type": "module",
|
|
16
16
|
"sideEffects": false,
|
|
17
17
|
"engines": {
|
|
@@ -33,6 +33,11 @@
|
|
|
33
33
|
"types": "./dist/testing.d.mts",
|
|
34
34
|
"default": "./dist/testing.mjs"
|
|
35
35
|
},
|
|
36
|
+
"./yoga": {
|
|
37
|
+
"@znight/source": "./src/yoga.ts",
|
|
38
|
+
"types": "./dist/yoga.d.mts",
|
|
39
|
+
"default": "./dist/yoga.mjs"
|
|
40
|
+
},
|
|
36
41
|
"./package.json": "./package.json"
|
|
37
42
|
},
|
|
38
43
|
"types": "./dist/index.d.mts",
|
|
@@ -40,20 +45,22 @@
|
|
|
40
45
|
"scripts": {
|
|
41
46
|
"build": "tsdown",
|
|
42
47
|
"typecheck": "tsc --noEmit && tsc --noEmit -p test",
|
|
43
|
-
"test": "bun test --conditions=@znight/source test"
|
|
48
|
+
"test": "bun test --conditions=@znight/source test",
|
|
49
|
+
"test:integration": "ZNIGHT_INTEGRATION=1 bun test --conditions=@znight/source --timeout 180000 test/recipes/redis-pubsub.integration.test.ts"
|
|
44
50
|
},
|
|
45
51
|
"publishConfig": {
|
|
46
52
|
"access": "public"
|
|
47
53
|
},
|
|
48
54
|
"dependencies": {
|
|
49
|
-
"@znight/core": "0.
|
|
55
|
+
"@znight/core": "0.3.0",
|
|
50
56
|
"dataloader": "^2.2.3"
|
|
51
57
|
},
|
|
52
58
|
"peerDependencies": {
|
|
53
|
-
"@znight/http": "^0.
|
|
54
|
-
"@znight/compiler": "^0.
|
|
55
|
-
"@znight/testing": "^0.
|
|
56
|
-
"graphql": "^17.0.2"
|
|
59
|
+
"@znight/http": "^0.3.0",
|
|
60
|
+
"@znight/compiler": "^0.3.0",
|
|
61
|
+
"@znight/testing": "^0.3.0",
|
|
62
|
+
"graphql": "^17.0.2",
|
|
63
|
+
"graphql-yoga": "^6.0.1"
|
|
57
64
|
},
|
|
58
65
|
"peerDependenciesMeta": {
|
|
59
66
|
"@znight/compiler": {
|
|
@@ -61,15 +68,22 @@
|
|
|
61
68
|
},
|
|
62
69
|
"@znight/testing": {
|
|
63
70
|
"optional": true
|
|
71
|
+
},
|
|
72
|
+
"graphql-yoga": {
|
|
73
|
+
"optional": true
|
|
64
74
|
}
|
|
65
75
|
},
|
|
66
76
|
"devDependencies": {
|
|
67
|
-
"@
|
|
68
|
-
"@znight/compiler": "0.
|
|
69
|
-
"@znight/
|
|
77
|
+
"@testcontainers/redis": "12.2.1",
|
|
78
|
+
"@znight/compiler": "0.3.0",
|
|
79
|
+
"@znight/http": "0.3.0",
|
|
80
|
+
"@znight/testing": "0.3.0",
|
|
70
81
|
"graphql": "17.0.2",
|
|
71
82
|
"graphql-http": "1.23.1",
|
|
72
83
|
"graphql-sse": "2.6.3",
|
|
84
|
+
"graphql-yoga": "6.0.1",
|
|
85
|
+
"ioredis": "6.0.0",
|
|
86
|
+
"testcontainers": "12.2.1",
|
|
73
87
|
"valibot": "1.5.0",
|
|
74
88
|
"zod": "4.6.7"
|
|
75
89
|
},
|