@leaven-graphql/plugins 0.1.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.
package/dist/index.js CHANGED
@@ -1,20 +1,49 @@
1
1
  // @bun
2
2
  // src/manager.ts
3
+ var HOOK_NAMES = [
4
+ "beforeParse",
5
+ "afterParse",
6
+ "beforeValidate",
7
+ "afterValidate",
8
+ "beforeExecute",
9
+ "afterExecute",
10
+ "onError"
11
+ ];
12
+
3
13
  class PluginManager {
4
14
  schema;
5
15
  plugins;
6
16
  pluginOrder;
17
+ hookPlugins;
18
+ pendingOnRegister;
19
+ initPromise;
20
+ initialized;
7
21
  constructor(config) {
8
22
  this.schema = config.schema;
9
23
  this.plugins = new Map;
10
24
  this.pluginOrder = [];
25
+ this.hookPlugins = {
26
+ beforeParse: [],
27
+ afterParse: [],
28
+ beforeValidate: [],
29
+ afterValidate: [],
30
+ beforeExecute: [],
31
+ afterExecute: [],
32
+ onError: []
33
+ };
34
+ this.pendingOnRegister = [];
35
+ this.initPromise = undefined;
11
36
  if (config.plugins) {
12
37
  for (const plugin of config.plugins) {
13
- this.register(plugin);
38
+ this.addPlugin(plugin);
39
+ if (plugin.onRegister) {
40
+ this.pendingOnRegister.push(plugin);
41
+ }
14
42
  }
15
43
  }
44
+ this.initialized = this.pendingOnRegister.length === 0;
16
45
  }
17
- async register(plugin) {
46
+ addPlugin(plugin) {
18
47
  const { name } = plugin.metadata;
19
48
  if (this.plugins.has(name)) {
20
49
  throw new Error(`Plugin "${name}" is already registered`);
@@ -28,9 +57,77 @@ class PluginManager {
28
57
  }
29
58
  this.plugins.set(name, plugin);
30
59
  this.pluginOrder.push(name);
31
- await plugin.onRegister?.(this.schema);
60
+ for (const hook of HOOK_NAMES) {
61
+ if (plugin[hook]) {
62
+ this.hookPlugins[hook].push(plugin);
63
+ }
64
+ }
65
+ }
66
+ async init() {
67
+ if (this.initialized) {
68
+ return;
69
+ }
70
+ const attempt = this.initPromise ??= this.runPendingOnRegister();
71
+ try {
72
+ await attempt;
73
+ } catch (error) {
74
+ if (this.initPromise === attempt) {
75
+ this.initPromise = undefined;
76
+ }
77
+ throw error;
78
+ }
79
+ }
80
+ async runPendingOnRegister() {
81
+ const pending = this.pendingOnRegister;
82
+ this.pendingOnRegister = [];
83
+ const registered = [];
84
+ for (const plugin of pending) {
85
+ try {
86
+ await plugin.onRegister?.(this.schema);
87
+ } catch (error) {
88
+ for (let i = registered.length - 1;i >= 0; i--) {
89
+ try {
90
+ await registered[i].onUnregister?.();
91
+ } catch {}
92
+ }
93
+ this.pendingOnRegister = pending;
94
+ throw new Error("Plugin initialization failed", { cause: error });
95
+ }
96
+ registered.push(plugin);
97
+ }
98
+ this.initialized = true;
99
+ }
100
+ removePlugin(plugin) {
101
+ const { name } = plugin.metadata;
102
+ this.plugins.delete(name);
103
+ const index = this.pluginOrder.indexOf(name);
104
+ if (index !== -1) {
105
+ this.pluginOrder.splice(index, 1);
106
+ }
107
+ for (const hook of HOOK_NAMES) {
108
+ const list = this.hookPlugins[hook];
109
+ const hookIndex = list.indexOf(plugin);
110
+ if (hookIndex !== -1) {
111
+ list.splice(hookIndex, 1);
112
+ }
113
+ }
114
+ }
115
+ async register(plugin) {
116
+ if (!this.initialized) {
117
+ await this.init();
118
+ }
119
+ this.addPlugin(plugin);
120
+ try {
121
+ await plugin.onRegister?.(this.schema);
122
+ } catch (error) {
123
+ this.removePlugin(plugin);
124
+ throw error;
125
+ }
32
126
  }
33
127
  async unregister(name) {
128
+ if (!this.initialized) {
129
+ await this.init();
130
+ }
34
131
  const plugin = this.plugins.get(name);
35
132
  if (!plugin) {
36
133
  return false;
@@ -41,11 +138,7 @@ class PluginManager {
41
138
  }
42
139
  }
43
140
  await plugin.onUnregister?.();
44
- this.plugins.delete(name);
45
- const index = this.pluginOrder.indexOf(name);
46
- if (index !== -1) {
47
- this.pluginOrder.splice(index, 1);
48
- }
141
+ this.removePlugin(plugin);
49
142
  return true;
50
143
  }
51
144
  get(name) {
@@ -66,78 +159,82 @@ class PluginManager {
66
159
  };
67
160
  }
68
161
  async beforeParse(query, context) {
162
+ if (!this.initialized) {
163
+ await this.init();
164
+ }
69
165
  let result = query;
70
- for (const name of this.pluginOrder) {
71
- const plugin = this.plugins.get(name);
72
- if (plugin.beforeParse) {
73
- const modified = await plugin.beforeParse(result, context);
74
- if (typeof modified === "string") {
75
- result = modified;
76
- }
166
+ for (const plugin of this.hookPlugins.beforeParse) {
167
+ const modified = await plugin.beforeParse(result, context);
168
+ if (typeof modified === "string") {
169
+ result = modified;
77
170
  }
78
171
  }
79
172
  return result;
80
173
  }
81
174
  async afterParse(document, context) {
175
+ if (!this.initialized) {
176
+ await this.init();
177
+ }
82
178
  let result = document;
83
- for (const name of this.pluginOrder) {
84
- const plugin = this.plugins.get(name);
85
- if (plugin.afterParse) {
86
- const modified = await plugin.afterParse(result, context);
87
- if (modified) {
88
- result = modified;
89
- }
179
+ for (const plugin of this.hookPlugins.afterParse) {
180
+ const modified = await plugin.afterParse(result, context);
181
+ if (modified) {
182
+ result = modified;
90
183
  }
91
184
  }
92
185
  return result;
93
186
  }
94
187
  async beforeValidate(document, context) {
95
- for (const name of this.pluginOrder) {
96
- const plugin = this.plugins.get(name);
97
- if (plugin.beforeValidate) {
98
- await plugin.beforeValidate(document, context);
99
- }
188
+ if (!this.initialized) {
189
+ await this.init();
190
+ }
191
+ for (const plugin of this.hookPlugins.beforeValidate) {
192
+ await plugin.beforeValidate(document, context);
100
193
  }
101
194
  }
102
195
  async afterValidate(result, context) {
103
- for (const name of this.pluginOrder) {
104
- const plugin = this.plugins.get(name);
105
- if (plugin.afterValidate) {
106
- await plugin.afterValidate(result, context);
107
- }
196
+ if (!this.initialized) {
197
+ await this.init();
198
+ }
199
+ for (const plugin of this.hookPlugins.afterValidate) {
200
+ await plugin.afterValidate(result, context);
108
201
  }
109
202
  }
110
203
  async beforeExecute(document, context) {
111
- for (const name of this.pluginOrder) {
112
- const plugin = this.plugins.get(name);
113
- if (plugin.beforeExecute) {
114
- await plugin.beforeExecute(document, context);
204
+ if (!this.initialized) {
205
+ await this.init();
206
+ }
207
+ for (const plugin of this.hookPlugins.beforeExecute) {
208
+ const response = await plugin.beforeExecute(document, context);
209
+ if (response) {
210
+ return response;
115
211
  }
116
212
  }
213
+ return;
117
214
  }
118
215
  async afterExecute(response, context) {
216
+ if (!this.initialized) {
217
+ await this.init();
218
+ }
119
219
  let result = response;
120
- for (let i = this.pluginOrder.length - 1;i >= 0; i--) {
121
- const name = this.pluginOrder[i];
122
- const plugin = this.plugins.get(name);
123
- if (plugin.afterExecute) {
124
- const modified = await plugin.afterExecute(result, context);
125
- if (modified) {
126
- result = modified;
127
- }
220
+ const list = this.hookPlugins.afterExecute;
221
+ for (let i = list.length - 1;i >= 0; i--) {
222
+ const modified = await list[i].afterExecute(result, context);
223
+ if (modified) {
224
+ result = modified;
128
225
  }
129
226
  }
130
227
  return result;
131
228
  }
132
229
  async onError(error, context) {
230
+ if (!this.initialized) {
231
+ await this.init();
232
+ }
133
233
  let result = error;
134
- for (const name of this.pluginOrder) {
135
- const plugin = this.plugins.get(name);
136
- if (plugin.onError) {
137
- const modified = await plugin.onError(result, context);
138
- if (modified instanceof Error) {
139
- result = modified;
140
- }
234
+ for (const plugin of this.hookPlugins.onError) {
235
+ const modified = await plugin.onError(result, context);
236
+ if (modified instanceof Error) {
237
+ result = modified;
141
238
  }
142
239
  }
143
240
  return result;
@@ -154,6 +251,77 @@ class PluginManager {
154
251
  function createPluginManager(config) {
155
252
  return new PluginManager(config);
156
253
  }
254
+ // ../../node_modules/.pnpm/graphql@16.12.0/node_modules/graphql/jsutils/invariant.mjs
255
+ function invariant(condition, message) {
256
+ const booleanCondition = Boolean(condition);
257
+ if (!booleanCondition) {
258
+ throw new Error(message != null ? message : "Unexpected invariant triggered.");
259
+ }
260
+ }
261
+
262
+ // ../../node_modules/.pnpm/graphql@16.12.0/node_modules/graphql/language/location.mjs
263
+ var LineRegExp = /\r\n|[\n\r]/g;
264
+ function getLocation(source, position) {
265
+ let lastLineStart = 0;
266
+ let line = 1;
267
+ for (const match of source.body.matchAll(LineRegExp)) {
268
+ typeof match.index === "number" || invariant(false);
269
+ if (match.index >= position) {
270
+ break;
271
+ }
272
+ lastLineStart = match.index + match[0].length;
273
+ line += 1;
274
+ }
275
+ return {
276
+ line,
277
+ column: position + 1 - lastLineStart
278
+ };
279
+ }
280
+
281
+ // ../../node_modules/.pnpm/graphql@16.12.0/node_modules/graphql/language/printLocation.mjs
282
+ function printLocation(location) {
283
+ return printSourceLocation(location.source, getLocation(location.source, location.start));
284
+ }
285
+ function printSourceLocation(source, sourceLocation) {
286
+ const firstLineColumnOffset = source.locationOffset.column - 1;
287
+ const body = "".padStart(firstLineColumnOffset) + source.body;
288
+ const lineIndex = sourceLocation.line - 1;
289
+ const lineOffset = source.locationOffset.line - 1;
290
+ const lineNum = sourceLocation.line + lineOffset;
291
+ const columnOffset = sourceLocation.line === 1 ? firstLineColumnOffset : 0;
292
+ const columnNum = sourceLocation.column + columnOffset;
293
+ const locationStr = `${source.name}:${lineNum}:${columnNum}
294
+ `;
295
+ const lines = body.split(/\r\n|[\n\r]/g);
296
+ const locationLine = lines[lineIndex];
297
+ if (locationLine.length > 120) {
298
+ const subLineIndex = Math.floor(columnNum / 80);
299
+ const subLineColumnNum = columnNum % 80;
300
+ const subLines = [];
301
+ for (let i = 0;i < locationLine.length; i += 80) {
302
+ subLines.push(locationLine.slice(i, i + 80));
303
+ }
304
+ return locationStr + printPrefixedLines([
305
+ [`${lineNum} |`, subLines[0]],
306
+ ...subLines.slice(1, subLineIndex + 1).map((subLine) => ["|", subLine]),
307
+ ["|", "^".padStart(subLineColumnNum)],
308
+ ["|", subLines[subLineIndex + 1]]
309
+ ]);
310
+ }
311
+ return locationStr + printPrefixedLines([
312
+ [`${lineNum - 1} |`, lines[lineIndex - 1]],
313
+ [`${lineNum} |`, locationLine],
314
+ ["|", "^".padStart(columnNum)],
315
+ [`${lineNum + 1} |`, lines[lineIndex + 1]]
316
+ ]);
317
+ }
318
+ function printPrefixedLines(lines) {
319
+ const existingLines = lines.filter(([_, line]) => line !== undefined);
320
+ const padLen = Math.max(...existingLines.map(([prefix]) => prefix.length));
321
+ return existingLines.map(([prefix, line]) => prefix.padStart(padLen) + (line ? " " + line : "")).join(`
322
+ `);
323
+ }
324
+
157
325
  // ../../node_modules/.pnpm/graphql@16.12.0/node_modules/graphql/language/kinds.mjs
158
326
  var Kind;
159
327
  (function(Kind2) {
@@ -206,6 +374,276 @@ var Kind;
206
374
  Kind2["DIRECTIVE_COORDINATE"] = "DirectiveCoordinate";
207
375
  Kind2["DIRECTIVE_ARGUMENT_COORDINATE"] = "DirectiveArgumentCoordinate";
208
376
  })(Kind || (Kind = {}));
377
+ // ../../node_modules/.pnpm/graphql@16.12.0/node_modules/graphql/jsutils/isObjectLike.mjs
378
+ function isObjectLike(value) {
379
+ return typeof value == "object" && value !== null;
380
+ }
381
+
382
+ // ../../node_modules/.pnpm/graphql@16.12.0/node_modules/graphql/error/GraphQLError.mjs
383
+ function toNormalizedOptions(args) {
384
+ const firstArg = args[0];
385
+ if (firstArg == null || "kind" in firstArg || "length" in firstArg) {
386
+ return {
387
+ nodes: firstArg,
388
+ source: args[1],
389
+ positions: args[2],
390
+ path: args[3],
391
+ originalError: args[4],
392
+ extensions: args[5]
393
+ };
394
+ }
395
+ return firstArg;
396
+ }
397
+
398
+ class GraphQLError extends Error {
399
+ constructor(message, ...rawArgs) {
400
+ var _this$nodes, _nodeLocations$, _ref;
401
+ const { nodes, source, positions, path, originalError, extensions } = toNormalizedOptions(rawArgs);
402
+ super(message);
403
+ this.name = "GraphQLError";
404
+ this.path = path !== null && path !== undefined ? path : undefined;
405
+ this.originalError = originalError !== null && originalError !== undefined ? originalError : undefined;
406
+ this.nodes = undefinedIfEmpty(Array.isArray(nodes) ? nodes : nodes ? [nodes] : undefined);
407
+ const nodeLocations = undefinedIfEmpty((_this$nodes = this.nodes) === null || _this$nodes === undefined ? undefined : _this$nodes.map((node) => node.loc).filter((loc) => loc != null));
408
+ this.source = source !== null && source !== undefined ? source : nodeLocations === null || nodeLocations === undefined ? undefined : (_nodeLocations$ = nodeLocations[0]) === null || _nodeLocations$ === undefined ? undefined : _nodeLocations$.source;
409
+ this.positions = positions !== null && positions !== undefined ? positions : nodeLocations === null || nodeLocations === undefined ? undefined : nodeLocations.map((loc) => loc.start);
410
+ this.locations = positions && source ? positions.map((pos) => getLocation(source, pos)) : nodeLocations === null || nodeLocations === undefined ? undefined : nodeLocations.map((loc) => getLocation(loc.source, loc.start));
411
+ const originalExtensions = isObjectLike(originalError === null || originalError === undefined ? undefined : originalError.extensions) ? originalError === null || originalError === undefined ? undefined : originalError.extensions : undefined;
412
+ this.extensions = (_ref = extensions !== null && extensions !== undefined ? extensions : originalExtensions) !== null && _ref !== undefined ? _ref : Object.create(null);
413
+ Object.defineProperties(this, {
414
+ message: {
415
+ writable: true,
416
+ enumerable: true
417
+ },
418
+ name: {
419
+ enumerable: false
420
+ },
421
+ nodes: {
422
+ enumerable: false
423
+ },
424
+ source: {
425
+ enumerable: false
426
+ },
427
+ positions: {
428
+ enumerable: false
429
+ },
430
+ originalError: {
431
+ enumerable: false
432
+ }
433
+ });
434
+ if (originalError !== null && originalError !== undefined && originalError.stack) {
435
+ Object.defineProperty(this, "stack", {
436
+ value: originalError.stack,
437
+ writable: true,
438
+ configurable: true
439
+ });
440
+ } else if (Error.captureStackTrace) {
441
+ Error.captureStackTrace(this, GraphQLError);
442
+ } else {
443
+ Object.defineProperty(this, "stack", {
444
+ value: Error().stack,
445
+ writable: true,
446
+ configurable: true
447
+ });
448
+ }
449
+ }
450
+ get [Symbol.toStringTag]() {
451
+ return "GraphQLError";
452
+ }
453
+ toString() {
454
+ let output = this.message;
455
+ if (this.nodes) {
456
+ for (const node of this.nodes) {
457
+ if (node.loc) {
458
+ output += `
459
+
460
+ ` + printLocation(node.loc);
461
+ }
462
+ }
463
+ } else if (this.source && this.locations) {
464
+ for (const location of this.locations) {
465
+ output += `
466
+
467
+ ` + printSourceLocation(this.source, location);
468
+ }
469
+ }
470
+ return output;
471
+ }
472
+ toJSON() {
473
+ const formattedError = {
474
+ message: this.message
475
+ };
476
+ if (this.locations != null) {
477
+ formattedError.locations = this.locations;
478
+ }
479
+ if (this.path != null) {
480
+ formattedError.path = this.path;
481
+ }
482
+ if (this.extensions != null && Object.keys(this.extensions).length > 0) {
483
+ formattedError.extensions = this.extensions;
484
+ }
485
+ return formattedError;
486
+ }
487
+ }
488
+ function undefinedIfEmpty(array) {
489
+ return array === undefined || array.length === 0 ? undefined : array;
490
+ }
491
+ // ../errors/src/codes.ts
492
+ var ErrorCode;
493
+ ((ErrorCode2) => {
494
+ ErrorCode2["INTERNAL_ERROR"] = "INTERNAL_ERROR";
495
+ ErrorCode2["BAD_REQUEST"] = "BAD_REQUEST";
496
+ ErrorCode2["PAYLOAD_TOO_LARGE"] = "PAYLOAD_TOO_LARGE";
497
+ ErrorCode2["VALIDATION_ERROR"] = "VALIDATION_ERROR";
498
+ ErrorCode2["PARSE_ERROR"] = "PARSE_ERROR";
499
+ ErrorCode2["UNAUTHENTICATED"] = "UNAUTHENTICATED";
500
+ ErrorCode2["FORBIDDEN"] = "FORBIDDEN";
501
+ ErrorCode2["NOT_FOUND"] = "NOT_FOUND";
502
+ ErrorCode2["ALREADY_EXISTS"] = "ALREADY_EXISTS";
503
+ ErrorCode2["RATE_LIMITED"] = "RATE_LIMITED";
504
+ ErrorCode2["COMPLEXITY_LIMIT"] = "COMPLEXITY_LIMIT";
505
+ ErrorCode2["DEPTH_LIMIT"] = "DEPTH_LIMIT";
506
+ ErrorCode2["PERSISTED_QUERY_NOT_FOUND"] = "PERSISTED_QUERY_NOT_FOUND";
507
+ ErrorCode2["PERSISTED_QUERY_INVALID"] = "PERSISTED_QUERY_INVALID";
508
+ ErrorCode2["INVALID_INPUT"] = "INVALID_INPUT";
509
+ ErrorCode2["MISSING_REQUIRED_FIELD"] = "MISSING_REQUIRED_FIELD";
510
+ })(ErrorCode ||= {});
511
+ var ERROR_CODES = {
512
+ ["INTERNAL_ERROR" /* INTERNAL_ERROR */]: {
513
+ status: 500,
514
+ message: "An internal error occurred"
515
+ },
516
+ ["BAD_REQUEST" /* BAD_REQUEST */]: {
517
+ status: 400,
518
+ message: "Bad request"
519
+ },
520
+ ["PAYLOAD_TOO_LARGE" /* PAYLOAD_TOO_LARGE */]: {
521
+ status: 413,
522
+ message: "Request payload is too large"
523
+ },
524
+ ["VALIDATION_ERROR" /* VALIDATION_ERROR */]: {
525
+ status: 400,
526
+ message: "Validation failed"
527
+ },
528
+ ["PARSE_ERROR" /* PARSE_ERROR */]: {
529
+ status: 400,
530
+ message: "Failed to parse GraphQL query"
531
+ },
532
+ ["UNAUTHENTICATED" /* UNAUTHENTICATED */]: {
533
+ status: 401,
534
+ message: "Authentication required"
535
+ },
536
+ ["FORBIDDEN" /* FORBIDDEN */]: {
537
+ status: 403,
538
+ message: "Access denied"
539
+ },
540
+ ["NOT_FOUND" /* NOT_FOUND */]: {
541
+ status: 404,
542
+ message: "Resource not found"
543
+ },
544
+ ["ALREADY_EXISTS" /* ALREADY_EXISTS */]: {
545
+ status: 409,
546
+ message: "Resource already exists"
547
+ },
548
+ ["RATE_LIMITED" /* RATE_LIMITED */]: {
549
+ status: 429,
550
+ message: "Rate limit exceeded"
551
+ },
552
+ ["COMPLEXITY_LIMIT" /* COMPLEXITY_LIMIT */]: {
553
+ status: 400,
554
+ message: "Query complexity limit exceeded"
555
+ },
556
+ ["DEPTH_LIMIT" /* DEPTH_LIMIT */]: {
557
+ status: 400,
558
+ message: "Query depth limit exceeded"
559
+ },
560
+ ["PERSISTED_QUERY_NOT_FOUND" /* PERSISTED_QUERY_NOT_FOUND */]: {
561
+ status: 400,
562
+ message: "Persisted query not found"
563
+ },
564
+ ["PERSISTED_QUERY_INVALID" /* PERSISTED_QUERY_INVALID */]: {
565
+ status: 400,
566
+ message: "Invalid persisted query"
567
+ },
568
+ ["INVALID_INPUT" /* INVALID_INPUT */]: {
569
+ status: 400,
570
+ message: "Invalid input provided"
571
+ },
572
+ ["MISSING_REQUIRED_FIELD" /* MISSING_REQUIRED_FIELD */]: {
573
+ status: 400,
574
+ message: "Required field is missing"
575
+ }
576
+ };
577
+ var ERROR_CODE_SET = new Set(Object.values(ErrorCode));
578
+
579
+ // ../errors/src/errors.ts
580
+ class LeavenError extends Error {
581
+ code;
582
+ statusCode;
583
+ extensions;
584
+ originalError;
585
+ constructor(message, code = "INTERNAL_ERROR" /* INTERNAL_ERROR */, options) {
586
+ super(message);
587
+ this.name = "LeavenError";
588
+ this.code = code;
589
+ this.statusCode = options?.statusCode ?? ERROR_CODES[code]?.status ?? 500;
590
+ this.extensions = {
591
+ ...options?.extensions,
592
+ code
593
+ };
594
+ this.originalError = options?.originalError;
595
+ Error.captureStackTrace?.(this, this.constructor);
596
+ }
597
+ toGraphQLError() {
598
+ return new GraphQLError(this.message, {
599
+ extensions: this.extensions,
600
+ originalError: this
601
+ });
602
+ }
603
+ toJSON() {
604
+ return {
605
+ message: this.message,
606
+ code: this.code,
607
+ statusCode: this.statusCode,
608
+ extensions: this.extensions
609
+ };
610
+ }
611
+ }
612
+ class ComplexityError extends LeavenError {
613
+ complexity;
614
+ maxComplexity;
615
+ constructor(complexity, maxComplexity, options) {
616
+ super(`Query complexity of ${complexity} exceeds maximum allowed complexity of ${maxComplexity}`, "COMPLEXITY_LIMIT" /* COMPLEXITY_LIMIT */, {
617
+ statusCode: 400,
618
+ extensions: {
619
+ ...options?.extensions,
620
+ complexity,
621
+ maxComplexity
622
+ }
623
+ });
624
+ this.name = "ComplexityError";
625
+ this.complexity = complexity;
626
+ this.maxComplexity = maxComplexity;
627
+ }
628
+ }
629
+
630
+ class DepthLimitError extends LeavenError {
631
+ depth;
632
+ maxDepth;
633
+ constructor(depth, maxDepth, options) {
634
+ super(`Query depth of ${depth} exceeds maximum allowed depth of ${maxDepth}`, "DEPTH_LIMIT" /* DEPTH_LIMIT */, {
635
+ statusCode: 400,
636
+ extensions: {
637
+ ...options?.extensions,
638
+ depth,
639
+ maxDepth
640
+ }
641
+ });
642
+ this.name = "DepthLimitError";
643
+ this.depth = depth;
644
+ this.maxDepth = maxDepth;
645
+ }
646
+ }
209
647
  // src/builtin.ts
210
648
  function createPlugin(metadata, hooks) {
211
649
  return {
@@ -214,21 +652,8 @@ function createPlugin(metadata, hooks) {
214
652
  };
215
653
  }
216
654
  function composePlugins(name, ...plugins) {
217
- return {
218
- metadata: {
219
- name,
220
- description: `Composed plugin from: ${plugins.map((p) => p.metadata.name).join(", ")}`
221
- },
222
- async onRegister(schema) {
223
- for (const plugin of plugins) {
224
- await plugin.onRegister?.(schema);
225
- }
226
- },
227
- async onUnregister() {
228
- for (const plugin of plugins) {
229
- await plugin.onUnregister?.();
230
- }
231
- },
655
+ const reversed = [...plugins].reverse();
656
+ const hooks = {
232
657
  async beforeParse(query, context) {
233
658
  let result = query;
234
659
  for (const plugin of plugins) {
@@ -253,14 +678,28 @@ function composePlugins(name, ...plugins) {
253
678
  }
254
679
  return result;
255
680
  },
681
+ async beforeValidate(document, context) {
682
+ for (const plugin of plugins) {
683
+ await plugin.beforeValidate?.(document, context);
684
+ }
685
+ },
686
+ async afterValidate(result, context) {
687
+ for (const plugin of plugins) {
688
+ await plugin.afterValidate?.(result, context);
689
+ }
690
+ },
256
691
  async beforeExecute(document, context) {
257
692
  for (const plugin of plugins) {
258
- await plugin.beforeExecute?.(document, context);
693
+ const response = await plugin.beforeExecute?.(document, context);
694
+ if (response) {
695
+ return response;
696
+ }
259
697
  }
698
+ return;
260
699
  },
261
700
  async afterExecute(response, context) {
262
701
  let result = response;
263
- for (const plugin of plugins.slice().reverse()) {
702
+ for (const plugin of reversed) {
264
703
  if (plugin.afterExecute) {
265
704
  const modified = await plugin.afterExecute(result, context);
266
705
  if (modified) {
@@ -283,27 +722,59 @@ function composePlugins(name, ...plugins) {
283
722
  return result;
284
723
  }
285
724
  };
725
+ return {
726
+ metadata: {
727
+ name,
728
+ description: `Composed plugin from: ${plugins.map((p) => p.metadata.name).join(", ")}`
729
+ },
730
+ async onRegister(schema) {
731
+ for (const plugin of plugins) {
732
+ await plugin.onRegister?.(schema);
733
+ }
734
+ },
735
+ async onUnregister() {
736
+ for (const plugin of plugins) {
737
+ await plugin.onUnregister?.();
738
+ }
739
+ },
740
+ ...hooks
741
+ };
286
742
  }
287
743
  function createCachingPlugin(options) {
288
744
  const cache = new Map;
289
745
  const maxSize = options?.maxSize ?? 100;
290
746
  const ttl = options?.ttl ?? 60000;
747
+ const keyFn = options?.keyFn;
748
+ if (!keyFn && options?.allowSharedCache !== true) {
749
+ throw new TypeError("createCachingPlugin requires an explicit cache-identity decision: pass a " + "`keyFn` that incorporates the caller identity (recommended), or set " + "`allowSharedCache: true` to serve one cached response to every caller. " + "A cache hit skips execution, so without caller identity in the key one " + "user can receive another user's response.");
750
+ }
751
+ const makeKey = (context) => keyFn ? keyFn(context) : JSON.stringify({
752
+ query: context.request.query,
753
+ operationName: context.request.operationName ?? null,
754
+ variables: context.request.variables ?? null
755
+ });
291
756
  return {
292
757
  metadata: {
293
758
  name: "caching",
294
759
  version: "1.0.0",
295
- description: "Caches query results"
760
+ description: "Caches successful query responses and serves them on subsequent requests"
296
761
  },
297
- async beforeExecute(document, context) {
298
- const key = JSON.stringify({
299
- query: context.request.query,
300
- variables: context.request.variables
301
- });
762
+ async beforeExecute(_document, context) {
763
+ const key = makeKey(context);
764
+ context.state.set("cacheKey", key);
302
765
  const cached = cache.get(key);
303
- if (cached && cached.expires > Date.now()) {
304
- context.state.set("cacheHit", true);
305
- context.state.set("cachedResponse", cached.data);
766
+ if (!cached) {
767
+ return;
768
+ }
769
+ if (cached.expires <= Date.now()) {
770
+ cache.delete(key);
771
+ return;
306
772
  }
773
+ cache.delete(key);
774
+ cache.set(key, cached);
775
+ context.state.set("cacheHit", true);
776
+ context.state.set("cachedResponse", cached.data);
777
+ return cached.data;
307
778
  },
308
779
  async afterExecute(response, context) {
309
780
  if (context.state.get("cacheHit")) {
@@ -312,30 +783,28 @@ function createCachingPlugin(options) {
312
783
  if (response.errors && response.errors.length > 0) {
313
784
  return response;
314
785
  }
315
- const key = JSON.stringify({
316
- query: context.request.query,
317
- variables: context.request.variables
318
- });
319
- if (cache.size >= maxSize) {
320
- const firstKey = cache.keys().next().value;
321
- if (firstKey)
322
- cache.delete(firstKey);
786
+ const key = context.state.get("cacheKey") ?? makeKey(context);
787
+ const now = Date.now();
788
+ while (!cache.has(key) && cache.size >= maxSize) {
789
+ const oldestKey = cache.keys().next().value;
790
+ if (oldestKey === undefined)
791
+ break;
792
+ cache.delete(oldestKey);
323
793
  }
324
794
  cache.set(key, {
325
795
  data: response,
326
- expires: Date.now() + ttl
796
+ expires: now + ttl
327
797
  });
328
798
  return response;
329
799
  }
330
800
  };
331
801
  }
802
+ var LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
332
803
  function createLoggingPlugin(options) {
333
804
  const logger = options?.logger ?? console;
334
805
  const level = options?.level ?? "info";
335
- const shouldLog = (msgLevel) => {
336
- const levels = ["debug", "info", "warn", "error"];
337
- return levels.indexOf(msgLevel) >= levels.indexOf(level);
338
- };
806
+ const threshold = LOG_LEVELS[level];
807
+ const shouldLog = (msgLevel) => LOG_LEVELS[msgLevel] >= threshold;
339
808
  return {
340
809
  metadata: {
341
810
  name: "logging",
@@ -384,119 +853,297 @@ function createLoggingPlugin(options) {
384
853
  };
385
854
  }
386
855
  function createTracingPlugin() {
856
+ const TRACE_KEY = "tracing";
857
+ const ensureTrace = (context) => {
858
+ let trace = context.state.get(TRACE_KEY);
859
+ if (!trace) {
860
+ trace = {
861
+ startTime: new Date().toISOString(),
862
+ startNs: Bun.nanoseconds()
863
+ };
864
+ context.state.set(TRACE_KEY, trace);
865
+ }
866
+ return trace;
867
+ };
387
868
  return {
388
869
  metadata: {
389
870
  name: "tracing",
390
871
  version: "1.0.0",
391
872
  description: "Adds tracing information to responses"
392
873
  },
393
- async beforeExecute(document, context) {
394
- context.state.set("tracing", {
395
- version: 1,
396
- startTime: new Date().toISOString(),
397
- startHrTime: process.hrtime.bigint(),
398
- parsing: { startOffset: 0, duration: 0 },
399
- validation: { startOffset: 0, duration: 0 },
400
- execution: { startOffset: 0, duration: 0 }
401
- });
874
+ async beforeParse(_query, context) {
875
+ const trace = ensureTrace(context);
876
+ trace.parsingStartNs = Bun.nanoseconds();
877
+ },
878
+ async afterParse(document, context) {
879
+ const trace = ensureTrace(context);
880
+ if (trace.parsingStartNs !== undefined) {
881
+ trace.parsing = {
882
+ startOffset: trace.parsingStartNs - trace.startNs,
883
+ duration: Bun.nanoseconds() - trace.parsingStartNs
884
+ };
885
+ }
886
+ return document;
887
+ },
888
+ async beforeValidate(_document, context) {
889
+ const trace = ensureTrace(context);
890
+ trace.validationStartNs = Bun.nanoseconds();
891
+ },
892
+ async afterValidate(_result, context) {
893
+ const trace = ensureTrace(context);
894
+ if (trace.validationStartNs !== undefined) {
895
+ trace.validation = {
896
+ startOffset: trace.validationStartNs - trace.startNs,
897
+ duration: Bun.nanoseconds() - trace.validationStartNs
898
+ };
899
+ }
900
+ },
901
+ async beforeExecute(_document, context) {
902
+ const trace = ensureTrace(context);
903
+ trace.executionStartNs = Bun.nanoseconds();
402
904
  },
403
905
  async afterExecute(response, context) {
404
- const tracing = context.state.get("tracing");
405
- if (!tracing)
906
+ const trace = context.state.get(TRACE_KEY);
907
+ if (!trace)
406
908
  return response;
407
- const endHrTime = process.hrtime.bigint();
408
- const duration = Number(endHrTime - tracing.startHrTime);
909
+ const endNs = Bun.nanoseconds();
910
+ const duration = endNs - trace.startNs;
911
+ const tracing = {
912
+ version: 1,
913
+ startTime: trace.startTime,
914
+ endTime: new Date().toISOString(),
915
+ duration
916
+ };
917
+ if (trace.parsing) {
918
+ tracing["parsing"] = trace.parsing;
919
+ }
920
+ if (trace.validation) {
921
+ tracing["validation"] = trace.validation;
922
+ }
923
+ if (trace.executionStartNs !== undefined) {
924
+ tracing["execution"] = {
925
+ startOffset: trace.executionStartNs - trace.startNs,
926
+ duration: endNs - trace.executionStartNs
927
+ };
928
+ }
409
929
  return {
410
930
  ...response,
411
931
  extensions: {
412
932
  ...response.extensions,
413
- tracing: {
414
- version: tracing.version,
415
- startTime: tracing.startTime,
416
- endTime: new Date().toISOString(),
417
- duration,
418
- parsing: tracing.parsing,
419
- validation: tracing.validation,
420
- execution: {
421
- ...tracing.execution,
422
- duration
423
- }
424
- }
933
+ tracing
425
934
  }
426
935
  };
427
936
  }
428
937
  };
429
938
  }
430
- function createDepthLimitPlugin(maxDepth) {
431
- function calculateDepth(node, currentDepth = 0) {
432
- if (!node || typeof node !== "object")
939
+ var MAX_WALKER_VISITS = 1e5;
940
+ function collectFragments(definitions) {
941
+ const fragments = new Map;
942
+ for (const definition of definitions) {
943
+ const typed = definition;
944
+ if (typed.kind === Kind.FRAGMENT_DEFINITION && typed.name) {
945
+ fragments.set(typed.name.value, definition);
946
+ }
947
+ }
948
+ return fragments;
949
+ }
950
+ function fragmentDepthContribution(walker, name) {
951
+ const memoized = walker.fragmentDepths.get(name);
952
+ if (memoized !== undefined) {
953
+ return memoized;
954
+ }
955
+ const fragment = walker.fragments.get(name);
956
+ if (!fragment || walker.inProgress.has(name)) {
957
+ return 0;
958
+ }
959
+ walker.inProgress.add(name);
960
+ const contribution = walkDepth(walker, fragment, 0);
961
+ walker.inProgress.delete(name);
962
+ walker.fragmentDepths.set(name, contribution);
963
+ return contribution;
964
+ }
965
+ function walkDepth(walker, node, currentDepth) {
966
+ if (!node || typeof node !== "object")
967
+ return currentDepth;
968
+ if (++walker.budget.visits > MAX_WALKER_VISITS) {
969
+ throw new DepthLimitError(walker.limit + 1, walker.limit, {
970
+ extensions: {
971
+ reason: "ANALYSIS_BUDGET_EXCEEDED",
972
+ maxNodesVisited: MAX_WALKER_VISITS
973
+ }
974
+ });
975
+ }
976
+ const typedNode = node;
977
+ if (typedNode.kind === Kind.FRAGMENT_SPREAD) {
978
+ const fragmentName = typedNode.name?.value;
979
+ if (fragmentName === undefined) {
433
980
  return currentDepth;
434
- const typedNode = node;
435
- let maxFoundDepth = currentDepth;
436
- if (typedNode.kind === Kind.FIELD) {
437
- maxFoundDepth = currentDepth + 1;
438
- }
439
- if (typedNode.selectionSet) {
440
- for (const selection of typedNode.selectionSet.selections) {
441
- const depth = calculateDepth(selection, maxFoundDepth);
442
- if (depth > maxFoundDepth) {
443
- maxFoundDepth = depth;
444
- }
981
+ }
982
+ return currentDepth + fragmentDepthContribution(walker, fragmentName);
983
+ }
984
+ const childDepth = typedNode.kind === Kind.FIELD ? currentDepth + 1 : currentDepth;
985
+ let maxFoundDepth = childDepth;
986
+ if (typedNode.selectionSet) {
987
+ for (const selection of typedNode.selectionSet.selections) {
988
+ const depth = walkDepth(walker, selection, childDepth);
989
+ if (depth > maxFoundDepth) {
990
+ maxFoundDepth = depth;
445
991
  }
446
992
  }
447
- return maxFoundDepth;
993
+ }
994
+ return maxFoundDepth;
995
+ }
996
+ function createDepthLimitPlugin(maxDepth) {
997
+ const limit = typeof maxDepth === "number" ? maxDepth : maxDepth?.maxDepth;
998
+ if (typeof limit !== "number" || !Number.isFinite(limit)) {
999
+ throw new TypeError(`createDepthLimitPlugin requires a finite numeric maxDepth; received ${JSON.stringify(maxDepth)}. ` + "Pass a number or an object of the form { maxDepth: number }.");
448
1000
  }
449
1001
  return {
450
1002
  metadata: {
451
1003
  name: "depth-limit",
452
1004
  version: "1.0.0",
453
- description: `Limits query depth to ${maxDepth}`
1005
+ description: `Limits query depth to ${limit}`
454
1006
  },
455
1007
  async afterParse(document, _context) {
1008
+ const walker = {
1009
+ fragments: collectFragments(document.definitions),
1010
+ fragmentDepths: new Map,
1011
+ inProgress: new Set,
1012
+ budget: { visits: 0 },
1013
+ limit
1014
+ };
456
1015
  let depth = 0;
457
1016
  for (const definition of document.definitions) {
458
1017
  if (definition.kind === Kind.OPERATION_DEFINITION) {
459
- const opDepth = calculateDepth(definition, 0);
1018
+ const opDepth = walkDepth(walker, definition, 0);
460
1019
  if (opDepth > depth) {
461
1020
  depth = opDepth;
462
1021
  }
463
1022
  }
464
1023
  }
465
- if (depth > maxDepth) {
466
- throw new Error(`Query depth of ${depth} exceeds maximum allowed depth of ${maxDepth}`);
1024
+ if (depth > limit) {
1025
+ throw new DepthLimitError(depth, limit);
467
1026
  }
468
1027
  return document;
469
1028
  }
470
1029
  };
471
1030
  }
472
- function createComplexityPlugin(options) {
473
- const { maxComplexity, defaultComplexity = 1 } = options;
474
- function calculateComplexity(node, depth = 0) {
475
- if (!node || typeof node !== "object")
476
- return 0;
477
- const typedNode = node;
478
- let complexity = 0;
479
- if (typedNode.kind === Kind.FIELD) {
480
- let multiplier = 1;
481
- if (typedNode.arguments) {
482
- for (const arg of typedNode.arguments) {
483
- if (arg.name.value === "first" || arg.name.value === "last" || arg.name.value === "limit") {
484
- const value = parseInt(arg.value.value ?? "10", 10);
485
- if (!isNaN(value)) {
486
- multiplier = value;
487
- }
488
- }
1031
+ var LIST_SIZE_ARGUMENTS = new Set(["first", "last", "limit"]);
1032
+ var NO_VARIABLE_DEFAULTS = new Map;
1033
+ function toPositiveMultiplier(raw) {
1034
+ const numeric = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw) : Number.NaN;
1035
+ return Number.isFinite(numeric) && numeric > 0 ? numeric : undefined;
1036
+ }
1037
+ function collectVariableDefaults(operation) {
1038
+ const definitions = operation.variableDefinitions;
1039
+ if (!definitions || definitions.length === 0) {
1040
+ return NO_VARIABLE_DEFAULTS;
1041
+ }
1042
+ const defaults = new Map;
1043
+ for (const definition of definitions) {
1044
+ const name = definition.variable?.name?.value;
1045
+ const defaultValue = definition.defaultValue;
1046
+ if (name === undefined || !defaultValue)
1047
+ continue;
1048
+ if (defaultValue.kind !== Kind.INT && defaultValue.kind !== Kind.FLOAT) {
1049
+ continue;
1050
+ }
1051
+ const numeric = toPositiveMultiplier(defaultValue.value);
1052
+ if (numeric !== undefined) {
1053
+ defaults.set(name, numeric);
1054
+ }
1055
+ }
1056
+ return defaults;
1057
+ }
1058
+ function resolveListSizeMultiplier(walker, value) {
1059
+ if (value.kind === Kind.INT || value.kind === Kind.FLOAT) {
1060
+ return toPositiveMultiplier(value.value);
1061
+ }
1062
+ if (value.kind === Kind.VARIABLE) {
1063
+ const name = value.name?.value;
1064
+ if (name !== undefined) {
1065
+ const supplied = walker.variables?.[name];
1066
+ if (supplied !== undefined) {
1067
+ const fromRequest = toPositiveMultiplier(supplied);
1068
+ if (fromRequest !== undefined) {
1069
+ return fromRequest;
489
1070
  }
490
1071
  }
491
- complexity = defaultComplexity * multiplier;
1072
+ const declaredDefault = walker.variableDefaults.get(name);
1073
+ if (declaredDefault !== undefined) {
1074
+ return declaredDefault;
1075
+ }
492
1076
  }
493
- if (typedNode.selectionSet) {
494
- for (const selection of typedNode.selectionSet.selections) {
495
- complexity += calculateComplexity(selection, depth + 1);
1077
+ return walker.unknownMultiplier;
1078
+ }
1079
+ return;
1080
+ }
1081
+ function fragmentComplexityContribution(walker, name) {
1082
+ const memoized = walker.fragmentComplexities.get(name);
1083
+ if (memoized !== undefined) {
1084
+ return memoized;
1085
+ }
1086
+ const fragment = walker.fragments.get(name);
1087
+ if (!fragment || walker.inProgress.has(name)) {
1088
+ return 0;
1089
+ }
1090
+ walker.inProgress.add(name);
1091
+ const contribution = walkComplexity(walker, fragment);
1092
+ walker.inProgress.delete(name);
1093
+ walker.fragmentComplexities.set(name, contribution);
1094
+ return contribution;
1095
+ }
1096
+ function walkComplexity(walker, node) {
1097
+ if (!node || typeof node !== "object")
1098
+ return 0;
1099
+ if (++walker.budget.visits > MAX_WALKER_VISITS) {
1100
+ throw new ComplexityError(walker.maxComplexity + 1, walker.maxComplexity, {
1101
+ extensions: {
1102
+ reason: "ANALYSIS_BUDGET_EXCEEDED",
1103
+ maxNodesVisited: MAX_WALKER_VISITS
1104
+ }
1105
+ });
1106
+ }
1107
+ const typedNode = node;
1108
+ if (typedNode.kind === Kind.FRAGMENT_SPREAD) {
1109
+ const fragmentName = typedNode.name?.value;
1110
+ if (fragmentName === undefined) {
1111
+ return 0;
1112
+ }
1113
+ return fragmentComplexityContribution(walker, fragmentName);
1114
+ }
1115
+ let childComplexity = 0;
1116
+ if (typedNode.selectionSet) {
1117
+ for (const selection of typedNode.selectionSet.selections) {
1118
+ childComplexity += walkComplexity(walker, selection);
1119
+ }
1120
+ }
1121
+ if (typedNode.kind === Kind.FIELD) {
1122
+ if (walker.calculator) {
1123
+ return walker.calculator(node, childComplexity);
1124
+ }
1125
+ let multiplier = 1;
1126
+ if (typedNode.arguments) {
1127
+ for (const arg of typedNode.arguments) {
1128
+ if (!LIST_SIZE_ARGUMENTS.has(arg.name.value))
1129
+ continue;
1130
+ const resolved = resolveListSizeMultiplier(walker, arg.value);
1131
+ if (resolved !== undefined) {
1132
+ multiplier = resolved;
1133
+ }
496
1134
  }
497
1135
  }
498
- return complexity;
1136
+ return (walker.defaultComplexity + childComplexity) * multiplier;
499
1137
  }
1138
+ return childComplexity;
1139
+ }
1140
+ function createComplexityPlugin(options) {
1141
+ const {
1142
+ maxComplexity,
1143
+ defaultComplexity = 1,
1144
+ unknownMultiplier = 100,
1145
+ calculator
1146
+ } = options;
500
1147
  return {
501
1148
  metadata: {
502
1149
  name: "complexity",
@@ -504,15 +1151,29 @@ function createComplexityPlugin(options) {
504
1151
  description: `Limits query complexity to ${maxComplexity}`
505
1152
  },
506
1153
  async afterParse(document, context) {
1154
+ const fragments = collectFragments(document.definitions);
1155
+ const budget = { visits: 0 };
507
1156
  let totalComplexity = 0;
508
1157
  for (const definition of document.definitions) {
509
1158
  if (definition.kind === Kind.OPERATION_DEFINITION) {
510
- totalComplexity += calculateComplexity(definition, 0);
1159
+ const walker = {
1160
+ fragments,
1161
+ fragmentComplexities: new Map,
1162
+ inProgress: new Set,
1163
+ budget,
1164
+ maxComplexity,
1165
+ defaultComplexity,
1166
+ calculator,
1167
+ variables: context.request.variables,
1168
+ variableDefaults: collectVariableDefaults(definition),
1169
+ unknownMultiplier
1170
+ };
1171
+ totalComplexity += walkComplexity(walker, definition);
511
1172
  }
512
1173
  }
513
1174
  context.state.set("complexity", totalComplexity);
514
1175
  if (totalComplexity > maxComplexity) {
515
- throw new Error(`Query complexity of ${totalComplexity} exceeds maximum allowed complexity of ${maxComplexity}`);
1176
+ throw new ComplexityError(totalComplexity, maxComplexity);
516
1177
  }
517
1178
  return document;
518
1179
  }