@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/LICENSE +1 -1
- package/README.md +395 -99
- package/dist/builtin.d.ts +75 -4
- package/dist/builtin.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +819 -158
- package/dist/manager.d.ts +50 -2
- package/dist/manager.d.ts.map +1 -1
- package/dist/types.d.ts +8 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -4
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
73
|
-
|
|
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
|
|
84
|
-
const
|
|
85
|
-
if (
|
|
86
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
if (
|
|
124
|
-
|
|
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
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
137
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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
|
|
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
|
|
760
|
+
description: "Caches successful query responses and serves them on subsequent requests"
|
|
296
761
|
},
|
|
297
|
-
async beforeExecute(
|
|
298
|
-
const key =
|
|
299
|
-
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
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 =
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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:
|
|
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
|
|
336
|
-
|
|
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
|
|
394
|
-
context
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
405
|
-
if (!
|
|
906
|
+
const trace = context.state.get(TRACE_KEY);
|
|
907
|
+
if (!trace)
|
|
406
908
|
return response;
|
|
407
|
-
const
|
|
408
|
-
const duration =
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
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 ${
|
|
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 =
|
|
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 >
|
|
466
|
-
throw new
|
|
1024
|
+
if (depth > limit) {
|
|
1025
|
+
throw new DepthLimitError(depth, limit);
|
|
467
1026
|
}
|
|
468
1027
|
return document;
|
|
469
1028
|
}
|
|
470
1029
|
};
|
|
471
1030
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
1072
|
+
const declaredDefault = walker.variableDefaults.get(name);
|
|
1073
|
+
if (declaredDefault !== undefined) {
|
|
1074
|
+
return declaredDefault;
|
|
1075
|
+
}
|
|
492
1076
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1176
|
+
throw new ComplexityError(totalComplexity, maxComplexity);
|
|
516
1177
|
}
|
|
517
1178
|
return document;
|
|
518
1179
|
}
|