@znight/graphql 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1063 -0
  3. package/dist/compiler.d.mts +93 -0
  4. package/dist/compiler.mjs +2832 -0
  5. package/dist/compiler.mjs.map +1 -0
  6. package/dist/index.d.mts +542 -0
  7. package/dist/index.mjs +128 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/dist/names-BlzDM5cF.mjs +111 -0
  10. package/dist/names-BlzDM5cF.mjs.map +1 -0
  11. package/dist/plugin-BdBxr5NQ.mjs +3222 -0
  12. package/dist/plugin-BdBxr5NQ.mjs.map +1 -0
  13. package/dist/testing.d.mts +101 -0
  14. package/dist/testing.mjs +306 -0
  15. package/dist/testing.mjs.map +1 -0
  16. package/dist/types-CgbV9LjD.d.mts +234 -0
  17. package/package.json +97 -1
  18. package/src/compiler/analyze.ts +608 -0
  19. package/src/compiler/builder.ts +1640 -0
  20. package/src/compiler/constants.ts +74 -0
  21. package/src/compiler/data.ts +73 -0
  22. package/src/compiler/defaults.ts +92 -0
  23. package/src/compiler/discriminators.ts +53 -0
  24. package/src/compiler/docs.ts +55 -0
  25. package/src/compiler/generate.ts +405 -0
  26. package/src/compiler/ids.ts +128 -0
  27. package/src/compiler/index.ts +93 -0
  28. package/src/compiler/locate.ts +44 -0
  29. package/src/compiler/model.ts +122 -0
  30. package/src/compiler/options.ts +54 -0
  31. package/src/compiler/scalars.ts +138 -0
  32. package/src/compiler/sdl-view.ts +59 -0
  33. package/src/context.ts +181 -0
  34. package/src/decisions.ts +169 -0
  35. package/src/decorators.ts +196 -0
  36. package/src/errors.ts +89 -0
  37. package/src/execution.ts +1064 -0
  38. package/src/format.ts +285 -0
  39. package/src/index.ts +84 -0
  40. package/src/limits.ts +177 -0
  41. package/src/loaders.ts +149 -0
  42. package/src/memory-pubsub.ts +133 -0
  43. package/src/metadata.ts +195 -0
  44. package/src/names.ts +47 -0
  45. package/src/options.ts +309 -0
  46. package/src/pipeline.ts +277 -0
  47. package/src/plugin.ts +557 -0
  48. package/src/pubsub.ts +36 -0
  49. package/src/scalars.ts +131 -0
  50. package/src/schema-hash.ts +18 -0
  51. package/src/schema.ts +557 -0
  52. package/src/sdl.ts +515 -0
  53. package/src/testing.ts +454 -0
  54. package/src/tokens.ts +22 -0
  55. package/src/transport/abort.ts +80 -0
  56. package/src/transport/accept.ts +155 -0
  57. package/src/transport/graphiql.ts +106 -0
  58. package/src/transport/http.ts +373 -0
  59. package/src/transport/sse.ts +45 -0
  60. package/src/type-ref.ts +89 -0
  61. package/src/types.ts +190 -0
  62. package/src/validate-args.ts +48 -0
