@leaven-graphql/plugins 0.1.0 → 0.2.1

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,59 +251,9 @@ class PluginManager {
154
251
  function createPluginManager(config) {
155
252
  return new PluginManager(config);
156
253
  }
157
- // ../../node_modules/.pnpm/graphql@16.12.0/node_modules/graphql/language/kinds.mjs
158
- var Kind;
159
- (function(Kind2) {
160
- Kind2["NAME"] = "Name";
161
- Kind2["DOCUMENT"] = "Document";
162
- Kind2["OPERATION_DEFINITION"] = "OperationDefinition";
163
- Kind2["VARIABLE_DEFINITION"] = "VariableDefinition";
164
- Kind2["SELECTION_SET"] = "SelectionSet";
165
- Kind2["FIELD"] = "Field";
166
- Kind2["ARGUMENT"] = "Argument";
167
- Kind2["FRAGMENT_SPREAD"] = "FragmentSpread";
168
- Kind2["INLINE_FRAGMENT"] = "InlineFragment";
169
- Kind2["FRAGMENT_DEFINITION"] = "FragmentDefinition";
170
- Kind2["VARIABLE"] = "Variable";
171
- Kind2["INT"] = "IntValue";
172
- Kind2["FLOAT"] = "FloatValue";
173
- Kind2["STRING"] = "StringValue";
174
- Kind2["BOOLEAN"] = "BooleanValue";
175
- Kind2["NULL"] = "NullValue";
176
- Kind2["ENUM"] = "EnumValue";
177
- Kind2["LIST"] = "ListValue";
178
- Kind2["OBJECT"] = "ObjectValue";
179
- Kind2["OBJECT_FIELD"] = "ObjectField";
180
- Kind2["DIRECTIVE"] = "Directive";
181
- Kind2["NAMED_TYPE"] = "NamedType";
182
- Kind2["LIST_TYPE"] = "ListType";
183
- Kind2["NON_NULL_TYPE"] = "NonNullType";
184
- Kind2["SCHEMA_DEFINITION"] = "SchemaDefinition";
185
- Kind2["OPERATION_TYPE_DEFINITION"] = "OperationTypeDefinition";
186
- Kind2["SCALAR_TYPE_DEFINITION"] = "ScalarTypeDefinition";
187
- Kind2["OBJECT_TYPE_DEFINITION"] = "ObjectTypeDefinition";
188
- Kind2["FIELD_DEFINITION"] = "FieldDefinition";
189
- Kind2["INPUT_VALUE_DEFINITION"] = "InputValueDefinition";
190
- Kind2["INTERFACE_TYPE_DEFINITION"] = "InterfaceTypeDefinition";
191
- Kind2["UNION_TYPE_DEFINITION"] = "UnionTypeDefinition";
192
- Kind2["ENUM_TYPE_DEFINITION"] = "EnumTypeDefinition";
193
- Kind2["ENUM_VALUE_DEFINITION"] = "EnumValueDefinition";
194
- Kind2["INPUT_OBJECT_TYPE_DEFINITION"] = "InputObjectTypeDefinition";
195
- Kind2["DIRECTIVE_DEFINITION"] = "DirectiveDefinition";
196
- Kind2["SCHEMA_EXTENSION"] = "SchemaExtension";
197
- Kind2["SCALAR_TYPE_EXTENSION"] = "ScalarTypeExtension";
198
- Kind2["OBJECT_TYPE_EXTENSION"] = "ObjectTypeExtension";
199
- Kind2["INTERFACE_TYPE_EXTENSION"] = "InterfaceTypeExtension";
200
- Kind2["UNION_TYPE_EXTENSION"] = "UnionTypeExtension";
201
- Kind2["ENUM_TYPE_EXTENSION"] = "EnumTypeExtension";
202
- Kind2["INPUT_OBJECT_TYPE_EXTENSION"] = "InputObjectTypeExtension";
203
- Kind2["TYPE_COORDINATE"] = "TypeCoordinate";
204
- Kind2["MEMBER_COORDINATE"] = "MemberCoordinate";
205
- Kind2["ARGUMENT_COORDINATE"] = "ArgumentCoordinate";
206
- Kind2["DIRECTIVE_COORDINATE"] = "DirectiveCoordinate";
207
- Kind2["DIRECTIVE_ARGUMENT_COORDINATE"] = "DirectiveArgumentCoordinate";
208
- })(Kind || (Kind = {}));
209
254
  // src/builtin.ts
255
+ import { Kind } from "graphql";
256
+ import { ComplexityError, DepthLimitError } from "@leaven-graphql/errors";
210
257
  function createPlugin(metadata, hooks) {
211
258
  return {
212
259
  metadata,
@@ -214,21 +261,8 @@ function createPlugin(metadata, hooks) {
214
261
  };
215
262
  }
216
263
  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
- },
264
+ const reversed = [...plugins].reverse();
265
+ const hooks = {
232
266
  async beforeParse(query, context) {
233
267
  let result = query;
234
268
  for (const plugin of plugins) {
@@ -253,14 +287,28 @@ function composePlugins(name, ...plugins) {
253
287
  }
254
288
  return result;
255
289
  },
290
+ async beforeValidate(document, context) {
291
+ for (const plugin of plugins) {
292
+ await plugin.beforeValidate?.(document, context);
293
+ }
294
+ },
295
+ async afterValidate(result, context) {
296
+ for (const plugin of plugins) {
297
+ await plugin.afterValidate?.(result, context);
298
+ }
299
+ },
256
300
  async beforeExecute(document, context) {
257
301
  for (const plugin of plugins) {
258
- await plugin.beforeExecute?.(document, context);
302
+ const response = await plugin.beforeExecute?.(document, context);
303
+ if (response) {
304
+ return response;
305
+ }
259
306
  }
307
+ return;
260
308
  },
261
309
  async afterExecute(response, context) {
262
310
  let result = response;
263
- for (const plugin of plugins.slice().reverse()) {
311
+ for (const plugin of reversed) {
264
312
  if (plugin.afterExecute) {
265
313
  const modified = await plugin.afterExecute(result, context);
266
314
  if (modified) {
@@ -283,27 +331,59 @@ function composePlugins(name, ...plugins) {
283
331
  return result;
284
332
  }
285
333
  };
334
+ return {
335
+ metadata: {
336
+ name,
337
+ description: `Composed plugin from: ${plugins.map((p) => p.metadata.name).join(", ")}`
338
+ },
339
+ async onRegister(schema) {
340
+ for (const plugin of plugins) {
341
+ await plugin.onRegister?.(schema);
342
+ }
343
+ },
344
+ async onUnregister() {
345
+ for (const plugin of plugins) {
346
+ await plugin.onUnregister?.();
347
+ }
348
+ },
349
+ ...hooks
350
+ };
286
351
  }
287
352
  function createCachingPlugin(options) {
288
353
  const cache = new Map;
289
354
  const maxSize = options?.maxSize ?? 100;
290
355
  const ttl = options?.ttl ?? 60000;
356
+ const keyFn = options?.keyFn;
357
+ if (!keyFn && options?.allowSharedCache !== true) {
358
+ 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.");
359
+ }
360
+ const makeKey = (context) => keyFn ? keyFn(context) : JSON.stringify({
361
+ query: context.request.query,
362
+ operationName: context.request.operationName ?? null,
363
+ variables: context.request.variables ?? null
364
+ });
291
365
  return {
292
366
  metadata: {
293
367
  name: "caching",
294
368
  version: "1.0.0",
295
- description: "Caches query results"
369
+ description: "Caches successful query responses and serves them on subsequent requests"
296
370
  },
297
- async beforeExecute(document, context) {
298
- const key = JSON.stringify({
299
- query: context.request.query,
300
- variables: context.request.variables
301
- });
371
+ async beforeExecute(_document, context) {
372
+ const key = makeKey(context);
373
+ context.state.set("cacheKey", key);
302
374
  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);
375
+ if (!cached) {
376
+ return;
377
+ }
378
+ if (cached.expires <= Date.now()) {
379
+ cache.delete(key);
380
+ return;
306
381
  }
382
+ cache.delete(key);
383
+ cache.set(key, cached);
384
+ context.state.set("cacheHit", true);
385
+ context.state.set("cachedResponse", cached.data);
386
+ return cached.data;
307
387
  },
308
388
  async afterExecute(response, context) {
309
389
  if (context.state.get("cacheHit")) {
@@ -312,30 +392,28 @@ function createCachingPlugin(options) {
312
392
  if (response.errors && response.errors.length > 0) {
313
393
  return response;
314
394
  }
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);
395
+ const key = context.state.get("cacheKey") ?? makeKey(context);
396
+ const now = Date.now();
397
+ while (!cache.has(key) && cache.size >= maxSize) {
398
+ const oldestKey = cache.keys().next().value;
399
+ if (oldestKey === undefined)
400
+ break;
401
+ cache.delete(oldestKey);
323
402
  }
324
403
  cache.set(key, {
325
404
  data: response,
326
- expires: Date.now() + ttl
405
+ expires: now + ttl
327
406
  });
328
407
  return response;
329
408
  }
330
409
  };
331
410
  }
411
+ var LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
332
412
  function createLoggingPlugin(options) {
333
413
  const logger = options?.logger ?? console;
334
414
  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
- };
415
+ const threshold = LOG_LEVELS[level];
416
+ const shouldLog = (msgLevel) => LOG_LEVELS[msgLevel] >= threshold;
339
417
  return {
340
418
  metadata: {
341
419
  name: "logging",
@@ -384,119 +462,297 @@ function createLoggingPlugin(options) {
384
462
  };
385
463
  }
386
464
  function createTracingPlugin() {
465
+ const TRACE_KEY = "tracing";
466
+ const ensureTrace = (context) => {
467
+ let trace = context.state.get(TRACE_KEY);
468
+ if (!trace) {
469
+ trace = {
470
+ startTime: new Date().toISOString(),
471
+ startNs: Bun.nanoseconds()
472
+ };
473
+ context.state.set(TRACE_KEY, trace);
474
+ }
475
+ return trace;
476
+ };
387
477
  return {
388
478
  metadata: {
389
479
  name: "tracing",
390
480
  version: "1.0.0",
391
481
  description: "Adds tracing information to responses"
392
482
  },
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
- });
483
+ async beforeParse(_query, context) {
484
+ const trace = ensureTrace(context);
485
+ trace.parsingStartNs = Bun.nanoseconds();
486
+ },
487
+ async afterParse(document, context) {
488
+ const trace = ensureTrace(context);
489
+ if (trace.parsingStartNs !== undefined) {
490
+ trace.parsing = {
491
+ startOffset: trace.parsingStartNs - trace.startNs,
492
+ duration: Bun.nanoseconds() - trace.parsingStartNs
493
+ };
494
+ }
495
+ return document;
496
+ },
497
+ async beforeValidate(_document, context) {
498
+ const trace = ensureTrace(context);
499
+ trace.validationStartNs = Bun.nanoseconds();
500
+ },
501
+ async afterValidate(_result, context) {
502
+ const trace = ensureTrace(context);
503
+ if (trace.validationStartNs !== undefined) {
504
+ trace.validation = {
505
+ startOffset: trace.validationStartNs - trace.startNs,
506
+ duration: Bun.nanoseconds() - trace.validationStartNs
507
+ };
508
+ }
509
+ },
510
+ async beforeExecute(_document, context) {
511
+ const trace = ensureTrace(context);
512
+ trace.executionStartNs = Bun.nanoseconds();
402
513
  },
403
514
  async afterExecute(response, context) {
404
- const tracing = context.state.get("tracing");
405
- if (!tracing)
515
+ const trace = context.state.get(TRACE_KEY);
516
+ if (!trace)
406
517
  return response;
407
- const endHrTime = process.hrtime.bigint();
408
- const duration = Number(endHrTime - tracing.startHrTime);
518
+ const endNs = Bun.nanoseconds();
519
+ const duration = endNs - trace.startNs;
520
+ const tracing = {
521
+ version: 1,
522
+ startTime: trace.startTime,
523
+ endTime: new Date().toISOString(),
524
+ duration
525
+ };
526
+ if (trace.parsing) {
527
+ tracing["parsing"] = trace.parsing;
528
+ }
529
+ if (trace.validation) {
530
+ tracing["validation"] = trace.validation;
531
+ }
532
+ if (trace.executionStartNs !== undefined) {
533
+ tracing["execution"] = {
534
+ startOffset: trace.executionStartNs - trace.startNs,
535
+ duration: endNs - trace.executionStartNs
536
+ };
537
+ }
409
538
  return {
410
539
  ...response,
411
540
  extensions: {
412
541
  ...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
- }
542
+ tracing
425
543
  }
426
544
  };
427
545
  }
428
546
  };
429
547
  }
430
- function createDepthLimitPlugin(maxDepth) {
431
- function calculateDepth(node, currentDepth = 0) {
432
- if (!node || typeof node !== "object")
548
+ var MAX_WALKER_VISITS = 1e5;
549
+ function collectFragments(definitions) {
550
+ const fragments = new Map;
551
+ for (const definition of definitions) {
552
+ const typed = definition;
553
+ if (typed.kind === Kind.FRAGMENT_DEFINITION && typed.name) {
554
+ fragments.set(typed.name.value, definition);
555
+ }
556
+ }
557
+ return fragments;
558
+ }
559
+ function fragmentDepthContribution(walker, name) {
560
+ const memoized = walker.fragmentDepths.get(name);
561
+ if (memoized !== undefined) {
562
+ return memoized;
563
+ }
564
+ const fragment = walker.fragments.get(name);
565
+ if (!fragment || walker.inProgress.has(name)) {
566
+ return 0;
567
+ }
568
+ walker.inProgress.add(name);
569
+ const contribution = walkDepth(walker, fragment, 0);
570
+ walker.inProgress.delete(name);
571
+ walker.fragmentDepths.set(name, contribution);
572
+ return contribution;
573
+ }
574
+ function walkDepth(walker, node, currentDepth) {
575
+ if (!node || typeof node !== "object")
576
+ return currentDepth;
577
+ if (++walker.budget.visits > MAX_WALKER_VISITS) {
578
+ throw new DepthLimitError(walker.limit + 1, walker.limit, {
579
+ extensions: {
580
+ reason: "ANALYSIS_BUDGET_EXCEEDED",
581
+ maxNodesVisited: MAX_WALKER_VISITS
582
+ }
583
+ });
584
+ }
585
+ const typedNode = node;
586
+ if (typedNode.kind === Kind.FRAGMENT_SPREAD) {
587
+ const fragmentName = typedNode.name?.value;
588
+ if (fragmentName === undefined) {
433
589
  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
- }
590
+ }
591
+ return currentDepth + fragmentDepthContribution(walker, fragmentName);
592
+ }
593
+ const childDepth = typedNode.kind === Kind.FIELD ? currentDepth + 1 : currentDepth;
594
+ let maxFoundDepth = childDepth;
595
+ if (typedNode.selectionSet) {
596
+ for (const selection of typedNode.selectionSet.selections) {
597
+ const depth = walkDepth(walker, selection, childDepth);
598
+ if (depth > maxFoundDepth) {
599
+ maxFoundDepth = depth;
445
600
  }
446
601
  }
447
- return maxFoundDepth;
602
+ }
603
+ return maxFoundDepth;
604
+ }
605
+ function createDepthLimitPlugin(maxDepth) {
606
+ const limit = typeof maxDepth === "number" ? maxDepth : maxDepth?.maxDepth;
607
+ if (typeof limit !== "number" || !Number.isFinite(limit)) {
608
+ 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
609
  }
449
610
  return {
450
611
  metadata: {
451
612
  name: "depth-limit",
452
613
  version: "1.0.0",
453
- description: `Limits query depth to ${maxDepth}`
614
+ description: `Limits query depth to ${limit}`
454
615
  },
455
616
  async afterParse(document, _context) {
617
+ const walker = {
618
+ fragments: collectFragments(document.definitions),
619
+ fragmentDepths: new Map,
620
+ inProgress: new Set,
621
+ budget: { visits: 0 },
622
+ limit
623
+ };
456
624
  let depth = 0;
457
625
  for (const definition of document.definitions) {
458
626
  if (definition.kind === Kind.OPERATION_DEFINITION) {
459
- const opDepth = calculateDepth(definition, 0);
627
+ const opDepth = walkDepth(walker, definition, 0);
460
628
  if (opDepth > depth) {
461
629
  depth = opDepth;
462
630
  }
463
631
  }
464
632
  }
465
- if (depth > maxDepth) {
466
- throw new Error(`Query depth of ${depth} exceeds maximum allowed depth of ${maxDepth}`);
633
+ if (depth > limit) {
634
+ throw new DepthLimitError(depth, limit);
467
635
  }
468
636
  return document;
469
637
  }
470
638
  };
471
639
  }
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
- }
640
+ var LIST_SIZE_ARGUMENTS = new Set(["first", "last", "limit"]);
641
+ var NO_VARIABLE_DEFAULTS = new Map;
642
+ function toPositiveMultiplier(raw) {
643
+ const numeric = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw) : Number.NaN;
644
+ return Number.isFinite(numeric) && numeric > 0 ? numeric : undefined;
645
+ }
646
+ function collectVariableDefaults(operation) {
647
+ const definitions = operation.variableDefinitions;
648
+ if (!definitions || definitions.length === 0) {
649
+ return NO_VARIABLE_DEFAULTS;
650
+ }
651
+ const defaults = new Map;
652
+ for (const definition of definitions) {
653
+ const name = definition.variable?.name?.value;
654
+ const defaultValue = definition.defaultValue;
655
+ if (name === undefined || !defaultValue)
656
+ continue;
657
+ if (defaultValue.kind !== Kind.INT && defaultValue.kind !== Kind.FLOAT) {
658
+ continue;
659
+ }
660
+ const numeric = toPositiveMultiplier(defaultValue.value);
661
+ if (numeric !== undefined) {
662
+ defaults.set(name, numeric);
663
+ }
664
+ }
665
+ return defaults;
666
+ }
667
+ function resolveListSizeMultiplier(walker, value) {
668
+ if (value.kind === Kind.INT || value.kind === Kind.FLOAT) {
669
+ return toPositiveMultiplier(value.value);
670
+ }
671
+ if (value.kind === Kind.VARIABLE) {
672
+ const name = value.name?.value;
673
+ if (name !== undefined) {
674
+ const supplied = walker.variables?.[name];
675
+ if (supplied !== undefined) {
676
+ const fromRequest = toPositiveMultiplier(supplied);
677
+ if (fromRequest !== undefined) {
678
+ return fromRequest;
489
679
  }
490
680
  }
491
- complexity = defaultComplexity * multiplier;
681
+ const declaredDefault = walker.variableDefaults.get(name);
682
+ if (declaredDefault !== undefined) {
683
+ return declaredDefault;
684
+ }
685
+ }
686
+ return walker.unknownMultiplier;
687
+ }
688
+ return;
689
+ }
690
+ function fragmentComplexityContribution(walker, name) {
691
+ const memoized = walker.fragmentComplexities.get(name);
692
+ if (memoized !== undefined) {
693
+ return memoized;
694
+ }
695
+ const fragment = walker.fragments.get(name);
696
+ if (!fragment || walker.inProgress.has(name)) {
697
+ return 0;
698
+ }
699
+ walker.inProgress.add(name);
700
+ const contribution = walkComplexity(walker, fragment);
701
+ walker.inProgress.delete(name);
702
+ walker.fragmentComplexities.set(name, contribution);
703
+ return contribution;
704
+ }
705
+ function walkComplexity(walker, node) {
706
+ if (!node || typeof node !== "object")
707
+ return 0;
708
+ if (++walker.budget.visits > MAX_WALKER_VISITS) {
709
+ throw new ComplexityError(walker.maxComplexity + 1, walker.maxComplexity, {
710
+ extensions: {
711
+ reason: "ANALYSIS_BUDGET_EXCEEDED",
712
+ maxNodesVisited: MAX_WALKER_VISITS
713
+ }
714
+ });
715
+ }
716
+ const typedNode = node;
717
+ if (typedNode.kind === Kind.FRAGMENT_SPREAD) {
718
+ const fragmentName = typedNode.name?.value;
719
+ if (fragmentName === undefined) {
720
+ return 0;
721
+ }
722
+ return fragmentComplexityContribution(walker, fragmentName);
723
+ }
724
+ let childComplexity = 0;
725
+ if (typedNode.selectionSet) {
726
+ for (const selection of typedNode.selectionSet.selections) {
727
+ childComplexity += walkComplexity(walker, selection);
728
+ }
729
+ }
730
+ if (typedNode.kind === Kind.FIELD) {
731
+ if (walker.calculator) {
732
+ return walker.calculator(node, childComplexity);
492
733
  }
493
- if (typedNode.selectionSet) {
494
- for (const selection of typedNode.selectionSet.selections) {
495
- complexity += calculateComplexity(selection, depth + 1);
734
+ let multiplier = 1;
735
+ if (typedNode.arguments) {
736
+ for (const arg of typedNode.arguments) {
737
+ if (!LIST_SIZE_ARGUMENTS.has(arg.name.value))
738
+ continue;
739
+ const resolved = resolveListSizeMultiplier(walker, arg.value);
740
+ if (resolved !== undefined) {
741
+ multiplier = resolved;
742
+ }
496
743
  }
497
744
  }
498
- return complexity;
745
+ return (walker.defaultComplexity + childComplexity) * multiplier;
499
746
  }
747
+ return childComplexity;
748
+ }
749
+ function createComplexityPlugin(options) {
750
+ const {
751
+ maxComplexity,
752
+ defaultComplexity = 1,
753
+ unknownMultiplier = 100,
754
+ calculator
755
+ } = options;
500
756
  return {
501
757
  metadata: {
502
758
  name: "complexity",
@@ -504,28 +760,42 @@ function createComplexityPlugin(options) {
504
760
  description: `Limits query complexity to ${maxComplexity}`
505
761
  },
506
762
  async afterParse(document, context) {
763
+ const fragments = collectFragments(document.definitions);
764
+ const budget = { visits: 0 };
507
765
  let totalComplexity = 0;
508
766
  for (const definition of document.definitions) {
509
767
  if (definition.kind === Kind.OPERATION_DEFINITION) {
510
- totalComplexity += calculateComplexity(definition, 0);
768
+ const walker = {
769
+ fragments,
770
+ fragmentComplexities: new Map,
771
+ inProgress: new Set,
772
+ budget,
773
+ maxComplexity,
774
+ defaultComplexity,
775
+ calculator,
776
+ variables: context.request.variables,
777
+ variableDefaults: collectVariableDefaults(definition),
778
+ unknownMultiplier
779
+ };
780
+ totalComplexity += walkComplexity(walker, definition);
511
781
  }
512
782
  }
513
783
  context.state.set("complexity", totalComplexity);
514
784
  if (totalComplexity > maxComplexity) {
515
- throw new Error(`Query complexity of ${totalComplexity} exceeds maximum allowed complexity of ${maxComplexity}`);
785
+ throw new ComplexityError(totalComplexity, maxComplexity);
516
786
  }
517
787
  return document;
518
788
  }
519
789
  };
520
790
  }
521
791
  export {
522
- createTracingPlugin,
523
- createPluginManager,
524
- createPlugin,
525
- createLoggingPlugin,
526
- createDepthLimitPlugin,
527
- createComplexityPlugin,
528
- createCachingPlugin,
792
+ PluginManager,
529
793
  composePlugins,
530
- PluginManager
794
+ createCachingPlugin,
795
+ createComplexityPlugin,
796
+ createDepthLimitPlugin,
797
+ createLoggingPlugin,
798
+ createPlugin,
799
+ createPluginManager,
800
+ createTracingPlugin
531
801
  };