@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/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 +488 -218
- 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 +16 -7
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,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
|
-
|
|
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
|
-
},
|
|
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
|
|
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
|
|
369
|
+
description: "Caches successful query responses and serves them on subsequent requests"
|
|
296
370
|
},
|
|
297
|
-
async beforeExecute(
|
|
298
|
-
const key =
|
|
299
|
-
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
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 =
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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:
|
|
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
|
|
336
|
-
|
|
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
|
|
394
|
-
context
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
405
|
-
if (!
|
|
515
|
+
const trace = context.state.get(TRACE_KEY);
|
|
516
|
+
if (!trace)
|
|
406
517
|
return response;
|
|
407
|
-
const
|
|
408
|
-
const duration =
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
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 ${
|
|
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 =
|
|
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 >
|
|
466
|
-
throw new
|
|
633
|
+
if (depth > limit) {
|
|
634
|
+
throw new DepthLimitError(depth, limit);
|
|
467
635
|
}
|
|
468
636
|
return document;
|
|
469
637
|
}
|
|
470
638
|
};
|
|
471
639
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
785
|
+
throw new ComplexityError(totalComplexity, maxComplexity);
|
|
516
786
|
}
|
|
517
787
|
return document;
|
|
518
788
|
}
|
|
519
789
|
};
|
|
520
790
|
}
|
|
521
791
|
export {
|
|
522
|
-
|
|
523
|
-
createPluginManager,
|
|
524
|
-
createPlugin,
|
|
525
|
-
createLoggingPlugin,
|
|
526
|
-
createDepthLimitPlugin,
|
|
527
|
-
createComplexityPlugin,
|
|
528
|
-
createCachingPlugin,
|
|
792
|
+
PluginManager,
|
|
529
793
|
composePlugins,
|
|
530
|
-
|
|
794
|
+
createCachingPlugin,
|
|
795
|
+
createComplexityPlugin,
|
|
796
|
+
createDepthLimitPlugin,
|
|
797
|
+
createLoggingPlugin,
|
|
798
|
+
createPlugin,
|
|
799
|
+
createPluginManager,
|
|
800
|
+
createTracingPlugin
|
|
531
801
|
};
|