@@ -0,0 +1,3222 @@
1
+ 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
+ import { ErrorCode, ZnightError, displayName, resolveOptions } from "@znight/core";
3
+ import DataLoader from "dataloader";
4
+ import { EventChannel, HttpResponse, HttpTokens, ValidationException, isHttpException, isThenable, parseByteSize, parseMediaType } from "@znight/http";
5
+ import { AbortedGraphQLExecutionError, GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLID, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, GraphQLUnionType, Kind, NoSchemaIntrospectionCustomRule, createSourceEventStream, execute, executeSubscriptionEvent, getNamedType, isObjectType, parse, printSchema, specifiedRules, validate, validateSchema, validateSubscriptionArgs, valueFromASTUntyped } from "graphql";
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
+ };
51
+ // src/loaders.ts
52
+ /**
53
+ * Creates the per-execution DataLoaders of loader providers: the provider is resolved in the
54
+ * current scope (loaders may be request-scoped), its `options` are read once per token.
55
+ */
56
+ var LoaderFactory = class {
57
+ #container;
58
+ #options = /* @__PURE__ */ new Map();
59
+ constructor(container) {
60
+ this.#container = container;
61
+ }
62
+ /** A new DataLoader for the loader provider `token`, bound to the instance of the current scope. */
63
+ create(token) {
64
+ const provider = this.#container.get(token);
65
+ if (provider === null || typeof provider !== "object" || typeof provider.load !== "function") throw new ZnightError(ErrorCode.InvalidLoader, `The loader ${token} does not implement BatchLoad (no load(keys) method)`, { hints: ["A loader is a provider implementing BatchLoad<K, V>: async load(keys) { ... }"] });
66
+ let options = this.#options.get(token);
67
+ if (options === void 0) {
68
+ options = readOptions(token, provider.options);
69
+ this.#options.set(token, options);
70
+ }
71
+ return new DataLoader((keys) => loadBatch(token, keys, provider), options);
72
+ }
73
+ };
74
+ /**
75
+ * The values of a batch, or ZNIGHT065 (failing every key) when there is not one value per key.
76
+ * `async` so that a `load()` throwing synchronously rejects with its own error (DataLoader would
77
+ * replace it with a TypeError).
78
+ */
79
+ async function loadBatch(token, keys, provider) {
80
+ const values = await provider.load(keys);
81
+ if (Array.isArray(values) && values.length === keys.length) return values;
82
+ throw new ZnightError(ErrorCode.InvalidLoader, `The loader ${token} returned ${Array.isArray(values) ? `${values.length} values` : "no array"} for ${keys.length} keys`, { hints: ["load(keys) returns one value (or Error) per key, in the order of the keys: use orderByKeys()"] });
83
+ }
84
+ function readOptions(token, options) {
85
+ if (options === void 0) return {};
86
+ const invalid = (reason) => new ZnightError(ErrorCode.InvalidLoader, `Invalid options of the loader ${token}: ${reason}`, { hints: ["options: { maxBatchSize?: positive integer, cache?: boolean, cacheKeyFn?: (key) => unknown }"] });
87
+ if (options === null || typeof options !== "object") throw invalid("expected an object");
88
+ const { maxBatchSize, cache, cacheKeyFn } = options;
89
+ if (maxBatchSize !== void 0 && !(Number.isSafeInteger(maxBatchSize) && maxBatchSize >= 1)) throw invalid(`maxBatchSize must be a positive integer (received ${String(maxBatchSize)})`);
90
+ if (cache !== void 0 && typeof cache !== "boolean") throw invalid("cache must be a boolean");
91
+ if (cacheKeyFn !== void 0 && typeof cacheKeyFn !== "function") throw invalid("cacheKeyFn must be a function");
92
+ return {
93
+ ...maxBatchSize === void 0 ? {} : { maxBatchSize },
94
+ ...cache === void 0 ? {} : { cache },
95
+ ...cacheKeyFn === void 0 ? {} : { cacheKeyFn: (key) => cacheKeyFn.call(options, key) }
96
+ };
97
+ }
98
+ function orderByKeys(keys, values, keyOf, options = {}) {
99
+ const missing = options.missing ?? "error";
100
+ if (missing !== "error" && missing !== "null") throw new TypeError(`orderByKeys(): missing must be 'error' or 'null' (received ${String(missing)})`);
101
+ const byKey = /* @__PURE__ */ new Map();
102
+ for (const value of values) byKey.set(keyOf(value), value);
103
+ const result = new Array(keys.length);
104
+ for (let index = 0; index < keys.length; index++) {
105
+ const key = keys[index];
106
+ const value = byKey.get(key);
107
+ if (value !== void 0 || byKey.has(key)) result[index] = value;
108
+ else result[index] = missing === "null" ? null : notFound(key);
109
+ }
110
+ return result;
111
+ }
112
+ function notFound(key) {
113
+ return new GraphQLException(`Not found: ${typeof key === "string" ? key : String(key)}`, { code: GraphQLErrorCode.NotFound });
114
+ }
115
+ // src/pubsub.ts
116
+ /**
117
+ * Publishes events to the subscribers of a topic. `Events` maps each topic to its payload type.
118
+ *
119
+ * ```ts
120
+ * interface CatalogEvents {
121
+ * bookAdded: Book
122
+ * }
123
+ *
124
+ * constructor(private readonly events: PubSub<CatalogEvents>) {}
125
+ *
126
+ * await this.events.publish('bookAdded', book)
127
+ * for await (const book of this.events.subscribe('bookAdded', { signal: ctx.signal })) yield book
128
+ * ```
129
+ */
130
+ var PubSub = class {};
131
+ // src/memory-pubsub.ts
132
+ const DEFAULT_BUFFER_SIZE = 100;
133
+ const PUBLISHED = Promise.resolve();
134
+ /** One subscriber: its channel, and whether it overflowed. */
135
+ var Subscriber = class {
136
+ channel;
137
+ #iterator;
138
+ overflowed = false;
139
+ constructor(channel) {
140
+ this.channel = channel;
141
+ this.#iterator = channel[Symbol.asyncIterator]();
142
+ }
143
+ async next() {
144
+ const step = await this.#iterator.next();
145
+ if (step.done === true && this.overflowed) {
146
+ this.overflowed = false;
147
+ throw new GraphQLException("The subscriber fell behind the events: the subscription ended", { code: GraphQLErrorCode.SubscriptionOverflow });
148
+ }
149
+ return step;
150
+ }
151
+ return() {
152
+ this.overflowed = false;
153
+ return this.#iterator.return?.() ?? Promise.resolve({
154
+ done: true,
155
+ value: void 0
156
+ });
157
+ }
158
+ [Symbol.asyncIterator]() {
159
+ return this;
160
+ }
161
+ };
162
+ /**
163
+ * In-process fan-out of events to subscribers: what `GraphQLPlugin` provides as `PubSub` by default.
164
+ * Deployments with several instances supply their own `PubSub` (`pubsub` option).
165
+ *
166
+ * ```ts
167
+ * GraphQLPlugin.configure({ pubsub: new MemoryPubSub({ bufferSize: 500 }) })
168
+ * ```
169
+ */
170
+ var MemoryPubSub = class extends PubSub {
171
+ #bufferSize;
172
+ #topics = /* @__PURE__ */ new Map();
173
+ #closed = false;
174
+ constructor(options = {}) {
175
+ super();
176
+ const bufferSize = options.bufferSize ?? DEFAULT_BUFFER_SIZE;
177
+ if (!(Number.isSafeInteger(bufferSize) && bufferSize >= 1)) throw new TypeError(`MemoryPubSub: bufferSize must be a positive integer (received ${String(bufferSize)})`);
178
+ this.#bufferSize = bufferSize;
179
+ }
180
+ /** Subscribers of every topic. */
181
+ get subscribers() {
182
+ let count = 0;
183
+ for (const subscribers of this.#topics.values()) count += subscribers.size;
184
+ return count;
185
+ }
186
+ publish(topic, payload) {
187
+ const subscribers = this.#topics.get(topic);
188
+ if (subscribers !== void 0) for (const subscriber of subscribers) {
189
+ const channel = subscriber.channel;
190
+ if (!channel.closed && !channel.push(payload)) subscriber.overflowed = true;
191
+ }
192
+ return PUBLISHED;
193
+ }
194
+ subscribe(topic, options = {}) {
195
+ const channel = new EventChannel({
196
+ capacity: this.#bufferSize,
197
+ overflow: "close",
198
+ ...options.signal === void 0 ? {} : { signal: options.signal }
199
+ });
200
+ const subscriber = new Subscriber(channel);
201
+ if (this.#closed) {
202
+ channel.close();
203
+ return subscriber;
204
+ }
205
+ let subscribers = this.#topics.get(topic);
206
+ if (subscribers === void 0) {
207
+ subscribers = /* @__PURE__ */ new Set();
208
+ this.#topics.set(topic, subscribers);
209
+ }
210
+ const set = subscribers;
211
+ set.add(subscriber);
212
+ channel.onClose(() => {
213
+ set.delete(subscriber);
214
+ if (set.size === 0 && this.#topics.get(topic) === set) this.#topics.delete(topic);
215
+ });
216
+ return subscriber;
217
+ }
218
+ /** Ends every subscription; later subscriptions end at once. */
219
+ close() {
220
+ this.#closed = true;
221
+ for (const subscribers of [...this.#topics.values()]) for (const subscriber of [...subscribers]) subscriber.channel.close();
222
+ this.#topics.clear();
223
+ }
224
+ };
225
+ // src/decisions.ts
226
+ /** The groups listed by `static enforces` of a guard class (ZNIGHT005 when malformed). */
227
+ function enforcedBy(guard, name) {
228
+ if (typeof guard !== "function") return [];
229
+ let enforces;
230
+ try {
231
+ enforces = guard.enforces;
232
+ } catch (error) {
233
+ throw invalidEnforces(name, "reading it threw", error);
234
+ }
235
+ if (enforces === void 0) return [];
236
+ if (!Array.isArray(enforces)) throw invalidEnforces(name, "expected an array of route decorators");
237
+ for (const entry of enforces) {
238
+ const info = typeof entry === "function" || typeof entry === "object" && entry !== null ? entry : {};
239
+ if (typeof info.key !== "string" || typeof info.group !== "string" || typeof info.required !== "boolean") throw invalidEnforces(name, "every entry must be a route decorator ({ key, group, required })");
240
+ }
241
+ return enforces;
242
+ }
243
+ function invalidEnforces(name, reason, cause) {
244
+ return new ZnightError(ErrorCode.InvalidDecorator, `GraphQLPlugin: invalid static enforces of the guard ${name}: ${reason}`, {
245
+ hints: ["static readonly enforces = [Public, Authenticated, Roles] (decorators created with createRouteDecorator)"],
246
+ ...cause === void 0 ? {} : { cause }
247
+ });
248
+ }
249
+ /** Runs the check. `groups`: `metadata.extensions.graphql.groups`. */
250
+ function checkFieldDecisions(fields, groups, mode, logger) {
251
+ const keys = /* @__PURE__ */ new Map();
252
+ const required = /* @__PURE__ */ new Set();
253
+ for (const [group, definition] of Object.entries(groups ?? {})) {
254
+ for (const key of definition.keys) keys.set(key, {
255
+ group,
256
+ required: definition.required
257
+ });
258
+ if (definition.required) required.add(group);
259
+ }
260
+ const enforcedOf = /* @__PURE__ */ new Map();
261
+ const enforced = (guard) => {
262
+ let groupsOf = enforcedOf.get(guard.cls);
263
+ if (groupsOf === void 0) {
264
+ const infos = enforcedBy(guard.cls, guard.name);
265
+ for (const info of infos) {
266
+ if (!keys.has(info.key)) keys.set(info.key, {
267
+ group: info.group,
268
+ required: info.required
269
+ });
270
+ if (info.required) required.add(info.group);
271
+ }
272
+ groupsOf = new Set(infos.map((info) => info.group));
273
+ enforcedOf.set(guard.cls, groupsOf);
274
+ }
275
+ return groupsOf;
276
+ };
277
+ const chains = fields.map((field) => {
278
+ const set = /* @__PURE__ */ new Set();
279
+ for (const guard of field.guards) for (const group of enforced(guard)) set.add(group);
280
+ return set;
281
+ });
282
+ const known = /* @__PURE__ */ new Set();
283
+ for (const chain of chains) for (const group of chain) known.add(group);
284
+ const groupOf = (key) => keys.get(key)?.group ?? key;
285
+ const problems = /* @__PURE__ */ new Map();
286
+ const problem = (group) => {
287
+ let entry = problems.get(group);
288
+ if (entry === void 0) {
289
+ entry = {
290
+ missing: [],
291
+ unenforced: []
292
+ };
293
+ problems.set(group, entry);
294
+ }
295
+ return entry;
296
+ };
297
+ fields.forEach((field, index) => {
298
+ const decided = new Set(Object.keys(field.meta).map(groupOf));
299
+ if (field.root) {
300
+ for (const group of required) if (!decided.has(group)) problem(group).missing.push(field.description);
301
+ }
302
+ const chain = chains[index];
303
+ for (const group of decided) if ((required.has(group) || known.has(group)) && !chain.has(group)) problem(group).unenforced.push(field.description);
304
+ });
305
+ if (problems.size === 0) return;
306
+ const messages = [];
307
+ const hints = [];
308
+ for (const group of [...problems.keys()].sort()) {
309
+ const entry = problems.get(group);
310
+ const members = [...keys].filter(([, info]) => info.group === group).map(([key]) => key).sort();
311
+ const parts = [];
312
+ if (entry.missing.length > 0) {
313
+ parts.push(`${count$1(entry.missing.length)} ${entry.missing.length === 1 ? "has" : "have"} no "${group}" decision: ${entry.missing.join(", ")}`);
314
+ hints.push(`Add a decision of the "${group}" group (${members.join(", ") || group}) to the resolver method or to its class`);
315
+ }
316
+ if (entry.unenforced.length > 0) {
317
+ parts.push(`no guard enforces the "${group}" group on ${count$1(entry.unenforced.length)}: ${entry.unenforced.join(", ")}`);
318
+ hints.push(`Register a GraphQL guard declaring \`static enforces\` with the decorators of the "${group}" group: GraphQLPlugin.configure({ guards: [...] }) or @UseGuard(...)`);
319
+ }
320
+ const message = parts.join("; ");
321
+ if (mode === "warn") logger.warn(`GraphQLPlugin: ${message}`, {
322
+ code: ErrorCode.MissingRouteDecision,
323
+ group
324
+ });
325
+ else messages.push(message);
326
+ }
327
+ if (mode === "error") {
328
+ hints.push("Tests that deliberately run without guards can set enforcement: 'warn'");
329
+ const text = messages.join("; ");
330
+ throw new ZnightError(ErrorCode.MissingRouteDecision, `GraphQLPlugin: ${text.charAt(0).toUpperCase()}${text.slice(1)}`, { hints });
331
+ }
332
+ }
333
+ function count$1(fields) {
334
+ return fields === 1 ? "1 field" : `${fields} fields`;
335
+ }
336
+ // src/context.ts
337
+ const NO_VARIABLES = Object.freeze({});
338
+ /**
339
+ * The `GraphQLContext` of one operation. Built from references the executor already holds (the
340
+ * operation node, the request, the options, the enclosing scope): one allocation per operation,
341
+ * every field read on demand.
342
+ */
343
+ var OperationContext = class {
344
+ document;
345
+ #runtime;
346
+ #operation;
347
+ #request;
348
+ #options;
349
+ #scope;
350
+ #requestId;
351
+ #http;
352
+ #signal;
353
+ #ended;
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;
855
+ }
856
+ };
857
+ /** A request refused before execution. */
858
+ var Rejection = class {
859
+ status;
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() {}
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) {
1281
+ return {
1282
+ operationType: ctx.operationType,
1283
+ operationName: ctx.operationName,
1284
+ documentId: id,
1285
+ transport: ctx.transport,
1286
+ requestId: ctx.requestId,
1287
+ context: ctx,
1288
+ startTime: performance.now()
1289
+ };
1290
+ }
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);
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);
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
+ // src/options.ts
1458
+ const KEYS = /* @__PURE__ */ new Set([
1459
+ "path",
1460
+ "http",
1461
+ "guards",
1462
+ "errorMappers",
1463
+ "exposeErrors",
1464
+ "introspection",
1465
+ "graphiql",
1466
+ "limits",
1467
+ "documentCache",
1468
+ "subscriptions",
1469
+ "pubsub",
1470
+ "enforcement"
1471
+ ]);
1472
+ const PATH = /^\/[A-Za-z0-9\-._~!$&'()*+,;=:@/]*$/;
1473
+ const LIMIT_DEFAULTS = {
1474
+ maxTokens: 2e3,
1475
+ maxDepth: 12,
1476
+ maxAliases: 30,
1477
+ maxDirectives: 50
1478
+ };
1479
+ function show$1(value) {
1480
+ if (typeof value === "function") return "a function";
1481
+ try {
1482
+ return JSON.stringify(value) ?? String(value);
1483
+ } catch {
1484
+ return typeof value;
1485
+ }
1486
+ }
1487
+ /** ZNIGHT066 for an invalid option. */
1488
+ function invalidOption(name, expected, received) {
1489
+ return new ZnightError(ErrorCode.InvalidGraphQLSetup, `GraphQLPlugin: invalid option ${name}: expected ${expected} (received ${show$1(received)})`, { hints: ["See the options of GraphQLPlugin.configure() (RFC 0005 §5.11)"] });
1490
+ }
1491
+ function isRecord$1(value) {
1492
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1493
+ }
1494
+ function boolean(name, value, fallback) {
1495
+ if (value === void 0) return fallback;
1496
+ if (typeof value !== "boolean") throw invalidOption(name, "a boolean", value);
1497
+ return value;
1498
+ }
1499
+ function positiveInteger(name, value, fallback) {
1500
+ if (value === void 0) return fallback;
1501
+ if (!(Number.isSafeInteger(value) && value >= 1)) throw invalidOption(name, "a positive integer", value);
1502
+ return value;
1503
+ }
1504
+ function limit(name, value) {
1505
+ if (value === false) return void 0;
1506
+ return positiveInteger(`limits.${name}`, value, LIMIT_DEFAULTS[name]);
1507
+ }
1508
+ /** Refs given as classes or instances with `method`. */
1509
+ function refs(name, value, method) {
1510
+ if (value === void 0) return [];
1511
+ if (!Array.isArray(value)) throw invalidOption(name, `an array of classes or objects with a ${method}() method`, value);
1512
+ for (const ref of value) if (!(typeof ref === "function" || ref !== null && typeof ref === "object" && typeof ref[method] === "function")) throw invalidOption(name, `classes or objects with a ${method}() method`, ref);
1513
+ return value;
1514
+ }
1515
+ /** Validates the options and applies the defaults (`env`: `process.env`). */
1516
+ function resolveGraphQLOptions(options, env = process.env) {
1517
+ if (!isRecord$1(options)) throw invalidOption("options", "an object", options);
1518
+ for (const key of Object.keys(options)) if (!KEYS.has(key)) throw invalidOption(key, `one of ${[...KEYS].join(", ")}`, key);
1519
+ const nodeEnv = env["NODE_ENV"];
1520
+ const path = options["path"] ?? "/graphql";
1521
+ if (typeof path !== "string" || !PATH.test(path)) throw invalidOption("path", "a path starting with '/'", path);
1522
+ const rawHttp = options["http"] ?? {};
1523
+ let http;
1524
+ if (rawHttp === false) http = false;
1525
+ else {
1526
+ if (!isRecord$1(rawHttp)) throw invalidOption("http", "false or { meta?, guards?, bodyLimit?, get? }", rawHttp);
1527
+ const meta = rawHttp["meta"] ?? [];
1528
+ if (!Array.isArray(meta) || !meta.every((entry) => isRecord$1(entry) && typeof entry["key"] === "string" && entry["key"] !== "")) throw invalidOption("http.meta", "an array of route metadata entries (Decorator.meta())", meta);
1529
+ const bodyLimit = rawHttp["bodyLimit"] ?? "100kb";
1530
+ if (parseByteSize(bodyLimit) === void 0) throw invalidOption("http.bodyLimit", "a byte size ('100kb', 1048576)", bodyLimit);
1531
+ http = {
1532
+ meta,
1533
+ guards: refs("http.guards", rawHttp["guards"], "canActivate"),
1534
+ bodyLimit,
1535
+ get: boolean("http.get", rawHttp["get"], true)
1536
+ };
1537
+ }
1538
+ const rawLimits = options["limits"] ?? {};
1539
+ if (!isRecord$1(rawLimits)) throw invalidOption("limits", "{ maxTokens?, maxDepth?, maxAliases?, maxDirectives? }", rawLimits);
1540
+ for (const key of Object.keys(rawLimits)) if (!(key in LIMIT_DEFAULTS)) throw invalidOption(`limits.${key}`, "maxTokens, maxDepth, maxAliases or maxDirectives", key);
1541
+ const documentCache = options["documentCache"] ?? 1e3;
1542
+ if (!(Number.isSafeInteger(documentCache) && documentCache >= 0)) throw invalidOption("documentCache", "a non-negative integer", documentCache);
1543
+ const rawSubscriptions = options["subscriptions"] ?? {};
1544
+ let subscriptions;
1545
+ let bufferSize = 100;
1546
+ if (rawSubscriptions === false) subscriptions = false;
1547
+ else {
1548
+ if (!isRecord$1(rawSubscriptions)) throw invalidOption("subscriptions", "false or { maxActive?, bufferSize?, heartbeat?, maxDuration? }", rawSubscriptions);
1549
+ bufferSize = positiveInteger("subscriptions.bufferSize", rawSubscriptions["bufferSize"], 100);
1550
+ const heartbeat = rawSubscriptions["heartbeat"];
1551
+ if (heartbeat !== void 0 && heartbeat !== false && !(typeof heartbeat === "number" && heartbeat > 0)) throw invalidOption("subscriptions.heartbeat", "a positive number of milliseconds or false", heartbeat);
1552
+ const maxDuration = rawSubscriptions["maxDuration"];
1553
+ if (maxDuration !== void 0 && !(typeof maxDuration === "number" && maxDuration > 0)) throw invalidOption("subscriptions.maxDuration", "a positive number of milliseconds", maxDuration);
1554
+ subscriptions = {
1555
+ maxActive: positiveInteger("subscriptions.maxActive", rawSubscriptions["maxActive"], 1e4),
1556
+ bufferSize,
1557
+ heartbeat,
1558
+ maxDuration
1559
+ };
1560
+ }
1561
+ const pubsub = options["pubsub"];
1562
+ 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);
1563
+ const enforcement = options["enforcement"] ?? "error";
1564
+ if (enforcement !== "error" && enforcement !== "warn") throw invalidOption("enforcement", "'error' or 'warn'", enforcement);
1565
+ return {
1566
+ path,
1567
+ http,
1568
+ guards: refs("guards", options["guards"], "canResolve"),
1569
+ errorMappers: refs("errorMappers", options["errorMappers"], "map"),
1570
+ exposeErrors: boolean("exposeErrors", options["exposeErrors"], false),
1571
+ introspection: boolean("introspection", options["introspection"], nodeEnv !== "production"),
1572
+ graphiql: boolean("graphiql", options["graphiql"], nodeEnv === "development"),
1573
+ limits: {
1574
+ maxTokens: limit("maxTokens", rawLimits["maxTokens"]),
1575
+ maxDepth: limit("maxDepth", rawLimits["maxDepth"]),
1576
+ maxAliases: limit("maxAliases", rawLimits["maxAliases"]),
1577
+ maxDirectives: limit("maxDirectives", rawLimits["maxDirectives"])
1578
+ },
1579
+ documentCache,
1580
+ subscriptions,
1581
+ bufferSize,
1582
+ pubsub,
1583
+ enforcement
1584
+ };
1585
+ }
1586
+ // src/pipeline.ts
1587
+ /** Whether global guards run on a field: root fields always, field resolvers when they carry metadata (§5.9). */
1588
+ function globalGuardsApply(binding) {
1589
+ return binding.operation !== "field" || Object.keys(binding.meta).length > 0;
1590
+ }
1591
+ /** The {@link FieldFunctionsFactory} of the plugin. */
1592
+ function pipelineFactory(options) {
1593
+ const normalizers = new NormalizerBuilder(options.inputs);
1594
+ return (binding, coordinate, field, parent) => fieldFunctions(options, normalizers, binding, coordinate, field, parent);
1595
+ }
1596
+ function fieldFunctions(options, normalizers, binding, coordinate, field, parent) {
1597
+ const container = options.container;
1598
+ const invoke = binding.invoke;
1599
+ const target = options.resolver(binding, coordinate);
1600
+ const guards = [...globalGuardsApply(binding) ? options.globalGuards : [], ...binding.guards.map((token) => options.guard(token, coordinate))];
1601
+ const normalize = field.args === void 0 ? void 0 : normalizers.args(field.args);
1602
+ let resolve;
1603
+ if (guards.length === 0) {
1604
+ if (target.token === void 0) {
1605
+ const instance = target.instance;
1606
+ resolve = normalize === void 0 ? (p, a, c, i) => invoke(instance, p, a, c, i) : (p, a, c, i) => {
1607
+ normalize(a);
1608
+ return invoke(instance, p, a, c, i);
1609
+ };
1610
+ } else {
1611
+ const token = target.token;
1612
+ resolve = normalize === void 0 ? (p, a, c, i) => invoke(container.get(token), p, a, c, i) : (p, a, c, i) => {
1613
+ normalize(a);
1614
+ return invoke(container.get(token), p, a, c, i);
1615
+ };
1616
+ }
1617
+ } else {
1618
+ const route = Object.freeze({
1619
+ meta: binding.meta,
1620
+ coordinate
1621
+ });
1622
+ const parentType = parent.name;
1623
+ const fieldName = field.name;
1624
+ const forbidden = () => {
1625
+ throw new GraphQLException(`Not allowed to resolve ${coordinate}`, { code: GraphQLErrorCode.Forbidden });
1626
+ };
1627
+ resolve = (p, a, c, i) => {
1628
+ const operation = c;
1629
+ const args = a;
1630
+ const allowed = runGuards(container, guards, new FieldContext(route, operation, parentType, fieldName, args, p, i));
1631
+ const call = () => {
1632
+ if (normalize !== void 0) normalize(args);
1633
+ return invoke(resolveIn(container, target), p, args, operation, i);
1634
+ };
1635
+ if (allowed === true) return call();
1636
+ if (allowed === false) return forbidden();
1637
+ return allowed.then((ok) => ok ? call() : forbidden());
1638
+ };
1639
+ }
1640
+ if (binding.operation === "subscription") return {
1641
+ subscribe: resolve,
1642
+ resolve: identity$1
1643
+ };
1644
+ return { resolve };
1645
+ }
1646
+ function identity$1(payload) {
1647
+ return payload;
1648
+ }
1649
+ /** Runs guards in order, synchronously until one returns a promise. */
1650
+ function runGuards(container, guards, ctx, start = 0) {
1651
+ for (let index = start; index < guards.length; index++) {
1652
+ const result = resolveIn(container, guards[index]).canResolve(ctx);
1653
+ if (isThenable(result)) return Promise.resolve(result).then((ok) => ok ? runGuards(container, guards, ctx, index + 1) : false);
1654
+ if (!result) return false;
1655
+ }
1656
+ return true;
1657
+ }
1658
+ /** What a guard receives (§5.9): a `RouteMetaCarrier` with the field's effective metadata. */
1659
+ var FieldContext = class {
1660
+ route;
1661
+ operation;
1662
+ parentType;
1663
+ field;
1664
+ args;
1665
+ parent;
1666
+ info;
1667
+ constructor(route, operation, parentType, field, args, parent, info) {
1668
+ this.route = route;
1669
+ this.operation = operation;
1670
+ this.parentType = parentType;
1671
+ this.field = field;
1672
+ this.args = args;
1673
+ this.parent = parent;
1674
+ this.info = info;
1675
+ }
1676
+ get http() {
1677
+ return this.operation.http;
1678
+ }
1679
+ };
1680
+ /**
1681
+ * Builds normalisers (§7.3) at boot. Values are mutated in place: graphql-js creates the argument
1682
+ * object of every call and the coerced input objects, and the flags of an input type are the same
1683
+ * wherever it is used, so a value shared through a variable is normalised the same way twice.
1684
+ */
1685
+ var NormalizerBuilder = class {
1686
+ #inputs;
1687
+ /** Input type → its normaliser (`null`: nothing to do), `PENDING` while being built (cycles). */
1688
+ #types = /* @__PURE__ */ new Map();
1689
+ constructor(inputs) {
1690
+ this.#inputs = inputs;
1691
+ }
1692
+ /** The normaliser of a field's arguments, `undefined` when no flag is reachable. */
1693
+ args(args) {
1694
+ const steps = this.#steps(args);
1695
+ return steps === void 0 ? void 0 : (record) => applySteps(record, steps);
1696
+ }
1697
+ #steps(values) {
1698
+ const steps = [];
1699
+ for (const value of values) {
1700
+ const own = value.nullAsUndefined === true;
1701
+ const ref = parseTypeRef(value.type);
1702
+ const nested = ref === void 0 ? void 0 : this.#value(ref);
1703
+ if (own || nested !== void 0) steps.push({
1704
+ name: value.name,
1705
+ own,
1706
+ nested
1707
+ });
1708
+ }
1709
+ return steps.length === 0 ? void 0 : steps;
1710
+ }
1711
+ #value(ref) {
1712
+ if (ref.kind === "list") {
1713
+ const item = this.#value(ref.of);
1714
+ return item === void 0 ? void 0 : (value) => {
1715
+ if (Array.isArray(value)) for (const entry of value) item(entry);
1716
+ };
1717
+ }
1718
+ return this.#input(ref.name);
1719
+ }
1720
+ #input(name) {
1721
+ const known = this.#types.get(name);
1722
+ if (known === PENDING) return (value) => {
1723
+ const normalize = this.#types.get(name);
1724
+ if (typeof normalize === "function") normalize(value);
1725
+ };
1726
+ if (known !== void 0) return known ?? void 0;
1727
+ const type = this.#inputs.get(name);
1728
+ if (type === void 0) return void 0;
1729
+ this.#types.set(name, PENDING);
1730
+ const steps = this.#steps(type.fields);
1731
+ const normalize = steps === void 0 ? null : (value) => applySteps(value, steps);
1732
+ this.#types.set(name, normalize);
1733
+ return normalize ?? void 0;
1734
+ }
1735
+ };
1736
+ const PENDING = Symbol("pending");
1737
+ function applySteps(value, steps) {
1738
+ if (value === null || typeof value !== "object") return;
1739
+ const record = value;
1740
+ for (const step of steps) {
1741
+ const entry = record[step.name];
1742
+ if (entry === null) {
1743
+ if (step.own) record[step.name] = void 0;
1744
+ } else if (entry !== void 0 && step.nested !== void 0) step.nested(entry);
1745
+ }
1746
+ }
1747
+ // src/scalars.ts
1748
+ /** RFC 3339 date-time with a mandatory offset (`2026-11-02T10:00:00Z`, `...+01:00`). */
1749
+ const DATE_TIME = /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])[Tt]([01]\d|2[0-3]):([0-5]\d):([0-5]\d|60)(\.\d+)?([Zz]|[+-]([01]\d|2[0-3]):[0-5]\d)$/;
1750
+ /** An integer in decimal notation (`BigInt` on the wire). */
1751
+ const DECIMAL_INTEGER = /^-?\d+$/;
1752
+ function describe(value) {
1753
+ if (typeof value === "string") return JSON.stringify(value);
1754
+ if (typeof value === "bigint") return `${value}n`;
1755
+ if (value === null || typeof value !== "object") return String(value);
1756
+ return Array.isArray(value) ? "a list" : "an object";
1757
+ }
1758
+ function parseDateTime(value) {
1759
+ if (typeof value === "string" && DATE_TIME.test(value)) {
1760
+ const date = new Date(value);
1761
+ if (!Number.isNaN(date.getTime())) return date;
1762
+ }
1763
+ throw new TypeError(`DateTime cannot represent ${describe(value)}: expected an RFC 3339 date-time with an offset`);
1764
+ }
1765
+ function serializeDateTime(value) {
1766
+ if (value instanceof Date) {
1767
+ if (Number.isNaN(value.getTime())) throw new TypeError("DateTime cannot represent an invalid Date");
1768
+ return value.toISOString();
1769
+ }
1770
+ if (typeof value === "string" && DATE_TIME.test(value) && !Number.isNaN(Date.parse(value))) return value;
1771
+ throw new TypeError(`DateTime cannot represent ${describe(value)}: expected a Date`);
1772
+ }
1773
+ function parseBigInt(value) {
1774
+ if (typeof value === "string" && DECIMAL_INTEGER.test(value)) return BigInt(value);
1775
+ if (typeof value === "number" && Number.isSafeInteger(value)) return BigInt(value);
1776
+ throw new TypeError(`BigInt cannot represent ${describe(value)}: expected an integer as a decimal string`);
1777
+ }
1778
+ function serializeBigInt(value) {
1779
+ if (typeof value === "bigint") return value.toString();
1780
+ if (typeof value === "number" && Number.isSafeInteger(value)) return String(value);
1781
+ if (typeof value === "string" && DECIMAL_INTEGER.test(value)) return value;
1782
+ throw new TypeError(`BigInt cannot represent ${describe(value)}: expected a bigint`);
1783
+ }
1784
+ function identity(value) {
1785
+ return value;
1786
+ }
1787
+ function specifiedBy(name) {
1788
+ const url = BUILTIN_SCALARS.get(name)?.specifiedByURL;
1789
+ return url === void 0 ? {} : { specifiedByURL: url };
1790
+ }
1791
+ function createBuiltin(name) {
1792
+ switch (name) {
1793
+ case "DateTime": return new GraphQLScalarType({
1794
+ name,
1795
+ ...specifiedBy(name),
1796
+ coerceOutputValue: serializeDateTime,
1797
+ coerceInputValue: parseDateTime,
1798
+ coerceInputLiteral: (node) => parseDateTime(node.kind === Kind.STRING ? node.value : node)
1799
+ });
1800
+ case "BigInt": return new GraphQLScalarType({
1801
+ name,
1802
+ ...specifiedBy(name),
1803
+ coerceOutputValue: serializeBigInt,
1804
+ coerceInputValue: parseBigInt,
1805
+ coerceInputLiteral: (node) => parseBigInt(node.kind === Kind.STRING || node.kind === Kind.INT ? node.value : node)
1806
+ });
1807
+ case "JSON": return new GraphQLScalarType({
1808
+ name,
1809
+ ...specifiedBy(name),
1810
+ coerceOutputValue: identity,
1811
+ coerceInputValue: identity,
1812
+ coerceInputLiteral: (node) => valueFromASTUntyped(node)
1813
+ });
1814
+ default: return;
1815
+ }
1816
+ }
1817
+ /** Built-in scalars, created on first use and shared by every schema (they hold no state). */
1818
+ const builtins = /* @__PURE__ */ new Map();
1819
+ /** The built-in scalar `name` (`DateTime`, `BigInt`, `JSON`), `undefined` for another name. */
1820
+ function builtinScalar(name) {
1821
+ let scalar = builtins.get(name);
1822
+ if (scalar === void 0) {
1823
+ scalar = createBuiltin(name);
1824
+ if (scalar !== void 0) builtins.set(name, scalar);
1825
+ }
1826
+ return scalar;
1827
+ }
1828
+ /**
1829
+ * The graphql-js scalar of a {@link ScalarDefinition}: `serialize` → `coerceOutputValue`,
1830
+ * `parseValue` → `coerceInputValue`, `parseLiteral` → `coerceInputLiteral` (default: `parseValue`
1831
+ * of the literal's JSON value). The definition's methods are called on the definition.
1832
+ */
1833
+ function definitionScalar(name, definition) {
1834
+ const parseLiteral = definition.parseLiteral;
1835
+ return new GraphQLScalarType({
1836
+ name,
1837
+ ...definition.description === void 0 ? {} : { description: definition.description },
1838
+ ...definition.specifiedByURL === void 0 ? {} : { specifiedByURL: definition.specifiedByURL },
1839
+ coerceOutputValue: (value) => definition.serialize(value),
1840
+ coerceInputValue: (value) => definition.parseValue(value),
1841
+ coerceInputLiteral: parseLiteral === void 0 ? (node) => definition.parseValue(valueFromASTUntyped(node)) : (node) => parseLiteral.call(definition, node)
1842
+ });
1843
+ }
1844
+ // src/schema.ts
1845
+ /** The graphql section of the metadata, or ZNIGHT066 when the application has none. */
1846
+ function graphqlSection(metadata) {
1847
+ const section = metadata.extensions?.graphql;
1848
+ if (section === void 0) throw new ZnightError(ErrorCode.InvalidGraphQLSetup, "GraphQLPlugin: the metadata has no GraphQL schema (metadata.extensions.graphql)", { hints: ["Declare at least one @Resolver() class with a @Query() method", "Regenerate the metadata (znight generate) when resolvers exist"] });
1849
+ return section;
1850
+ }
1851
+ const OPERATIONS = /* @__PURE__ */ new Set([
1852
+ "query",
1853
+ "mutation",
1854
+ "subscription",
1855
+ "field"
1856
+ ]);
1857
+ const INPUT_KINDS = /* @__PURE__ */ new Set([
1858
+ "input",
1859
+ "enum",
1860
+ "scalar"
1861
+ ]);
1862
+ const OUTPUT_KINDS = /* @__PURE__ */ new Set([
1863
+ "object",
1864
+ "enum",
1865
+ "scalar",
1866
+ "union"
1867
+ ]);
1868
+ function isRecord(value) {
1869
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1870
+ }
1871
+ function show(value) {
1872
+ if (typeof value === "function") return "a function";
1873
+ try {
1874
+ return JSON.stringify(value) ?? String(value);
1875
+ } catch {
1876
+ return typeof value;
1877
+ }
1878
+ }
1879
+ /**
1880
+ * The structural problems of a graphql section (empty when it can be built): version, roots,
1881
+ * names, kinds, type references, resolver bindings, discriminators, scalars and groups.
1882
+ */
1883
+ function checkGraphQLSection(section) {
1884
+ const problems = [];
1885
+ if (!isRecord(section)) return [`expected an object (received ${show(section)})`];
1886
+ if (section["version"] !== 1) {
1887
+ problems.push(`unsupported version ${show(section["version"])}: this @znight/graphql reads version 1 (rebuild the application with the same @znight/graphql version)`);
1888
+ return problems;
1889
+ }
1890
+ if (typeof section["schemaHash"] !== "string") problems.push("schemaHash must be a string");
1891
+ const types = section["types"];
1892
+ if (!Array.isArray(types)) {
1893
+ problems.push("types must be an array");
1894
+ return problems;
1895
+ }
1896
+ const kinds = /* @__PURE__ */ new Map();
1897
+ for (const [index, type] of types.entries()) {
1898
+ if (!isRecord(type) || typeof type["name"] !== "string" || typeof type["kind"] !== "string") {
1899
+ problems.push(`types[${index}] must be { kind, name, ... } (received ${show(type)})`);
1900
+ continue;
1901
+ }
1902
+ const name = type["name"];
1903
+ if (!isGraphQLName(name)) problems.push(`invalid type name ${show(name)}`);
1904
+ else if (SPECIFIED_SCALARS.has(name)) problems.push(`the specified scalar ${name} must not be declared`);
1905
+ else if (kinds.has(name)) problems.push(`the type ${name} is declared twice`);
1906
+ kinds.set(name, type["kind"]);
1907
+ }
1908
+ const kindOf = (name) => SPECIFIED_SCALARS.has(name) ? "scalar" : kinds.get(name);
1909
+ const checkRef = (where, text, allowed, position) => {
1910
+ const ref = typeof text === "string" ? parseTypeRef(text) : void 0;
1911
+ if (ref === void 0) {
1912
+ problems.push(`${where}: invalid type reference ${show(text)}`);
1913
+ return;
1914
+ }
1915
+ let named = ref;
1916
+ while (named.kind === "list") named = named.of;
1917
+ const kind = kindOf(named.name);
1918
+ if (kind === void 0) problems.push(`${where}: unknown type ${named.name}`);
1919
+ else if (!allowed.has(kind)) problems.push(`${where}: ${named.name} (${kind}) cannot be used as an ${position} type`);
1920
+ };
1921
+ const checkInputValues = (where, values, required) => {
1922
+ if (!Array.isArray(values) || required && values.length === 0) {
1923
+ problems.push(`${where} must be a ${required ? "non-empty " : ""}array`);
1924
+ return;
1925
+ }
1926
+ const seen = /* @__PURE__ */ new Set();
1927
+ for (const value of values) {
1928
+ if (!isRecord(value) || typeof value["name"] !== "string") {
1929
+ problems.push(`${where}: expected { name, type } (received ${show(value)})`);
1930
+ continue;
1931
+ }
1932
+ const name = value["name"];
1933
+ if (!isGraphQLName(name)) problems.push(`${where}: invalid name ${show(name)}`);
1934
+ if (seen.has(name)) problems.push(`${where}: ${name} is declared twice`);
1935
+ seen.add(name);
1936
+ checkRef(`${where}.${name}`, value["type"], INPUT_KINDS, "input");
1937
+ }
1938
+ };
1939
+ for (const type of types) {
1940
+ if (!isRecord(type) || typeof type["name"] !== "string") continue;
1941
+ const name = type["name"];
1942
+ switch (type["kind"]) {
1943
+ case "object": {
1944
+ const fields = type["fields"];
1945
+ if (!Array.isArray(fields) || fields.length === 0) {
1946
+ problems.push(`${name}: fields must be a non-empty array`);
1947
+ break;
1948
+ }
1949
+ const seen = /* @__PURE__ */ new Set();
1950
+ for (const field of fields) {
1951
+ if (!isRecord(field) || typeof field["name"] !== "string") {
1952
+ problems.push(`${name}: expected fields { name, type } (received ${show(field)})`);
1953
+ continue;
1954
+ }
1955
+ const coordinate = `${name}.${field["name"]}`;
1956
+ if (!isGraphQLName(field["name"])) problems.push(`${name}: invalid field name ${show(field["name"])}`);
1957
+ if (seen.has(field["name"])) problems.push(`${coordinate} is declared twice`);
1958
+ seen.add(field["name"]);
1959
+ checkRef(coordinate, field["type"], OUTPUT_KINDS, "output");
1960
+ if (field["args"] !== void 0) checkInputValues(`${coordinate} arguments`, field["args"], false);
1961
+ if (field["resolver"] !== void 0) checkBinding(coordinate, field["resolver"], problems);
1962
+ }
1963
+ break;
1964
+ }
1965
+ case "input":
1966
+ checkInputValues(`${name} fields`, type["fields"], true);
1967
+ break;
1968
+ case "enum": {
1969
+ const values = type["values"];
1970
+ if (!Array.isArray(values) || values.length === 0) {
1971
+ problems.push(`${name}: values must be a non-empty array`);
1972
+ break;
1973
+ }
1974
+ const seen = /* @__PURE__ */ new Set();
1975
+ for (const value of values) {
1976
+ const valueName = isRecord(value) ? value["name"] : void 0;
1977
+ if (!isRecord(value) || typeof valueName !== "string" || !isGraphQLName(valueName) || RESERVED_ENUM_VALUES.has(valueName) || typeof value["value"] !== "string" && typeof value["value"] !== "number") {
1978
+ problems.push(`${name}: invalid enum value ${show(value)} (expected { name, value: string | number })`);
1979
+ continue;
1980
+ }
1981
+ if (seen.has(valueName)) problems.push(`${name}.${valueName} is declared twice`);
1982
+ seen.add(valueName);
1983
+ }
1984
+ break;
1985
+ }
1986
+ case "union": {
1987
+ const members = type["members"];
1988
+ if (!Array.isArray(members) || members.length === 0) {
1989
+ problems.push(`${name}: members must be a non-empty array`);
1990
+ break;
1991
+ }
1992
+ for (const member of members) if (typeof member !== "string" || kinds.get(member) !== "object") problems.push(`${name}: the member ${show(member)} is not an object type`);
1993
+ checkDiscriminator(name, type["discriminator"], members, problems);
1994
+ break;
1995
+ }
1996
+ case "scalar":
1997
+ if (type["builtin"] === true) {
1998
+ if (!BUILTIN_SCALARS.has(name)) problems.push(`${name} is not a built-in scalar (${[...BUILTIN_SCALARS.keys()].join(", ")})`);
1999
+ } else {
2000
+ const scalar = type["scalar"];
2001
+ if (scalar instanceof GraphQLScalarType) {
2002
+ if (scalar.name !== name) problems.push(`the scalar ${name} is implemented by the graphql-js scalar ${scalar.name} (names must match)`);
2003
+ } else if (!isRecord(scalar) || typeof scalar["serialize"] !== "function" || typeof scalar["parseValue"] !== "function" || scalar["parseLiteral"] !== void 0 && typeof scalar["parseLiteral"] !== "function") problems.push(`the scalar ${name} needs a definition { serialize, parseValue, parseLiteral? } or a GraphQLScalarType (received ${show(scalar)})`);
2004
+ }
2005
+ break;
2006
+ default: problems.push(`${name}: unknown kind ${show(type["kind"])}`);
2007
+ }
2008
+ }
2009
+ checkRoots(section["roots"], kinds, types, problems);
2010
+ checkGroups(section["groups"], problems);
2011
+ return problems;
2012
+ }
2013
+ function checkBinding(coordinate, binding, problems) {
2014
+ if (!isRecord(binding)) {
2015
+ problems.push(`${coordinate}: resolver must be an object (received ${show(binding)})`);
2016
+ return;
2017
+ }
2018
+ const invalid = [];
2019
+ if (typeof binding["token"] !== "string" || binding["token"] === "") invalid.push("token");
2020
+ if (typeof binding["method"] !== "string") invalid.push("method");
2021
+ if (typeof binding["operation"] !== "string" || !OPERATIONS.has(binding["operation"])) invalid.push("operation");
2022
+ const guards = binding["guards"];
2023
+ if (!Array.isArray(guards) || !guards.every((guard) => typeof guard === "string" && guard !== "")) invalid.push("guards");
2024
+ if (!isRecord(binding["meta"])) invalid.push("meta");
2025
+ if (typeof binding["invoke"] !== "function") invalid.push("invoke");
2026
+ if (invalid.length > 0) problems.push(`${coordinate}: invalid resolver binding (${invalid.join(", ")})`);
2027
+ }
2028
+ function checkDiscriminator(name, discriminator, members, problems) {
2029
+ const memberSet = new Set(members);
2030
+ if (!isRecord(discriminator)) {
2031
+ problems.push(`${name}: discriminator must be an object (received ${show(discriminator)})`);
2032
+ return;
2033
+ }
2034
+ switch (discriminator["by"]) {
2035
+ case "typename": return;
2036
+ case "property": {
2037
+ const values = discriminator["values"];
2038
+ if (typeof discriminator["property"] !== "string" || !isRecord(values)) {
2039
+ problems.push(`${name}: a property discriminator needs { property, values }`);
2040
+ return;
2041
+ }
2042
+ for (const member of Object.values(values)) if (!memberSet.has(member)) problems.push(`${name}: the discriminator names ${show(member)}, not a member`);
2043
+ return;
2044
+ }
2045
+ case "class": {
2046
+ const classes = discriminator["classes"];
2047
+ if (!Array.isArray(classes)) {
2048
+ problems.push(`${name}: a class discriminator needs classes: [Class, member][]`);
2049
+ return;
2050
+ }
2051
+ for (const entry of classes) if (!Array.isArray(entry) || typeof entry[0] !== "function" || !memberSet.has(entry[1])) problems.push(`${name}: invalid class discriminator entry ${show(entry)} (expected [Class, member])`);
2052
+ return;
2053
+ }
2054
+ default: problems.push(`${name}: unknown discriminator ${show(discriminator["by"])}`);
2055
+ }
2056
+ }
2057
+ function checkRoots(roots, kinds, types, problems) {
2058
+ if (!isRecord(roots) || typeof roots["query"] !== "string") {
2059
+ problems.push(`roots must be { query, mutation?, subscription? } (received ${show(roots)})`);
2060
+ return;
2061
+ }
2062
+ for (const operation of [
2063
+ "query",
2064
+ "mutation",
2065
+ "subscription"
2066
+ ]) {
2067
+ const root = roots[operation];
2068
+ if (root === void 0) continue;
2069
+ if (typeof root !== "string" || kinds.get(root) !== "object") problems.push(`the ${operation} root ${show(root)} is not an object type`);
2070
+ }
2071
+ const subscription = roots["subscription"];
2072
+ if (typeof subscription !== "string") return;
2073
+ for (const type of types) {
2074
+ if (!isRecord(type) || type["name"] !== subscription || !Array.isArray(type["fields"])) continue;
2075
+ for (const field of type["fields"]) {
2076
+ if (!isRecord(field)) continue;
2077
+ const binding = field["resolver"];
2078
+ if (!isRecord(binding) || binding["operation"] !== "subscription") problems.push(`${subscription}.${show(field["name"])} needs a subscription resolver (its source event stream)`);
2079
+ }
2080
+ }
2081
+ }
2082
+ function checkGroups(groups, problems) {
2083
+ if (groups === void 0) return;
2084
+ if (!isRecord(groups)) {
2085
+ problems.push(`groups must be an object (received ${show(groups)})`);
2086
+ return;
2087
+ }
2088
+ for (const [group, entry] of Object.entries(groups)) {
2089
+ const keys = isRecord(entry) ? entry["keys"] : void 0;
2090
+ if (!isRecord(entry) || !Array.isArray(keys) || keys.length === 0 || !keys.every((key) => typeof key === "string" && key !== "") || typeof entry["required"] !== "boolean") problems.push(`group ${show(group)} must be { keys: [non-empty strings], required: boolean }`);
2091
+ }
2092
+ }
2093
+ /** Throws one ZNIGHT066 error listing `problems`. */
2094
+ function invalidSchema(problems) {
2095
+ throw new ZnightError(ErrorCode.InvalidGraphQLSetup, `GraphQLPlugin: invalid GraphQL schema (metadata.extensions.graphql), ${problems.length} ${problems.length === 1 ? "problem" : "problems"}:\n${problems.map((problem) => ` - ${problem}`).join("\n")}`, { hints: ["Regenerate the metadata with the @znight/graphql version the application runs (znight generate)", "Hand-written metadata must follow GraphQLMetadata (@znight/graphql, RFC 0005 §7.1)"] });
2096
+ }
2097
+ /** A union's `resolveType`, from its discriminator: data read once, at boot. */
2098
+ function typeResolver(discriminator) {
2099
+ switch (discriminator.by) {
2100
+ case "property": {
2101
+ const property = discriminator.property;
2102
+ const values = new Map(Object.entries(discriminator.values));
2103
+ return (value) => value !== null && typeof value === "object" ? values.get(String(value[property])) ?? typenameOf(value) : void 0;
2104
+ }
2105
+ case "class": {
2106
+ const classes = discriminator.classes;
2107
+ return (value) => {
2108
+ for (const [cls, name] of classes) if (value instanceof cls) return name;
2109
+ return typenameOf(value);
2110
+ };
2111
+ }
2112
+ default: return typenameOf;
2113
+ }
2114
+ }
2115
+ function typenameOf(value) {
2116
+ if (value === null || typeof value !== "object") return void 0;
2117
+ const name = value.__typename;
2118
+ return typeof name === "string" ? name : void 0;
2119
+ }
2120
+ /**
2121
+ * Builds the schema of a section. `functions` decides the resolve functions of every
2122
+ * resolver-backed field; plain property fields keep the graphql-js default resolver.
2123
+ *
2124
+ * @throws ZnightError ZNIGHT066 listing every structural, construction and `validateSchema` problem.
2125
+ */
2126
+ function buildGraphQLSchema(section, functions) {
2127
+ const structural = checkGraphQLSection(section);
2128
+ if (structural.length > 0) invalidSchema(structural);
2129
+ const named = /* @__PURE__ */ new Map([
2130
+ ["String", GraphQLString],
2131
+ ["Int", GraphQLInt],
2132
+ ["Float", GraphQLFloat],
2133
+ ["Boolean", GraphQLBoolean],
2134
+ ["ID", GraphQLID]
2135
+ ]);
2136
+ const problems = [];
2137
+ const lookup = (name) => named.get(name);
2138
+ const typeOf = (ref) => {
2139
+ const inner = ref.kind === "named" ? lookup(ref.name) : new GraphQLList(typeOf(ref.of));
2140
+ return ref.nonNull ? new GraphQLNonNull(inner) : inner;
2141
+ };
2142
+ const typeRef = (text) => typeOf(parseTypeRef(text));
2143
+ const inputValue = (value) => ({
2144
+ type: typeRef(value.type),
2145
+ ...value.description === void 0 ? {} : { description: value.description },
2146
+ ...value.deprecationReason === void 0 ? {} : { deprecationReason: value.deprecationReason },
2147
+ ...value.defaultValue === void 0 ? {} : { default: { value: value.defaultValue } }
2148
+ });
2149
+ const fieldConfig = (type, field) => {
2150
+ const config = {
2151
+ type: typeRef(field.type),
2152
+ ...field.description === void 0 ? {} : { description: field.description },
2153
+ ...field.deprecationReason === void 0 ? {} : { deprecationReason: field.deprecationReason },
2154
+ ...field.args === void 0 || field.args.length === 0 ? {} : { args: Object.fromEntries(field.args.map((arg) => [arg.name, inputValue(arg)])) }
2155
+ };
2156
+ if (field.resolver === void 0) return config;
2157
+ const resolved = functions(field.resolver, `${type.name}.${field.name}`, field, type);
2158
+ return {
2159
+ ...config,
2160
+ ...resolved.resolve === void 0 ? {} : { resolve: resolved.resolve },
2161
+ ...resolved.subscribe === void 0 ? {} : { subscribe: resolved.subscribe }
2162
+ };
2163
+ };
2164
+ try {
2165
+ for (const type of section.types) named.set(type.name, createType(type, fieldConfig, inputValue, lookup));
2166
+ } catch (error) {
2167
+ invalidSchema([`cannot build the schema: ${error instanceof Error ? error.message : String(error)}`]);
2168
+ }
2169
+ const root = (name) => name === void 0 ? void 0 : lookup(name);
2170
+ let schema;
2171
+ try {
2172
+ schema = new GraphQLSchema({
2173
+ query: root(section.roots.query),
2174
+ mutation: root(section.roots.mutation),
2175
+ subscription: root(section.roots.subscription),
2176
+ types: section.types.map((type) => lookup(type.name))
2177
+ });
2178
+ for (const error of validateSchema(schema)) problems.push(error.message);
2179
+ } catch (error) {
2180
+ if (error instanceof ZnightError) throw error;
2181
+ problems.push(error instanceof Error ? error.message : String(error));
2182
+ }
2183
+ if (problems.length > 0 || schema === void 0) invalidSchema(problems);
2184
+ return schema;
2185
+ }
2186
+ function createType(type, fieldConfig, inputValue, lookup) {
2187
+ const description = "description" in type && type.description !== void 0 ? { description: type.description } : {};
2188
+ switch (type.kind) {
2189
+ case "object": return new GraphQLObjectType({
2190
+ name: type.name,
2191
+ ...description,
2192
+ fields: () => Object.fromEntries(type.fields.map((field) => [field.name, fieldConfig(type, field)]))
2193
+ });
2194
+ case "input": return new GraphQLInputObjectType({
2195
+ name: type.name,
2196
+ ...description,
2197
+ fields: () => Object.fromEntries(type.fields.map((field) => [field.name, inputValue(field)]))
2198
+ });
2199
+ case "enum": return new GraphQLEnumType({
2200
+ name: type.name,
2201
+ ...description,
2202
+ values: Object.fromEntries(type.values.map((value) => [value.name, {
2203
+ value: value.value,
2204
+ ...value.description === void 0 ? {} : { description: value.description },
2205
+ ...value.deprecationReason === void 0 ? {} : { deprecationReason: value.deprecationReason }
2206
+ }]))
2207
+ });
2208
+ case "union": return new GraphQLUnionType({
2209
+ name: type.name,
2210
+ ...description,
2211
+ types: () => type.members.map((member) => lookup(member)),
2212
+ resolveType: typeResolver(type.discriminator)
2213
+ });
2214
+ default: {
2215
+ if ("builtin" in type) return builtinScalar(type.name);
2216
+ const scalar = type.scalar;
2217
+ return scalar instanceof GraphQLScalarType ? scalar : definitionScalar(type.name, scalar);
2218
+ }
2219
+ }
2220
+ }
2221
+ // src/transport/abort.ts
2222
+ /** The stand-in passed as graphql-js `abortSignal` by the HTTP transport (one per request). */
2223
+ var DeferredAbort = class {
2224
+ aborted = false;
2225
+ reason = void 0;
2226
+ /** graphql-js registers one listener per execution; more are kept in {@link #others}. */
2227
+ #listener = void 0;
2228
+ #others = void 0;
2229
+ throwIfAborted() {
2230
+ if (this.aborted) throw this.reason;
2231
+ }
2232
+ addEventListener(type, listener) {
2233
+ if (type !== "abort") return;
2234
+ if (this.#listener === void 0) {
2235
+ this.#listener = listener;
2236
+ return;
2237
+ }
2238
+ this.#others ??= [];
2239
+ this.#others.push(listener);
2240
+ }
2241
+ removeEventListener(type, listener) {
2242
+ if (type !== "abort") return;
2243
+ if (this.#listener === listener) {
2244
+ this.#listener = void 0;
2245
+ return;
2246
+ }
2247
+ const others = this.#others;
2248
+ const index = others === void 0 ? -1 : others.indexOf(listener);
2249
+ if (index !== -1) others?.splice(index, 1);
2250
+ }
2251
+ /**
2252
+ * The execution is asynchronous: from now on, `signal` aborting aborts the execution. Reads the
2253
+ * signal only when graphql-js still listens (a pending execution).
2254
+ */
2255
+ follow(read) {
2256
+ if (this.#listener === void 0 && (this.#others === void 0 || this.#others.length === 0)) return;
2257
+ const signal = read();
2258
+ if (signal.aborted) {
2259
+ this.#abort(signal.reason);
2260
+ return;
2261
+ }
2262
+ signal.addEventListener("abort", () => this.#abort(signal.reason), { once: true });
2263
+ }
2264
+ /** This stand-in, typed as the `AbortSignal` graphql-js expects. */
2265
+ asSignal() {
2266
+ return this;
2267
+ }
2268
+ #abort(reason) {
2269
+ if (this.aborted) return;
2270
+ this.aborted = true;
2271
+ this.reason = reason;
2272
+ const listener = this.#listener;
2273
+ const others = this.#others;
2274
+ this.#listener = void 0;
2275
+ this.#others = void 0;
2276
+ listener?.();
2277
+ if (others !== void 0) for (const other of others) other();
2278
+ }
2279
+ };
2280
+ // src/transport/accept.ts
2281
+ /**
2282
+ * Content negotiation of the GraphQL endpoint (RFC 0005 §8.3): which of the media types the
2283
+ * endpoint produces a request accepts best. Import-free.
2284
+ *
2285
+ * `application/json` is the only type matched by wildcards (`*\/*`, `application/*`): the
2286
+ * GraphQL-over-HTTP draft asks for `application/json` when the client accepts anything, and the
2287
+ * other types change the protocol (`application/graphql-response+json` statuses, an event stream,
2288
+ * the GraphiQL page), so they are chosen only when listed. Among types of equal quality,
2289
+ * `application/graphql-response+json` wins, then `application/json`, then HTML (a browser listing
2290
+ * `text/html` first gets GraphiQL). An event stream is chosen only when it is strictly preferred, or
2291
+ * the only acceptable type: a client listing JSON and `text/event-stream` alike (urql, for
2292
+ * incremental delivery) gets its queries and mutations as one JSON body, and its subscriptions as
2293
+ * a stream ({@link STREAMABLE}).
2294
+ */
2295
+ /** What a response is encoded as. */
2296
+ const Media = Object.freeze({
2297
+ EventStream: 0,
2298
+ GraphQLJson: 1,
2299
+ Json: 2,
2300
+ Html: 3,
2301
+ None: 4
2302
+ });
2303
+ /** The media type names, by {@link Media} value (`None` excluded). */
2304
+ const NAMES = [
2305
+ ["text", "event-stream"],
2306
+ ["application", "graphql-response+json"],
2307
+ ["application", "json"],
2308
+ ["text", "html"]
2309
+ ];
2310
+ /** The media ranges of an `accept` header (lower-case, without parameters other than `q`). */
2311
+ function parseRanges(header) {
2312
+ const ranges = [];
2313
+ for (const item of header.split(",")) {
2314
+ const parts = item.split(";");
2315
+ const media = parts[0].trim().toLowerCase();
2316
+ const slash = media.indexOf("/");
2317
+ if (slash <= 0 || slash === media.length - 1) continue;
2318
+ let q = 1;
2319
+ for (let index = 1; index < parts.length; index++) {
2320
+ const parameter = parts[index].trim();
2321
+ if (parameter.length < 2 || (parameter.charCodeAt(0) | 32) !== 113 || parameter[1] !== "=") continue;
2322
+ const value = Number(parameter.slice(2));
2323
+ if (value >= 0 && value <= 1) q = value;
2324
+ }
2325
+ ranges.push({
2326
+ type: media.slice(0, slash),
2327
+ subtype: media.slice(slash + 1),
2328
+ q
2329
+ });
2330
+ }
2331
+ return ranges;
2332
+ }
2333
+ /**
2334
+ * The quality of `type/subtype` in `ranges`: that of the most specific matching range (RFC 9110
2335
+ * §12.5.1), wildcards only when `wildcards` is set; 0 when nothing matches.
2336
+ */
2337
+ function quality(ranges, type, subtype, wildcards) {
2338
+ let best = -1;
2339
+ let specificity = -1;
2340
+ for (const range of ranges) {
2341
+ let level;
2342
+ if (range.type === type && range.subtype === subtype) level = 2;
2343
+ else if (!wildcards) continue;
2344
+ else if (range.type === type && range.subtype === "*") level = 1;
2345
+ else if (range.type === "*" && range.subtype === "*") level = 0;
2346
+ else continue;
2347
+ if (level > specificity || level === specificity && range.q > best) {
2348
+ specificity = level;
2349
+ best = range.q;
2350
+ }
2351
+ }
2352
+ return best < 0 ? 0 : best;
2353
+ }
2354
+ /**
2355
+ * Chooses the media type of a response among `produced` (a bit mask of `1 << Media.X`), per
2356
+ * `accept` (`undefined`: no header, i.e. `application/json`): a {@link MediaKind}, plus
2357
+ * {@link STREAMABLE} when a JSON type wins and an event stream is acceptable too.
2358
+ */
2359
+ function negotiate(accept, produced) {
2360
+ if (accept === void 0) return produced & 1 << Media.Json ? Media.Json : Media.None;
2361
+ const ranges = parseRanges(accept);
2362
+ if (ranges.length === 0) return produced & 1 << Media.Json ? Media.Json : Media.None;
2363
+ let chosen = Media.None;
2364
+ let best = 0;
2365
+ for (let media = Media.GraphQLJson; media < NAMES.length; media++) {
2366
+ if ((produced & 1 << media) === 0) continue;
2367
+ const [type, subtype] = NAMES[media];
2368
+ const q = quality(ranges, type, subtype, media === Media.Json);
2369
+ if (q > best) {
2370
+ best = q;
2371
+ chosen = media;
2372
+ }
2373
+ }
2374
+ if (produced & 1 << Media.EventStream) {
2375
+ const q = quality(ranges, "text", "event-stream", false);
2376
+ if (q > best) return Media.EventStream;
2377
+ if (q > 0 && (chosen === Media.GraphQLJson || chosen === Media.Json)) chosen |= 8;
2378
+ }
2379
+ return chosen;
2380
+ }
2381
+ const CACHE_MAX = 64;
2382
+ /**
2383
+ * {@link negotiate} for one route, memoised by header value: a server sees a handful of `accept`
2384
+ * values, so a request costs a map lookup. Bounded, since clients choose the values: cleared when
2385
+ * full.
2386
+ */
2387
+ var Negotiator = class {
2388
+ #produced;
2389
+ #cache = /* @__PURE__ */ new Map();
2390
+ /** The answer without `accept` header. */
2391
+ #absent;
2392
+ /** `produced`: a bit mask of `1 << Media.X`. */
2393
+ constructor(produced) {
2394
+ this.#produced = produced;
2395
+ this.#absent = negotiate(void 0, produced);
2396
+ }
2397
+ /** The {@link negotiate} answer: a {@link MediaKind}, possibly with {@link STREAMABLE}. */
2398
+ choose(accept) {
2399
+ if (accept === void 0) return this.#absent;
2400
+ let media = this.#cache.get(accept);
2401
+ if (media === void 0) {
2402
+ media = negotiate(accept, this.#produced);
2403
+ if (this.#cache.size === CACHE_MAX) this.#cache.clear();
2404
+ this.#cache.set(accept, media);
2405
+ }
2406
+ return media;
2407
+ }
2408
+ };
2409
+ // src/transport/graphiql.ts
2410
+ /**
2411
+ * The GraphiQL page (RFC 0005 §8.3, §8.9): served on `GET path` with `accept: text/html` when the
2412
+ * `graphiql` option is on (default: `NODE_ENV === 'development'`). React and GraphiQL load from a
2413
+ * pinned CDN with Subresource Integrity; the inline script is fixed at boot and allowed by hash in
2414
+ * the page's own `content-security-policy`, which overrides the strict API default of RFC 0001 on
2415
+ * this response only.
2416
+ */
2417
+ /** jsDelivr. */
2418
+ const GRAPHIQL_CDN = "https://cdn.jsdelivr.net";
2419
+ /**
2420
+ * The assets of the page: GraphiQL 3 (the last major with a UMD build) on React 18. Bumped by hand,
2421
+ * with their SRI hashes (`sha384` of the files on the CDN).
2422
+ */
2423
+ const GRAPHIQL_ASSETS = Object.freeze([
2424
+ {
2425
+ url: `${GRAPHIQL_CDN}/npm/graphiql@3.9.0/graphiql.min.css`,
2426
+ integrity: "sha384-QMux00XgRtwRLSYIY3kw2rj1ovk5AuuliAchk+HSQbqdbGFnz9GYuqIlOqxhwCE2",
2427
+ kind: "style"
2428
+ },
2429
+ {
2430
+ url: `${GRAPHIQL_CDN}/npm/react@18.3.1/umd/react.production.min.js`,
2431
+ integrity: "sha384-DGyLxAyjq0f9SPpVevD6IgztCFlnMF6oW/XQGmfe+IsZ8TqEiDrcHkMLKI6fiB/Z",
2432
+ kind: "script"
2433
+ },
2434
+ {
2435
+ url: `${GRAPHIQL_CDN}/npm/react-dom@18.3.1/umd/react-dom.production.min.js`,
2436
+ integrity: "sha384-gTGxhz21lVGYNMcdJOyq01Edg0jhn/c22nsx0kyqP0TxaV5WVdsSH1fSDUf5YJj1",
2437
+ kind: "script"
2438
+ },
2439
+ {
2440
+ url: `${GRAPHIQL_CDN}/npm/graphiql@3.9.0/graphiql.min.js`,
2441
+ integrity: "sha384-8NGfVj4CVlqHajlZj+bPJT4thxPMHMJYn7DWK2CvtopLd02E7qsPHazniBYvVjOO",
2442
+ kind: "script"
2443
+ }
2444
+ ]);
2445
+ /** `<`, `>` and `&` escaped: JSON safe inside a `<script>` element. */
2446
+ function scriptJson(value) {
2447
+ return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
2448
+ }
2449
+ function escapeHtml(text) {
2450
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2451
+ }
2452
+ /** The GraphiQL page of the endpoint `endpoint` (an absolute path, prefix included) and its CSP. */
2453
+ function renderGraphiQL(endpoint) {
2454
+ 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>`);
2455
+ const inline = [`const fetcher = GraphiQL.createFetcher({ url: ${scriptJson(endpoint)} })`, `ReactDOM.createRoot(document.getElementById('graphiql')).render(React.createElement(GraphiQL, { fetcher }))`].join("\n");
2456
+ const hash = `'sha256-${createHash("sha256").update(inline).digest("base64")}'`;
2457
+ return {
2458
+ html: [
2459
+ "<!doctype html>",
2460
+ "<html lang=\"en\">",
2461
+ "<head>",
2462
+ "<meta charset=\"utf-8\">",
2463
+ "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">",
2464
+ "<title>GraphiQL</title>",
2465
+ "<style>body { margin: 0; } #graphiql { height: 100dvh; }</style>",
2466
+ ...tags.filter((tag) => tag.startsWith("<link")),
2467
+ "</head>",
2468
+ "<body>",
2469
+ "<div id=\"graphiql\">Loading GraphiQL…</div>",
2470
+ ...tags.filter((tag) => tag.startsWith("<script")),
2471
+ `<script>${inline}<\/script>`,
2472
+ "</body>",
2473
+ "</html>",
2474
+ ""
2475
+ ].join("\n"),
2476
+ csp: [
2477
+ "default-src 'none'",
2478
+ `script-src ${GRAPHIQL_CDN} ${hash}`,
2479
+ `style-src 'unsafe-inline' ${GRAPHIQL_CDN}`,
2480
+ `font-src data: ${GRAPHIQL_CDN}`,
2481
+ "img-src 'self' data:",
2482
+ "connect-src 'self'",
2483
+ "base-uri 'none'",
2484
+ "form-action 'none'",
2485
+ "frame-ancestors 'self'"
2486
+ ].join("; ")
2487
+ };
2488
+ }
2489
+ // src/transport/sse.ts
2490
+ /** The last event of a stream that ended normally. */
2491
+ const COMPLETE = Object.freeze({
2492
+ event: "complete",
2493
+ data: ""
2494
+ });
2495
+ const DONE = Object.freeze({
2496
+ done: true,
2497
+ value: void 0
2498
+ });
2499
+ /**
2500
+ * The event source of an operation: one `next` event per result, then `complete` when the
2501
+ * operation ended by itself. A stream ended by `ctx.signal` (client gone, shutdown, `maxDuration`)
2502
+ * gets no `complete`: GraphQL-over-SSE clients then treat it as a lost connection and retry.
2503
+ */
2504
+ var ResultEvents = class {
2505
+ #results;
2506
+ #signal;
2507
+ #done = false;
2508
+ constructor(results, signal) {
2509
+ this.#results = results;
2510
+ this.#signal = signal;
2511
+ }
2512
+ async next() {
2513
+ if (this.#done) return DONE;
2514
+ const step = await this.#results.next();
2515
+ if (step.done !== true) return {
2516
+ done: false,
2517
+ value: {
2518
+ event: "next",
2519
+ data: step.value
2520
+ }
2521
+ };
2522
+ this.#done = true;
2523
+ return this.#signal.aborted ? DONE : {
2524
+ done: false,
2525
+ value: COMPLETE
2526
+ };
2527
+ }
2528
+ async return() {
2529
+ this.#done = true;
2530
+ await this.#results.return();
2531
+ return DONE;
2532
+ }
2533
+ [Symbol.asyncIterator]() {
2534
+ return this;
2535
+ }
2536
+ };
2537
+ // src/transport/http.ts
2538
+ const JSON_TYPE = "application/json; charset=utf-8";
2539
+ const GRAPHQL_JSON_TYPE = "application/graphql-response+json; charset=utf-8";
2540
+ /** Response options of `application/graphql-response+json` replies, by status. */
2541
+ const GRAPHQL_JSON_200 = Object.freeze({
2542
+ status: 200,
2543
+ headers: Object.freeze({ "content-type": GRAPHQL_JSON_TYPE })
2544
+ });
2545
+ const GRAPHQL_JSON_400 = Object.freeze({
2546
+ status: 400,
2547
+ headers: Object.freeze({ "content-type": GRAPHQL_JSON_TYPE })
2548
+ });
2549
+ /** `application/json` is what the serialiser gives an object body: only the status is set. */
2550
+ const JSON_200 = Object.freeze({ status: 200 });
2551
+ const UTF8 = new TextDecoder("utf-8", { fatal: true });
2552
+ /** A request error before execution: `{ errors: [{ message, extensions: { code } }] }`. */
2553
+ function errorBody(message, code = GraphQLErrorCode.BadRequest) {
2554
+ return { errors: [{
2555
+ message,
2556
+ extensions: { code }
2557
+ }] };
2558
+ }
2559
+ /** A reply of `status` carrying a GraphQL error body in the negotiated JSON type (`application/json` before negotiation). */
2560
+ function errorReply(media, status, body, headers) {
2561
+ const type = media === Media.GraphQLJson ? GRAPHQL_JSON_TYPE : JSON_TYPE;
2562
+ return new HttpResponse(body, {
2563
+ status,
2564
+ headers: {
2565
+ ...headers,
2566
+ "content-type": type
2567
+ }
2568
+ });
2569
+ }
2570
+ 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"));
2571
+ const GET_NOT_ALLOWED = errorReply(Media.Json, 405, errorBody("GraphQL operations are sent with POST on this endpoint"), Object.freeze({ allow: "POST" }));
2572
+ /** `content-type` values of JSON bodies seen most often, compared before parsing. */
2573
+ function isJsonRequest(contentType) {
2574
+ if (contentType === "application/json" || contentType === "application/json; charset=utf-8") return true;
2575
+ const media = parseMediaType(contentType);
2576
+ if (media === void 0 || media.type !== "application/json") return false;
2577
+ return media.charset === void 0 || media.charset === "utf-8" || media.charset === "utf8";
2578
+ }
2579
+ /** Why the parameters of a request are invalid, `undefined` when they are valid. */
2580
+ function invalidParams(params) {
2581
+ const query = params["query"];
2582
+ if (typeof query !== "string") return query === void 0 || query === null ? "Missing query: send { query, variables?, operationName?, extensions? }" : "The query must be a string";
2583
+ const operationName = params["operationName"];
2584
+ if (operationName !== void 0 && operationName !== null && typeof operationName !== "string") return "operationName must be a string";
2585
+ if (!isMapOrNull(params["variables"])) return "variables must be an object";
2586
+ if (!isMapOrNull(params["extensions"])) return "extensions must be an object";
2587
+ }
2588
+ /** The parameters of a GraphQL `GET` request. */
2589
+ const GET_PARAMS = [
2590
+ "query",
2591
+ "operationName",
2592
+ "variables",
2593
+ "extensions"
2594
+ ];
2595
+ /** A query string component decoded like `URLSearchParams` (`+` is a space); `undefined` when malformed. */
2596
+ function decodeComponent(text) {
2597
+ if (text.indexOf("%") === -1 && text.indexOf("+") === -1) return text;
2598
+ try {
2599
+ return decodeURIComponent(text.replaceAll("+", " "));
2600
+ } catch {
2601
+ return;
2602
+ }
2603
+ }
2604
+ /**
2605
+ * The GraphQL parameters of a request target, read directly: `ctx.query` builds a record of every
2606
+ * parameter through `URLSearchParams` (about 0.35 µs of a `GET` on Node.js, 0.15 µs on Bun, see
2607
+ * `test/bench/http.bench.ts`). A repeated parameter
2608
+ * becomes an array, like `ctx.query`. `undefined` when a component is malformed percent-encoding:
2609
+ * the caller falls back to `ctx.query` (which keeps such sequences as they are).
2610
+ */
2611
+ function readGetParams(url) {
2612
+ const params = {};
2613
+ const mark = url.indexOf("?");
2614
+ if (mark === -1) return params;
2615
+ const length = url.length;
2616
+ let start = mark + 1;
2617
+ while (start < length) {
2618
+ let end = url.indexOf("&", start);
2619
+ if (end === -1) end = length;
2620
+ if (end > start) {
2621
+ let equals = url.indexOf("=", start);
2622
+ if (equals === -1 || equals > end) equals = end;
2623
+ const name = decodeComponent(url.slice(start, equals));
2624
+ if (name === void 0) return void 0;
2625
+ if (name === "query" || name === "operationName" || name === "variables" || name === "extensions") {
2626
+ const value = equals === end ? "" : decodeComponent(url.slice(equals + 1, end));
2627
+ if (value === void 0) return void 0;
2628
+ const existing = params[name];
2629
+ if (existing === void 0) params[name] = value;
2630
+ else params[name] = typeof existing === "string" ? [existing, value] : [...existing, value];
2631
+ }
2632
+ }
2633
+ start = end + 1;
2634
+ }
2635
+ return params;
2636
+ }
2637
+ function isMapOrNull(value) {
2638
+ return value === void 0 || value === null || typeof value === "object" && !Array.isArray(value);
2639
+ }
2640
+ function isPromise$1(value) {
2641
+ return typeof value?.then === "function";
2642
+ }
2643
+ /** The handlers of the GraphQL endpoint. */
2644
+ var HttpTransport = class {
2645
+ #resolveExecutor;
2646
+ #executor;
2647
+ #getQueries;
2648
+ #sse;
2649
+ #post;
2650
+ #get;
2651
+ /** The GraphiQL page, `undefined` when disabled. */
2652
+ #graphiql;
2653
+ constructor(options) {
2654
+ this.#resolveExecutor = options.executor;
2655
+ this.#getQueries = options.get;
2656
+ this.#sse = options.sse;
2657
+ const json = 1 << Media.GraphQLJson | 1 << Media.Json | (options.sse === false ? 0 : 1 << Media.EventStream);
2658
+ this.#post = new Negotiator(json);
2659
+ this.#get = new Negotiator(options.graphiql ? json | 1 << Media.Html : json);
2660
+ if (options.graphiql) {
2661
+ const page = renderGraphiQL(options.endpoint);
2662
+ this.#graphiql = new HttpResponse(new TextEncoder().encode(page.html), {
2663
+ status: 200,
2664
+ headers: {
2665
+ "content-type": "text/html; charset=utf-8",
2666
+ "content-security-policy": page.csp,
2667
+ "cache-control": "no-store"
2668
+ }
2669
+ });
2670
+ }
2671
+ }
2672
+ /** `POST path`. */
2673
+ post = (ctx) => {
2674
+ const headers = ctx.headers;
2675
+ const negotiated = this.#post.choose(headers["accept"]);
2676
+ const media = negotiated & 7;
2677
+ if (media === Media.None) return NOT_ACCEPTABLE;
2678
+ if (!isJsonRequest(headers["content-type"])) return errorReply(media, 415, errorBody("Send GraphQL requests as application/json (content-type: application/json)"));
2679
+ const encoding = headers["content-encoding"];
2680
+ if (encoding !== void 0 && encoding.trim().toLowerCase() !== "identity" && encoding.trim() !== "") return errorReply(media, 415, errorBody(`Unsupported content encoding "${encoding}"`));
2681
+ return ctx.rawBody().then((bytes) => this.#posted(ctx, bytes, media, negotiated >= 8), (error) => transportError(error, media));
2682
+ };
2683
+ /** `GET path`. */
2684
+ get = (ctx) => {
2685
+ const media = this.#get.choose(ctx.headers["accept"]) & 7;
2686
+ if (media === Media.Html) return this.#graphiql;
2687
+ if (media === Media.None) return NOT_ACCEPTABLE;
2688
+ if (!this.#getQueries) return GET_NOT_ALLOWED;
2689
+ const params = readGetParams(ctx.url) ?? ctx.query;
2690
+ const request = {};
2691
+ for (const name of GET_PARAMS) {
2692
+ const value = params[name];
2693
+ if (value === void 0) continue;
2694
+ if (typeof value !== "string") return errorReply(media, 400, errorBody(`Repeated ${name} parameter`));
2695
+ if (name === "variables" || name === "extensions") try {
2696
+ request[name] = JSON.parse(value);
2697
+ } catch {
2698
+ return errorReply(media, 400, errorBody(`The ${name} parameter must be JSON`));
2699
+ }
2700
+ else request[name] = value;
2701
+ }
2702
+ const invalid = invalidParams(request);
2703
+ if (invalid !== void 0) return errorReply(media, 400, errorBody(invalid));
2704
+ if (media === Media.EventStream) return this.#stream(ctx, request, true);
2705
+ return this.#execute(ctx, request, media, true, false);
2706
+ };
2707
+ /** A `POST` body read: parsed, checked, executed or streamed. */
2708
+ #posted(ctx, bytes, media, streamable) {
2709
+ if (bytes.byteLength === 0) return errorReply(media, 400, errorBody("Missing request body: send { query, variables?, operationName? }"));
2710
+ let body;
2711
+ try {
2712
+ body = JSON.parse(UTF8.decode(bytes));
2713
+ } catch {
2714
+ return errorReply(media, 400, errorBody("Malformed JSON body"));
2715
+ }
2716
+ if (body === null || typeof body !== "object") return errorReply(media, 400, errorBody("The body must be a JSON object: { query, variables?, operationName? }"));
2717
+ if (Array.isArray(body)) return errorReply(media, 400, errorBody("Operation batching is not supported: send one operation per request"));
2718
+ const invalid = invalidParams(body);
2719
+ if (invalid !== void 0) return errorReply(media, 400, errorBody(invalid));
2720
+ if (media === Media.EventStream) return this.#stream(ctx, body, false);
2721
+ return this.#execute(ctx, body, media, false, streamable);
2722
+ }
2723
+ /**
2724
+ * Executes a query or a mutation; synchronous while the execution is. A subscription is streamed
2725
+ * when the client accepts an event stream (`streamable`), a 400 otherwise.
2726
+ */
2727
+ #execute(ctx, request, media, get, streamable) {
2728
+ const executor = this.#executor ?? this.#bind();
2729
+ const abort = new DeferredAbort();
2730
+ const options = {
2731
+ transport: "http",
2732
+ http: ctx,
2733
+ abortSignal: abort.asSignal(),
2734
+ queriesOnly: get,
2735
+ streamSubscriptions: streamable
2736
+ };
2737
+ const response = executor.execute(request, options);
2738
+ if (response === SUBSCRIBE) return this.#stream(ctx, request, get);
2739
+ if (!isPromise$1(response)) return reply(response, media, get);
2740
+ abort.follow(() => ctx.signal);
2741
+ return response.then((settled) => reply(settled, media, get), (error) => {
2742
+ if (!abort.aborted || error !== abort.reason) throw error;
2743
+ return errorReply(media, 503, errorBody("The request was aborted", GraphQLErrorCode.ServiceUnavailable));
2744
+ });
2745
+ }
2746
+ /** GraphQL over SSE: any operation, its results as `next` events. */
2747
+ #stream(ctx, request, get) {
2748
+ const executor = this.#executor ?? this.#bind();
2749
+ const signal = ctx.signal;
2750
+ const options = {
2751
+ transport: "sse",
2752
+ http: ctx,
2753
+ signal,
2754
+ queriesOnly: get
2755
+ };
2756
+ return executor.subscribe(request, options).then((outcome) => {
2757
+ if ("status" in outcome) return errorReply(Media.Json, outcome.status, outcome.result, outcome.headers);
2758
+ return HttpResponse.sse(new ResultEvents(outcome, signal), this.#sse);
2759
+ }, (error) => {
2760
+ if (!signal.aborted || error !== signal.reason) throw error;
2761
+ return errorReply(Media.Json, 503, errorBody("The request was aborted", GraphQLErrorCode.ServiceUnavailable));
2762
+ });
2763
+ }
2764
+ #bind() {
2765
+ this.#executor = this.#resolveExecutor();
2766
+ return this.#executor;
2767
+ }
2768
+ };
2769
+ /**
2770
+ * The HTTP reply of an executor response. `application/json` answers 200 to every well-formed
2771
+ * request (request errors included); `application/graphql-response+json` keeps the status.
2772
+ */
2773
+ function reply(response, media, get) {
2774
+ const status = response.status;
2775
+ const result = response.result;
2776
+ if (media === Media.Json) {
2777
+ if (status === 200 || status === 400) return get ? result : new HttpResponse(result, JSON_200);
2778
+ return errorReply(media, status, result, response.headers);
2779
+ }
2780
+ if (response.headers === void 0) {
2781
+ if (status === 200) return new HttpResponse(result, GRAPHQL_JSON_200);
2782
+ if (status === 400) return new HttpResponse(result, GRAPHQL_JSON_400);
2783
+ }
2784
+ return errorReply(media, status, result, response.headers);
2785
+ }
2786
+ /** A failure reading the body (413 past the body limit, 400 for an aborted body) with a GraphQL error body. */
2787
+ function transportError(error, media) {
2788
+ if (!isHttpException(error)) throw error;
2789
+ return errorReply(media, error.status, errorBody(error.message));
2790
+ }
2791
+ /** Checks the SSE options once (`HttpResponse.sse()` validates them): `undefined` when they are valid. */
2792
+ function checkSseOptions(options) {
2793
+ try {
2794
+ HttpResponse.sse([], options);
2795
+ return;
2796
+ } catch (error) {
2797
+ return error instanceof Error ? error.message : String(error);
2798
+ }
2799
+ }
2800
+ // src/plugin.ts
2801
+ let readExecutor;
2802
+ let readEndpoint;
2803
+ /**
2804
+ * The executor of a bootstrapped plugin: the internal API of the transports (statuses, streams).
2805
+ * Not exported from the package entry.
2806
+ */
2807
+ function executorOf(plugin) {
2808
+ return readExecutor(plugin);
2809
+ }
2810
+ /**
2811
+ * The absolute path of the GraphQL-over-HTTP endpoint (prefix included), `undefined` without HTTP
2812
+ * transport (`http: false`, or before `HttpPlugin` collected the routes). Not exported from the
2813
+ * package entry: `@znight/graphql/testing` sends its requests there.
2814
+ */
2815
+ function endpointOf(plugin) {
2816
+ return readEndpoint(plugin);
2817
+ }
2818
+ /**
2819
+ * The GraphQL plugin: `GraphQLPlugin.configure(options)` (name `graphql`).
2820
+ *
2821
+ * ```ts
2822
+ * await Application.create({
2823
+ * metadata: __ZNIGHT_METADATA__,
2824
+ * plugins: [HttpPlugin.configure(), GraphQLPlugin.configure({ guards: [AccessGuard] })],
2825
+ * })
2826
+ * ```
2827
+ */
2828
+ var GraphQLPlugin = class GraphQLPlugin {
2829
+ name = "graphql";
2830
+ #options;
2831
+ #resolved;
2832
+ #container;
2833
+ #metadata;
2834
+ #logger;
2835
+ #hooks;
2836
+ #pubsub;
2837
+ #ownPubSub;
2838
+ #runtime;
2839
+ #stop;
2840
+ #endpoint;
2841
+ #active = false;
2842
+ constructor(options) {
2843
+ this.#options = options;
2844
+ }
2845
+ static {
2846
+ readExecutor = (plugin) => plugin.#ready().executor;
2847
+ readEndpoint = (plugin) => plugin.#endpoint;
2848
+ }
2849
+ /**
2850
+ * Creates the plugin. `options` may be a function of the container, evaluated once, when first
2851
+ * needed after the container is initialised: `GraphQLPlugin.configure(c => ({ exposeErrors: c.get(Env).dev }))`.
2852
+ */
2853
+ static configure(options = {}) {
2854
+ return new GraphQLPlugin(options);
2855
+ }
2856
+ /**
2857
+ * Providers `TestApp` creates at boot: the classes of the `guards` and `errorMappers` options, and
2858
+ * the resolvers, guards and loaders of the metadata.
2859
+ */
2860
+ initRoots(context) {
2861
+ const roots = /* @__PURE__ */ new Set();
2862
+ try {
2863
+ const options = this.#options;
2864
+ if (options !== null && typeof options === "object") for (const key of ["guards", "errorMappers"]) {
2865
+ const refs = options[key];
2866
+ if (Array.isArray(refs)) {
2867
+ for (const ref of refs) if (typeof ref === "function") roots.add(ref);
2868
+ }
2869
+ }
2870
+ const types = (context?.metadata.extensions?.graphql)?.types;
2871
+ if (Array.isArray(types)) for (const type of types) {
2872
+ if (!Array.isArray(type?.fields)) continue;
2873
+ for (const field of type.fields) {
2874
+ const binding = field?.resolver;
2875
+ if (binding === void 0 || typeof binding.token !== "string") continue;
2876
+ roots.add(binding.token);
2877
+ for (const token of [...binding.guards ?? [], ...binding.loaders ?? []]) if (typeof token === "string") roots.add(token);
2878
+ }
2879
+ }
2880
+ } catch {}
2881
+ return [...roots];
2882
+ }
2883
+ register(ctx) {
2884
+ if (this.#active) throw new ZnightError(ErrorCode.DuplicateProvider, "This GraphQLPlugin is already used by another application", { hints: ["Call GraphQLPlugin.configure() once per application"] });
2885
+ this.#resolved = void 0;
2886
+ this.#runtime = void 0;
2887
+ this.#ownPubSub = void 0;
2888
+ this.#endpoint = void 0;
2889
+ this.#container = ctx.container;
2890
+ this.#metadata = ctx.metadata;
2891
+ this.#logger = ctx.logger;
2892
+ this.#hooks = ctx.hooks;
2893
+ if (typeof this.#options !== "function") this.#checkHttp(ctx, this.#resolve());
2894
+ this.#active = true;
2895
+ this.#stop = new AbortController();
2896
+ ctx.container.registerExternal({
2897
+ token: GraphQLTokens.Context,
2898
+ scope: "request",
2899
+ providedBy: "GraphQLPlugin"
2900
+ });
2901
+ ctx.container.registerValue(GraphQLTokens.PubSub, typeof this.#options === "function" ? new DeferredPubSub(() => this.pubsub) : this.pubsub);
2902
+ ctx.container.registerValue(GraphQLTokens.Plugin, this);
2903
+ }
2904
+ /**
2905
+ * The `PluginHttpRoutes` contract of `@znight/http`: the GraphQL endpoint (`GET` and `POST` on
2906
+ * `path`). `[]` with `http: false`.
2907
+ */
2908
+ httpRoutes(context) {
2909
+ this.#container ??= context.container;
2910
+ const options = this.#resolve();
2911
+ const http = options.http;
2912
+ if (http === false) return [];
2913
+ const endpoint = endpointPath(context.prefix, options.path);
2914
+ const transport = new HttpTransport({
2915
+ executor: () => this.#ready().executor,
2916
+ endpoint,
2917
+ get: http.get,
2918
+ graphiql: options.graphiql,
2919
+ sse: sseOptions(options)
2920
+ });
2921
+ this.#endpoint = endpoint;
2922
+ const common = {
2923
+ name: "handle",
2924
+ path: options.path,
2925
+ ...http.meta.length === 0 ? {} : { meta: http.meta },
2926
+ ...http.guards.length === 0 ? {} : { guards: http.guards }
2927
+ };
2928
+ const routes = [{
2929
+ ...common,
2930
+ method: "POST",
2931
+ handler: transport.post,
2932
+ bodyLimit: http.bodyLimit
2933
+ }];
2934
+ if (http.get || options.graphiql) routes.unshift({
2935
+ ...common,
2936
+ method: "GET",
2937
+ handler: transport.get
2938
+ });
2939
+ return routes;
2940
+ }
2941
+ onBootstrap(ctx) {
2942
+ this.#container = ctx.container;
2943
+ const options = this.#resolve();
2944
+ if (typeof this.#options === "function") this.#checkHttp(ctx, options);
2945
+ this.#checkSse(options);
2946
+ this.#runtime = this.#build(ctx, options);
2947
+ }
2948
+ onStart(ctx) {
2949
+ const runtime = this.#runtime;
2950
+ const options = this.#resolved;
2951
+ if (runtime === void 0 || options === void 0) return;
2952
+ const details = {
2953
+ types: runtime.section.types.length,
2954
+ schemaHash: runtime.section.schemaHash
2955
+ };
2956
+ if (options.http === false) ctx.logger.info("GraphQL schema ready", details);
2957
+ else ctx.logger.info("GraphQL endpoint ready", {
2958
+ path: options.path,
2959
+ ...details
2960
+ });
2961
+ }
2962
+ async onStop() {
2963
+ this.#stop?.abort(/* @__PURE__ */ new Error("The application is stopping"));
2964
+ await this.#runtime?.executor.stopDirect();
2965
+ }
2966
+ onDispose() {
2967
+ this.#active = false;
2968
+ this.#ownPubSub?.close();
2969
+ this.#ownPubSub = void 0;
2970
+ this.#pubsub = void 0;
2971
+ this.#runtime?.executor.clear();
2972
+ this.#runtime = void 0;
2973
+ this.#resolved = void 0;
2974
+ }
2975
+ /** The schema (after bootstrap). */
2976
+ get schema() {
2977
+ return this.#ready().schema;
2978
+ }
2979
+ /** The SDL of the schema (graphql-js `printSchema`). */
2980
+ printSchema() {
2981
+ return printSchema(this.#ready().schema);
2982
+ }
2983
+ /** The `PubSub` of the application: the `pubsub` option, or the plugin's `MemoryPubSub`. */
2984
+ get pubsub() {
2985
+ let pubsub = this.#pubsub;
2986
+ if (pubsub === void 0) {
2987
+ const options = this.#resolve();
2988
+ if (options.pubsub === void 0) {
2989
+ this.#ownPubSub = new MemoryPubSub({ bufferSize: options.bufferSize });
2990
+ pubsub = this.#ownPubSub;
2991
+ } else pubsub = options.pubsub;
2992
+ this.#pubsub = pubsub;
2993
+ }
2994
+ return pubsub;
2995
+ }
2996
+ /** Active subscriptions (every transport). */
2997
+ get activeSubscriptions() {
2998
+ return this.#runtime?.executor.activeSubscriptions ?? 0;
2999
+ }
3000
+ /**
3001
+ * Executes a query or a mutation outside of HTTP (custom transports, tests, a controller calling
3002
+ * GraphQL), in the current request scope or in a scope of its own. Returns the result directly
3003
+ * when every resolver is synchronous; `await` it. A subscription is an error result: use
3004
+ * {@link subscribe}. Rejects only when `options.signal` aborts the execution.
3005
+ */
3006
+ execute(request, options) {
3007
+ const response = this.#ready().executor.execute(request, direct(options));
3008
+ return isPromise(response) ? response.then((settled) => settled.result) : response.result;
3009
+ }
3010
+ /**
3011
+ * Subscribes outside of HTTP: the results of the subscription, one per event, until the source
3012
+ * ends, `options.signal` aborts, `return()` is called or the application stops. A request error
3013
+ * or a query gives a stream of one result.
3014
+ */
3015
+ async subscribe(request, options) {
3016
+ const response = await this.#ready().executor.subscribe(request, direct(options));
3017
+ return "status" in response ? singleResult(response.result) : response;
3018
+ }
3019
+ #ready() {
3020
+ const runtime = this.#runtime;
3021
+ if (runtime === void 0) throw new ZnightError(ErrorCode.LifecycleFailed, "GraphQLPlugin is not bootstrapped yet", { hints: ["The schema and execute() are available once Application.create() resolved"] });
3022
+ return runtime;
3023
+ }
3024
+ #resolve() {
3025
+ if (this.#resolved !== void 0) return this.#resolved;
3026
+ const container = this.#container;
3027
+ if (container === void 0 && typeof this.#options === "function") throw new ZnightError(ErrorCode.LifecycleFailed, "GraphQLPlugin: the options are not available before register()");
3028
+ this.#resolved = resolveGraphQLOptions(resolveOptions(this.#options, container));
3029
+ return this.#resolved;
3030
+ }
3031
+ /** The SSE options are those of `HttpResponse.sse()` (heartbeat and maxDuration from 1 000 ms): checked at boot. */
3032
+ #checkSse(options) {
3033
+ const sse = sseOptions(options);
3034
+ if (sse === false) return;
3035
+ const problem = checkSseOptions(sse);
3036
+ if (problem !== void 0) throw invalidOption("subscriptions", `valid event stream options (${problem})`, sse);
3037
+ }
3038
+ #checkHttp(ctx, options) {
3039
+ 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"] });
3040
+ }
3041
+ #build(ctx, options) {
3042
+ const container = ctx.container;
3043
+ const metadata = ctx.metadata;
3044
+ const section = graphqlSection(metadata);
3045
+ const providers = new Providers(container, metadata);
3046
+ const globalGuards = options.guards.map((ref) => providers.ref(ref, "guard", "canResolve"));
3047
+ const mappers = options.errorMappers.map((ref) => providers.ref(ref, "error mapper", "map"));
3048
+ const inputs = /* @__PURE__ */ new Map();
3049
+ const fields = [];
3050
+ const types = Array.isArray(section?.types) ? section.types : [];
3051
+ for (const type of types) {
3052
+ if (type?.kind === "input") inputs.set(type.name, type);
3053
+ if (type?.kind !== "object" || !Array.isArray(type.fields)) continue;
3054
+ for (const field of type.fields) {
3055
+ const binding = field?.resolver;
3056
+ if (binding === void 0 || binding === null || typeof binding !== "object") continue;
3057
+ const root = binding.operation !== "field";
3058
+ const guardRefs = Array.isArray(binding.guards) ? binding.guards : [];
3059
+ fields.push({
3060
+ description: `${type.name}.${field.name} (${displayName(String(binding.token))}.${String(binding.method)})`,
3061
+ root,
3062
+ meta: binding.meta ?? {},
3063
+ guards: [...globalGuardsApply(binding) ? globalGuards.map((guard) => ({
3064
+ cls: guard.cls,
3065
+ name: guard.name
3066
+ })) : [], ...guardRefs.map((token) => ({
3067
+ cls: metadata.providers[token]?.class,
3068
+ name: displayName(token)
3069
+ }))]
3070
+ });
3071
+ }
3072
+ }
3073
+ const schema = buildGraphQLSchema(section, pipelineFactory({
3074
+ container,
3075
+ globalGuards: globalGuards.map((guard) => guard.resolvable),
3076
+ guard: (token, coordinate) => providers.token(token, `guard of ${coordinate}`, "canResolve"),
3077
+ resolver: (binding, coordinate) => providers.token(binding.token, `resolver of ${coordinate}`, binding.method),
3078
+ inputs
3079
+ }));
3080
+ checkFieldDecisions(fields, section.groups, options.enforcement, ctx.logger);
3081
+ const formatter = new ErrorFormatter({
3082
+ container,
3083
+ mappers: mappers.map((mapper) => mapper.resolvable),
3084
+ exposeErrors: options.exposeErrors,
3085
+ logger: ctx.logger,
3086
+ schema
3087
+ });
3088
+ const executor = new GraphQLExecutor({
3089
+ schema,
3090
+ container,
3091
+ hooks: ctx.hooks,
3092
+ logger: ctx.logger,
3093
+ formatter,
3094
+ runtime: {
3095
+ container,
3096
+ loaders: new LoaderFactory(container),
3097
+ signal: this.#stop.signal
3098
+ },
3099
+ limits: options.limits,
3100
+ introspection: options.introspection,
3101
+ documentCache: options.documentCache,
3102
+ maxActiveSubscriptions: options.subscriptions === false ? void 0 : options.subscriptions.maxActive
3103
+ });
3104
+ ctx.logger.debug("GraphQL schema built", {
3105
+ types: types.length,
3106
+ schemaHash: section.schemaHash
3107
+ });
3108
+ return {
3109
+ schema,
3110
+ section,
3111
+ executor
3112
+ };
3113
+ }
3114
+ };
3115
+ function direct(options) {
3116
+ if (options === void 0) return DIRECT;
3117
+ return {
3118
+ transport: "direct",
3119
+ ...options.signal === void 0 ? {} : { signal: options.signal },
3120
+ ...options.requestId === void 0 ? {} : { requestId: options.requestId }
3121
+ };
3122
+ }
3123
+ const DIRECT = Object.freeze({ transport: "direct" });
3124
+ /** `prefix` (`''`, `/api`) joined with `path` (`/graphql`), without trailing slash: `/api/graphql`. */
3125
+ function endpointPath(prefix, path) {
3126
+ const joined = `${prefix.replace(/\/+$/, "")}/${path.replace(/^\/+|\/+$/g, "")}`;
3127
+ return joined.length > 1 && joined.endsWith("/") ? joined.slice(0, -1) : joined;
3128
+ }
3129
+ /** The `HttpResponse.sse()` options of GraphQL over SSE (`subscriptions`), `false` when disabled. */
3130
+ function sseOptions(options) {
3131
+ const subscriptions = options.subscriptions;
3132
+ if (subscriptions === false) return false;
3133
+ return Object.freeze({
3134
+ ...subscriptions.heartbeat === void 0 ? {} : { heartbeat: subscriptions.heartbeat },
3135
+ ...subscriptions.maxDuration === void 0 ? {} : { maxDuration: subscriptions.maxDuration }
3136
+ });
3137
+ }
3138
+ function isPromise(value) {
3139
+ return typeof value?.then === "function";
3140
+ }
3141
+ /**
3142
+ * Resolves providers once at bootstrap: singletons (and registered values) to their instance,
3143
+ * request-scoped and transient providers to their token, resolved per use in the current scope.
3144
+ */
3145
+ var Providers = class {
3146
+ #container;
3147
+ #metadata;
3148
+ #resolvables = /* @__PURE__ */ new Map();
3149
+ constructor(container, metadata) {
3150
+ this.#container = container;
3151
+ this.#metadata = metadata;
3152
+ }
3153
+ /** A provider token (`@UseGuard`, a resolver), checked to have `method` when its class is known. */
3154
+ token(token, what, method) {
3155
+ const cls = Object.hasOwn(this.#metadata.providers, token) ? this.#metadata.providers[token]?.class : void 0;
3156
+ if (typeof cls === "function" && typeof cls.prototype?.[method] !== "function") throw new ZnightError(ErrorCode.InvalidGraphQLSetup, `GraphQLPlugin: the ${what} (${displayName(token)}) has no ${method}() method`, { hints: method === "canResolve" ? ["A GraphQL guard implements GraphQLGuard: canResolve(ctx: ResolverContext)"] : [] });
3157
+ return this.#resolvable(token, what);
3158
+ }
3159
+ /** A global ref: a provider class or an instance with `method`. */
3160
+ ref(ref, what, method) {
3161
+ if (typeof ref === "function") {
3162
+ const token = this.#container.tokenOf(ref);
3163
+ if (token === void 0) {
3164
+ const name = ref.name || "<anonymous>";
3165
+ throw new ZnightError(ErrorCode.MissingProvider, `GraphQLPlugin: the global ${what} ${name} is not a provider`, { hints: ["Add @Injectable() to the class, or pass an instance"] });
3166
+ }
3167
+ if (typeof ref.prototype?.[method] !== "function") throw invalidOption(`${what}s`, `classes with a ${method}() method`, ref);
3168
+ return {
3169
+ resolvable: this.#resolvable(token, `global ${what}`),
3170
+ cls: ref,
3171
+ name: displayName(token)
3172
+ };
3173
+ }
3174
+ const cls = ref.constructor;
3175
+ const name = typeof cls === "function" && cls !== Object ? cls.name : void 0;
3176
+ return {
3177
+ resolvable: { instance: ref },
3178
+ cls,
3179
+ name: name || `${what} instance`
3180
+ };
3181
+ }
3182
+ #resolvable(token, what) {
3183
+ let found = this.#resolvables.get(token);
3184
+ if (found === void 0) {
3185
+ found = this.#create(token, what);
3186
+ this.#resolvables.set(token, found);
3187
+ }
3188
+ return found;
3189
+ }
3190
+ #create(token, what) {
3191
+ const container = this.#container;
3192
+ if (!container.has(token)) throw new ZnightError(ErrorCode.MissingProvider, `GraphQLPlugin: the ${what} (${token}) is not a provider`, { hints: ["Add @Injectable() to the class (resolvers are providers through @Resolver())"] });
3193
+ const scope = Object.hasOwn(this.#metadata.providers, token) ? this.#metadata.providers[token]?.scope : void 0;
3194
+ if (scope === "request" || scope === "transient") return { token };
3195
+ try {
3196
+ return { instance: container.get(token) };
3197
+ } catch (error) {
3198
+ if (error?.code === ErrorCode.NoActiveScope) return { token };
3199
+ throw error;
3200
+ }
3201
+ }
3202
+ };
3203
+ /**
3204
+ * The `PubSub` registered when the options are a function of the container (they are only known
3205
+ * after `container.init()`): delegates to the plugin's `PubSub`, resolved on first use.
3206
+ */
3207
+ var DeferredPubSub = class extends PubSub {
3208
+ #target;
3209
+ constructor(target) {
3210
+ super();
3211
+ this.#target = target;
3212
+ }
3213
+ publish(topic, payload) {
3214
+ return this.#target().publish(topic, payload);
3215
+ }
3216
+ subscribe(topic, options) {
3217
+ return this.#target().subscribe(topic, options);
3218
+ }
3219
+ };
3220
+ export { MemoryPubSub as a, GraphQLErrorCode as c, GraphQLTokens as i, GraphQLException as l, endpointOf as n, PubSub as o, executorOf as r, orderByKeys as s, GraphQLPlugin as t };
3221
+
3222
+ //# sourceMappingURL=plugin-BdBxr5NQ.mjs.map