@znight/graphql 0.2.2 → 0.4.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 +262 -17
- package/dist/compiler.d.mts +21 -1
- package/dist/compiler.mjs +116 -15
- package/dist/compiler.mjs.map +1 -1
- package/dist/{types-DQ713UgI.d.mts → driver-DgbpFj4-.d.mts} +193 -9
- package/dist/http-BWcayUtH.mjs +2165 -0
- package/dist/http-BWcayUtH.mjs.map +1 -0
- package/dist/index.d.mts +52 -51
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -1
- package/dist/names-BlzDM5cF.mjs.map +1 -1
- package/dist/{plugin-CR_wVfRU.mjs → plugin-BssB7wtn.mjs} +242 -1778
- package/dist/plugin-BssB7wtn.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 +19 -10
- package/src/compiler/analyze.ts +27 -0
- package/src/compiler/data.ts +20 -1
- package/src/compiler/generate.ts +137 -19
- 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/metadata.ts +20 -4
- package/src/options.ts +53 -4
- package/src/pipeline.ts +72 -9
- package/src/plugin.ts +172 -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 +22 -4
- package/src/yoga.ts +471 -0
- package/dist/plugin-CR_wVfRU.mjs.map +0 -1
|
@@ -1,53 +1,9 @@
|
|
|
1
|
+
import { c as singleResult, d as resolveIn, f as GraphQLErrorCode, l as GraphQLTokens, n as checkSseOptions, p as GraphQLException, s as GraphQLExecutor, t as HttpTransport, u as ErrorFormatter } from "./http-BWcayUtH.mjs";
|
|
1
2
|
import { a as SPECIFIED_SCALARS, o as isGraphQLName, r as RESERVED_ENUM_VALUES, s as parseTypeRef, t as BUILTIN_SCALARS } from "./names-BlzDM5cF.mjs";
|
|
2
3
|
import { ErrorCode, ZnightError, displayName, resolveOptions } from "@znight/core";
|
|
3
4
|
import DataLoader from "dataloader";
|
|
4
|
-
import { EventChannel,
|
|
5
|
-
import {
|
|
6
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
-
// src/errors.ts
|
|
8
|
-
/**
|
|
9
|
-
* GraphQL errors shown to clients (RFC 0005 §5.10, §8.8): `GraphQLException` and the
|
|
10
|
-
* `extensions.code` values the plugin produces (Apollo conventions, which clients already know).
|
|
11
|
-
*
|
|
12
|
-
* Import-free: the exception is a plain `Error` subclass, rendered by the runtime as
|
|
13
|
-
* `{ message, locations, path, extensions: { code, ...extensions } }`.
|
|
14
|
-
*/
|
|
15
|
-
/** `extensions.code` values produced by `@znight/graphql`. Applications may use their own codes too. */
|
|
16
|
-
const GraphQLErrorCode = Object.freeze({
|
|
17
|
-
ParseFailed: "GRAPHQL_PARSE_FAILED",
|
|
18
|
-
ValidationFailed: "GRAPHQL_VALIDATION_FAILED",
|
|
19
|
-
BadUserInput: "BAD_USER_INPUT",
|
|
20
|
-
QueryTooComplex: "QUERY_TOO_COMPLEX",
|
|
21
|
-
Unauthenticated: "UNAUTHENTICATED",
|
|
22
|
-
Forbidden: "FORBIDDEN",
|
|
23
|
-
NotFound: "NOT_FOUND",
|
|
24
|
-
Conflict: "CONFLICT",
|
|
25
|
-
TooManyRequests: "TOO_MANY_REQUESTS",
|
|
26
|
-
BadRequest: "BAD_REQUEST",
|
|
27
|
-
SubscriptionOverflow: "SUBSCRIPTION_OVERFLOW",
|
|
28
|
-
ServiceUnavailable: "SERVICE_UNAVAILABLE",
|
|
29
|
-
InternalServerError: "INTERNAL_SERVER_ERROR"
|
|
30
|
-
});
|
|
31
|
-
/**
|
|
32
|
-
* An error whose message and extensions are sent to the client as they are (§5.10): throw it from a
|
|
33
|
-
* resolver or return it from a `GraphQLErrorMapper`.
|
|
34
|
-
*
|
|
35
|
-
* ```ts
|
|
36
|
-
* throw new GraphQLException(`No book ${id}`, { code: 'NOT_FOUND', extensions: { bookId: id } })
|
|
37
|
-
* ```
|
|
38
|
-
*/
|
|
39
|
-
var GraphQLException = class extends Error {
|
|
40
|
-
/** `extensions.code`. */
|
|
41
|
-
code;
|
|
42
|
-
/** Extra `extensions` entries. */
|
|
43
|
-
extensions;
|
|
44
|
-
constructor(message, options) {
|
|
45
|
-
super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
46
|
-
this.name = "GraphQLException";
|
|
47
|
-
this.code = options.code;
|
|
48
|
-
this.extensions = options.extensions;
|
|
49
|
-
}
|
|
50
|
-
};
|
|
5
|
+
import { EventChannel, isThenable, parseByteSize } from "@znight/http";
|
|
6
|
+
import { GraphQLBoolean, GraphQLEnumType, GraphQLFloat, GraphQLID, GraphQLInputObjectType, GraphQLInt, GraphQLInterfaceType, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, GraphQLUnionType, Kind, printSchema, validateSchema, valueFromASTUntyped } from "graphql";
|
|
51
7
|
// src/loaders.ts
|
|
52
8
|
/**
|
|
53
9
|
* Creates the per-execution DataLoaders of loader providers: the provider is resolved in the
|
|
@@ -310,11 +266,11 @@ function checkFieldDecisions(fields, groups, mode, logger) {
|
|
|
310
266
|
const members = [...keys].filter(([, info]) => info.group === group).map(([key]) => key).sort();
|
|
311
267
|
const parts = [];
|
|
312
268
|
if (entry.missing.length > 0) {
|
|
313
|
-
parts.push(`${count
|
|
269
|
+
parts.push(`${count(entry.missing.length)} ${entry.missing.length === 1 ? "has" : "have"} no "${group}" decision: ${entry.missing.join(", ")}`);
|
|
314
270
|
hints.push(`Add a decision of the "${group}" group (${members.join(", ") || group}) to the resolver method or to its class`);
|
|
315
271
|
}
|
|
316
272
|
if (entry.unenforced.length > 0) {
|
|
317
|
-
parts.push(`no guard enforces the "${group}" group on ${count
|
|
273
|
+
parts.push(`no guard enforces the "${group}" group on ${count(entry.unenforced.length)}: ${entry.unenforced.join(", ")}`);
|
|
318
274
|
hints.push(`Register a GraphQL guard declaring \`static enforces\` with the decorators of the "${group}" group: GraphQLPlugin.configure({ guards: [...] }) or @UseGuard(...)`);
|
|
319
275
|
}
|
|
320
276
|
const message = parts.join("; ");
|
|
@@ -330,1130 +286,40 @@ function checkFieldDecisions(fields, groups, mode, logger) {
|
|
|
330
286
|
throw new ZnightError(ErrorCode.MissingRouteDecision, `GraphQLPlugin: ${text.charAt(0).toUpperCase()}${text.slice(1)}`, { hints });
|
|
331
287
|
}
|
|
332
288
|
}
|
|
333
|
-
function count
|
|
289
|
+
function count(fields) {
|
|
334
290
|
return fields === 1 ? "1 field" : `${fields} fields`;
|
|
335
291
|
}
|
|
336
|
-
// src/
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
#state;
|
|
355
|
-
#loaders;
|
|
356
|
-
constructor(runtime, document, operation, request, options, scope) {
|
|
357
|
-
this.#runtime = runtime;
|
|
358
|
-
this.document = document;
|
|
359
|
-
this.#operation = operation;
|
|
360
|
-
this.#request = request;
|
|
361
|
-
this.#options = options;
|
|
362
|
-
this.#scope = scope;
|
|
363
|
-
this.#requestId = options.requestId;
|
|
364
|
-
}
|
|
365
|
-
get transport() {
|
|
366
|
-
return this.#options.transport ?? "direct";
|
|
367
|
-
}
|
|
368
|
-
get operationType() {
|
|
369
|
-
return this.#operation.operation;
|
|
370
|
-
}
|
|
371
|
-
get operationName() {
|
|
372
|
-
return this.#operation.name?.value;
|
|
373
|
-
}
|
|
374
|
-
get requestId() {
|
|
375
|
-
let id = this.#requestId;
|
|
376
|
-
if (id === void 0) {
|
|
377
|
-
const http = this.http;
|
|
378
|
-
const scoped = http === void 0 ? this.#scope?.get(HttpTokens.RequestId) : void 0;
|
|
379
|
-
id = http?.requestId ?? (typeof scoped === "string" ? scoped : randomUUID());
|
|
380
|
-
this.#requestId = id;
|
|
381
|
-
}
|
|
382
|
-
return id;
|
|
383
|
-
}
|
|
384
|
-
get http() {
|
|
385
|
-
let http = this.#http;
|
|
386
|
-
if (http === void 0) {
|
|
387
|
-
http = this.#options.http ?? this.#scope?.get(HttpTokens.Context) ?? null;
|
|
388
|
-
this.#http = http;
|
|
389
|
-
}
|
|
390
|
-
return http ?? void 0;
|
|
391
|
-
}
|
|
392
|
-
get variables() {
|
|
393
|
-
return this.#request.variables ?? NO_VARIABLES;
|
|
394
|
-
}
|
|
395
|
-
get signal() {
|
|
396
|
-
let signal = this.#signal;
|
|
397
|
-
if (signal === void 0) {
|
|
398
|
-
const base = this.#options.signal ?? this.http?.signal ?? this.#runtime.signal;
|
|
399
|
-
const ended = this.#ended;
|
|
400
|
-
signal = ended === void 0 ? base : AbortSignal.any([base, ended]);
|
|
401
|
-
this.#signal = signal;
|
|
402
|
-
}
|
|
403
|
-
return signal;
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* Ties {@link signal} to the end of a subscription: it also aborts when `ended` does (the stream
|
|
407
|
-
* ended by itself or with `return()`). Called before any resolver runs.
|
|
408
|
-
*/
|
|
409
|
-
endsWith(ended) {
|
|
410
|
-
this.#ended = ended;
|
|
411
|
-
}
|
|
412
|
-
get state() {
|
|
413
|
-
this.#state ??= /* @__PURE__ */ new Map();
|
|
414
|
-
return this.#state;
|
|
415
|
-
}
|
|
416
|
-
loader(key) {
|
|
417
|
-
const token = typeof key === "string" ? key : this.#tokenOf(key);
|
|
418
|
-
let loaders = this.#loaders;
|
|
419
|
-
if (loaders === void 0) {
|
|
420
|
-
loaders = /* @__PURE__ */ new Map();
|
|
421
|
-
this.#loaders = loaders;
|
|
422
|
-
}
|
|
423
|
-
let loader = loaders.get(token);
|
|
424
|
-
if (loader === void 0) {
|
|
425
|
-
loader = this.#runtime.loaders.create(token);
|
|
426
|
-
loaders.set(token, loader);
|
|
427
|
-
}
|
|
428
|
-
return loader;
|
|
429
|
-
}
|
|
430
|
-
/** Starts a new execution (a subscription event): loaders and their caches are renewed. */
|
|
431
|
-
resetLoaders() {
|
|
432
|
-
this.#loaders = void 0;
|
|
433
|
-
}
|
|
434
|
-
#tokenOf(loader) {
|
|
435
|
-
const token = typeof loader === "function" ? this.#runtime.container.tokenOf(loader) : void 0;
|
|
436
|
-
if (token === void 0) {
|
|
437
|
-
const name = typeof loader === "function" ? loader.name || "<anonymous>" : String(loader);
|
|
438
|
-
throw new ZnightError(ErrorCode.MissingProvider, `GraphQLContext.loader(): ${name} is not a provider`, { hints: ["Add @Injectable() to the loader class (it implements BatchLoad<K, V>)"] });
|
|
439
|
-
}
|
|
440
|
-
return token;
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
// src/format.ts
|
|
444
|
-
/** Resolves a {@link Resolvable} (in the current scope for scoped providers). */
|
|
445
|
-
function resolveIn(container, resolvable) {
|
|
446
|
-
return resolvable.token === void 0 ? resolvable.instance : container.get(resolvable.token);
|
|
447
|
-
}
|
|
448
|
-
/** Message of masked errors. */
|
|
449
|
-
const MASKED_MESSAGE = "Unexpected error.";
|
|
450
|
-
/** `HttpException` status → `extensions.code` (other 4xx: `BAD_REQUEST`, 5xx: masked). */
|
|
451
|
-
const STATUS_CODES = /* @__PURE__ */ new Map([
|
|
452
|
-
[400, GraphQLErrorCode.BadUserInput],
|
|
453
|
-
[401, GraphQLErrorCode.Unauthenticated],
|
|
454
|
-
[403, GraphQLErrorCode.Forbidden],
|
|
455
|
-
[404, GraphQLErrorCode.NotFound],
|
|
456
|
-
[409, GraphQLErrorCode.Conflict],
|
|
457
|
-
[429, GraphQLErrorCode.TooManyRequests]
|
|
458
|
-
]);
|
|
459
|
-
/** Formats the errors of execution results. One per plugin, built at boot. */
|
|
460
|
-
var ErrorFormatter = class {
|
|
461
|
-
#container;
|
|
462
|
-
#mappers;
|
|
463
|
-
#expose;
|
|
464
|
-
#logger;
|
|
465
|
-
#schema;
|
|
466
|
-
constructor(options) {
|
|
467
|
-
this.#container = options.container;
|
|
468
|
-
this.#mappers = options.mappers;
|
|
469
|
-
this.#expose = options.exposeErrors;
|
|
470
|
-
this.#logger = options.logger;
|
|
471
|
-
this.#schema = options.schema;
|
|
472
|
-
}
|
|
473
|
-
/** Formats the errors of an executed operation (field errors). */
|
|
474
|
-
format(errors, ctx) {
|
|
475
|
-
const result = new Array(errors.length);
|
|
476
|
-
let logged;
|
|
477
|
-
for (let index = 0; index < errors.length; index++) {
|
|
478
|
-
const error = errors[index];
|
|
479
|
-
const original = error.originalError;
|
|
480
|
-
if (original === void 0 || original === null) {
|
|
481
|
-
result[index] = withCode(error, error.extensions["code"] === void 0 ? GraphQLErrorCode.InternalServerError : void 0);
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
const exposed = this.#exposed(original, ctx);
|
|
485
|
-
if (exposed !== void 0) {
|
|
486
|
-
result[index] = render(error, exposed);
|
|
487
|
-
const cause = exposed.cause;
|
|
488
|
-
if (cause !== void 0) {
|
|
489
|
-
logged ??= /* @__PURE__ */ new Set();
|
|
490
|
-
if (!logged.has(cause)) {
|
|
491
|
-
logged.add(cause);
|
|
492
|
-
this.#log(error, cause, ctx, exposed.code);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
if (original instanceof GraphQLError && !isCompletionError(original)) {
|
|
498
|
-
result[index] = {
|
|
499
|
-
...original.toJSON(),
|
|
500
|
-
...located(error)
|
|
501
|
-
};
|
|
502
|
-
continue;
|
|
503
|
-
}
|
|
504
|
-
logged ??= /* @__PURE__ */ new Set();
|
|
505
|
-
if (!logged.has(original)) {
|
|
506
|
-
logged.add(original);
|
|
507
|
-
this.#log(error, original, ctx, void 0);
|
|
508
|
-
}
|
|
509
|
-
result[index] = {
|
|
510
|
-
message: this.#expose && original instanceof Error ? original.message : MASKED_MESSAGE,
|
|
511
|
-
...located(error),
|
|
512
|
-
extensions: { code: GraphQLErrorCode.InternalServerError }
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
return result;
|
|
516
|
-
}
|
|
517
|
-
/** The exception shown for `original`, `undefined` when it must be masked. */
|
|
518
|
-
#exposed(original, ctx) {
|
|
519
|
-
if (original instanceof GraphQLException) return original;
|
|
520
|
-
for (const resolvable of this.#mappers) {
|
|
521
|
-
let mapped;
|
|
522
|
-
try {
|
|
523
|
-
mapped = resolveIn(this.#container, resolvable).map(original, ctx);
|
|
524
|
-
} catch (error) {
|
|
525
|
-
this.#logger.error("GraphQLPlugin: an error mapper failed", {
|
|
526
|
-
err: error,
|
|
527
|
-
requestId: ctx.requestId
|
|
528
|
-
});
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
531
|
-
if (mapped !== void 0) return mapped;
|
|
532
|
-
}
|
|
533
|
-
if (isHttpException(original)) {
|
|
534
|
-
const status = original.status;
|
|
535
|
-
if (original instanceof ValidationException) return new GraphQLException(original.message, {
|
|
536
|
-
code: GraphQLErrorCode.BadUserInput,
|
|
537
|
-
extensions: { issues: original.details }
|
|
538
|
-
});
|
|
539
|
-
if (status >= 500) return void 0;
|
|
540
|
-
return new GraphQLException(original.message, { code: STATUS_CODES.get(status) ?? GraphQLErrorCode.BadRequest });
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
/** Logs an unexpected error, or the cause of a presented one (`code`), at `error` level. */
|
|
544
|
-
#log(error, cause, ctx, code) {
|
|
545
|
-
const coordinate = coordinateOf(this.#schema, ctx.document, ctx.operationName, error.path);
|
|
546
|
-
const what = code === void 0 ? "unexpected error" : `${code} error`;
|
|
547
|
-
this.#logger.error(`GraphQL: ${what}${coordinate === void 0 ? "" : ` in ${coordinate}`}`, {
|
|
548
|
-
err: cause,
|
|
549
|
-
requestId: ctx.requestId,
|
|
550
|
-
...code === void 0 ? {} : { code },
|
|
551
|
-
...coordinate === void 0 ? {} : { coordinate },
|
|
552
|
-
...error.path === void 0 ? {} : { path: error.path.join(".") }
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
};
|
|
556
|
-
/**
|
|
557
|
-
* What graphql-js raises while completing a resolver's value: a built-in scalar or an enum that
|
|
558
|
-
* cannot represent it (`String cannot represent value: { passwordHash: ... }`), a list value that
|
|
559
|
-
* is not iterable, a union or interface value that resolves to no member. A server bug whose message
|
|
560
|
-
* holds the value, masked and logged like any unexpected error. graphql-js throws plain
|
|
561
|
-
* `GraphQLError`s without code there, told apart from the ones of user code by their message.
|
|
562
|
-
*/
|
|
563
|
-
const COMPLETION_ERROR = /^(?:(?:Int|Float|String|Boolean|ID) cannot represent |Enum "[^"]*" cannot represent |Expected Iterable, but did not find one for field "|Abstract type "|Runtime Object type ")/;
|
|
564
|
-
function isCompletionError(error) {
|
|
565
|
-
return error.constructor === GraphQLError && error.extensions["code"] === void 0 && COMPLETION_ERROR.test(error.message);
|
|
566
|
-
}
|
|
567
|
-
function located(error) {
|
|
568
|
-
return {
|
|
569
|
-
...error.locations === void 0 ? {} : { locations: error.locations },
|
|
570
|
-
...error.path === void 0 ? {} : { path: error.path }
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
/** `{ message, locations, path, extensions: { ...extensions, code } }`. */
|
|
574
|
-
function render(error, exception) {
|
|
575
|
-
return {
|
|
576
|
-
message: exception.message,
|
|
577
|
-
...located(error),
|
|
578
|
-
extensions: {
|
|
579
|
-
...exception.extensions,
|
|
580
|
-
code: exception.code
|
|
581
|
-
}
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
/** A graphql-js error formatted with `extensions.code` set to `code` (kept as is without code). */
|
|
585
|
-
function withCode(error, code) {
|
|
586
|
-
const formatted = error.toJSON();
|
|
587
|
-
if (code === void 0) return formatted;
|
|
588
|
-
return {
|
|
589
|
-
...formatted,
|
|
590
|
-
extensions: {
|
|
591
|
-
...formatted.extensions,
|
|
592
|
-
code
|
|
593
|
-
}
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
/** A request error (no location): `{ message, extensions: { code } }`. */
|
|
597
|
-
function requestError(message, code, nodes) {
|
|
598
|
-
return withCode(new GraphQLError(message, nodes === void 0 ? {} : { nodes }), code);
|
|
599
|
-
}
|
|
600
|
-
/**
|
|
601
|
-
* The schema coordinate (`Book.author`) of the field at `path`, found by walking the operation's
|
|
602
|
-
* selections (aliases, fragments, inline fragments). Best effort, for logs: `undefined` when the
|
|
603
|
-
* path crosses a union without a type condition.
|
|
604
|
-
*/
|
|
605
|
-
function coordinateOf(schema, document, operationName, path) {
|
|
606
|
-
if (path === void 0 || path.length === 0) return void 0;
|
|
607
|
-
const fragments = /* @__PURE__ */ new Map();
|
|
608
|
-
let operation;
|
|
609
|
-
for (const definition of document.definitions) if (definition.kind === Kind.FRAGMENT_DEFINITION) fragments.set(definition.name.value, definition);
|
|
610
|
-
else if (definition.kind === Kind.OPERATION_DEFINITION && (operationName === void 0 || definition.name?.value === operationName)) operation ??= definition;
|
|
611
|
-
if (operation === void 0) return void 0;
|
|
612
|
-
let type = schema.getRootType(operation.operation);
|
|
613
|
-
let selections = [operation.selectionSet];
|
|
614
|
-
let coordinate;
|
|
615
|
-
for (const segment of path) {
|
|
616
|
-
if (typeof segment === "number") continue;
|
|
617
|
-
const found = findField(selections, segment, fragments, type?.name);
|
|
618
|
-
if (found === void 0 || type === void 0 || type === null) return void 0;
|
|
619
|
-
const parent = found.typeCondition === void 0 ? type : schema.getType(found.typeCondition);
|
|
620
|
-
if (!isObjectType(parent)) return void 0;
|
|
621
|
-
const field = parent.getFields()[found.field.name.value];
|
|
622
|
-
coordinate = `${parent.name}.${found.field.name.value}`;
|
|
623
|
-
if (field === void 0) return coordinate;
|
|
624
|
-
type = getNamedType(field.type);
|
|
625
|
-
selections = found.field.selectionSet === void 0 ? [] : [found.field.selectionSet];
|
|
626
|
-
}
|
|
627
|
-
return coordinate;
|
|
628
|
-
}
|
|
629
|
-
function findField(sets, key, fragments, typeCondition) {
|
|
630
|
-
for (const set of sets) for (const selection of set.selections) if (selection.kind === Kind.FIELD) {
|
|
631
|
-
if ((selection.alias ?? selection.name).value === key) return {
|
|
632
|
-
field: selection,
|
|
633
|
-
typeCondition
|
|
634
|
-
};
|
|
635
|
-
} else {
|
|
636
|
-
const nested = selection.kind === Kind.INLINE_FRAGMENT ? selection : fragments.get(selection.name.value);
|
|
637
|
-
if (nested === void 0) continue;
|
|
638
|
-
const found = findField([nested.selectionSet], key, fragments, nested.typeCondition?.name.value ?? typeCondition);
|
|
639
|
-
if (found !== void 0) return found;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
// src/limits.ts
|
|
643
|
-
/** The limit errors of `document` (`QUERY_TOO_COMPLEX`), `undefined` when it is within its limits. */
|
|
644
|
-
function checkLimits(document, limits) {
|
|
645
|
-
const { maxDepth, maxAliases, maxDirectives } = limits;
|
|
646
|
-
if (maxDepth === void 0 && maxAliases === void 0 && maxDirectives === void 0) return void 0;
|
|
647
|
-
const counter = {
|
|
648
|
-
aliases: 0,
|
|
649
|
-
directives: 0,
|
|
650
|
-
spreads: 0
|
|
651
|
-
};
|
|
652
|
-
const fragments = /* @__PURE__ */ new Map();
|
|
653
|
-
for (const definition of document.definitions) if (definition.kind === Kind.FRAGMENT_DEFINITION) fragments.set(definition.name.value, definition);
|
|
654
|
-
let errors;
|
|
655
|
-
const fail = (message) => {
|
|
656
|
-
errors ??= [];
|
|
657
|
-
errors.push(new GraphQLError(message, { extensions: { code: GraphQLErrorCode.QueryTooComplex } }));
|
|
658
|
-
};
|
|
659
|
-
for (const definition of document.definitions) if (definition.kind === Kind.OPERATION_DEFINITION || definition.kind === Kind.FRAGMENT_DEFINITION) {
|
|
660
|
-
countDirectives(definition.directives, counter);
|
|
661
|
-
if (definition.kind === Kind.OPERATION_DEFINITION) for (const variable of definition.variableDefinitions ?? []) countDirectives(variable.directives, counter);
|
|
662
|
-
count(definition.selectionSet, counter);
|
|
663
|
-
}
|
|
664
|
-
if (maxAliases !== void 0) {
|
|
665
|
-
if (counter.aliases > maxAliases) fail(`The document has ${counter.aliases} aliases, more than the limit of ${maxAliases}.`);
|
|
666
|
-
else if (counter.spreads > 0) {
|
|
667
|
-
const memo = /* @__PURE__ */ new Map();
|
|
668
|
-
for (const definition of document.definitions) {
|
|
669
|
-
if (definition.kind !== Kind.OPERATION_DEFINITION) continue;
|
|
670
|
-
if (aliasesOf(definition.selectionSet, fragments, memo, /* @__PURE__ */ new Set(), maxAliases + 1) > maxAliases) {
|
|
671
|
-
fail(`${definition.name === void 0 ? "The operation" : `The operation ${definition.name.value}`} has more than ${maxAliases} aliases once its fragments are expanded.`);
|
|
672
|
-
break;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
if (maxDirectives !== void 0 && counter.directives > maxDirectives) fail(`The document has ${counter.directives} directives, more than the limit of ${maxDirectives}.`);
|
|
678
|
-
if (maxDepth !== void 0) {
|
|
679
|
-
const depths = /* @__PURE__ */ new Map();
|
|
680
|
-
for (const definition of document.definitions) {
|
|
681
|
-
if (definition.kind !== Kind.OPERATION_DEFINITION) continue;
|
|
682
|
-
const depth = depthOf(definition.selectionSet, fragments, depths, /* @__PURE__ */ new Set());
|
|
683
|
-
if (depth > maxDepth) fail(`${definition.name === void 0 ? "The operation" : `The operation ${definition.name.value}`} is ${depth} levels deep, more than the limit of ${maxDepth}.`);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
return errors;
|
|
687
|
-
}
|
|
688
|
-
function countDirectives(directives, counter) {
|
|
689
|
-
if (directives !== void 0) counter.directives += directives.length;
|
|
690
|
-
}
|
|
691
|
-
function count(set, counter) {
|
|
692
|
-
for (const selection of set.selections) {
|
|
693
|
-
countDirectives(selection.directives, counter);
|
|
694
|
-
if (selection.kind === Kind.FIELD) {
|
|
695
|
-
if (selection.alias !== void 0) counter.aliases++;
|
|
696
|
-
if (selection.selectionSet !== void 0) count(selection.selectionSet, counter);
|
|
697
|
-
} else if (selection.kind === Kind.INLINE_FRAGMENT) count(selection.selectionSet, counter);
|
|
698
|
-
else counter.spreads++;
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* Aliases a selection set executes, fragment spreads expanded (`a: f { ...F } b: f { ...F }` counts
|
|
703
|
-
* the aliases of `F` twice). Fragment counts are memoised, so the walk stays linear in the document;
|
|
704
|
-
* counts saturate at `cap` (one more than the limit) and a fragment cycle counts 0 (validation
|
|
705
|
-
* rejects it).
|
|
706
|
-
*/
|
|
707
|
-
function aliasesOf(set, fragments, memo, visiting, cap) {
|
|
708
|
-
let total = 0;
|
|
709
|
-
for (const selection of set.selections) {
|
|
710
|
-
if (selection.kind === Kind.FIELD) {
|
|
711
|
-
if (selection.alias !== void 0) total++;
|
|
712
|
-
if (selection.selectionSet !== void 0) total += aliasesOf(selection.selectionSet, fragments, memo, visiting, cap);
|
|
713
|
-
} else if (selection.kind === Kind.INLINE_FRAGMENT) total += aliasesOf(selection.selectionSet, fragments, memo, visiting, cap);
|
|
714
|
-
else {
|
|
715
|
-
const name = selection.name.value;
|
|
716
|
-
let known = memo.get(name);
|
|
717
|
-
if (known === void 0) {
|
|
718
|
-
const fragment = fragments.get(name);
|
|
719
|
-
if (fragment === void 0 || visiting.has(name)) continue;
|
|
720
|
-
visiting.add(name);
|
|
721
|
-
known = aliasesOf(fragment.selectionSet, fragments, memo, visiting, cap);
|
|
722
|
-
visiting.delete(name);
|
|
723
|
-
memo.set(name, known);
|
|
724
|
-
}
|
|
725
|
-
total += known;
|
|
726
|
-
}
|
|
727
|
-
if (total >= cap) return cap;
|
|
728
|
-
}
|
|
729
|
-
return total;
|
|
730
|
-
}
|
|
731
|
-
/**
|
|
732
|
-
* Field nesting of a selection set (`{ a { b } }` is 2). Introspection fields (`__schema`,
|
|
733
|
-
* `__type`) do not count: `MaxIntrospectionDepthRule` bounds them. Fragment depths are memoised; a
|
|
734
|
-
* fragment cycle counts 0 (validation rejects it).
|
|
735
|
-
*/
|
|
736
|
-
function depthOf(set, fragments, depths, visiting) {
|
|
737
|
-
let max = 0;
|
|
738
|
-
for (const selection of set.selections) {
|
|
739
|
-
let depth = 0;
|
|
740
|
-
if (selection.kind === Kind.FIELD) {
|
|
741
|
-
if (selection.name.value.startsWith("__")) continue;
|
|
742
|
-
depth = selection.selectionSet === void 0 ? 1 : 1 + depthOf(selection.selectionSet, fragments, depths, visiting);
|
|
743
|
-
} else if (selection.kind === Kind.INLINE_FRAGMENT) depth = depthOf(selection.selectionSet, fragments, depths, visiting);
|
|
744
|
-
else {
|
|
745
|
-
const name = selection.name.value;
|
|
746
|
-
const known = depths.get(name);
|
|
747
|
-
if (known === void 0) {
|
|
748
|
-
const fragment = fragments.get(name);
|
|
749
|
-
if (fragment === void 0 || visiting.has(name)) continue;
|
|
750
|
-
visiting.add(name);
|
|
751
|
-
depth = depthOf(fragment.selectionSet, fragments, depths, visiting);
|
|
752
|
-
visiting.delete(name);
|
|
753
|
-
depths.set(name, depth);
|
|
754
|
-
} else depth = known;
|
|
755
|
-
}
|
|
756
|
-
if (depth > max) max = depth;
|
|
757
|
-
}
|
|
758
|
-
return max;
|
|
759
|
-
}
|
|
760
|
-
// src/tokens.ts
|
|
761
|
-
/**
|
|
762
|
-
* Tokens of the values `GraphQLPlugin` supplies to the container (declared in the package
|
|
763
|
-
* manifest). `GraphQLContext` is a request-scoped external: inject it in request-scoped providers
|
|
764
|
-
* or through `Lazy<GraphQLContext>`.
|
|
765
|
-
*/
|
|
766
|
-
const GraphQLTokens = Object.freeze({
|
|
767
|
-
Context: "@znight/graphql#GraphQLContext",
|
|
768
|
-
PubSub: "@znight/graphql#PubSub",
|
|
769
|
-
Plugin: "@znight/graphql#GraphQLPlugin"
|
|
770
|
-
});
|
|
771
|
-
// src/execution.ts
|
|
772
|
-
/**
|
|
773
|
-
* The answer of {@link GraphQLExecutor.execute} to a subscription with
|
|
774
|
-
* {@link ExecuteOptions.streamSubscriptions}: start it with {@link GraphQLExecutor.subscribe}.
|
|
775
|
-
*/
|
|
776
|
-
const SUBSCRIBE = Object.freeze({
|
|
777
|
-
status: 0,
|
|
778
|
-
result: Object.freeze({})
|
|
779
|
-
});
|
|
780
|
-
const OPERATION_HOOK = "graphql:operation";
|
|
781
|
-
const DIRECT$1 = Object.freeze({});
|
|
782
|
-
const DONE$1 = Object.freeze({
|
|
783
|
-
done: true,
|
|
784
|
-
value: void 0
|
|
785
|
-
});
|
|
786
|
-
/** SHA-256 (hex) of a query text: `GraphQLOperationEvent.documentId`. */
|
|
787
|
-
function documentId(query) {
|
|
788
|
-
return createHash("sha256").update(query, "utf8").digest("hex");
|
|
789
|
-
}
|
|
790
|
-
/** A parsed and validated document, kept by the document cache. */
|
|
791
|
-
var CachedDocument = class {
|
|
792
|
-
query;
|
|
793
|
-
document;
|
|
794
|
-
/** The operation of a single-operation document. */
|
|
795
|
-
single;
|
|
796
|
-
/** Operations by name (documents with several operations). */
|
|
797
|
-
named;
|
|
798
|
-
#id;
|
|
799
|
-
constructor(query, document) {
|
|
800
|
-
this.query = query;
|
|
801
|
-
this.document = document;
|
|
802
|
-
let single;
|
|
803
|
-
let named;
|
|
804
|
-
let count = 0;
|
|
805
|
-
for (const definition of document.definitions) {
|
|
806
|
-
if (definition.kind !== Kind.OPERATION_DEFINITION) continue;
|
|
807
|
-
count++;
|
|
808
|
-
single = definition;
|
|
809
|
-
if (definition.name !== void 0) {
|
|
810
|
-
named ??= /* @__PURE__ */ new Map();
|
|
811
|
-
named.set(definition.name.value, definition);
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
this.single = count === 1 ? single : void 0;
|
|
815
|
-
this.named = named;
|
|
816
|
-
}
|
|
817
|
-
/** SHA-256 of the query, computed on first read (only the hook needs it). */
|
|
818
|
-
get id() {
|
|
819
|
-
this.#id ??= documentId(this.query);
|
|
820
|
-
return this.#id;
|
|
821
|
-
}
|
|
822
|
-
};
|
|
823
|
-
/** A bounded LRU of documents by exact query text. */
|
|
824
|
-
var DocumentCache = class {
|
|
825
|
-
#max;
|
|
826
|
-
#entries = /* @__PURE__ */ new Map();
|
|
827
|
-
#last;
|
|
828
|
-
constructor(max) {
|
|
829
|
-
this.#max = max;
|
|
830
|
-
}
|
|
831
|
-
get size() {
|
|
832
|
-
return this.#entries.size;
|
|
833
|
-
}
|
|
834
|
-
get(query) {
|
|
835
|
-
const entry = this.#entries.get(query);
|
|
836
|
-
if (entry !== void 0 && this.#last !== query) {
|
|
837
|
-
this.#entries.delete(query);
|
|
838
|
-
this.#entries.set(query, entry);
|
|
839
|
-
this.#last = query;
|
|
840
|
-
}
|
|
841
|
-
return entry;
|
|
842
|
-
}
|
|
843
|
-
set(query, entry) {
|
|
844
|
-
if (this.#max === 0) return;
|
|
845
|
-
if (this.#entries.size >= this.#max) {
|
|
846
|
-
const oldest = this.#entries.keys().next();
|
|
847
|
-
if (oldest.done !== true) this.#entries.delete(oldest.value);
|
|
848
|
-
}
|
|
849
|
-
this.#entries.set(query, entry);
|
|
850
|
-
this.#last = query;
|
|
851
|
-
}
|
|
852
|
-
clear() {
|
|
853
|
-
this.#entries.clear();
|
|
854
|
-
this.#last = void 0;
|
|
292
|
+
// src/engine.ts
|
|
293
|
+
/** The engine of a plugin: bound functions over its executor. */
|
|
294
|
+
var Engine = class {
|
|
295
|
+
schema;
|
|
296
|
+
settings;
|
|
297
|
+
rules;
|
|
298
|
+
parse;
|
|
299
|
+
validate;
|
|
300
|
+
execute;
|
|
301
|
+
subscribe;
|
|
302
|
+
constructor(executor, settings, driver) {
|
|
303
|
+
this.schema = executor.schema;
|
|
304
|
+
this.settings = Object.freeze({ ...settings });
|
|
305
|
+
this.rules = executor.engineRules;
|
|
306
|
+
this.parse = (source) => executor.parseDocument(source, driver);
|
|
307
|
+
this.validate = (schema, document, rules) => executor.validateDocument(schema, document, rules, driver);
|
|
308
|
+
this.execute = (args, options) => executor.executeDocument(args, executeOptions(args, options, driver));
|
|
309
|
+
this.subscribe = (args, options) => executor.subscribeDocument(args, executeOptions(args, options, driver));
|
|
855
310
|
}
|
|
856
311
|
};
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
errors;
|
|
861
|
-
operationType;
|
|
862
|
-
operationName;
|
|
863
|
-
documentId;
|
|
864
|
-
headers;
|
|
865
|
-
constructor(status, errors, documentId, operation, headers) {
|
|
866
|
-
this.status = status;
|
|
867
|
-
this.errors = errors;
|
|
868
|
-
this.documentId = documentId;
|
|
869
|
-
this.operationType = operation?.operation;
|
|
870
|
-
this.operationName = operation?.name?.value;
|
|
871
|
-
this.headers = headers;
|
|
872
|
-
}
|
|
873
|
-
response() {
|
|
874
|
-
return {
|
|
875
|
-
status: this.status,
|
|
876
|
-
result: { errors: this.errors },
|
|
877
|
-
...this.headers === void 0 ? {} : { headers: this.headers }
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
};
|
|
881
|
-
/** The GraphQL executor of a plugin. */
|
|
882
|
-
var GraphQLExecutor = class {
|
|
883
|
-
#schema;
|
|
884
|
-
#container;
|
|
885
|
-
#hooks;
|
|
886
|
-
#logger;
|
|
887
|
-
#formatter;
|
|
888
|
-
#runtime;
|
|
889
|
-
#rules;
|
|
890
|
-
#limits;
|
|
891
|
-
#maxTokens;
|
|
892
|
-
#hideSuggestions;
|
|
893
|
-
#cache;
|
|
894
|
-
#maxActive;
|
|
895
|
-
#streams = /* @__PURE__ */ new Set();
|
|
896
|
-
#direct = /* @__PURE__ */ new Set();
|
|
897
|
-
/** Subscriptions counted against `maxActive` while their source starts (not yet in `#streams`). */
|
|
898
|
-
#starting = 0;
|
|
899
|
-
constructor(options) {
|
|
900
|
-
this.#schema = options.schema;
|
|
901
|
-
this.#container = options.container;
|
|
902
|
-
this.#hooks = options.hooks;
|
|
903
|
-
this.#logger = options.logger;
|
|
904
|
-
this.#formatter = options.formatter;
|
|
905
|
-
this.#runtime = options.runtime;
|
|
906
|
-
this.#rules = options.introspection ? specifiedRules : [...specifiedRules, NoSchemaIntrospectionCustomRule];
|
|
907
|
-
this.#limits = options.limits;
|
|
908
|
-
this.#maxTokens = options.limits.maxTokens;
|
|
909
|
-
this.#hideSuggestions = !options.introspection;
|
|
910
|
-
this.#cache = new DocumentCache(options.documentCache);
|
|
911
|
-
this.#maxActive = options.maxActiveSubscriptions;
|
|
912
|
-
}
|
|
913
|
-
/** Active subscriptions (every transport). */
|
|
914
|
-
get activeSubscriptions() {
|
|
915
|
-
return this.#streams.size;
|
|
916
|
-
}
|
|
917
|
-
/** Documents in the cache. */
|
|
918
|
-
get cachedDocuments() {
|
|
919
|
-
return this.#cache.size;
|
|
920
|
-
}
|
|
921
|
-
/**
|
|
922
|
-
* Executes a query or a mutation. Resolves (or returns, when every resolver is synchronous) a
|
|
923
|
-
* response; rejects only when `options.signal` aborts the execution (with the signal's reason).
|
|
924
|
-
* A subscription is a 400: it needs {@link subscribe} ({@link SUBSCRIBE} with
|
|
925
|
-
* `streamSubscriptions`).
|
|
926
|
-
*/
|
|
927
|
-
execute(request, options = DIRECT$1) {
|
|
928
|
-
const prepared = this.#prepare(request, options);
|
|
929
|
-
if (prepared instanceof Rejection) return this.#reject(prepared, request, options);
|
|
930
|
-
if (prepared.operation.operation === "subscription") {
|
|
931
|
-
if (options.streamSubscriptions === true) return SUBSCRIBE;
|
|
932
|
-
return this.#reject(new Rejection(400, [requestError("Subscriptions are delivered as a stream: send the request with accept: text/event-stream (GraphQL over SSE), or call GraphQLPlugin.subscribe()", GraphQLErrorCode.BadRequest)], prepared.entry.id, prepared.operation), request, options);
|
|
933
|
-
}
|
|
934
|
-
const scope = this.#container.currentScope();
|
|
935
|
-
const ctx = this.#context(prepared, request, options, scope);
|
|
936
|
-
const hooks = this.#hooks;
|
|
937
|
-
if (!hooks.hasWrappers(OPERATION_HOOK) && !hooks.hasListeners(OPERATION_HOOK)) return this.#inScope(scope, ctx, () => this.#run(prepared, request, options, ctx));
|
|
938
|
-
return this.#inScope(scope, ctx, () => this.#observed(ctx, prepared.entry.id, () => this.#run(prepared, request, options, ctx)));
|
|
939
|
-
}
|
|
940
|
-
/**
|
|
941
|
-
* Subscribes: a stream of results, one per source event, or a rejection (status 400, 405 or 503).
|
|
942
|
-
* A query or a mutation gives a stream of its single result. The stream ends when the source
|
|
943
|
-
* ends, when `options.signal` aborts, or with `return()`; a source failure ends it with a final
|
|
944
|
-
* error result (`SUBSCRIPTION_OVERFLOW` for a subscriber that fell behind `MemoryPubSub`).
|
|
945
|
-
*/
|
|
946
|
-
async subscribe(request, options = DIRECT$1) {
|
|
947
|
-
const prepared = this.#prepare(request, options);
|
|
948
|
-
if (prepared instanceof Rejection) return this.#reject(prepared, request, options);
|
|
949
|
-
if (prepared.operation.operation !== "subscription") {
|
|
950
|
-
const response = await this.execute(request, options);
|
|
951
|
-
return response.status === 200 ? new SingleResultStream(response.result) : response;
|
|
952
|
-
}
|
|
953
|
-
const limited = this.#maxActive !== void 0;
|
|
954
|
-
if (limited && this.#streams.size + this.#starting >= this.#maxActive) return this.#reject(new Rejection(503, [requestError("Too many active subscriptions, retry later", GraphQLErrorCode.ServiceUnavailable)], prepared.entry.id, prepared.operation), request, options);
|
|
955
|
-
let reserved = limited;
|
|
956
|
-
if (reserved) this.#starting++;
|
|
957
|
-
const release = () => {
|
|
958
|
-
if (reserved) {
|
|
959
|
-
reserved = false;
|
|
960
|
-
this.#starting--;
|
|
961
|
-
}
|
|
962
|
-
};
|
|
963
|
-
const outer = this.#container.currentScope();
|
|
964
|
-
const ctx = this.#context(prepared, request, options, outer);
|
|
965
|
-
const ended = new AbortController();
|
|
966
|
-
ctx.endsWith(ended.signal);
|
|
967
|
-
let run;
|
|
968
|
-
let close;
|
|
969
|
-
if (outer === void 0) {
|
|
970
|
-
const opened = this.#container.openScope([[GraphQLTokens.Context, ctx]]);
|
|
971
|
-
run = (fn) => opened.run(fn);
|
|
972
|
-
close = () => {
|
|
973
|
-
ended.abort();
|
|
974
|
-
opened.close()?.catch((error) => this.#runtimeLogError("Failed to close the scope of a subscription", error));
|
|
975
|
-
};
|
|
976
|
-
} else {
|
|
977
|
-
outer.set(GraphQLTokens.Context, ctx);
|
|
978
|
-
run = (fn) => fn();
|
|
979
|
-
close = () => ended.abort();
|
|
980
|
-
}
|
|
981
|
-
const observer = this.#observer(ctx, prepared.entry.id);
|
|
982
|
-
try {
|
|
983
|
-
const stream = await run(() => this.#startSubscription(prepared, request, options, ctx, observer, run, close, release));
|
|
984
|
-
release();
|
|
985
|
-
if (stream instanceof Rejection) {
|
|
986
|
-
close();
|
|
987
|
-
observer?.end("rejected", stream.errors);
|
|
988
|
-
return stream.response();
|
|
989
|
-
}
|
|
990
|
-
return stream;
|
|
991
|
-
} catch (error) {
|
|
992
|
-
release();
|
|
993
|
-
close();
|
|
994
|
-
observer?.end("error", void 0);
|
|
995
|
-
throw error;
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
/** Ends every subscription started with `transport: 'direct'` (plugin `onStop`). */
|
|
999
|
-
async stopDirect() {
|
|
1000
|
-
await Promise.all([...this.#direct].map((stream) => stream.return()));
|
|
1001
|
-
}
|
|
1002
|
-
/** Forgets every cached document. */
|
|
1003
|
-
clear() {
|
|
1004
|
-
this.#cache.clear();
|
|
1005
|
-
}
|
|
1006
|
-
/** Request checks, the document (cached, or parsed, limited and validated), the operation. */
|
|
1007
|
-
#prepare(request, options) {
|
|
1008
|
-
const query = request?.query;
|
|
1009
|
-
if (typeof query !== "string" || query === "") return badRequest(typeof query === "string" ? "The query is empty" : "Missing query (a string)");
|
|
1010
|
-
const operationName = request.operationName;
|
|
1011
|
-
if (operationName !== void 0 && operationName !== null && typeof operationName !== "string") return badRequest("operationName must be a string");
|
|
1012
|
-
const variables = request.variables;
|
|
1013
|
-
if (variables !== void 0 && variables !== null && (typeof variables !== "object" || Array.isArray(variables))) return badRequest("variables must be an object");
|
|
1014
|
-
const entry = this.#cache.get(query) ?? this.#load(query);
|
|
1015
|
-
if (entry instanceof Rejection) return entry;
|
|
1016
|
-
let operation;
|
|
1017
|
-
if (operationName === void 0 || operationName === null) {
|
|
1018
|
-
operation = entry.single;
|
|
1019
|
-
if (operation === void 0) return new Rejection(400, [requestError("Must provide operation name if query contains multiple operations.", GraphQLErrorCode.BadRequest)], entry.id);
|
|
1020
|
-
} else {
|
|
1021
|
-
operation = entry.named?.get(operationName);
|
|
1022
|
-
if (operation === void 0) return new Rejection(400, [requestError(`Unknown operation named "${operationName}".`, GraphQLErrorCode.BadRequest)], entry.id);
|
|
1023
|
-
}
|
|
1024
|
-
if (options.queriesOnly === true && operation.operation !== "query") return new Rejection(405, [requestError(`A ${operation.operation} cannot be sent with GET: use POST`, GraphQLErrorCode.BadRequest)], entry.id, operation, { allow: "POST" });
|
|
1025
|
-
return {
|
|
1026
|
-
entry,
|
|
1027
|
-
operation
|
|
1028
|
-
};
|
|
1029
|
-
}
|
|
1030
|
-
/** Parses, limits and validates a document; caches it when valid (rejected documents are not cached). */
|
|
1031
|
-
#load(query) {
|
|
1032
|
-
let document;
|
|
1033
|
-
try {
|
|
1034
|
-
document = parse(query, this.#maxTokens === void 0 ? void 0 : { maxTokens: this.#maxTokens });
|
|
1035
|
-
} catch (error) {
|
|
1036
|
-
if (!(error instanceof GraphQLError)) throw error;
|
|
1037
|
-
return new Rejection(400, [withCode(error, GraphQLErrorCode.ParseFailed)], documentId(query));
|
|
1038
|
-
}
|
|
1039
|
-
const limited = checkLimits(document, this.#limits);
|
|
1040
|
-
if (limited !== void 0) return new Rejection(400, limited.map((error) => withCode(error, void 0)), documentId(query));
|
|
1041
|
-
const errors = validate(this.#schema, document, this.#rules, { hideSuggestions: this.#hideSuggestions });
|
|
1042
|
-
if (errors.length > 0) return new Rejection(400, errors.map((error) => withCode(error, GraphQLErrorCode.ValidationFailed)), documentId(query));
|
|
1043
|
-
const entry = new CachedDocument(query, document);
|
|
1044
|
-
this.#cache.set(query, entry);
|
|
1045
|
-
return entry;
|
|
1046
|
-
}
|
|
1047
|
-
#context(prepared, request, options, scope) {
|
|
1048
|
-
return new OperationContext(this.#runtime, prepared.entry.document, prepared.operation, request, options, scope);
|
|
1049
|
-
}
|
|
1050
|
-
/** Runs `fn` in the current scope (the GraphQL context set in it) or in a new one, closed after. */
|
|
1051
|
-
#inScope(scope, ctx, fn) {
|
|
1052
|
-
if (scope !== void 0) {
|
|
1053
|
-
scope.set(GraphQLTokens.Context, ctx);
|
|
1054
|
-
return fn();
|
|
1055
|
-
}
|
|
1056
|
-
const opened = this.#container.openScope([[GraphQLTokens.Context, ctx]]);
|
|
1057
|
-
let result;
|
|
1058
|
-
try {
|
|
1059
|
-
result = opened.run(fn);
|
|
1060
|
-
} catch (error) {
|
|
1061
|
-
const closing = opened.close();
|
|
1062
|
-
if (closing === void 0) throw error;
|
|
1063
|
-
return closing.then(() => Promise.reject(error), () => Promise.reject(error));
|
|
1064
|
-
}
|
|
1065
|
-
if (isPromise$2(result)) return result.then((value) => {
|
|
1066
|
-
const closing = opened.close();
|
|
1067
|
-
return closing === void 0 ? value : closing.then(() => value);
|
|
1068
|
-
}, (error) => {
|
|
1069
|
-
const closing = opened.close();
|
|
1070
|
-
if (closing === void 0) throw error;
|
|
1071
|
-
return closing.then(() => Promise.reject(error), () => Promise.reject(error));
|
|
1072
|
-
});
|
|
1073
|
-
const closing = opened.close();
|
|
1074
|
-
return closing === void 0 ? result : closing.then(() => result);
|
|
1075
|
-
}
|
|
1076
|
-
#run(prepared, request, options, ctx) {
|
|
1077
|
-
let result;
|
|
1078
|
-
try {
|
|
1079
|
-
result = execute({
|
|
1080
|
-
schema: this.#schema,
|
|
1081
|
-
document: prepared.entry.document,
|
|
1082
|
-
operationName: request.operationName,
|
|
1083
|
-
variableValues: request.variables,
|
|
1084
|
-
contextValue: ctx,
|
|
1085
|
-
abortSignal: options.abortSignal ?? options.signal,
|
|
1086
|
-
hideSuggestions: this.#hideSuggestions
|
|
1087
|
-
});
|
|
1088
|
-
} catch (error) {
|
|
1089
|
-
return Promise.reject(abortReason(error, options));
|
|
1090
|
-
}
|
|
1091
|
-
if (isPromise$2(result)) return result.then((settled) => this.#respond(settled, ctx), (error) => Promise.reject(abortReason(error, options)));
|
|
1092
|
-
return this.#respond(result, ctx);
|
|
1093
|
-
}
|
|
1094
|
-
/** The response of an execution result: as is without errors, formatted errors otherwise. */
|
|
1095
|
-
#respond(result, ctx) {
|
|
1096
|
-
const errors = result.errors;
|
|
1097
|
-
if (errors === void 0) return {
|
|
1098
|
-
status: 200,
|
|
1099
|
-
result
|
|
1100
|
-
};
|
|
1101
|
-
if (!("data" in result)) return {
|
|
1102
|
-
status: 400,
|
|
1103
|
-
result: { errors: errors.map(variableError) }
|
|
1104
|
-
};
|
|
1105
|
-
return {
|
|
1106
|
-
status: 200,
|
|
1107
|
-
result: {
|
|
1108
|
-
data: result.data,
|
|
1109
|
-
errors: this.#formatter.format(errors, ctx),
|
|
1110
|
-
...result.extensions === void 0 ? {} : { extensions: result.extensions }
|
|
1111
|
-
}
|
|
1112
|
-
};
|
|
1113
|
-
}
|
|
1114
|
-
/** Formats a subscription result: an event, a source that could not start or failed (field errors). */
|
|
1115
|
-
formatEvent(result, ctx) {
|
|
1116
|
-
const errors = result.errors;
|
|
1117
|
-
if (errors === void 0) return result;
|
|
1118
|
-
return {
|
|
1119
|
-
..."data" in result ? { data: result.data } : {},
|
|
1120
|
-
errors: this.#formatter.format(errors, ctx)
|
|
1121
|
-
};
|
|
1122
|
-
}
|
|
1123
|
-
#startSubscription(prepared, request, options, ctx, observer, run, close, release) {
|
|
1124
|
-
const validated = validateSubscriptionArgs({
|
|
1125
|
-
schema: this.#schema,
|
|
1126
|
-
document: prepared.entry.document,
|
|
1127
|
-
operationName: request.operationName,
|
|
1128
|
-
variableValues: request.variables,
|
|
1129
|
-
contextValue: ctx,
|
|
1130
|
-
abortSignal: options.signal,
|
|
1131
|
-
hideSuggestions: this.#hideSuggestions
|
|
1132
|
-
});
|
|
1133
|
-
if (!("schema" in validated)) return new Rejection(400, validated.map(variableError), prepared.entry.id, prepared.operation);
|
|
1134
|
-
const start = (source) => {
|
|
1135
|
-
release();
|
|
1136
|
-
if (!isAsyncIterable(source)) {
|
|
1137
|
-
const result = this.formatEvent(source, ctx);
|
|
1138
|
-
close();
|
|
1139
|
-
observer?.end("error", result.errors);
|
|
1140
|
-
return new SingleResultStream(result);
|
|
1141
|
-
}
|
|
1142
|
-
const stream = new SubscriptionStream({
|
|
1143
|
-
executor: this,
|
|
1144
|
-
source: source[Symbol.asyncIterator](),
|
|
1145
|
-
validated,
|
|
1146
|
-
ctx,
|
|
1147
|
-
run,
|
|
1148
|
-
signal: options.signal,
|
|
1149
|
-
field: responseKey(prepared.operation),
|
|
1150
|
-
onEnd: (outcome, errors) => {
|
|
1151
|
-
this.#streams.delete(stream);
|
|
1152
|
-
this.#direct.delete(stream);
|
|
1153
|
-
close();
|
|
1154
|
-
observer?.end(outcome, errors);
|
|
1155
|
-
}
|
|
1156
|
-
});
|
|
1157
|
-
this.#streams.add(stream);
|
|
1158
|
-
if (ctx.transport === "direct") this.#direct.add(stream);
|
|
1159
|
-
return stream;
|
|
1160
|
-
};
|
|
1161
|
-
const source = createSourceEventStream(validated);
|
|
1162
|
-
return isPromise$2(source) ? source.then(start) : start(source);
|
|
1163
|
-
}
|
|
1164
|
-
/** A rejection as a response, observed by the hook (outcome `rejected`). */
|
|
1165
|
-
#reject(rejection, request, options) {
|
|
1166
|
-
const hooks = this.#hooks;
|
|
1167
|
-
const wrapped = hooks.hasWrappers(OPERATION_HOOK);
|
|
1168
|
-
if (!wrapped && !hooks.hasListeners(OPERATION_HOOK)) return rejection.response();
|
|
1169
|
-
const event = {
|
|
1170
|
-
operationType: rejection.operationType,
|
|
1171
|
-
operationName: rejection.operationName ?? (typeof request?.operationName === "string" ? request.operationName : void 0),
|
|
1172
|
-
documentId: rejection.documentId,
|
|
1173
|
-
transport: options.transport ?? "direct",
|
|
1174
|
-
requestId: options.requestId ?? options.http?.requestId ?? this.#scopedRequestId(),
|
|
1175
|
-
context: void 0,
|
|
1176
|
-
startTime: performance.now(),
|
|
1177
|
-
outcome: "rejected",
|
|
1178
|
-
errors: summaries(rejection.errors)
|
|
1179
|
-
};
|
|
1180
|
-
if (!wrapped) {
|
|
1181
|
-
hooks.emit(OPERATION_HOOK, event);
|
|
1182
|
-
return rejection.response();
|
|
1183
|
-
}
|
|
1184
|
-
return hooks.run(OPERATION_HOOK, event, () => Promise.resolve(rejection.response())).then((response) => {
|
|
1185
|
-
if (hooks.hasListeners(OPERATION_HOOK)) hooks.emit(OPERATION_HOOK, event);
|
|
1186
|
-
return response;
|
|
1187
|
-
});
|
|
1188
|
-
}
|
|
1189
|
-
/** An executed query or mutation, observed by the hook. */
|
|
1190
|
-
#observed(ctx, id, run) {
|
|
1191
|
-
const hooks = this.#hooks;
|
|
1192
|
-
const event = operationEvent(ctx, id);
|
|
1193
|
-
const done = (response) => {
|
|
1194
|
-
event.outcome = outcomeOf(response);
|
|
1195
|
-
if (response.result.errors !== void 0) event.errors = summaries(response.result.errors);
|
|
1196
|
-
return response;
|
|
1197
|
-
};
|
|
1198
|
-
const failed = (error) => {
|
|
1199
|
-
event.outcome = "error";
|
|
1200
|
-
throw error;
|
|
1201
|
-
};
|
|
1202
|
-
const emit = () => {
|
|
1203
|
-
if (hooks.hasListeners(OPERATION_HOOK)) hooks.emit(OPERATION_HOOK, event);
|
|
1204
|
-
};
|
|
1205
|
-
if (hooks.hasWrappers(OPERATION_HOOK)) return hooks.run(OPERATION_HOOK, event, () => Promise.resolve(run()).then(done, failed)).then((response) => {
|
|
1206
|
-
emit();
|
|
1207
|
-
return response;
|
|
1208
|
-
}, (error) => {
|
|
1209
|
-
emit();
|
|
1210
|
-
throw error;
|
|
1211
|
-
});
|
|
1212
|
-
let response;
|
|
1213
|
-
try {
|
|
1214
|
-
response = run();
|
|
1215
|
-
} catch (error) {
|
|
1216
|
-
event.outcome = "error";
|
|
1217
|
-
emit();
|
|
1218
|
-
throw error;
|
|
1219
|
-
}
|
|
1220
|
-
if (!isPromise$2(response)) {
|
|
1221
|
-
done(response);
|
|
1222
|
-
emit();
|
|
1223
|
-
return response;
|
|
1224
|
-
}
|
|
1225
|
-
return response.then((settled) => {
|
|
1226
|
-
done(settled);
|
|
1227
|
-
emit();
|
|
1228
|
-
return settled;
|
|
1229
|
-
}, (error) => {
|
|
1230
|
-
event.outcome = "error";
|
|
1231
|
-
emit();
|
|
1232
|
-
throw error;
|
|
1233
|
-
});
|
|
1234
|
-
}
|
|
1235
|
-
/** The hook of a subscription, for its whole life (`end()` settles it), or `undefined` without wrappers nor listeners. */
|
|
1236
|
-
#observer(ctx, id) {
|
|
1237
|
-
const hooks = this.#hooks;
|
|
1238
|
-
const wrapped = hooks.hasWrappers(OPERATION_HOOK);
|
|
1239
|
-
if (!wrapped && !hooks.hasListeners(OPERATION_HOOK)) return void 0;
|
|
1240
|
-
const event = operationEvent(ctx, id);
|
|
1241
|
-
let resolve;
|
|
1242
|
-
if (wrapped) {
|
|
1243
|
-
const ended = new Promise((settle) => {
|
|
1244
|
-
resolve = settle;
|
|
1245
|
-
});
|
|
1246
|
-
hooks.run(OPERATION_HOOK, event, () => ended).catch((error) => this.#runtimeLogError("A \"graphql:operation\" hook wrapper failed", error));
|
|
1247
|
-
}
|
|
1248
|
-
let ended = false;
|
|
1249
|
-
return { end: (outcome, errors) => {
|
|
1250
|
-
if (ended) return;
|
|
1251
|
-
ended = true;
|
|
1252
|
-
event.outcome = outcome;
|
|
1253
|
-
if (errors !== void 0 && errors.length > 0) event.errors = summaries(errors);
|
|
1254
|
-
resolve?.();
|
|
1255
|
-
if (hooks.hasListeners(OPERATION_HOOK)) hooks.emit(OPERATION_HOOK, event);
|
|
1256
|
-
} };
|
|
1257
|
-
}
|
|
1258
|
-
/** The request id of the enclosing scope (a controller calling GraphQL), or a new one. */
|
|
1259
|
-
#scopedRequestId() {
|
|
1260
|
-
const id = this.#container.currentScope()?.get(HttpTokens.RequestId);
|
|
1261
|
-
return typeof id === "string" ? id : randomUUID();
|
|
1262
|
-
}
|
|
1263
|
-
#runtimeLogError(message, error) {
|
|
1264
|
-
this.#logger.error(`GraphQLPlugin: ${message}`, { err: error });
|
|
1265
|
-
}
|
|
1266
|
-
};
|
|
1267
|
-
function noop$1() {}
|
|
1268
|
-
function badRequest(message) {
|
|
1269
|
-
return new Rejection(400, [requestError(message, GraphQLErrorCode.BadRequest)], void 0);
|
|
1270
|
-
}
|
|
1271
|
-
function variableError(error) {
|
|
1272
|
-
return withCode(error, error.extensions["code"] === void 0 ? GraphQLErrorCode.BadUserInput : void 0);
|
|
1273
|
-
}
|
|
1274
|
-
/** graphql-js rejects with `AbortedGraphQLExecutionError`: callers get the reason of their signal. */
|
|
1275
|
-
function abortReason(error, options) {
|
|
1276
|
-
const signal = options.abortSignal ?? options.signal;
|
|
1277
|
-
if (error instanceof AbortedGraphQLExecutionError && signal?.aborted === true) return signal.reason;
|
|
1278
|
-
return error;
|
|
1279
|
-
}
|
|
1280
|
-
function operationEvent(ctx, id) {
|
|
312
|
+
function executeOptions(args, options, driver) {
|
|
313
|
+
const signal = options?.signal ?? args?.abortSignal ?? void 0;
|
|
314
|
+
const requestId = options?.requestId;
|
|
1281
315
|
return {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
requestId:
|
|
1287
|
-
context: ctx,
|
|
1288
|
-
startTime: performance.now()
|
|
316
|
+
transport: options?.transport ?? "http",
|
|
317
|
+
driver,
|
|
318
|
+
serverContext: args?.contextValue,
|
|
319
|
+
...signal === void 0 ? {} : { signal },
|
|
320
|
+
...requestId === void 0 ? {} : { requestId }
|
|
1289
321
|
};
|
|
1290
322
|
}
|
|
1291
|
-
function outcomeOf(response) {
|
|
1292
|
-
if (response.status !== 200) return "rejected";
|
|
1293
|
-
const result = response.result;
|
|
1294
|
-
if (result.errors === void 0 || result.errors.length === 0) return "success";
|
|
1295
|
-
return result.data === null || result.data === void 0 ? "error" : "partial";
|
|
1296
|
-
}
|
|
1297
|
-
function summaries(errors) {
|
|
1298
|
-
return errors.map((error) => {
|
|
1299
|
-
const code = error.extensions?.["code"];
|
|
1300
|
-
return {
|
|
1301
|
-
message: error.message,
|
|
1302
|
-
code: typeof code === "string" ? code : void 0,
|
|
1303
|
-
...error.path === void 0 ? {} : { path: error.path }
|
|
1304
|
-
};
|
|
1305
|
-
});
|
|
1306
|
-
}
|
|
1307
|
-
/** The response key of the root field of a subscription (its single selection). */
|
|
1308
|
-
function responseKey(operation) {
|
|
1309
|
-
const selection = operation.selectionSet.selections[0];
|
|
1310
|
-
return selection?.kind === Kind.FIELD ? (selection.alias ?? selection.name).value : void 0;
|
|
1311
|
-
}
|
|
1312
|
-
function isPromise$2(value) {
|
|
1313
|
-
return typeof value?.then === "function";
|
|
1314
|
-
}
|
|
1315
|
-
function isAsyncIterable(value) {
|
|
1316
|
-
return value !== null && typeof value === "object" && typeof value[Symbol.asyncIterator] === "function";
|
|
1317
|
-
}
|
|
1318
|
-
/** A stream of one result (a query sent as a stream, a rejection for `plugin.subscribe()`). */
|
|
1319
|
-
function singleResult(result) {
|
|
1320
|
-
return new SingleResultStream(result);
|
|
1321
|
-
}
|
|
1322
|
-
/** The stream of a query or a mutation sent as a stream (SSE): its single result. */
|
|
1323
|
-
var SingleResultStream = class {
|
|
1324
|
-
#result;
|
|
1325
|
-
constructor(result) {
|
|
1326
|
-
this.#result = result;
|
|
1327
|
-
}
|
|
1328
|
-
next() {
|
|
1329
|
-
const result = this.#result;
|
|
1330
|
-
if (result === void 0) return Promise.resolve(DONE$1);
|
|
1331
|
-
this.#result = void 0;
|
|
1332
|
-
return Promise.resolve({
|
|
1333
|
-
done: false,
|
|
1334
|
-
value: result
|
|
1335
|
-
});
|
|
1336
|
-
}
|
|
1337
|
-
return() {
|
|
1338
|
-
this.#result = void 0;
|
|
1339
|
-
return Promise.resolve(DONE$1);
|
|
1340
|
-
}
|
|
1341
|
-
[Symbol.asyncIterator]() {
|
|
1342
|
-
return this;
|
|
1343
|
-
}
|
|
1344
|
-
};
|
|
1345
|
-
/**
|
|
1346
|
-
* The results of a subscription: each source event executed with `executeSubscriptionEvent`, a
|
|
1347
|
-
* fresh per-execution state (loaders) and the operation's `GraphQLContext`.
|
|
1348
|
-
*
|
|
1349
|
-
* The source is started at once: its first `next()` is requested when the stream is created, so
|
|
1350
|
-
* that a resolver written as an async generator (`for await (const x of pubsub.subscribe(...))`)
|
|
1351
|
-
* has subscribed when `subscribe()` resolves, whatever the transport and however late its consumer
|
|
1352
|
-
* reads (an SSE body pulled on demand). Only that first event is read ahead; the next ones are read
|
|
1353
|
-
* when the consumer asks (backpressure).
|
|
1354
|
-
*/
|
|
1355
|
-
var SubscriptionStream = class {
|
|
1356
|
-
#init;
|
|
1357
|
-
#onAbort;
|
|
1358
|
-
/** The first `next()` of the source, requested at creation and consumed by the first read. */
|
|
1359
|
-
#first;
|
|
1360
|
-
/** `next()` calls of the source still pending. */
|
|
1361
|
-
#reading = 0;
|
|
1362
|
-
#ended = false;
|
|
1363
|
-
#partial = false;
|
|
1364
|
-
constructor(init) {
|
|
1365
|
-
this.#init = init;
|
|
1366
|
-
const signal = init.signal;
|
|
1367
|
-
if (signal !== void 0) {
|
|
1368
|
-
this.#onAbort = () => {
|
|
1369
|
-
this.return();
|
|
1370
|
-
};
|
|
1371
|
-
signal.addEventListener("abort", this.#onAbort, { once: true });
|
|
1372
|
-
}
|
|
1373
|
-
const first = init.run(() => this.#read());
|
|
1374
|
-
first.catch(noop$1);
|
|
1375
|
-
this.#first = first;
|
|
1376
|
-
if (signal?.aborted === true) this.return();
|
|
1377
|
-
}
|
|
1378
|
-
next() {
|
|
1379
|
-
if (this.#ended) return Promise.resolve(DONE$1);
|
|
1380
|
-
return this.#init.run(() => this.#pull());
|
|
1381
|
-
}
|
|
1382
|
-
/** The next event of the source. */
|
|
1383
|
-
async #read() {
|
|
1384
|
-
this.#reading++;
|
|
1385
|
-
try {
|
|
1386
|
-
return await this.#init.source.next();
|
|
1387
|
-
} finally {
|
|
1388
|
-
this.#reading--;
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
async #pull() {
|
|
1392
|
-
const init = this.#init;
|
|
1393
|
-
let step;
|
|
1394
|
-
try {
|
|
1395
|
-
const first = this.#first;
|
|
1396
|
-
this.#first = void 0;
|
|
1397
|
-
step = await (first ?? this.#read());
|
|
1398
|
-
} catch (error) {
|
|
1399
|
-
if (this.#ended) return DONE$1;
|
|
1400
|
-
const result = init.executor.formatEvent({ errors: [new GraphQLError(errorMessage(error), {
|
|
1401
|
-
originalError: asError(error),
|
|
1402
|
-
path: init.field === void 0 ? void 0 : [init.field]
|
|
1403
|
-
})] }, init.ctx);
|
|
1404
|
-
this.#end("error", result.errors);
|
|
1405
|
-
return {
|
|
1406
|
-
done: false,
|
|
1407
|
-
value: result
|
|
1408
|
-
};
|
|
1409
|
-
}
|
|
1410
|
-
if (step.done === true || this.#ended) {
|
|
1411
|
-
this.#end(this.#partial ? "partial" : "success", void 0);
|
|
1412
|
-
return DONE$1;
|
|
1413
|
-
}
|
|
1414
|
-
init.ctx.resetLoaders();
|
|
1415
|
-
const executed = executeSubscriptionEvent({
|
|
1416
|
-
...init.validated,
|
|
1417
|
-
rootValue: step.value
|
|
1418
|
-
});
|
|
1419
|
-
const result = init.executor.formatEvent(isPromise$2(executed) ? await executed : executed, init.ctx);
|
|
1420
|
-
if (result.errors !== void 0) this.#partial = true;
|
|
1421
|
-
return {
|
|
1422
|
-
done: false,
|
|
1423
|
-
value: result
|
|
1424
|
-
};
|
|
1425
|
-
}
|
|
1426
|
-
async return() {
|
|
1427
|
-
if (this.#ended) return DONE$1;
|
|
1428
|
-
this.#first = void 0;
|
|
1429
|
-
this.#end(this.#partial ? "partial" : "success", void 0);
|
|
1430
|
-
const reading = this.#reading > 0;
|
|
1431
|
-
try {
|
|
1432
|
-
const returned = Promise.resolve(this.#init.source.return?.());
|
|
1433
|
-
if (reading) returned.catch(noop$1);
|
|
1434
|
-
else await returned;
|
|
1435
|
-
} catch {}
|
|
1436
|
-
return DONE$1;
|
|
1437
|
-
}
|
|
1438
|
-
throw() {
|
|
1439
|
-
return this.return();
|
|
1440
|
-
}
|
|
1441
|
-
[Symbol.asyncIterator]() {
|
|
1442
|
-
return this;
|
|
1443
|
-
}
|
|
1444
|
-
#end(outcome, errors) {
|
|
1445
|
-
if (this.#ended) return;
|
|
1446
|
-
this.#ended = true;
|
|
1447
|
-
if (this.#onAbort !== void 0) this.#init.signal?.removeEventListener("abort", this.#onAbort);
|
|
1448
|
-
this.#init.onEnd(outcome, errors);
|
|
1449
|
-
}
|
|
1450
|
-
};
|
|
1451
|
-
function asError(error) {
|
|
1452
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
1453
|
-
}
|
|
1454
|
-
function errorMessage(error) {
|
|
1455
|
-
return error instanceof Error ? error.message : String(error);
|
|
1456
|
-
}
|
|
1457
323
|
// src/options.ts
|
|
1458
324
|
const KEYS = /* @__PURE__ */ new Set([
|
|
1459
325
|
"path",
|
|
@@ -1468,7 +334,8 @@ const KEYS = /* @__PURE__ */ new Set([
|
|
|
1468
334
|
"documentCache",
|
|
1469
335
|
"subscriptions",
|
|
1470
336
|
"pubsub",
|
|
1471
|
-
"enforcement"
|
|
337
|
+
"enforcement",
|
|
338
|
+
"driver"
|
|
1472
339
|
]);
|
|
1473
340
|
const PATH = /^\/[A-Za-z0-9\-._~!$&'()*+,;=:@/]*$/;
|
|
1474
341
|
const LIMIT_DEFAULTS = {
|
|
@@ -1563,6 +430,10 @@ function resolveGraphQLOptions(options, env = process.env) {
|
|
|
1563
430
|
if (pubsub !== void 0 && !(isRecord$1(pubsub) && typeof pubsub.publish === "function" && typeof pubsub.subscribe === "function")) throw invalidOption("pubsub", "a PubSub (publish() and subscribe() methods)", pubsub);
|
|
1564
431
|
const enforcement = options["enforcement"] ?? "error";
|
|
1565
432
|
if (enforcement !== "error" && enforcement !== "warn") throw invalidOption("enforcement", "'error' or 'warn'", enforcement);
|
|
433
|
+
const driver = resolveDriver(options["driver"]);
|
|
434
|
+
if (driver !== void 0 && subscriptions !== false) {
|
|
435
|
+
for (const key of ["heartbeat", "maxDuration"]) if (subscriptions[key] !== void 0) throw new ZnightError(ErrorCode.InvalidGraphQLSetup, `GraphQLPlugin: subscriptions.${key} configures the built-in SSE transport, not the ${driver.name} driver`, { hints: [`Remove subscriptions.${key}: the ${driver.name} driver's own keep-alive options apply`] });
|
|
436
|
+
}
|
|
1566
437
|
return {
|
|
1567
438
|
path,
|
|
1568
439
|
http,
|
|
@@ -1582,9 +453,23 @@ function resolveGraphQLOptions(options, env = process.env) {
|
|
|
1582
453
|
subscriptions,
|
|
1583
454
|
bufferSize,
|
|
1584
455
|
pubsub,
|
|
1585
|
-
enforcement
|
|
456
|
+
enforcement,
|
|
457
|
+
driver
|
|
1586
458
|
};
|
|
1587
459
|
}
|
|
460
|
+
/** The `driver` option: an object with a `name` (not `builtin`) and `start()`, `routes()` and `stop()` functions when present. */
|
|
461
|
+
function resolveDriver(value) {
|
|
462
|
+
if (value === void 0) return void 0;
|
|
463
|
+
const expected = "a GraphQL driver ({ name, start(), routes?(), stop?() }, from yogaDriver() for instance)";
|
|
464
|
+
if (value === null || typeof value !== "object") throw invalidOption("driver", expected, value);
|
|
465
|
+
const driver = value;
|
|
466
|
+
const name = driver["name"];
|
|
467
|
+
if (typeof name !== "string" || name === "") throw invalidOption("driver", `${expected} with a name`, value);
|
|
468
|
+
if (name === "builtin") throw invalidOption("driver.name", "a name other than 'builtin' (reserved for the built-in transport)", name);
|
|
469
|
+
if (typeof driver["start"] !== "function") throw invalidOption("driver", `${expected} with a start() method`, value);
|
|
470
|
+
for (const method of ["routes", "stop"]) if (driver[method] !== void 0 && typeof driver[method] !== "function") throw invalidOption(`driver.${method}`, "a function", driver[method]);
|
|
471
|
+
return value;
|
|
472
|
+
}
|
|
1588
473
|
// src/pipeline.ts
|
|
1589
474
|
/** Whether global guards run on a field: root fields always, field resolvers when they carry metadata (§5.9). */
|
|
1590
475
|
function globalGuardsApply(binding) {
|
|
@@ -1597,17 +482,18 @@ function pipelineFactory(options) {
|
|
|
1597
482
|
}
|
|
1598
483
|
function fieldFunctions(options, normalizers, binding, coordinate, field, parent) {
|
|
1599
484
|
const container = options.container;
|
|
1600
|
-
const invoke = binding.invoke;
|
|
1601
485
|
const target = options.resolver(binding, coordinate);
|
|
1602
486
|
const guards = [...globalGuardsApply(binding) ? options.globalGuards : [], ...binding.guards.map((token) => options.guard(token, coordinate))];
|
|
1603
487
|
const normalize = field.args === void 0 ? void 0 : normalizers.args(field.args);
|
|
1604
488
|
const recover = options.results?.(field, coordinate);
|
|
1605
|
-
const
|
|
489
|
+
const custom = binding.context === true;
|
|
490
|
+
const route = guards.length === 0 && recover === void 0 && !custom ? void 0 : Object.freeze({
|
|
1606
491
|
meta: binding.meta,
|
|
1607
492
|
coordinate
|
|
1608
493
|
});
|
|
1609
494
|
const parentType = parent.name;
|
|
1610
495
|
const fieldName = field.name;
|
|
496
|
+
const invoke = custom ? customInvoke(options, binding, coordinate, route, parentType, fieldName) : binding.invoke;
|
|
1611
497
|
let resolve;
|
|
1612
498
|
if (guards.length === 0) {
|
|
1613
499
|
if (target.token === void 0) {
|
|
@@ -1630,10 +516,11 @@ function fieldFunctions(options, normalizers, binding, coordinate, field, parent
|
|
|
1630
516
|
resolve = (p, a, c, i) => {
|
|
1631
517
|
const operation = c;
|
|
1632
518
|
const args = a;
|
|
1633
|
-
const
|
|
519
|
+
const fieldContext = new FieldContext(route, operation, parentType, fieldName, args, p, i);
|
|
520
|
+
const allowed = runGuards(container, guards, fieldContext);
|
|
1634
521
|
const call = () => {
|
|
1635
522
|
if (normalize !== void 0) normalize(args);
|
|
1636
|
-
return invoke(resolveIn(container, target), p, args, operation, i);
|
|
523
|
+
return invoke(resolveIn(container, target), p, args, operation, i, fieldContext);
|
|
1637
524
|
};
|
|
1638
525
|
if (allowed === true) return call();
|
|
1639
526
|
if (allowed === false) return forbidden();
|
|
@@ -1652,6 +539,27 @@ function identity$1(payload) {
|
|
|
1652
539
|
return payload;
|
|
1653
540
|
}
|
|
1654
541
|
/**
|
|
542
|
+
* The invoke of a method with custom parameters (RFC 0010): the generated `invoke` receives the
|
|
543
|
+
* field's `ResolverContext` (the guards' one when the field has guards, else a new one) and the
|
|
544
|
+
* instances of its parameter resolvers: one frozen array resolved at boot when they are all
|
|
545
|
+
* singletons, otherwise resolved in the operation scope on every call.
|
|
546
|
+
*/
|
|
547
|
+
function customInvoke(options, binding, coordinate, route, parentType, fieldName) {
|
|
548
|
+
const generated = binding.invoke;
|
|
549
|
+
const container = options.container;
|
|
550
|
+
const resolvers = (binding.paramResolvers ?? []).map((token) => {
|
|
551
|
+
if (options.paramResolver === void 0) throw new TypeError(`No parameter resolver for ${coordinate}`);
|
|
552
|
+
return options.paramResolver(token, coordinate);
|
|
553
|
+
});
|
|
554
|
+
const context = (p, a, c, i, f) => f ?? new FieldContext(route, c, parentType, fieldName, a, p, i);
|
|
555
|
+
if (resolvers.length === 0) return (r, p, a, c, i, f) => generated(r, p, a, c, i, context(p, a, c, i, f));
|
|
556
|
+
if (resolvers.every((resolver) => resolver.token === void 0)) {
|
|
557
|
+
const instances = Object.freeze(resolvers.map((resolver) => resolveIn(container, resolver)));
|
|
558
|
+
return (r, p, a, c, i, f) => generated(r, p, a, c, i, context(p, a, c, i, f), instances);
|
|
559
|
+
}
|
|
560
|
+
return (r, p, a, c, i, f) => generated(r, p, a, c, i, context(p, a, c, i, f), resolvers.map((resolver) => resolveIn(container, resolver)));
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
1655
563
|
* The resolve function of a field that carries results (RFC 0008): errors of the whole pipeline
|
|
1656
564
|
* (guards, null handling, `invoke`), thrown or rejected, go through `recover`. Synchronous results
|
|
1657
565
|
* stay synchronous; a promise gets one rejection handler. For a subscription, a value is a source
|
|
@@ -2425,585 +1333,43 @@ function pathOf(info) {
|
|
|
2425
1333
|
for (let step = info.path; step !== void 0; step = step.prev) path.push(step.key);
|
|
2426
1334
|
return path.reverse();
|
|
2427
1335
|
}
|
|
2428
|
-
// src/transport/
|
|
2429
|
-
/** The
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
#
|
|
2435
|
-
#others = void 0;
|
|
2436
|
-
throwIfAborted() {
|
|
2437
|
-
if (this.aborted) throw this.reason;
|
|
2438
|
-
}
|
|
2439
|
-
addEventListener(type, listener) {
|
|
2440
|
-
if (type !== "abort") return;
|
|
2441
|
-
if (this.#listener === void 0) {
|
|
2442
|
-
this.#listener = listener;
|
|
2443
|
-
return;
|
|
2444
|
-
}
|
|
2445
|
-
this.#others ??= [];
|
|
2446
|
-
this.#others.push(listener);
|
|
2447
|
-
}
|
|
2448
|
-
removeEventListener(type, listener) {
|
|
2449
|
-
if (type !== "abort") return;
|
|
2450
|
-
if (this.#listener === listener) {
|
|
2451
|
-
this.#listener = void 0;
|
|
2452
|
-
return;
|
|
2453
|
-
}
|
|
2454
|
-
const others = this.#others;
|
|
2455
|
-
const index = others === void 0 ? -1 : others.indexOf(listener);
|
|
2456
|
-
if (index !== -1) others?.splice(index, 1);
|
|
2457
|
-
}
|
|
2458
|
-
/**
|
|
2459
|
-
* The execution is asynchronous: from now on, `signal` aborting aborts the execution. Reads the
|
|
2460
|
-
* signal only when graphql-js still listens (a pending execution).
|
|
2461
|
-
*/
|
|
2462
|
-
follow(read) {
|
|
2463
|
-
if (this.#listener === void 0 && (this.#others === void 0 || this.#others.length === 0)) return;
|
|
2464
|
-
const signal = read();
|
|
2465
|
-
if (signal.aborted) {
|
|
2466
|
-
this.#abort(signal.reason);
|
|
2467
|
-
return;
|
|
2468
|
-
}
|
|
2469
|
-
signal.addEventListener("abort", () => this.#abort(signal.reason), { once: true });
|
|
2470
|
-
}
|
|
2471
|
-
/** This stand-in, typed as the `AbortSignal` graphql-js expects. */
|
|
2472
|
-
asSignal() {
|
|
2473
|
-
return this;
|
|
2474
|
-
}
|
|
2475
|
-
#abort(reason) {
|
|
2476
|
-
if (this.aborted) return;
|
|
2477
|
-
this.aborted = true;
|
|
2478
|
-
this.reason = reason;
|
|
2479
|
-
const listener = this.#listener;
|
|
2480
|
-
const others = this.#others;
|
|
2481
|
-
this.#listener = void 0;
|
|
2482
|
-
this.#others = void 0;
|
|
2483
|
-
listener?.();
|
|
2484
|
-
if (others !== void 0) for (const other of others) other();
|
|
2485
|
-
}
|
|
2486
|
-
};
|
|
2487
|
-
// src/transport/accept.ts
|
|
2488
|
-
/**
|
|
2489
|
-
* Content negotiation of the GraphQL endpoint (RFC 0005 §8.3): which of the media types the
|
|
2490
|
-
* endpoint produces a request accepts best. Import-free.
|
|
2491
|
-
*
|
|
2492
|
-
* `application/json` is the only type matched by wildcards (`*\/*`, `application/*`): the
|
|
2493
|
-
* GraphQL-over-HTTP draft asks for `application/json` when the client accepts anything, and the
|
|
2494
|
-
* other types change the protocol (`application/graphql-response+json` statuses, an event stream,
|
|
2495
|
-
* the GraphiQL page), so they are chosen only when listed. Among types of equal quality,
|
|
2496
|
-
* `application/graphql-response+json` wins, then `application/json`, then HTML (a browser listing
|
|
2497
|
-
* `text/html` first gets GraphiQL). An event stream is chosen only when it is strictly preferred, or
|
|
2498
|
-
* the only acceptable type: a client listing JSON and `text/event-stream` alike (urql, for
|
|
2499
|
-
* incremental delivery) gets its queries and mutations as one JSON body, and its subscriptions as
|
|
2500
|
-
* a stream ({@link STREAMABLE}).
|
|
2501
|
-
*/
|
|
2502
|
-
/** What a response is encoded as. */
|
|
2503
|
-
const Media = Object.freeze({
|
|
2504
|
-
EventStream: 0,
|
|
2505
|
-
GraphQLJson: 1,
|
|
2506
|
-
Json: 2,
|
|
2507
|
-
Html: 3,
|
|
2508
|
-
None: 4
|
|
2509
|
-
});
|
|
2510
|
-
/** The media type names, by {@link Media} value (`None` excluded). */
|
|
2511
|
-
const NAMES = [
|
|
2512
|
-
["text", "event-stream"],
|
|
2513
|
-
["application", "graphql-response+json"],
|
|
2514
|
-
["application", "json"],
|
|
2515
|
-
["text", "html"]
|
|
2516
|
-
];
|
|
2517
|
-
/** The media ranges of an `accept` header (lower-case, without parameters other than `q`). */
|
|
2518
|
-
function parseRanges(header) {
|
|
2519
|
-
const ranges = [];
|
|
2520
|
-
for (const item of header.split(",")) {
|
|
2521
|
-
const parts = item.split(";");
|
|
2522
|
-
const media = parts[0].trim().toLowerCase();
|
|
2523
|
-
const slash = media.indexOf("/");
|
|
2524
|
-
if (slash <= 0 || slash === media.length - 1) continue;
|
|
2525
|
-
let q = 1;
|
|
2526
|
-
for (let index = 1; index < parts.length; index++) {
|
|
2527
|
-
const parameter = parts[index].trim();
|
|
2528
|
-
if (parameter.length < 2 || (parameter.charCodeAt(0) | 32) !== 113 || parameter[1] !== "=") continue;
|
|
2529
|
-
const value = Number(parameter.slice(2));
|
|
2530
|
-
if (value >= 0 && value <= 1) q = value;
|
|
2531
|
-
}
|
|
2532
|
-
ranges.push({
|
|
2533
|
-
type: media.slice(0, slash),
|
|
2534
|
-
subtype: media.slice(slash + 1),
|
|
2535
|
-
q
|
|
2536
|
-
});
|
|
2537
|
-
}
|
|
2538
|
-
return ranges;
|
|
2539
|
-
}
|
|
2540
|
-
/**
|
|
2541
|
-
* The quality of `type/subtype` in `ranges`: that of the most specific matching range (RFC 9110
|
|
2542
|
-
* §12.5.1), wildcards only when `wildcards` is set; 0 when nothing matches.
|
|
2543
|
-
*/
|
|
2544
|
-
function quality(ranges, type, subtype, wildcards) {
|
|
2545
|
-
let best = -1;
|
|
2546
|
-
let specificity = -1;
|
|
2547
|
-
for (const range of ranges) {
|
|
2548
|
-
let level;
|
|
2549
|
-
if (range.type === type && range.subtype === subtype) level = 2;
|
|
2550
|
-
else if (!wildcards) continue;
|
|
2551
|
-
else if (range.type === type && range.subtype === "*") level = 1;
|
|
2552
|
-
else if (range.type === "*" && range.subtype === "*") level = 0;
|
|
2553
|
-
else continue;
|
|
2554
|
-
if (level > specificity || level === specificity && range.q > best) {
|
|
2555
|
-
specificity = level;
|
|
2556
|
-
best = range.q;
|
|
2557
|
-
}
|
|
2558
|
-
}
|
|
2559
|
-
return best < 0 ? 0 : best;
|
|
2560
|
-
}
|
|
2561
|
-
/**
|
|
2562
|
-
* Chooses the media type of a response among `produced` (a bit mask of `1 << Media.X`), per
|
|
2563
|
-
* `accept` (`undefined`: no header, i.e. `application/json`): a {@link MediaKind}, plus
|
|
2564
|
-
* {@link STREAMABLE} when a JSON type wins and an event stream is acceptable too.
|
|
2565
|
-
*/
|
|
2566
|
-
function negotiate(accept, produced) {
|
|
2567
|
-
if (accept === void 0) return produced & 1 << Media.Json ? Media.Json : Media.None;
|
|
2568
|
-
const ranges = parseRanges(accept);
|
|
2569
|
-
if (ranges.length === 0) return produced & 1 << Media.Json ? Media.Json : Media.None;
|
|
2570
|
-
let chosen = Media.None;
|
|
2571
|
-
let best = 0;
|
|
2572
|
-
for (let media = Media.GraphQLJson; media < NAMES.length; media++) {
|
|
2573
|
-
if ((produced & 1 << media) === 0) continue;
|
|
2574
|
-
const [type, subtype] = NAMES[media];
|
|
2575
|
-
const q = quality(ranges, type, subtype, media === Media.Json);
|
|
2576
|
-
if (q > best) {
|
|
2577
|
-
best = q;
|
|
2578
|
-
chosen = media;
|
|
2579
|
-
}
|
|
2580
|
-
}
|
|
2581
|
-
if (produced & 1 << Media.EventStream) {
|
|
2582
|
-
const q = quality(ranges, "text", "event-stream", false);
|
|
2583
|
-
if (q > best) return Media.EventStream;
|
|
2584
|
-
if (q > 0 && (chosen === Media.GraphQLJson || chosen === Media.Json)) chosen |= 8;
|
|
2585
|
-
}
|
|
2586
|
-
return chosen;
|
|
2587
|
-
}
|
|
2588
|
-
const CACHE_MAX = 64;
|
|
2589
|
-
/**
|
|
2590
|
-
* {@link negotiate} for one route, memoised by header value: a server sees a handful of `accept`
|
|
2591
|
-
* values, so a request costs a map lookup. Bounded, since clients choose the values: cleared when
|
|
2592
|
-
* full.
|
|
2593
|
-
*/
|
|
2594
|
-
var Negotiator = class {
|
|
2595
|
-
#produced;
|
|
2596
|
-
#cache = /* @__PURE__ */ new Map();
|
|
2597
|
-
/** The answer without `accept` header. */
|
|
2598
|
-
#absent;
|
|
2599
|
-
/** `produced`: a bit mask of `1 << Media.X`. */
|
|
2600
|
-
constructor(produced) {
|
|
2601
|
-
this.#produced = produced;
|
|
2602
|
-
this.#absent = negotiate(void 0, produced);
|
|
2603
|
-
}
|
|
2604
|
-
/** The {@link negotiate} answer: a {@link MediaKind}, possibly with {@link STREAMABLE}. */
|
|
2605
|
-
choose(accept) {
|
|
2606
|
-
if (accept === void 0) return this.#absent;
|
|
2607
|
-
let media = this.#cache.get(accept);
|
|
2608
|
-
if (media === void 0) {
|
|
2609
|
-
media = negotiate(accept, this.#produced);
|
|
2610
|
-
if (this.#cache.size === CACHE_MAX) this.#cache.clear();
|
|
2611
|
-
this.#cache.set(accept, media);
|
|
2612
|
-
}
|
|
2613
|
-
return media;
|
|
2614
|
-
}
|
|
2615
|
-
};
|
|
2616
|
-
// src/transport/graphiql.ts
|
|
2617
|
-
/**
|
|
2618
|
-
* The GraphiQL page (RFC 0005 §8.3, §8.9): served on `GET path` with `accept: text/html` when the
|
|
2619
|
-
* `graphiql` option is on (default: `NODE_ENV === 'development'`). React and GraphiQL load from a
|
|
2620
|
-
* pinned CDN with Subresource Integrity; the inline script is fixed at boot and allowed by hash in
|
|
2621
|
-
* the page's own `content-security-policy`, which overrides the strict API default of RFC 0001 on
|
|
2622
|
-
* this response only.
|
|
2623
|
-
*/
|
|
2624
|
-
/** jsDelivr. */
|
|
2625
|
-
const GRAPHIQL_CDN = "https://cdn.jsdelivr.net";
|
|
2626
|
-
/**
|
|
2627
|
-
* The assets of the page: GraphiQL 3 (the last major with a UMD build) on React 18. Bumped by hand,
|
|
2628
|
-
* with their SRI hashes (`sha384` of the files on the CDN).
|
|
2629
|
-
*/
|
|
2630
|
-
const GRAPHIQL_ASSETS = Object.freeze([
|
|
2631
|
-
{
|
|
2632
|
-
url: `${GRAPHIQL_CDN}/npm/graphiql@3.9.0/graphiql.min.css`,
|
|
2633
|
-
integrity: "sha384-QMux00XgRtwRLSYIY3kw2rj1ovk5AuuliAchk+HSQbqdbGFnz9GYuqIlOqxhwCE2",
|
|
2634
|
-
kind: "style"
|
|
2635
|
-
},
|
|
2636
|
-
{
|
|
2637
|
-
url: `${GRAPHIQL_CDN}/npm/react@18.3.1/umd/react.production.min.js`,
|
|
2638
|
-
integrity: "sha384-DGyLxAyjq0f9SPpVevD6IgztCFlnMF6oW/XQGmfe+IsZ8TqEiDrcHkMLKI6fiB/Z",
|
|
2639
|
-
kind: "script"
|
|
2640
|
-
},
|
|
2641
|
-
{
|
|
2642
|
-
url: `${GRAPHIQL_CDN}/npm/react-dom@18.3.1/umd/react-dom.production.min.js`,
|
|
2643
|
-
integrity: "sha384-gTGxhz21lVGYNMcdJOyq01Edg0jhn/c22nsx0kyqP0TxaV5WVdsSH1fSDUf5YJj1",
|
|
2644
|
-
kind: "script"
|
|
2645
|
-
},
|
|
2646
|
-
{
|
|
2647
|
-
url: `${GRAPHIQL_CDN}/npm/graphiql@3.9.0/graphiql.min.js`,
|
|
2648
|
-
integrity: "sha384-8NGfVj4CVlqHajlZj+bPJT4thxPMHMJYn7DWK2CvtopLd02E7qsPHazniBYvVjOO",
|
|
2649
|
-
kind: "script"
|
|
2650
|
-
}
|
|
2651
|
-
]);
|
|
2652
|
-
/** `<`, `>` and `&` escaped: JSON safe inside a `<script>` element. */
|
|
2653
|
-
function scriptJson(value) {
|
|
2654
|
-
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
2655
|
-
}
|
|
2656
|
-
function escapeHtml(text) {
|
|
2657
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2658
|
-
}
|
|
2659
|
-
/** The GraphiQL page of the endpoint `endpoint` (an absolute path, prefix included) and its CSP. */
|
|
2660
|
-
function renderGraphiQL(endpoint) {
|
|
2661
|
-
const tags = GRAPHIQL_ASSETS.map((asset) => asset.kind === "style" ? `<link rel="stylesheet" href="${escapeHtml(asset.url)}" integrity="${asset.integrity}" crossorigin="anonymous">` : `<script src="${escapeHtml(asset.url)}" integrity="${asset.integrity}" crossorigin="anonymous"><\/script>`);
|
|
2662
|
-
const inline = [`const fetcher = GraphiQL.createFetcher({ url: ${scriptJson(endpoint)} })`, `ReactDOM.createRoot(document.getElementById('graphiql')).render(React.createElement(GraphiQL, { fetcher }))`].join("\n");
|
|
2663
|
-
const hash = `'sha256-${createHash("sha256").update(inline).digest("base64")}'`;
|
|
2664
|
-
return {
|
|
2665
|
-
html: [
|
|
2666
|
-
"<!doctype html>",
|
|
2667
|
-
"<html lang=\"en\">",
|
|
2668
|
-
"<head>",
|
|
2669
|
-
"<meta charset=\"utf-8\">",
|
|
2670
|
-
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">",
|
|
2671
|
-
"<title>GraphiQL</title>",
|
|
2672
|
-
"<style>body { margin: 0; } #graphiql { height: 100dvh; }</style>",
|
|
2673
|
-
...tags.filter((tag) => tag.startsWith("<link")),
|
|
2674
|
-
"</head>",
|
|
2675
|
-
"<body>",
|
|
2676
|
-
"<div id=\"graphiql\">Loading GraphiQL…</div>",
|
|
2677
|
-
...tags.filter((tag) => tag.startsWith("<script")),
|
|
2678
|
-
`<script>${inline}<\/script>`,
|
|
2679
|
-
"</body>",
|
|
2680
|
-
"</html>",
|
|
2681
|
-
""
|
|
2682
|
-
].join("\n"),
|
|
2683
|
-
csp: [
|
|
2684
|
-
"default-src 'none'",
|
|
2685
|
-
`script-src ${GRAPHIQL_CDN} ${hash}`,
|
|
2686
|
-
`style-src 'unsafe-inline' ${GRAPHIQL_CDN}`,
|
|
2687
|
-
`font-src data: ${GRAPHIQL_CDN}`,
|
|
2688
|
-
"img-src 'self' data:",
|
|
2689
|
-
"connect-src 'self'",
|
|
2690
|
-
"base-uri 'none'",
|
|
2691
|
-
"form-action 'none'",
|
|
2692
|
-
"frame-ancestors 'self'"
|
|
2693
|
-
].join("; ")
|
|
2694
|
-
};
|
|
2695
|
-
}
|
|
2696
|
-
// src/transport/sse.ts
|
|
2697
|
-
/** The last event of a stream that ended normally. */
|
|
2698
|
-
const COMPLETE = Object.freeze({
|
|
2699
|
-
event: "complete",
|
|
2700
|
-
data: ""
|
|
2701
|
-
});
|
|
2702
|
-
const DONE = Object.freeze({
|
|
2703
|
-
done: true,
|
|
2704
|
-
value: void 0
|
|
2705
|
-
});
|
|
2706
|
-
/**
|
|
2707
|
-
* The event source of an operation: one `next` event per result, then `complete` when the
|
|
2708
|
-
* operation ended by itself. A stream ended by `ctx.signal` (client gone, shutdown, `maxDuration`)
|
|
2709
|
-
* gets no `complete`: GraphQL-over-SSE clients then treat it as a lost connection and retry.
|
|
2710
|
-
*/
|
|
2711
|
-
var ResultEvents = class {
|
|
2712
|
-
#results;
|
|
2713
|
-
#signal;
|
|
2714
|
-
#done = false;
|
|
2715
|
-
constructor(results, signal) {
|
|
2716
|
-
this.#results = results;
|
|
2717
|
-
this.#signal = signal;
|
|
2718
|
-
}
|
|
2719
|
-
async next() {
|
|
2720
|
-
if (this.#done) return DONE;
|
|
2721
|
-
const step = await this.#results.next();
|
|
2722
|
-
if (step.done !== true) return {
|
|
2723
|
-
done: false,
|
|
2724
|
-
value: {
|
|
2725
|
-
event: "next",
|
|
2726
|
-
data: step.value
|
|
2727
|
-
}
|
|
2728
|
-
};
|
|
2729
|
-
this.#done = true;
|
|
2730
|
-
return this.#signal.aborted ? DONE : {
|
|
2731
|
-
done: false,
|
|
2732
|
-
value: COMPLETE
|
|
2733
|
-
};
|
|
2734
|
-
}
|
|
2735
|
-
async return() {
|
|
2736
|
-
this.#done = true;
|
|
2737
|
-
await this.#results.return();
|
|
2738
|
-
return DONE;
|
|
2739
|
-
}
|
|
2740
|
-
[Symbol.asyncIterator]() {
|
|
2741
|
-
return this;
|
|
2742
|
-
}
|
|
2743
|
-
};
|
|
2744
|
-
// src/transport/http.ts
|
|
2745
|
-
const JSON_TYPE = "application/json; charset=utf-8";
|
|
2746
|
-
const GRAPHQL_JSON_TYPE = "application/graphql-response+json; charset=utf-8";
|
|
2747
|
-
/** Response options of `application/graphql-response+json` replies, by status. */
|
|
2748
|
-
const GRAPHQL_JSON_200 = Object.freeze({
|
|
2749
|
-
status: 200,
|
|
2750
|
-
headers: Object.freeze({ "content-type": GRAPHQL_JSON_TYPE })
|
|
2751
|
-
});
|
|
2752
|
-
const GRAPHQL_JSON_400 = Object.freeze({
|
|
2753
|
-
status: 400,
|
|
2754
|
-
headers: Object.freeze({ "content-type": GRAPHQL_JSON_TYPE })
|
|
2755
|
-
});
|
|
2756
|
-
/** `application/json` is what the serialiser gives an object body: only the status is set. */
|
|
2757
|
-
const JSON_200 = Object.freeze({ status: 200 });
|
|
2758
|
-
const UTF8 = new TextDecoder("utf-8", { fatal: true });
|
|
2759
|
-
/** A request error before execution: `{ errors: [{ message, extensions: { code } }] }`. */
|
|
2760
|
-
function errorBody(message, code = GraphQLErrorCode.BadRequest) {
|
|
2761
|
-
return { errors: [{
|
|
2762
|
-
message,
|
|
2763
|
-
extensions: { code }
|
|
2764
|
-
}] };
|
|
2765
|
-
}
|
|
2766
|
-
/** A reply of `status` carrying a GraphQL error body in the negotiated JSON type (`application/json` before negotiation). */
|
|
2767
|
-
function errorReply(media, status, body, headers) {
|
|
2768
|
-
const type = media === Media.GraphQLJson ? GRAPHQL_JSON_TYPE : JSON_TYPE;
|
|
2769
|
-
return new HttpResponse(body, {
|
|
2770
|
-
status,
|
|
2771
|
-
headers: {
|
|
2772
|
-
...headers,
|
|
2773
|
-
"content-type": type
|
|
2774
|
-
}
|
|
2775
|
-
});
|
|
2776
|
-
}
|
|
2777
|
-
const NOT_ACCEPTABLE = errorReply(Media.Json, 406, errorBody("None of the accepted media types can be produced: accept application/graphql-response+json, application/json or text/event-stream"));
|
|
2778
|
-
const GET_NOT_ALLOWED = errorReply(Media.Json, 405, errorBody("GraphQL operations are sent with POST on this endpoint"), Object.freeze({ allow: "POST" }));
|
|
2779
|
-
/** `content-type` values of JSON bodies seen most often, compared before parsing. */
|
|
2780
|
-
function isJsonRequest(contentType) {
|
|
2781
|
-
if (contentType === "application/json" || contentType === "application/json; charset=utf-8") return true;
|
|
2782
|
-
const media = parseMediaType(contentType);
|
|
2783
|
-
if (media === void 0 || media.type !== "application/json") return false;
|
|
2784
|
-
return media.charset === void 0 || media.charset === "utf-8" || media.charset === "utf8";
|
|
2785
|
-
}
|
|
2786
|
-
/** Why the parameters of a request are invalid, `undefined` when they are valid. */
|
|
2787
|
-
function invalidParams(params) {
|
|
2788
|
-
const query = params["query"];
|
|
2789
|
-
if (typeof query !== "string") return query === void 0 || query === null ? "Missing query: send { query, variables?, operationName?, extensions? }" : "The query must be a string";
|
|
2790
|
-
const operationName = params["operationName"];
|
|
2791
|
-
if (operationName !== void 0 && operationName !== null && typeof operationName !== "string") return "operationName must be a string";
|
|
2792
|
-
if (!isMapOrNull(params["variables"])) return "variables must be an object";
|
|
2793
|
-
if (!isMapOrNull(params["extensions"])) return "extensions must be an object";
|
|
2794
|
-
}
|
|
2795
|
-
/** The parameters of a GraphQL `GET` request. */
|
|
2796
|
-
const GET_PARAMS = [
|
|
2797
|
-
"query",
|
|
2798
|
-
"operationName",
|
|
2799
|
-
"variables",
|
|
2800
|
-
"extensions"
|
|
2801
|
-
];
|
|
2802
|
-
/** A query string component decoded like `URLSearchParams` (`+` is a space); `undefined` when malformed. */
|
|
2803
|
-
function decodeComponent(text) {
|
|
2804
|
-
if (text.indexOf("%") === -1 && text.indexOf("+") === -1) return text;
|
|
2805
|
-
try {
|
|
2806
|
-
return decodeURIComponent(text.replaceAll("+", " "));
|
|
2807
|
-
} catch {
|
|
2808
|
-
return;
|
|
2809
|
-
}
|
|
2810
|
-
}
|
|
2811
|
-
/**
|
|
2812
|
-
* The GraphQL parameters of a request target, read directly: `ctx.query` builds a record of every
|
|
2813
|
-
* parameter through `URLSearchParams` (about 0.35 µs of a `GET` on Node.js, 0.15 µs on Bun, see
|
|
2814
|
-
* `test/bench/http.bench.ts`). A repeated parameter
|
|
2815
|
-
* becomes an array, like `ctx.query`. `undefined` when a component is malformed percent-encoding:
|
|
2816
|
-
* the caller falls back to `ctx.query` (which keeps such sequences as they are).
|
|
2817
|
-
*/
|
|
2818
|
-
function readGetParams(url) {
|
|
2819
|
-
const params = {};
|
|
2820
|
-
const mark = url.indexOf("?");
|
|
2821
|
-
if (mark === -1) return params;
|
|
2822
|
-
const length = url.length;
|
|
2823
|
-
let start = mark + 1;
|
|
2824
|
-
while (start < length) {
|
|
2825
|
-
let end = url.indexOf("&", start);
|
|
2826
|
-
if (end === -1) end = length;
|
|
2827
|
-
if (end > start) {
|
|
2828
|
-
let equals = url.indexOf("=", start);
|
|
2829
|
-
if (equals === -1 || equals > end) equals = end;
|
|
2830
|
-
const name = decodeComponent(url.slice(start, equals));
|
|
2831
|
-
if (name === void 0) return void 0;
|
|
2832
|
-
if (name === "query" || name === "operationName" || name === "variables" || name === "extensions") {
|
|
2833
|
-
const value = equals === end ? "" : decodeComponent(url.slice(equals + 1, end));
|
|
2834
|
-
if (value === void 0) return void 0;
|
|
2835
|
-
const existing = params[name];
|
|
2836
|
-
if (existing === void 0) params[name] = value;
|
|
2837
|
-
else params[name] = typeof existing === "string" ? [existing, value] : [...existing, value];
|
|
2838
|
-
}
|
|
2839
|
-
}
|
|
2840
|
-
start = end + 1;
|
|
2841
|
-
}
|
|
2842
|
-
return params;
|
|
2843
|
-
}
|
|
2844
|
-
function isMapOrNull(value) {
|
|
2845
|
-
return value === void 0 || value === null || typeof value === "object" && !Array.isArray(value);
|
|
2846
|
-
}
|
|
2847
|
-
function isPromise$1(value) {
|
|
2848
|
-
return typeof value?.then === "function";
|
|
2849
|
-
}
|
|
2850
|
-
/** The handlers of the GraphQL endpoint. */
|
|
2851
|
-
var HttpTransport = class {
|
|
2852
|
-
#resolveExecutor;
|
|
2853
|
-
#executor;
|
|
2854
|
-
#getQueries;
|
|
2855
|
-
#sse;
|
|
2856
|
-
#post;
|
|
2857
|
-
#get;
|
|
2858
|
-
/** The GraphiQL page, `undefined` when disabled. */
|
|
2859
|
-
#graphiql;
|
|
1336
|
+
// src/transport/builtin.ts
|
|
1337
|
+
/** The name of the built-in transport (`GraphQLOperationEvent.driver`), reserved. */
|
|
1338
|
+
const BUILTIN_DRIVER = "builtin";
|
|
1339
|
+
/** The built-in GraphQL-over-HTTP and SSE endpoint. */
|
|
1340
|
+
var BuiltinDriver = class {
|
|
1341
|
+
name = BUILTIN_DRIVER;
|
|
1342
|
+
#options;
|
|
2860
1343
|
constructor(options) {
|
|
2861
|
-
this.#
|
|
2862
|
-
this.#getQueries = options.get;
|
|
2863
|
-
this.#sse = options.sse;
|
|
2864
|
-
const json = 1 << Media.GraphQLJson | 1 << Media.Json | (options.sse === false ? 0 : 1 << Media.EventStream);
|
|
2865
|
-
this.#post = new Negotiator(json);
|
|
2866
|
-
this.#get = new Negotiator(options.graphiql ? json | 1 << Media.Html : json);
|
|
2867
|
-
if (options.graphiql) {
|
|
2868
|
-
const page = renderGraphiQL(options.endpoint);
|
|
2869
|
-
this.#graphiql = new HttpResponse(new TextEncoder().encode(page.html), {
|
|
2870
|
-
status: 200,
|
|
2871
|
-
headers: {
|
|
2872
|
-
"content-type": "text/html; charset=utf-8",
|
|
2873
|
-
"content-security-policy": page.csp,
|
|
2874
|
-
"cache-control": "no-store"
|
|
2875
|
-
}
|
|
2876
|
-
});
|
|
2877
|
-
}
|
|
2878
|
-
}
|
|
2879
|
-
/** `POST path`. */
|
|
2880
|
-
post = (ctx) => {
|
|
2881
|
-
const headers = ctx.headers;
|
|
2882
|
-
const negotiated = this.#post.choose(headers["accept"]);
|
|
2883
|
-
const media = negotiated & 7;
|
|
2884
|
-
if (media === Media.None) return NOT_ACCEPTABLE;
|
|
2885
|
-
if (!isJsonRequest(headers["content-type"])) return errorReply(media, 415, errorBody("Send GraphQL requests as application/json (content-type: application/json)"));
|
|
2886
|
-
const encoding = headers["content-encoding"];
|
|
2887
|
-
if (encoding !== void 0 && encoding.trim().toLowerCase() !== "identity" && encoding.trim() !== "") return errorReply(media, 415, errorBody(`Unsupported content encoding "${encoding}"`));
|
|
2888
|
-
return ctx.rawBody().then((bytes) => this.#posted(ctx, bytes, media, negotiated >= 8), (error) => transportError(error, media));
|
|
2889
|
-
};
|
|
2890
|
-
/** `GET path`. */
|
|
2891
|
-
get = (ctx) => {
|
|
2892
|
-
const media = this.#get.choose(ctx.headers["accept"]) & 7;
|
|
2893
|
-
if (media === Media.Html) return this.#graphiql;
|
|
2894
|
-
if (media === Media.None) return NOT_ACCEPTABLE;
|
|
2895
|
-
if (!this.#getQueries) return GET_NOT_ALLOWED;
|
|
2896
|
-
const params = readGetParams(ctx.url) ?? ctx.query;
|
|
2897
|
-
const request = {};
|
|
2898
|
-
for (const name of GET_PARAMS) {
|
|
2899
|
-
const value = params[name];
|
|
2900
|
-
if (value === void 0) continue;
|
|
2901
|
-
if (typeof value !== "string") return errorReply(media, 400, errorBody(`Repeated ${name} parameter`));
|
|
2902
|
-
if (name === "variables" || name === "extensions") try {
|
|
2903
|
-
request[name] = JSON.parse(value);
|
|
2904
|
-
} catch {
|
|
2905
|
-
return errorReply(media, 400, errorBody(`The ${name} parameter must be JSON`));
|
|
2906
|
-
}
|
|
2907
|
-
else request[name] = value;
|
|
2908
|
-
}
|
|
2909
|
-
const invalid = invalidParams(request);
|
|
2910
|
-
if (invalid !== void 0) return errorReply(media, 400, errorBody(invalid));
|
|
2911
|
-
if (media === Media.EventStream) return this.#stream(ctx, request, true);
|
|
2912
|
-
return this.#execute(ctx, request, media, true, false);
|
|
2913
|
-
};
|
|
2914
|
-
/** A `POST` body read: parsed, checked, executed or streamed. */
|
|
2915
|
-
#posted(ctx, bytes, media, streamable) {
|
|
2916
|
-
if (bytes.byteLength === 0) return errorReply(media, 400, errorBody("Missing request body: send { query, variables?, operationName? }"));
|
|
2917
|
-
let body;
|
|
2918
|
-
try {
|
|
2919
|
-
body = JSON.parse(UTF8.decode(bytes));
|
|
2920
|
-
} catch {
|
|
2921
|
-
return errorReply(media, 400, errorBody("Malformed JSON body"));
|
|
2922
|
-
}
|
|
2923
|
-
if (body === null || typeof body !== "object") return errorReply(media, 400, errorBody("The body must be a JSON object: { query, variables?, operationName? }"));
|
|
2924
|
-
if (Array.isArray(body)) return errorReply(media, 400, errorBody("Operation batching is not supported: send one operation per request"));
|
|
2925
|
-
const invalid = invalidParams(body);
|
|
2926
|
-
if (invalid !== void 0) return errorReply(media, 400, errorBody(invalid));
|
|
2927
|
-
if (media === Media.EventStream) return this.#stream(ctx, body, false);
|
|
2928
|
-
return this.#execute(ctx, body, media, false, streamable);
|
|
1344
|
+
this.#options = options;
|
|
2929
1345
|
}
|
|
2930
|
-
/**
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
http: ctx,
|
|
2940
|
-
abortSignal: abort.asSignal(),
|
|
2941
|
-
queriesOnly: get,
|
|
2942
|
-
streamSubscriptions: streamable
|
|
2943
|
-
};
|
|
2944
|
-
const response = executor.execute(request, options);
|
|
2945
|
-
if (response === SUBSCRIBE) return this.#stream(ctx, request, get);
|
|
2946
|
-
if (!isPromise$1(response)) return reply(response, media, get);
|
|
2947
|
-
abort.follow(() => ctx.signal);
|
|
2948
|
-
return response.then((settled) => reply(settled, media, get), (error) => {
|
|
2949
|
-
if (!abort.aborted || error !== abort.reason) throw error;
|
|
2950
|
-
return errorReply(media, 503, errorBody("The request was aborted", GraphQLErrorCode.ServiceUnavailable));
|
|
1346
|
+
/** `POST` on the path, and `GET` when queries over `GET` or GraphiQL are on (the router answers 405 otherwise). */
|
|
1347
|
+
routes(context) {
|
|
1348
|
+
const options = this.#options;
|
|
1349
|
+
const transport = new HttpTransport({
|
|
1350
|
+
executor: options.executor,
|
|
1351
|
+
endpoint: context.endpoint,
|
|
1352
|
+
get: options.get,
|
|
1353
|
+
graphiql: options.graphiql,
|
|
1354
|
+
sse: options.sse
|
|
2951
1355
|
});
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
return executor.subscribe(request, options).then((outcome) => {
|
|
2964
|
-
if ("status" in outcome) return errorReply(Media.Json, outcome.status, outcome.result, outcome.headers);
|
|
2965
|
-
return HttpResponse.sse(new ResultEvents(outcome, signal), this.#sse);
|
|
2966
|
-
}, (error) => {
|
|
2967
|
-
if (!signal.aborted || error !== signal.reason) throw error;
|
|
2968
|
-
return errorReply(Media.Json, 503, errorBody("The request was aborted", GraphQLErrorCode.ServiceUnavailable));
|
|
1356
|
+
const routes = [{
|
|
1357
|
+
method: "POST",
|
|
1358
|
+
path: context.path,
|
|
1359
|
+
name: "handle",
|
|
1360
|
+
handler: transport.post
|
|
1361
|
+
}];
|
|
1362
|
+
if (options.get || options.graphiql) routes.unshift({
|
|
1363
|
+
method: "GET",
|
|
1364
|
+
path: context.path,
|
|
1365
|
+
name: "handle",
|
|
1366
|
+
handler: transport.get
|
|
2969
1367
|
});
|
|
1368
|
+
return routes;
|
|
2970
1369
|
}
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
return this.#executor;
|
|
2974
|
-
}
|
|
1370
|
+
/** Nothing to start: the handlers read the executor on the first request. */
|
|
1371
|
+
start() {}
|
|
2975
1372
|
};
|
|
2976
|
-
/**
|
|
2977
|
-
* The HTTP reply of an executor response. `application/json` answers 200 to every well-formed
|
|
2978
|
-
* request (request errors included); `application/graphql-response+json` keeps the status.
|
|
2979
|
-
*/
|
|
2980
|
-
function reply(response, media, get) {
|
|
2981
|
-
const status = response.status;
|
|
2982
|
-
const result = response.result;
|
|
2983
|
-
if (media === Media.Json) {
|
|
2984
|
-
if (status === 200 || status === 400) return get ? result : new HttpResponse(result, JSON_200);
|
|
2985
|
-
return errorReply(media, status, result, response.headers);
|
|
2986
|
-
}
|
|
2987
|
-
if (response.headers === void 0) {
|
|
2988
|
-
if (status === 200) return new HttpResponse(result, GRAPHQL_JSON_200);
|
|
2989
|
-
if (status === 400) return new HttpResponse(result, GRAPHQL_JSON_400);
|
|
2990
|
-
}
|
|
2991
|
-
return errorReply(media, status, result, response.headers);
|
|
2992
|
-
}
|
|
2993
|
-
/** A failure reading the body (413 past the body limit, 400 for an aborted body) with a GraphQL error body. */
|
|
2994
|
-
function transportError(error, media) {
|
|
2995
|
-
if (!isHttpException(error)) throw error;
|
|
2996
|
-
return errorReply(media, error.status, errorBody(error.message));
|
|
2997
|
-
}
|
|
2998
|
-
/** Checks the SSE options once (`HttpResponse.sse()` validates them): `undefined` when they are valid. */
|
|
2999
|
-
function checkSseOptions(options) {
|
|
3000
|
-
try {
|
|
3001
|
-
HttpResponse.sse([], options);
|
|
3002
|
-
return;
|
|
3003
|
-
} catch (error) {
|
|
3004
|
-
return error instanceof Error ? error.message : String(error);
|
|
3005
|
-
}
|
|
3006
|
-
}
|
|
3007
1373
|
// src/plugin.ts
|
|
3008
1374
|
let readExecutor;
|
|
3009
1375
|
let readEndpoint;
|
|
@@ -3045,6 +1411,10 @@ var GraphQLPlugin = class GraphQLPlugin {
|
|
|
3045
1411
|
#runtime;
|
|
3046
1412
|
#stop;
|
|
3047
1413
|
#endpoint;
|
|
1414
|
+
/** The driver serving the schema: the `driver` option, or the built-in transport. */
|
|
1415
|
+
#driver;
|
|
1416
|
+
/** The driver was started (`onBootstrap`): `onStop` stops it. */
|
|
1417
|
+
#started = false;
|
|
3048
1418
|
#active = false;
|
|
3049
1419
|
constructor(options) {
|
|
3050
1420
|
this.#options = options;
|
|
@@ -3097,6 +1467,8 @@ var GraphQLPlugin = class GraphQLPlugin {
|
|
|
3097
1467
|
this.#runtime = void 0;
|
|
3098
1468
|
this.#ownPubSub = void 0;
|
|
3099
1469
|
this.#endpoint = void 0;
|
|
1470
|
+
this.#driver = void 0;
|
|
1471
|
+
this.#started = false;
|
|
3100
1472
|
this.#container = ctx.container;
|
|
3101
1473
|
this.#metadata = ctx.metadata;
|
|
3102
1474
|
this.#logger = ctx.logger;
|
|
@@ -3113,48 +1485,54 @@ var GraphQLPlugin = class GraphQLPlugin {
|
|
|
3113
1485
|
ctx.container.registerValue(GraphQLTokens.Plugin, this);
|
|
3114
1486
|
}
|
|
3115
1487
|
/**
|
|
3116
|
-
* The `PluginHttpRoutes` contract of `@znight/http`: the
|
|
3117
|
-
*
|
|
1488
|
+
* The `PluginHttpRoutes` contract of `@znight/http`: the routes of the driver, with the `http`
|
|
1489
|
+
* option's meta and guards, and its body limit on the routes that are not `GET`. For the built-in
|
|
1490
|
+
* transport, the GraphQL endpoint (`GET` and `POST` on `path`). `[]` with `http: false`.
|
|
3118
1491
|
*/
|
|
3119
1492
|
httpRoutes(context) {
|
|
3120
1493
|
this.#container ??= context.container;
|
|
3121
1494
|
const options = this.#resolve();
|
|
3122
1495
|
const http = options.http;
|
|
3123
1496
|
if (http === false) return [];
|
|
1497
|
+
const driver = this.#driverOf(options);
|
|
1498
|
+
if (driver.routes === void 0) return [];
|
|
3124
1499
|
const endpoint = endpointPath(context.prefix, options.path);
|
|
3125
|
-
const
|
|
3126
|
-
executor: () => this.#ready().executor,
|
|
3127
|
-
endpoint,
|
|
3128
|
-
get: http.get,
|
|
3129
|
-
graphiql: options.graphiql,
|
|
3130
|
-
sse: sseOptions(options)
|
|
3131
|
-
});
|
|
3132
|
-
this.#endpoint = endpoint;
|
|
3133
|
-
const common = {
|
|
3134
|
-
name: "handle",
|
|
1500
|
+
const routes = driver.routes({
|
|
3135
1501
|
path: options.path,
|
|
3136
|
-
|
|
3137
|
-
...http.guards.length === 0 ? {} : { guards: http.guards }
|
|
3138
|
-
};
|
|
3139
|
-
const routes = [{
|
|
3140
|
-
...common,
|
|
3141
|
-
method: "POST",
|
|
3142
|
-
handler: transport.post,
|
|
3143
|
-
bodyLimit: http.bodyLimit
|
|
3144
|
-
}];
|
|
3145
|
-
if (http.get || options.graphiql) routes.unshift({
|
|
3146
|
-
...common,
|
|
3147
|
-
method: "GET",
|
|
3148
|
-
handler: transport.get
|
|
1502
|
+
endpoint
|
|
3149
1503
|
});
|
|
3150
|
-
|
|
1504
|
+
if (!Array.isArray(routes)) throw invalidOption(`routes() of the ${driver.name} driver`, "an array of routes", routes);
|
|
1505
|
+
this.#endpoint = endpoint;
|
|
1506
|
+
return routes.map((route) => mount(route, http, driver));
|
|
3151
1507
|
}
|
|
3152
1508
|
onBootstrap(ctx) {
|
|
3153
1509
|
this.#container = ctx.container;
|
|
3154
1510
|
const options = this.#resolve();
|
|
3155
1511
|
if (typeof this.#options === "function") this.#checkHttp(ctx, options);
|
|
3156
|
-
this.#checkSse(options);
|
|
3157
|
-
|
|
1512
|
+
if (options.driver === void 0) this.#checkSse(options);
|
|
1513
|
+
const runtime = this.#build(ctx, options);
|
|
1514
|
+
this.#runtime = runtime;
|
|
1515
|
+
const driver = this.#driverOf(options);
|
|
1516
|
+
if (options.driver === void 0) return;
|
|
1517
|
+
let started;
|
|
1518
|
+
try {
|
|
1519
|
+
started = driver.start(runtime.engine, {
|
|
1520
|
+
logger: ctx.logger,
|
|
1521
|
+
container: ctx.container,
|
|
1522
|
+
http: ctx.getPlugin("http")
|
|
1523
|
+
});
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
throw driverFailed(driver, error);
|
|
1526
|
+
}
|
|
1527
|
+
if (!isPromise(started)) {
|
|
1528
|
+
this.#started = true;
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
return started.then(() => {
|
|
1532
|
+
this.#started = true;
|
|
1533
|
+
}, (error) => {
|
|
1534
|
+
throw driverFailed(driver, error);
|
|
1535
|
+
});
|
|
3158
1536
|
}
|
|
3159
1537
|
onStart(ctx) {
|
|
3160
1538
|
const runtime = this.#runtime;
|
|
@@ -3170,11 +1548,36 @@ var GraphQLPlugin = class GraphQLPlugin {
|
|
|
3170
1548
|
...details
|
|
3171
1549
|
});
|
|
3172
1550
|
}
|
|
1551
|
+
/**
|
|
1552
|
+
* The `PluginHttpRoutes` contract: `HttpPlugin` starts draining, whatever the plugin order. The
|
|
1553
|
+
* subscriptions of the engine end (a route-mounted driver serves them as streamed responses
|
|
1554
|
+
* `HttpPlugin` does not close), and new ones are refused (503); queries and mutations in flight
|
|
1555
|
+
* are served. Built-in SSE streams are closed by `HttpPlugin` itself.
|
|
1556
|
+
*/
|
|
1557
|
+
onHttpDrain() {
|
|
1558
|
+
return this.#runtime?.executor.stopEngineStreams();
|
|
1559
|
+
}
|
|
3173
1560
|
async onStop() {
|
|
3174
1561
|
this.#stop?.abort(/* @__PURE__ */ new Error("The application is stopping"));
|
|
3175
|
-
await this.#runtime?.executor.
|
|
1562
|
+
await this.#runtime?.executor.stopStreams();
|
|
1563
|
+
if (this.#driver?.routes === void 0) await this.#stopDriver();
|
|
3176
1564
|
}
|
|
3177
1565
|
onDispose() {
|
|
1566
|
+
if (!this.#started) {
|
|
1567
|
+
this.#release();
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
this.#stop?.abort(/* @__PURE__ */ new Error("The application is stopping"));
|
|
1571
|
+
return Promise.resolve(this.#runtime?.executor.stopStreams()).then(() => this.#stopDriver()).finally(() => this.#release());
|
|
1572
|
+
}
|
|
1573
|
+
/** Stops the driver started in onBootstrap (once). */
|
|
1574
|
+
#stopDriver() {
|
|
1575
|
+
const driver = this.#driver;
|
|
1576
|
+
if (!this.#started) return;
|
|
1577
|
+
this.#started = false;
|
|
1578
|
+
return driver?.stop?.();
|
|
1579
|
+
}
|
|
1580
|
+
#release() {
|
|
3178
1581
|
this.#active = false;
|
|
3179
1582
|
this.#ownPubSub?.close();
|
|
3180
1583
|
this.#ownPubSub = void 0;
|
|
@@ -3187,6 +1590,14 @@ var GraphQLPlugin = class GraphQLPlugin {
|
|
|
3187
1590
|
get schema() {
|
|
3188
1591
|
return this.#ready().schema;
|
|
3189
1592
|
}
|
|
1593
|
+
/**
|
|
1594
|
+
* The GraphQL engine (after bootstrap, RFC 0009): graphql-js-shaped `parse`, `validate`, `execute`
|
|
1595
|
+
* and `subscribe` running this plugin's schema with its guards, loaders, request scope, error
|
|
1596
|
+
* formatting and hook. What drivers receive in `start()`; any GraphQL server can call it.
|
|
1597
|
+
*/
|
|
1598
|
+
get engine() {
|
|
1599
|
+
return this.#ready().engine;
|
|
1600
|
+
}
|
|
3190
1601
|
/** The SDL of the schema (graphql-js `printSchema`). */
|
|
3191
1602
|
printSchema() {
|
|
3192
1603
|
return printSchema(this.#ready().schema);
|
|
@@ -3239,6 +1650,21 @@ var GraphQLPlugin = class GraphQLPlugin {
|
|
|
3239
1650
|
this.#resolved = resolveGraphQLOptions(resolveOptions(this.#options, container));
|
|
3240
1651
|
return this.#resolved;
|
|
3241
1652
|
}
|
|
1653
|
+
/** The driver of the application: the `driver` option, or the built-in transport. */
|
|
1654
|
+
#driverOf(options) {
|
|
1655
|
+
let driver = this.#driver;
|
|
1656
|
+
if (driver === void 0) {
|
|
1657
|
+
const http = options.http;
|
|
1658
|
+
driver = options.driver ?? new BuiltinDriver({
|
|
1659
|
+
executor: () => this.#ready().executor,
|
|
1660
|
+
get: http !== false && http.get,
|
|
1661
|
+
graphiql: options.graphiql,
|
|
1662
|
+
sse: sseOptions(options)
|
|
1663
|
+
});
|
|
1664
|
+
this.#driver = driver;
|
|
1665
|
+
}
|
|
1666
|
+
return driver;
|
|
1667
|
+
}
|
|
3242
1668
|
/** The SSE options are those of `HttpResponse.sse()` (heartbeat and maxDuration from 1 000 ms): checked at boot. */
|
|
3243
1669
|
#checkSse(options) {
|
|
3244
1670
|
const sse = sseOptions(options);
|
|
@@ -3247,6 +1673,8 @@ var GraphQLPlugin = class GraphQLPlugin {
|
|
|
3247
1673
|
if (problem !== void 0) throw invalidOption("subscriptions", `valid event stream options (${problem})`, sse);
|
|
3248
1674
|
}
|
|
3249
1675
|
#checkHttp(ctx, options) {
|
|
1676
|
+
const driver = options.driver;
|
|
1677
|
+
if (options.http === false && driver?.routes !== void 0) throw new ZnightError(ErrorCode.InvalidGraphQLSetup, `GraphQLPlugin: the ${driver.name} driver serves routes, which http: false turns off`, { hints: ["Remove http: false: the driver serves the endpoint through HttpPlugin"] });
|
|
3250
1678
|
if (options.http !== false && ctx.getPlugin("http") === void 0) throw new ZnightError(ErrorCode.InvalidGraphQLSetup, "GraphQLPlugin serves its endpoint through HttpPlugin, which is not registered", { hints: ["Add HttpPlugin.configure() to the plugins of the application", "Or set http: false to use the schema and execute() only"] });
|
|
3251
1679
|
}
|
|
3252
1680
|
#build(ctx, options) {
|
|
@@ -3296,6 +1724,7 @@ var GraphQLPlugin = class GraphQLPlugin {
|
|
|
3296
1724
|
globalGuards: globalGuards.map((guard) => guard.resolvable),
|
|
3297
1725
|
guard: (token, coordinate) => providers.token(token, `guard of ${coordinate}`, "canResolve"),
|
|
3298
1726
|
resolver: (binding, coordinate) => providers.token(binding.token, `resolver of ${coordinate}`, binding.method),
|
|
1727
|
+
paramResolver: (token, coordinate) => providers.token(token, `parameter resolver of ${coordinate}`, "resolve"),
|
|
3299
1728
|
inputs,
|
|
3300
1729
|
results: (field, coordinate) => results.recover(field, coordinate)
|
|
3301
1730
|
}));
|
|
@@ -3321,8 +1750,22 @@ var GraphQLPlugin = class GraphQLPlugin {
|
|
|
3321
1750
|
limits: options.limits,
|
|
3322
1751
|
introspection: options.introspection,
|
|
3323
1752
|
documentCache: options.documentCache,
|
|
3324
|
-
maxActiveSubscriptions: options.subscriptions === false ? void 0 : options.subscriptions.maxActive
|
|
1753
|
+
maxActiveSubscriptions: options.subscriptions === false ? void 0 : options.subscriptions.maxActive,
|
|
1754
|
+
subscriptions: options.subscriptions !== false
|
|
3325
1755
|
});
|
|
1756
|
+
const http = ctx.getPlugin("http");
|
|
1757
|
+
const engine = new Engine(executor, {
|
|
1758
|
+
path: this.#endpoint ?? endpointPath(options.http === false || http === void 0 ? "" : http.prefix, options.path),
|
|
1759
|
+
introspection: options.introspection,
|
|
1760
|
+
ide: options.graphiql,
|
|
1761
|
+
exposeErrors: options.exposeErrors,
|
|
1762
|
+
maxTokens: options.limits.maxTokens,
|
|
1763
|
+
subscriptions: options.subscriptions === false ? false : { maxActive: options.subscriptions.maxActive },
|
|
1764
|
+
http: options.http === false ? false : {
|
|
1765
|
+
get: options.http.get,
|
|
1766
|
+
bodyLimit: options.http.bodyLimit
|
|
1767
|
+
}
|
|
1768
|
+
}, options.driver?.name ?? "builtin");
|
|
3326
1769
|
ctx.logger.debug("GraphQL schema built", {
|
|
3327
1770
|
types: types.length,
|
|
3328
1771
|
schemaHash: section.schemaHash,
|
|
@@ -3331,7 +1774,8 @@ var GraphQLPlugin = class GraphQLPlugin {
|
|
|
3331
1774
|
return {
|
|
3332
1775
|
schema,
|
|
3333
1776
|
section,
|
|
3334
|
-
executor
|
|
1777
|
+
executor,
|
|
1778
|
+
engine
|
|
3335
1779
|
};
|
|
3336
1780
|
}
|
|
3337
1781
|
};
|
|
@@ -3344,6 +1788,26 @@ function direct(options) {
|
|
|
3344
1788
|
};
|
|
3345
1789
|
}
|
|
3346
1790
|
const DIRECT = Object.freeze({ transport: "direct" });
|
|
1791
|
+
/** A route of the driver as a plugin route: the `http` option's meta and guards, its body limit unless `GET`. */
|
|
1792
|
+
function mount(route, http, driver) {
|
|
1793
|
+
if (route === null || typeof route !== "object" || typeof route.handler !== "function") throw invalidOption(`routes() of the ${driver.name} driver`, "routes { method, path, name, handler }", route);
|
|
1794
|
+
return {
|
|
1795
|
+
method: route.method,
|
|
1796
|
+
path: route.path,
|
|
1797
|
+
name: route.name,
|
|
1798
|
+
handler: route.handler,
|
|
1799
|
+
...http.meta.length === 0 ? {} : { meta: http.meta },
|
|
1800
|
+
...http.guards.length === 0 ? {} : { guards: http.guards },
|
|
1801
|
+
...route.method === "GET" || route.method === "HEAD" ? {} : { bodyLimit: http.bodyLimit }
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
/** ZNIGHT066 for a driver whose `start()` failed, the original error as `cause`. */
|
|
1805
|
+
function driverFailed(driver, error) {
|
|
1806
|
+
return new ZnightError(ErrorCode.InvalidGraphQLSetup, `GraphQLPlugin: the ${driver.name} driver failed to start`, {
|
|
1807
|
+
cause: error,
|
|
1808
|
+
hints: ["See the cause: the driver could not create its server"]
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
3347
1811
|
/** `prefix` (`''`, `/api`) joined with `path` (`/graphql`), without trailing slash: `/api/graphql`. */
|
|
3348
1812
|
function endpointPath(prefix, path) {
|
|
3349
1813
|
const joined = `${prefix.replace(/\/+$/, "")}/${path.replace(/^\/+|\/+$/g, "")}`;
|
|
@@ -3440,6 +1904,6 @@ var DeferredPubSub = class extends PubSub {
|
|
|
3440
1904
|
return this.#target().subscribe(topic, options);
|
|
3441
1905
|
}
|
|
3442
1906
|
};
|
|
3443
|
-
export {
|
|
1907
|
+
export { PubSub as a, MemoryPubSub as i, endpointOf as n, orderByKeys as o, executorOf as r, GraphQLPlugin as t };
|
|
3444
1908
|
|
|
3445
|
-
//# sourceMappingURL=plugin-
|
|
1909
|
+
//# sourceMappingURL=plugin-BssB7wtn.mjs.map
|