@leaven-graphql/nestjs 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 +408 -23
- package/dist/decorators.d.ts +92 -3
- package/dist/decorators.d.ts.map +1 -1
- package/dist/driver.d.ts +107 -8
- package/dist/driver.d.ts.map +1 -1
- package/dist/execution-context.d.ts +46 -2
- package/dist/execution-context.d.ts.map +1 -1
- package/dist/graphql-driver.d.ts +180 -0
- package/dist/graphql-driver.d.ts.map +1 -0
- package/dist/guards.d.ts +57 -11
- package/dist/guards.d.ts.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1554 -769
- package/dist/interceptors.d.ts +41 -5
- package/dist/interceptors.d.ts.map +1 -1
- package/dist/middleware.d.ts +100 -4
- package/dist/middleware.d.ts.map +1 -1
- package/dist/module.d.ts +77 -12
- package/dist/module.d.ts.map +1 -1
- package/dist/schema-builder.d.ts +39 -10
- package/dist/schema-builder.d.ts.map +1 -1
- package/dist/subscriptions.d.ts +346 -22
- package/dist/subscriptions.d.ts.map +1 -1
- package/dist/types.d.ts +188 -18
- package/dist/types.d.ts.map +1 -1
- package/package.json +18 -11
package/dist/index.js
CHANGED
|
@@ -14,323 +14,185 @@ var __legacyMetadataTS = (k, v) => {
|
|
|
14
14
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function")
|
|
15
15
|
return Reflect.metadata(k, v);
|
|
16
16
|
};
|
|
17
|
-
var __require = import.meta.require;
|
|
18
17
|
|
|
19
18
|
// src/module.ts
|
|
20
19
|
import {
|
|
21
20
|
Module,
|
|
22
|
-
Global
|
|
21
|
+
Global,
|
|
22
|
+
Inject as Inject5,
|
|
23
|
+
Optional
|
|
23
24
|
} from "@nestjs/common";
|
|
24
25
|
|
|
25
|
-
// src/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
// src/types.ts
|
|
27
|
+
var LEAVEN_MODULE_OPTIONS = "LEAVEN_MODULE_OPTIONS";
|
|
28
|
+
var LEAVEN_DRIVER = "LEAVEN_DRIVER";
|
|
29
|
+
var LEAVEN_PUBSUB = "LEAVEN_PUBSUB";
|
|
30
|
+
|
|
31
|
+
// src/module.ts
|
|
32
|
+
import { createPubSub } from "@leaven-graphql/ws";
|
|
33
|
+
|
|
34
|
+
// src/guards.ts
|
|
35
|
+
import {
|
|
36
|
+
Injectable,
|
|
37
|
+
UnauthorizedException,
|
|
38
|
+
ForbiddenException,
|
|
39
|
+
SetMetadata,
|
|
40
|
+
Inject
|
|
41
|
+
} from "@nestjs/common";
|
|
42
|
+
import { Reflector } from "@nestjs/core";
|
|
43
|
+
|
|
44
|
+
// src/execution-context.ts
|
|
45
|
+
var GQL_RESOLVER_ARGS = {
|
|
46
|
+
ROOT: 0,
|
|
47
|
+
ARGS: 1,
|
|
48
|
+
CONTEXT: 2,
|
|
49
|
+
INFO: 3
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
class GqlExecutionContext {
|
|
53
|
+
host;
|
|
54
|
+
args;
|
|
55
|
+
constructor(host) {
|
|
56
|
+
this.host = host;
|
|
57
|
+
this.args = host.getArgs();
|
|
34
58
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
this.schema = this.options.schema;
|
|
38
|
-
this.initializeExecutor();
|
|
39
|
-
}
|
|
59
|
+
static create(context) {
|
|
60
|
+
return new GqlExecutionContext(context);
|
|
40
61
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
this.executor.clearCaches();
|
|
44
|
-
this.executor = null;
|
|
45
|
-
}
|
|
62
|
+
getContext() {
|
|
63
|
+
return this.args[GQL_RESOLVER_ARGS.CONTEXT];
|
|
46
64
|
}
|
|
47
|
-
|
|
48
|
-
this.
|
|
49
|
-
this.initializeExecutor();
|
|
65
|
+
getRoot() {
|
|
66
|
+
return this.args[GQL_RESOLVER_ARGS.ROOT];
|
|
50
67
|
}
|
|
51
|
-
|
|
52
|
-
return this.
|
|
68
|
+
getArgs() {
|
|
69
|
+
return this.args[GQL_RESOLVER_ARGS.ARGS];
|
|
53
70
|
}
|
|
54
|
-
|
|
55
|
-
return this.
|
|
71
|
+
getInfo() {
|
|
72
|
+
return this.args[GQL_RESOLVER_ARGS.INFO];
|
|
56
73
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
const config = {
|
|
62
|
-
schema: this.schema,
|
|
63
|
-
cache: this.options.cache,
|
|
64
|
-
metrics: this.options.metrics,
|
|
65
|
-
maxComplexity: this.options.maxComplexity
|
|
66
|
-
};
|
|
67
|
-
this.executor = new LeavenExecutor(config);
|
|
74
|
+
getArg(name) {
|
|
75
|
+
const args = this.getArgs();
|
|
76
|
+
return args[name];
|
|
68
77
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
const queryString = typeof query === "string" ? query : this.documentToString(query);
|
|
74
|
-
try {
|
|
75
|
-
const result = await this.executor.execute({
|
|
76
|
-
query: queryString,
|
|
77
|
-
variables,
|
|
78
|
-
operationName
|
|
79
|
-
}, context);
|
|
80
|
-
const response = result.response;
|
|
81
|
-
let formattedErrors = response.errors;
|
|
82
|
-
if (formattedErrors && this.options.formatError) {
|
|
83
|
-
formattedErrors = formattedErrors.map(this.options.formatError);
|
|
84
|
-
}
|
|
85
|
-
let extensions = response.extensions;
|
|
86
|
-
if (this.options.metrics && result.metrics) {
|
|
87
|
-
extensions = {
|
|
88
|
-
...extensions,
|
|
89
|
-
metrics: result.metrics
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
return {
|
|
93
|
-
data: response.data,
|
|
94
|
-
errors: formattedErrors,
|
|
95
|
-
extensions
|
|
96
|
-
};
|
|
97
|
-
} catch (error) {
|
|
98
|
-
const message = error instanceof Error ? error.message : "Execution failed";
|
|
99
|
-
const formattedError = {
|
|
100
|
-
message,
|
|
101
|
-
extensions: {
|
|
102
|
-
code: "INTERNAL_SERVER_ERROR",
|
|
103
|
-
...this.options.includeStacktraceInErrorResponses && error instanceof Error ? { stacktrace: error.stack?.split(`
|
|
104
|
-
`) } : {}
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
return {
|
|
108
|
-
errors: [
|
|
109
|
-
this.options.formatError ? this.options.formatError(formattedError) : formattedError
|
|
110
|
-
]
|
|
111
|
-
};
|
|
112
|
-
}
|
|
78
|
+
getContextProperty(name) {
|
|
79
|
+
const ctx = this.getContext();
|
|
80
|
+
return ctx[name];
|
|
113
81
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const context = await this.createContext(request, response);
|
|
117
|
-
return this.execute(body.query, body.variables, context, body.operationName);
|
|
82
|
+
getHost() {
|
|
83
|
+
return this.host;
|
|
118
84
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (contentType.includes("application/json")) {
|
|
122
|
-
const body = await request.json();
|
|
123
|
-
return {
|
|
124
|
-
query: body.query,
|
|
125
|
-
variables: body.variables,
|
|
126
|
-
operationName: body.operationName
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
const url = new URL(request.url);
|
|
130
|
-
const query = url.searchParams.get("query");
|
|
131
|
-
const variables = url.searchParams.get("variables");
|
|
132
|
-
const operationName = url.searchParams.get("operationName");
|
|
133
|
-
if (!query) {
|
|
134
|
-
throw new Error("Query is required");
|
|
135
|
-
}
|
|
136
|
-
return {
|
|
137
|
-
query,
|
|
138
|
-
variables: variables ? JSON.parse(variables) : undefined,
|
|
139
|
-
operationName: operationName ?? undefined
|
|
140
|
-
};
|
|
85
|
+
getType() {
|
|
86
|
+
return this.host.getType?.() ?? "unknown";
|
|
141
87
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
req: request,
|
|
145
|
-
res: response
|
|
146
|
-
};
|
|
147
|
-
if (this.options.context) {
|
|
148
|
-
const customContext = await this.options.context(request, response);
|
|
149
|
-
return { ...baseContext, ...customContext };
|
|
150
|
-
}
|
|
151
|
-
return baseContext;
|
|
88
|
+
isGraphQL() {
|
|
89
|
+
return this.getType() === "graphql";
|
|
152
90
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return print(document);
|
|
91
|
+
getHandler() {
|
|
92
|
+
return this.host.getHandler?.();
|
|
156
93
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return this.options.playground;
|
|
160
|
-
}
|
|
161
|
-
return true;
|
|
94
|
+
getClass() {
|
|
95
|
+
return this.host.getClass?.();
|
|
162
96
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
return true;
|
|
97
|
+
getFieldName() {
|
|
98
|
+
const info = this.getInfo();
|
|
99
|
+
return info?.fieldName;
|
|
168
100
|
}
|
|
169
|
-
|
|
170
|
-
|
|
101
|
+
getParentTypeName() {
|
|
102
|
+
const info = this.getInfo();
|
|
103
|
+
return info?.parentType?.name;
|
|
171
104
|
}
|
|
172
|
-
|
|
173
|
-
|
|
105
|
+
getReturnTypeName() {
|
|
106
|
+
const info = this.getInfo();
|
|
107
|
+
return info?.returnType?.toString();
|
|
174
108
|
}
|
|
175
|
-
|
|
176
|
-
this.
|
|
109
|
+
getOperationName() {
|
|
110
|
+
const info = this.getInfo();
|
|
111
|
+
return info?.operation?.name?.value;
|
|
177
112
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
__legacyMetadataTS("design:paramtypes", [
|
|
182
|
-
typeof LeavenModuleOptions === "undefined" ? Object : LeavenModuleOptions
|
|
183
|
-
])
|
|
184
|
-
], LeavenDriver);
|
|
185
|
-
|
|
186
|
-
// src/module.ts
|
|
187
|
-
var LEAVEN_MODULE_OPTIONS = "LEAVEN_MODULE_OPTIONS";
|
|
188
|
-
var LEAVEN_DRIVER = "LEAVEN_DRIVER";
|
|
189
|
-
|
|
190
|
-
class LeavenModule {
|
|
191
|
-
static forRoot(options = {}) {
|
|
192
|
-
const optionsProvider = {
|
|
193
|
-
provide: LEAVEN_MODULE_OPTIONS,
|
|
194
|
-
useValue: options
|
|
195
|
-
};
|
|
196
|
-
const driverProvider = {
|
|
197
|
-
provide: LEAVEN_DRIVER,
|
|
198
|
-
useFactory: (opts) => new LeavenDriver(opts),
|
|
199
|
-
inject: [LEAVEN_MODULE_OPTIONS]
|
|
200
|
-
};
|
|
201
|
-
return {
|
|
202
|
-
module: LeavenModule,
|
|
203
|
-
providers: [optionsProvider, driverProvider],
|
|
204
|
-
exports: [LEAVEN_MODULE_OPTIONS, LEAVEN_DRIVER]
|
|
205
|
-
};
|
|
113
|
+
getOperationType() {
|
|
114
|
+
const info = this.getInfo();
|
|
115
|
+
return info?.operation?.operation;
|
|
206
116
|
}
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
provide: LEAVEN_DRIVER,
|
|
211
|
-
useFactory: (opts) => new LeavenDriver(opts),
|
|
212
|
-
inject: [LEAVEN_MODULE_OPTIONS]
|
|
213
|
-
};
|
|
214
|
-
return {
|
|
215
|
-
module: LeavenModule,
|
|
216
|
-
imports: options.imports ?? [],
|
|
217
|
-
providers: [...asyncProviders, driverProvider],
|
|
218
|
-
exports: [LEAVEN_MODULE_OPTIONS, LEAVEN_DRIVER]
|
|
219
|
-
};
|
|
117
|
+
getVariables() {
|
|
118
|
+
const info = this.getInfo();
|
|
119
|
+
return info?.variableValues ?? {};
|
|
220
120
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return [this.createAsyncOptionsProvider(options)];
|
|
224
|
-
}
|
|
225
|
-
const useClass = options.useClass;
|
|
226
|
-
return [
|
|
227
|
-
this.createAsyncOptionsProvider(options),
|
|
228
|
-
{
|
|
229
|
-
provide: useClass,
|
|
230
|
-
useClass
|
|
231
|
-
}
|
|
232
|
-
];
|
|
121
|
+
getPath() {
|
|
122
|
+
return this.getFullPath().filter((segment) => typeof segment === "string");
|
|
233
123
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
124
|
+
getFullPath() {
|
|
125
|
+
const info = this.getInfo();
|
|
126
|
+
if (!info?.path)
|
|
127
|
+
return [];
|
|
128
|
+
const path = [];
|
|
129
|
+
let current = info.path;
|
|
130
|
+
while (current) {
|
|
131
|
+
path.unshift(current.key);
|
|
132
|
+
current = current.prev;
|
|
241
133
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
134
|
+
return path;
|
|
135
|
+
}
|
|
136
|
+
getSelectedFields() {
|
|
137
|
+
const info = this.getInfo();
|
|
138
|
+
if (!info?.fieldNodes?.length)
|
|
139
|
+
return [];
|
|
140
|
+
const fields = new Set;
|
|
141
|
+
const visitedFragments = new Set;
|
|
142
|
+
const collect = (selectionSet) => {
|
|
143
|
+
if (!selectionSet?.selections)
|
|
144
|
+
return;
|
|
145
|
+
for (const selection of selectionSet.selections) {
|
|
146
|
+
if (selection.kind === "Field") {
|
|
147
|
+
fields.add(selection.name.value);
|
|
148
|
+
} else if (selection.kind === "InlineFragment") {
|
|
149
|
+
collect(selection.selectionSet);
|
|
150
|
+
} else if (selection.kind === "FragmentSpread") {
|
|
151
|
+
const fragmentName = selection.name.value;
|
|
152
|
+
if (visitedFragments.has(fragmentName))
|
|
153
|
+
continue;
|
|
154
|
+
visitedFragments.add(fragmentName);
|
|
155
|
+
collect(info.fragments?.[fragmentName]?.selectionSet);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
249
158
|
};
|
|
159
|
+
for (const fieldNode of info.fieldNodes) {
|
|
160
|
+
collect(fieldNode?.selectionSet);
|
|
161
|
+
}
|
|
162
|
+
return [...fields];
|
|
250
163
|
}
|
|
251
164
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
createParamDecorator,
|
|
259
|
-
SetMetadata,
|
|
260
|
-
applyDecorators
|
|
261
|
-
} from "@nestjs/common";
|
|
262
|
-
var COMPLEXITY_KEY = "leaven:complexity";
|
|
263
|
-
var DEPRECATED_KEY = "leaven:deprecated";
|
|
264
|
-
var DESCRIPTION_KEY = "leaven:description";
|
|
265
|
-
var CACHE_KEY = "leaven:cache";
|
|
266
|
-
var SUBSCRIPTION_FILTER_KEY = "leaven:subscription:filter";
|
|
267
|
-
var Context = createParamDecorator((data, ctx) => {
|
|
268
|
-
const gqlContext = getGqlContext(ctx);
|
|
269
|
-
if (data) {
|
|
270
|
-
return gqlContext[data];
|
|
165
|
+
var executionContextCache = new WeakMap;
|
|
166
|
+
function getCachedGqlExecutionContext(context) {
|
|
167
|
+
let gqlContext = executionContextCache.get(context);
|
|
168
|
+
if (!gqlContext) {
|
|
169
|
+
gqlContext = GqlExecutionContext.create(context);
|
|
170
|
+
executionContextCache.set(context, gqlContext);
|
|
271
171
|
}
|
|
272
172
|
return gqlContext;
|
|
273
|
-
});
|
|
274
|
-
var Info = createParamDecorator((_data, ctx) => {
|
|
275
|
-
const args = ctx.getArgs();
|
|
276
|
-
return args[3];
|
|
277
|
-
});
|
|
278
|
-
var Root = createParamDecorator((_data, ctx) => {
|
|
279
|
-
const args = ctx.getArgs();
|
|
280
|
-
return args[0];
|
|
281
|
-
});
|
|
282
|
-
var Parent = Root;
|
|
283
|
-
var Args = createParamDecorator((data, ctx) => {
|
|
284
|
-
const args = ctx.getArgs();
|
|
285
|
-
const gqlArgs = args[1];
|
|
286
|
-
if (data) {
|
|
287
|
-
return gqlArgs?.[data];
|
|
288
|
-
}
|
|
289
|
-
return gqlArgs;
|
|
290
|
-
});
|
|
291
|
-
function Complexity(value) {
|
|
292
|
-
return SetMetadata(COMPLEXITY_KEY, value);
|
|
293
|
-
}
|
|
294
|
-
function Deprecated(reason) {
|
|
295
|
-
return SetMetadata(DEPRECATED_KEY, reason);
|
|
296
|
-
}
|
|
297
|
-
function Description(text) {
|
|
298
|
-
return SetMetadata(DESCRIPTION_KEY, text);
|
|
299
|
-
}
|
|
300
|
-
function CacheHint(options) {
|
|
301
|
-
return SetMetadata(CACHE_KEY, options);
|
|
302
173
|
}
|
|
303
|
-
function
|
|
304
|
-
return
|
|
174
|
+
function getGqlContext(context) {
|
|
175
|
+
return getCachedGqlExecutionContext(context).getContext();
|
|
305
176
|
}
|
|
306
|
-
function
|
|
307
|
-
return
|
|
177
|
+
function getGqlArgs(context) {
|
|
178
|
+
return getCachedGqlExecutionContext(context).getArgs();
|
|
308
179
|
}
|
|
309
|
-
function
|
|
310
|
-
|
|
311
|
-
return args[2];
|
|
180
|
+
function getGqlInfo(context) {
|
|
181
|
+
return getCachedGqlExecutionContext(context).getInfo();
|
|
312
182
|
}
|
|
313
|
-
function
|
|
314
|
-
return ()
|
|
315
|
-
const gqlContext = getGqlContext(ctx);
|
|
316
|
-
return gqlContext[key];
|
|
317
|
-
})();
|
|
183
|
+
function getGqlRoot(context) {
|
|
184
|
+
return getCachedGqlExecutionContext(context).getRoot();
|
|
318
185
|
}
|
|
186
|
+
|
|
319
187
|
// src/guards.ts
|
|
320
|
-
import {
|
|
321
|
-
Injectable as Injectable2,
|
|
322
|
-
UnauthorizedException,
|
|
323
|
-
ForbiddenException,
|
|
324
|
-
SetMetadata as SetMetadata2,
|
|
325
|
-
Inject
|
|
326
|
-
} from "@nestjs/common";
|
|
327
|
-
import { Reflector } from "@nestjs/core";
|
|
328
188
|
var IS_PUBLIC_KEY = "leaven:isPublic";
|
|
329
189
|
var ROLES_KEY = "leaven:roles";
|
|
330
190
|
var PERMISSIONS_KEY = "leaven:permissions";
|
|
331
|
-
var
|
|
332
|
-
var
|
|
333
|
-
var
|
|
191
|
+
var LEAVEN_MAX_COMPLEXITY = "LEAVEN_MAX_COMPLEXITY";
|
|
192
|
+
var LEAVEN_MAX_DEPTH = "LEAVEN_MAX_DEPTH";
|
|
193
|
+
var Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
|
194
|
+
var Roles = (...roles) => SetMetadata(ROLES_KEY, roles);
|
|
195
|
+
var Permissions = (...permissions) => SetMetadata(PERMISSIONS_KEY, permissions);
|
|
334
196
|
|
|
335
197
|
class AuthGuard {
|
|
336
198
|
reflector;
|
|
@@ -353,15 +215,14 @@ class AuthGuard {
|
|
|
353
215
|
return true;
|
|
354
216
|
}
|
|
355
217
|
getGqlContext(context) {
|
|
356
|
-
|
|
357
|
-
return args[2];
|
|
218
|
+
return getGqlContext(context);
|
|
358
219
|
}
|
|
359
220
|
extractUser(context) {
|
|
360
221
|
return context.user ?? context.req?.user ?? null;
|
|
361
222
|
}
|
|
362
223
|
}
|
|
363
224
|
AuthGuard = __legacyDecorateClassTS([
|
|
364
|
-
|
|
225
|
+
Injectable(),
|
|
365
226
|
__legacyMetadataTS("design:paramtypes", [
|
|
366
227
|
typeof Reflector === "undefined" ? Object : Reflector
|
|
367
228
|
])
|
|
@@ -393,8 +254,7 @@ class RolesGuard {
|
|
|
393
254
|
return true;
|
|
394
255
|
}
|
|
395
256
|
getGqlContext(context) {
|
|
396
|
-
|
|
397
|
-
return args[2];
|
|
257
|
+
return getGqlContext(context);
|
|
398
258
|
}
|
|
399
259
|
extractUser(context) {
|
|
400
260
|
return context.user ?? context.req?.user ?? null;
|
|
@@ -412,7 +272,7 @@ class RolesGuard {
|
|
|
412
272
|
}
|
|
413
273
|
}
|
|
414
274
|
RolesGuard = __legacyDecorateClassTS([
|
|
415
|
-
|
|
275
|
+
Injectable(),
|
|
416
276
|
__legacyMetadataTS("design:paramtypes", [
|
|
417
277
|
typeof Reflector === "undefined" ? Object : Reflector
|
|
418
278
|
])
|
|
@@ -441,8 +301,7 @@ class PermissionsGuard {
|
|
|
441
301
|
return true;
|
|
442
302
|
}
|
|
443
303
|
getGqlContext(context) {
|
|
444
|
-
|
|
445
|
-
return args[2];
|
|
304
|
+
return getGqlContext(context);
|
|
446
305
|
}
|
|
447
306
|
extractUser(context) {
|
|
448
307
|
return context.user ?? context.req?.user ?? null;
|
|
@@ -460,7 +319,7 @@ class PermissionsGuard {
|
|
|
460
319
|
}
|
|
461
320
|
}
|
|
462
321
|
PermissionsGuard = __legacyDecorateClassTS([
|
|
463
|
-
|
|
322
|
+
Injectable(),
|
|
464
323
|
__legacyMetadataTS("design:paramtypes", [
|
|
465
324
|
typeof Reflector === "undefined" ? Object : Reflector
|
|
466
325
|
])
|
|
@@ -472,21 +331,23 @@ class ComplexityGuard {
|
|
|
472
331
|
this.maxComplexity = maxComplexity;
|
|
473
332
|
}
|
|
474
333
|
canActivate(context) {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
334
|
+
if (this.maxComplexity === Number.POSITIVE_INFINITY) {
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
const gqlContext = getGqlContext(context);
|
|
338
|
+
const currentComplexity = gqlContext?._queryComplexity;
|
|
339
|
+
if (typeof currentComplexity !== "number" || Number.isNaN(currentComplexity)) {
|
|
340
|
+
throw new ForbiddenException("Query complexity could not be determined; rejecting the request");
|
|
341
|
+
}
|
|
342
|
+
if (currentComplexity > this.maxComplexity) {
|
|
478
343
|
throw new ForbiddenException(`Query complexity ${currentComplexity} exceeds maximum allowed ${this.maxComplexity}`);
|
|
479
344
|
}
|
|
480
345
|
return true;
|
|
481
346
|
}
|
|
482
|
-
getGqlContext(context) {
|
|
483
|
-
const args = context.getArgs();
|
|
484
|
-
return args[2];
|
|
485
|
-
}
|
|
486
347
|
}
|
|
487
348
|
ComplexityGuard = __legacyDecorateClassTS([
|
|
488
|
-
|
|
489
|
-
__legacyDecorateParamTS(0, Inject(
|
|
349
|
+
Injectable(),
|
|
350
|
+
__legacyDecorateParamTS(0, Inject(LEAVEN_MAX_COMPLEXITY)),
|
|
490
351
|
__legacyMetadataTS("design:paramtypes", [
|
|
491
352
|
Number
|
|
492
353
|
])
|
|
@@ -498,188 +359,305 @@ class DepthGuard {
|
|
|
498
359
|
this.maxDepth = maxDepth;
|
|
499
360
|
}
|
|
500
361
|
canActivate(context) {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
362
|
+
if (this.maxDepth === Number.POSITIVE_INFINITY) {
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
const gqlContext = getGqlContext(context);
|
|
366
|
+
const currentDepth = gqlContext?._queryDepth;
|
|
367
|
+
if (typeof currentDepth !== "number" || Number.isNaN(currentDepth)) {
|
|
368
|
+
throw new ForbiddenException("Query depth could not be determined; rejecting the request");
|
|
369
|
+
}
|
|
370
|
+
if (currentDepth > this.maxDepth) {
|
|
504
371
|
throw new ForbiddenException(`Query depth ${currentDepth} exceeds maximum allowed ${this.maxDepth}`);
|
|
505
372
|
}
|
|
506
373
|
return true;
|
|
507
374
|
}
|
|
508
|
-
getGqlContext(context) {
|
|
509
|
-
const args = context.getArgs();
|
|
510
|
-
return args[2];
|
|
511
|
-
}
|
|
512
375
|
}
|
|
513
376
|
DepthGuard = __legacyDecorateClassTS([
|
|
514
|
-
|
|
515
|
-
__legacyDecorateParamTS(0, Inject(
|
|
377
|
+
Injectable(),
|
|
378
|
+
__legacyDecorateParamTS(0, Inject(LEAVEN_MAX_DEPTH)),
|
|
516
379
|
__legacyMetadataTS("design:paramtypes", [
|
|
517
380
|
Number
|
|
518
381
|
])
|
|
519
382
|
], DepthGuard);
|
|
520
|
-
|
|
383
|
+
|
|
384
|
+
// src/driver.ts
|
|
385
|
+
import { Injectable as Injectable2 } from "@nestjs/common";
|
|
521
386
|
import {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
387
|
+
GraphQLError,
|
|
388
|
+
parse,
|
|
389
|
+
print
|
|
390
|
+
} from "graphql";
|
|
391
|
+
import {
|
|
392
|
+
LeavenExecutor,
|
|
393
|
+
calculateQueryDepth,
|
|
394
|
+
compileQuery
|
|
395
|
+
} from "@leaven-graphql/core";
|
|
396
|
+
import { ValidationError, formatError, isLeavenError } from "@leaven-graphql/errors";
|
|
397
|
+
class LeavenDriver {
|
|
398
|
+
executor = null;
|
|
399
|
+
schema = null;
|
|
400
|
+
options;
|
|
401
|
+
static MAX_ANALYSIS_ENTRIES = 1000;
|
|
402
|
+
analysisCache = new Map;
|
|
403
|
+
constructor(options) {
|
|
404
|
+
this.options = options;
|
|
535
405
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
class ErrorFormattingInterceptor {
|
|
542
|
-
intercept(_context, next) {
|
|
543
|
-
return next.handle().pipe(catchError((error) => {
|
|
544
|
-
const formattedError = this.formatError(error);
|
|
545
|
-
return throwError(() => formattedError);
|
|
546
|
-
}));
|
|
406
|
+
async onModuleInit() {
|
|
407
|
+
if (this.options.schema) {
|
|
408
|
+
this.schema = this.options.schema;
|
|
409
|
+
this.initializeExecutor();
|
|
410
|
+
}
|
|
547
411
|
}
|
|
548
|
-
|
|
549
|
-
if (
|
|
550
|
-
|
|
551
|
-
|
|
412
|
+
async onModuleDestroy() {
|
|
413
|
+
if (this.executor) {
|
|
414
|
+
await this.executor.clearCaches();
|
|
415
|
+
this.executor = null;
|
|
552
416
|
}
|
|
553
|
-
|
|
417
|
+
this.analysisCache.clear();
|
|
554
418
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
], ErrorFormattingInterceptor);
|
|
559
|
-
|
|
560
|
-
class ComplexityInterceptor {
|
|
561
|
-
reflector;
|
|
562
|
-
constructor(reflector) {
|
|
563
|
-
this.reflector = reflector;
|
|
419
|
+
setSchema(schema) {
|
|
420
|
+
this.schema = schema;
|
|
421
|
+
this.initializeExecutor();
|
|
564
422
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
423
|
+
getSchema() {
|
|
424
|
+
return this.schema;
|
|
425
|
+
}
|
|
426
|
+
getExecutor() {
|
|
427
|
+
return this.executor;
|
|
428
|
+
}
|
|
429
|
+
initializeExecutor() {
|
|
430
|
+
if (!this.schema) {
|
|
431
|
+
throw new Error("Schema must be set before initializing executor");
|
|
432
|
+
}
|
|
433
|
+
const config = {
|
|
434
|
+
schema: this.schema,
|
|
435
|
+
cache: this.options.cache,
|
|
436
|
+
metrics: this.options.metrics,
|
|
437
|
+
maxComplexity: this.options.maxComplexity,
|
|
438
|
+
maxDepth: this.options.maxDepth,
|
|
439
|
+
introspection: this.isIntrospectionEnabled()
|
|
440
|
+
};
|
|
441
|
+
this.analysisCache.clear();
|
|
442
|
+
this.executor = new LeavenExecutor(config);
|
|
443
|
+
}
|
|
444
|
+
async execute(query, variables, context, operationName) {
|
|
445
|
+
if (!this.executor) {
|
|
446
|
+
throw new Error("Executor not initialized. Set schema first.");
|
|
447
|
+
}
|
|
448
|
+
const queryString = typeof query === "string" ? query : print(query);
|
|
449
|
+
if (context) {
|
|
450
|
+
this.attachQueryAnalysis(query, queryString, context, operationName);
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
const result = await this.executor.execute({
|
|
454
|
+
query: queryString,
|
|
455
|
+
variables,
|
|
456
|
+
operationName
|
|
457
|
+
}, context);
|
|
458
|
+
const response = result.response;
|
|
459
|
+
let formattedErrors = response.errors;
|
|
460
|
+
if (formattedErrors && this.options.formatError) {
|
|
461
|
+
const formatErrorFn = this.options.formatError;
|
|
462
|
+
formattedErrors = formattedErrors.map((error) => formatErrorFn(error));
|
|
463
|
+
}
|
|
464
|
+
let extensions = response.extensions;
|
|
465
|
+
if (this.options.metrics && result.metrics) {
|
|
466
|
+
extensions = {
|
|
467
|
+
...extensions,
|
|
468
|
+
metrics: result.metrics
|
|
469
|
+
};
|
|
572
470
|
}
|
|
471
|
+
return {
|
|
472
|
+
data: response.data,
|
|
473
|
+
errors: formattedErrors,
|
|
474
|
+
extensions
|
|
475
|
+
};
|
|
476
|
+
} catch (error) {
|
|
477
|
+
return { errors: [this.formatThrownError(error)] };
|
|
573
478
|
}
|
|
574
|
-
return next.handle();
|
|
575
479
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
class CachingInterceptor {
|
|
585
|
-
reflector;
|
|
586
|
-
constructor(reflector) {
|
|
587
|
-
this.reflector = reflector;
|
|
480
|
+
formatThrownError(error) {
|
|
481
|
+
const isProduction = false;
|
|
482
|
+
const includeStack = this.options.includeStacktraceInErrorResponses ?? false;
|
|
483
|
+
const formattedError = formatError(error, {
|
|
484
|
+
maskErrors: !isLeavenError(error) && isProduction && !includeStack,
|
|
485
|
+
includeStackTrace: includeStack
|
|
486
|
+
});
|
|
487
|
+
return this.options.formatError ? this.options.formatError(formattedError) : formattedError;
|
|
588
488
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
489
|
+
async subscribe(query, variables, context, operationName) {
|
|
490
|
+
if (!this.executor) {
|
|
491
|
+
throw new Error("Executor not initialized. Set schema first.");
|
|
492
|
+
}
|
|
493
|
+
const queryString = typeof query === "string" ? query : print(query);
|
|
494
|
+
if (context) {
|
|
495
|
+
this.attachQueryAnalysis(query, queryString, context, operationName);
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
const result = await this.executor.subscribe({ query: queryString, variables, operationName }, context);
|
|
499
|
+
if (!(Symbol.asyncIterator in result)) {
|
|
500
|
+
let errors = result.errors;
|
|
501
|
+
if (errors && this.options.formatError) {
|
|
502
|
+
const formatErrorFn = this.options.formatError;
|
|
503
|
+
errors = errors.map((error) => formatErrorFn(error));
|
|
600
504
|
}
|
|
505
|
+
return { errors };
|
|
601
506
|
}
|
|
602
|
-
|
|
507
|
+
return result;
|
|
508
|
+
} catch (error) {
|
|
509
|
+
return { errors: [this.formatThrownError(error)] };
|
|
510
|
+
}
|
|
603
511
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
typeof Reflector2 === "undefined" ? Object : Reflector2
|
|
609
|
-
])
|
|
610
|
-
], CachingInterceptor);
|
|
611
|
-
|
|
612
|
-
class MetricsInterceptor {
|
|
613
|
-
metrics = new Map;
|
|
614
|
-
intercept(context, next) {
|
|
615
|
-
const className = context.getClass().name;
|
|
616
|
-
const methodName = context.getHandler().name;
|
|
617
|
-
const key = `${className}.${methodName}`;
|
|
618
|
-
const start = performance.now();
|
|
619
|
-
return next.handle().pipe(tap(() => {
|
|
620
|
-
const duration = performance.now() - start;
|
|
621
|
-
this.recordSuccess(key, duration);
|
|
622
|
-
}), catchError((error) => {
|
|
623
|
-
const duration = performance.now() - start;
|
|
624
|
-
this.recordError(key, duration);
|
|
625
|
-
return throwError(() => error);
|
|
626
|
-
}));
|
|
512
|
+
async handleRequest(request, response) {
|
|
513
|
+
const body = await this.parseRequestBody(request);
|
|
514
|
+
const context = await this.createContext(request, response);
|
|
515
|
+
return this.execute(body.query, body.variables, context, body.operationName);
|
|
627
516
|
}
|
|
628
|
-
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
517
|
+
async parseRequestBody(request) {
|
|
518
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
519
|
+
if (request.method === "POST") {
|
|
520
|
+
if (contentType.includes("application/json")) {
|
|
521
|
+
const body = await request.json();
|
|
522
|
+
return {
|
|
523
|
+
query: body.query,
|
|
524
|
+
variables: body.variables,
|
|
525
|
+
operationName: body.operationName
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
if (contentType.includes("application/graphql")) {
|
|
529
|
+
const query2 = await request.text();
|
|
530
|
+
if (!query2) {
|
|
531
|
+
throw new ValidationError("Query is required");
|
|
532
|
+
}
|
|
533
|
+
return { query: query2 };
|
|
534
|
+
}
|
|
535
|
+
throw new ValidationError(`Unsupported content type "${contentType || "(none)"}": use application/json or application/graphql`);
|
|
536
|
+
}
|
|
537
|
+
const url = new URL(request.url);
|
|
538
|
+
const query = url.searchParams.get("query");
|
|
539
|
+
const variables = url.searchParams.get("variables");
|
|
540
|
+
const operationName = url.searchParams.get("operationName");
|
|
541
|
+
if (!query) {
|
|
542
|
+
throw new ValidationError("Query is required");
|
|
543
|
+
}
|
|
544
|
+
let parsedVariables;
|
|
545
|
+
if (variables) {
|
|
546
|
+
try {
|
|
547
|
+
parsedVariables = JSON.parse(variables);
|
|
548
|
+
} catch {
|
|
549
|
+
throw new ValidationError('The "variables" query parameter contains malformed JSON');
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
query,
|
|
554
|
+
variables: parsedVariables,
|
|
555
|
+
operationName: operationName ?? undefined
|
|
556
|
+
};
|
|
636
557
|
}
|
|
637
|
-
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
558
|
+
async createContext(request, response) {
|
|
559
|
+
const baseContext = {
|
|
560
|
+
req: request,
|
|
561
|
+
res: response
|
|
562
|
+
};
|
|
563
|
+
if (this.options.context) {
|
|
564
|
+
const customContext = await this.options.context(request, response);
|
|
565
|
+
return { ...baseContext, ...customContext };
|
|
566
|
+
}
|
|
567
|
+
return baseContext;
|
|
643
568
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
569
|
+
attachQueryAnalysis(query, queryString, context, operationName) {
|
|
570
|
+
if (this.options.maxComplexity === undefined && this.options.maxDepth === undefined) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const target = context;
|
|
574
|
+
const cacheKey = `${queryString}\x00${operationName ?? ""}`;
|
|
575
|
+
const memoized = this.analysisCache.get(cacheKey);
|
|
576
|
+
if (memoized) {
|
|
577
|
+
this.analysisCache.delete(cacheKey);
|
|
578
|
+
this.analysisCache.set(cacheKey, memoized);
|
|
579
|
+
target._queryDepth = memoized.depth;
|
|
580
|
+
target._queryComplexity = memoized.complexity;
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
let document;
|
|
584
|
+
try {
|
|
585
|
+
document = typeof query === "string" ? parse(query) : query;
|
|
586
|
+
} catch (error) {
|
|
587
|
+
if (error instanceof GraphQLError) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
this.recordAnalysisFailure(target, error);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
const depth = calculateQueryDepth(document);
|
|
595
|
+
if (!this.schema) {
|
|
596
|
+
throw new Error("Schema is not set; query complexity cannot be computed");
|
|
597
|
+
}
|
|
598
|
+
const complexity = compileQuery(this.schema, document, operationName, {
|
|
599
|
+
calculateComplexity: true
|
|
600
|
+
}).complexity;
|
|
601
|
+
target._queryDepth = depth;
|
|
602
|
+
target._queryComplexity = complexity;
|
|
603
|
+
this.memoizeAnalysis(cacheKey, { depth, complexity });
|
|
604
|
+
} catch (error) {
|
|
605
|
+
this.recordAnalysisFailure(target, error);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
memoizeAnalysis(key, analysis) {
|
|
609
|
+
if (this.analysisCache.size >= LeavenDriver.MAX_ANALYSIS_ENTRIES) {
|
|
610
|
+
const oldest = this.analysisCache.keys().next().value;
|
|
611
|
+
if (oldest !== undefined) {
|
|
612
|
+
this.analysisCache.delete(oldest);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
this.analysisCache.set(key, analysis);
|
|
616
|
+
}
|
|
617
|
+
recordAnalysisFailure(target, error) {
|
|
618
|
+
console.error("[Leaven] Query analysis failed; failing closed:", error);
|
|
619
|
+
target._queryDepth = Number.POSITIVE_INFINITY;
|
|
620
|
+
target._queryComplexity = Number.POSITIVE_INFINITY;
|
|
621
|
+
}
|
|
622
|
+
isPlaygroundEnabled() {
|
|
623
|
+
if (this.options.playground !== undefined) {
|
|
624
|
+
return this.options.playground;
|
|
625
|
+
}
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
isIntrospectionEnabled() {
|
|
629
|
+
if (this.options.introspection !== undefined) {
|
|
630
|
+
return this.options.introspection;
|
|
657
631
|
}
|
|
658
|
-
return
|
|
632
|
+
return true;
|
|
659
633
|
}
|
|
660
|
-
|
|
661
|
-
return
|
|
634
|
+
getPath() {
|
|
635
|
+
return this.options.path ?? "/graphql";
|
|
662
636
|
}
|
|
663
|
-
|
|
664
|
-
return
|
|
665
|
-
operation,
|
|
666
|
-
...metrics,
|
|
667
|
-
averageDuration: metrics.totalCount > 0 ? metrics.totalDuration / metrics.totalCount : 0,
|
|
668
|
-
errorRate: metrics.totalCount > 0 ? metrics.errorCount / metrics.totalCount : 0
|
|
669
|
-
}));
|
|
637
|
+
getCacheStats() {
|
|
638
|
+
return this.executor?.getCacheStats() ?? null;
|
|
670
639
|
}
|
|
671
|
-
|
|
672
|
-
this.
|
|
640
|
+
async clearCaches() {
|
|
641
|
+
this.analysisCache.clear();
|
|
642
|
+
await this.executor?.clearCaches();
|
|
673
643
|
}
|
|
674
644
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
645
|
+
LeavenDriver = __legacyDecorateClassTS([
|
|
646
|
+
Injectable2(),
|
|
647
|
+
__legacyMetadataTS("design:paramtypes", [
|
|
648
|
+
typeof LeavenModuleOptions === "undefined" ? Object : LeavenModuleOptions
|
|
649
|
+
])
|
|
650
|
+
], LeavenDriver);
|
|
651
|
+
|
|
678
652
|
// src/middleware.ts
|
|
679
653
|
import {
|
|
680
|
-
Injectable as
|
|
654
|
+
Injectable as Injectable3,
|
|
681
655
|
Inject as Inject2
|
|
682
656
|
} from "@nestjs/common";
|
|
657
|
+
import { formatError as formatError2, isLeavenError as isLeavenError2 } from "@leaven-graphql/errors";
|
|
658
|
+
import { renderGraphiQL } from "@leaven-graphql/playground";
|
|
659
|
+
var VALID_HOST = /^[a-z0-9.-]+(:\d+)?$/i;
|
|
660
|
+
|
|
683
661
|
class GraphQLMiddleware {
|
|
684
662
|
driver;
|
|
685
663
|
options;
|
|
@@ -701,12 +679,16 @@ class GraphQLMiddleware {
|
|
|
701
679
|
return;
|
|
702
680
|
}
|
|
703
681
|
if (req.method === "OPTIONS") {
|
|
704
|
-
this.
|
|
682
|
+
if (this.resolveCorsOptions() === null) {
|
|
683
|
+
return next();
|
|
684
|
+
}
|
|
685
|
+
this.handleCorsPreFlight(req, res);
|
|
705
686
|
return;
|
|
706
687
|
}
|
|
707
688
|
next();
|
|
708
689
|
}
|
|
709
690
|
async handleGraphQLRequest(req, res) {
|
|
691
|
+
this.applyCorsHeaders(req, res);
|
|
710
692
|
try {
|
|
711
693
|
const { query, variables, operationName } = req.body ?? {};
|
|
712
694
|
if (!query) {
|
|
@@ -718,74 +700,127 @@ class GraphQLMiddleware {
|
|
|
718
700
|
const context = {
|
|
719
701
|
req,
|
|
720
702
|
res,
|
|
721
|
-
...this.options.context ? await this.options.context(req, res) : {}
|
|
703
|
+
...this.options.context ? await this.options.context(this.toFetchRequest(req), res) : {}
|
|
722
704
|
};
|
|
723
705
|
const result = await this.driver.execute(query, variables, context, operationName);
|
|
724
|
-
|
|
725
|
-
res.status(status).json(result);
|
|
706
|
+
res.status(200).json(result);
|
|
726
707
|
} catch (error) {
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
708
|
+
const isProduction = false;
|
|
709
|
+
const includeStack = this.options.includeStacktraceInErrorResponses ?? false;
|
|
710
|
+
const formattedError = formatError2(error, {
|
|
711
|
+
maskErrors: !isLeavenError2(error) && isProduction && !includeStack,
|
|
712
|
+
includeStackTrace: includeStack
|
|
713
|
+
});
|
|
714
|
+
res.status(isLeavenError2(error) ? error.statusCode : 500).json({
|
|
715
|
+
errors: [formattedError]
|
|
730
716
|
});
|
|
731
717
|
}
|
|
732
718
|
}
|
|
733
719
|
servePlayground(res) {
|
|
734
|
-
const
|
|
735
|
-
const html = this.getPlaygroundHtml(path);
|
|
720
|
+
const html = renderGraphiQL({ endpoint: this.driver.getPath() });
|
|
736
721
|
res.setHeader("Content-Type", "text/html");
|
|
737
722
|
res.send(html);
|
|
738
723
|
}
|
|
739
|
-
handleCorsPreFlight(res) {
|
|
724
|
+
handleCorsPreFlight(req, res) {
|
|
725
|
+
this.applyCorsHeaders(req, res);
|
|
726
|
+
res.status(204).end();
|
|
727
|
+
}
|
|
728
|
+
resolveCorsOptions() {
|
|
740
729
|
const corsOptions = this.options.cors;
|
|
741
|
-
if (corsOptions === false) {
|
|
742
|
-
|
|
730
|
+
if (corsOptions === undefined || corsOptions === false) {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
return corsOptions === true ? {} : corsOptions;
|
|
734
|
+
}
|
|
735
|
+
applyCorsHeaders(req, res) {
|
|
736
|
+
const options = this.resolveCorsOptions();
|
|
737
|
+
if (options === null) {
|
|
743
738
|
return;
|
|
744
739
|
}
|
|
745
|
-
const
|
|
746
|
-
|
|
740
|
+
const requestOrigin = typeof req.headers.origin === "string" ? req.headers.origin : undefined;
|
|
741
|
+
const allowOrigin = this.resolveAllowedOrigin(options, requestOrigin);
|
|
742
|
+
if (allowOrigin.value !== undefined) {
|
|
743
|
+
res.setHeader("Access-Control-Allow-Origin", allowOrigin.value);
|
|
744
|
+
}
|
|
745
|
+
if (allowOrigin.varies) {
|
|
746
|
+
res.setHeader("Vary", "Origin");
|
|
747
|
+
}
|
|
747
748
|
res.setHeader("Access-Control-Allow-Methods", Array.isArray(options.methods) ? options.methods.join(", ") : options.methods ?? "GET, POST, OPTIONS");
|
|
748
749
|
res.setHeader("Access-Control-Allow-Headers", Array.isArray(options.allowedHeaders) ? options.allowedHeaders.join(", ") : options.allowedHeaders ?? "Content-Type, Authorization");
|
|
750
|
+
if (options.exposedHeaders) {
|
|
751
|
+
res.setHeader("Access-Control-Expose-Headers", Array.isArray(options.exposedHeaders) ? options.exposedHeaders.join(", ") : options.exposedHeaders);
|
|
752
|
+
}
|
|
749
753
|
if (options.credentials) {
|
|
750
754
|
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
751
755
|
}
|
|
752
756
|
if (options.maxAge) {
|
|
753
757
|
res.setHeader("Access-Control-Max-Age", String(options.maxAge));
|
|
754
758
|
}
|
|
755
|
-
res.status(204).end();
|
|
756
759
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
760
|
+
resolveAllowedOrigin(options, requestOrigin) {
|
|
761
|
+
const { origin, credentials } = options;
|
|
762
|
+
if (origin === false) {
|
|
763
|
+
return { varies: false };
|
|
764
|
+
}
|
|
765
|
+
if (Array.isArray(origin)) {
|
|
766
|
+
return requestOrigin !== undefined && origin.includes(requestOrigin) ? { value: requestOrigin, varies: true } : { varies: true };
|
|
767
|
+
}
|
|
768
|
+
if (typeof origin === "string" && origin !== "*") {
|
|
769
|
+
return { value: origin, varies: false };
|
|
770
|
+
}
|
|
771
|
+
if (credentials) {
|
|
772
|
+
return requestOrigin !== undefined ? { value: requestOrigin, varies: true } : { varies: true };
|
|
773
|
+
}
|
|
774
|
+
if (origin === true) {
|
|
775
|
+
return requestOrigin !== undefined ? { value: requestOrigin, varies: true } : { value: "*", varies: true };
|
|
776
|
+
}
|
|
777
|
+
return { value: "*", varies: false };
|
|
778
|
+
}
|
|
779
|
+
toFetchRequest(req) {
|
|
780
|
+
const headers = new Headers;
|
|
781
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
782
|
+
if (value === undefined) {
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
if (Array.isArray(value)) {
|
|
786
|
+
for (const entry of value) {
|
|
787
|
+
headers.append(name, entry);
|
|
788
|
+
}
|
|
789
|
+
} else {
|
|
790
|
+
headers.set(name, value);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
const path = req.originalUrl ?? req.url ?? req.path;
|
|
794
|
+
const url = `${this.resolveOrigin(req)}${path}`;
|
|
795
|
+
const init = { method: req.method, headers };
|
|
796
|
+
if (req.body !== undefined && req.method !== "GET" && req.method !== "HEAD") {
|
|
797
|
+
if (!headers.has("content-type")) {
|
|
798
|
+
headers.set("content-type", "application/json");
|
|
799
|
+
}
|
|
800
|
+
init.body = JSON.stringify(req.body);
|
|
801
|
+
}
|
|
802
|
+
try {
|
|
803
|
+
return new globalThis.Request(url, init);
|
|
804
|
+
} catch {
|
|
805
|
+
return new globalThis.Request(`${this.canonicalOrigin(req)}${this.driver.getPath()}`, init);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
resolveOrigin(req) {
|
|
809
|
+
if (this.options.publicUrl) {
|
|
810
|
+
return this.canonicalOrigin(req);
|
|
811
|
+
}
|
|
812
|
+
const host = req.headers.host;
|
|
813
|
+
if (typeof host === "string" && VALID_HOST.test(host)) {
|
|
814
|
+
return `${req.secure ? "https" : "http"}://${host}`;
|
|
815
|
+
}
|
|
816
|
+
return this.canonicalOrigin(req);
|
|
817
|
+
}
|
|
818
|
+
canonicalOrigin(req) {
|
|
819
|
+
return this.options.publicUrl?.replace(/\/+$/, "") ?? (req.secure ? "https://localhost" : "http://localhost");
|
|
785
820
|
}
|
|
786
821
|
}
|
|
787
822
|
GraphQLMiddleware = __legacyDecorateClassTS([
|
|
788
|
-
|
|
823
|
+
Injectable3(),
|
|
789
824
|
__legacyDecorateParamTS(0, Inject2(LEAVEN_DRIVER)),
|
|
790
825
|
__legacyDecorateParamTS(1, Inject2(LEAVEN_MODULE_OPTIONS)),
|
|
791
826
|
__legacyMetadataTS("design:paramtypes", [
|
|
@@ -796,111 +831,112 @@ GraphQLMiddleware = __legacyDecorateClassTS([
|
|
|
796
831
|
function createGraphQLMiddleware() {
|
|
797
832
|
return GraphQLMiddleware;
|
|
798
833
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
const ctx = this.getContext();
|
|
828
|
-
return ctx[name];
|
|
829
|
-
}
|
|
830
|
-
getHost() {
|
|
831
|
-
return this.host;
|
|
832
|
-
}
|
|
833
|
-
getType() {
|
|
834
|
-
return this.host.getType?.() ?? "unknown";
|
|
835
|
-
}
|
|
836
|
-
isGraphQL() {
|
|
837
|
-
return this.getType() === "graphql";
|
|
838
|
-
}
|
|
839
|
-
getHandler() {
|
|
840
|
-
return this.host.getHandler?.();
|
|
841
|
-
}
|
|
842
|
-
getClass() {
|
|
843
|
-
return this.host.getClass?.();
|
|
844
|
-
}
|
|
845
|
-
getFieldName() {
|
|
846
|
-
const info = this.getInfo();
|
|
847
|
-
return info?.fieldName;
|
|
848
|
-
}
|
|
849
|
-
getParentTypeName() {
|
|
850
|
-
const info = this.getInfo();
|
|
851
|
-
return info?.parentType?.name;
|
|
852
|
-
}
|
|
853
|
-
getReturnTypeName() {
|
|
854
|
-
const info = this.getInfo();
|
|
855
|
-
return info?.returnType?.toString();
|
|
856
|
-
}
|
|
857
|
-
getOperationName() {
|
|
858
|
-
const info = this.getInfo();
|
|
859
|
-
return info?.operation?.name?.value;
|
|
860
|
-
}
|
|
861
|
-
getOperationType() {
|
|
862
|
-
const info = this.getInfo();
|
|
863
|
-
return info?.operation?.operation;
|
|
834
|
+
|
|
835
|
+
// src/schema-builder.ts
|
|
836
|
+
import { Injectable as Injectable4, Inject as Inject3 } from "@nestjs/common";
|
|
837
|
+
import {
|
|
838
|
+
buildSchema as buildSchemaFromSDL,
|
|
839
|
+
lexicographicSortSchema,
|
|
840
|
+
parse as parse2,
|
|
841
|
+
print as print2,
|
|
842
|
+
printSchema,
|
|
843
|
+
visit
|
|
844
|
+
} from "graphql";
|
|
845
|
+
import { makeExecutableSchema } from "@graphql-tools/schema";
|
|
846
|
+
|
|
847
|
+
// src/decorators.ts
|
|
848
|
+
import {
|
|
849
|
+
createParamDecorator,
|
|
850
|
+
SetMetadata as SetMetadata2,
|
|
851
|
+
applyDecorators
|
|
852
|
+
} from "@nestjs/common";
|
|
853
|
+
var COMPLEXITY_KEY = "leaven:complexity";
|
|
854
|
+
var DEPRECATED_KEY = "leaven:deprecated";
|
|
855
|
+
var DESCRIPTION_KEY = "leaven:description";
|
|
856
|
+
var CACHE_KEY = "leaven:cache";
|
|
857
|
+
var SUBSCRIPTION_FILTER_KEY = "leaven:subscription:filter";
|
|
858
|
+
function contextExtractor(data, ctx) {
|
|
859
|
+
const gqlContext = getGqlContext(ctx);
|
|
860
|
+
if (data) {
|
|
861
|
+
return gqlContext[data];
|
|
864
862
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
863
|
+
return gqlContext;
|
|
864
|
+
}
|
|
865
|
+
var Context = createParamDecorator(contextExtractor);
|
|
866
|
+
function infoExtractor(_data, ctx) {
|
|
867
|
+
return ctx.getArgs()[GQL_RESOLVER_ARGS.INFO];
|
|
868
|
+
}
|
|
869
|
+
var Info = createParamDecorator(infoExtractor);
|
|
870
|
+
function rootExtractor(_data, ctx) {
|
|
871
|
+
return ctx.getArgs()[GQL_RESOLVER_ARGS.ROOT];
|
|
872
|
+
}
|
|
873
|
+
var Root = createParamDecorator(rootExtractor);
|
|
874
|
+
var Parent = Root;
|
|
875
|
+
function argsExtractor(data, ctx) {
|
|
876
|
+
const gqlArgs = ctx.getArgs()[GQL_RESOLVER_ARGS.ARGS];
|
|
877
|
+
if (data) {
|
|
878
|
+
return gqlArgs?.[data];
|
|
868
879
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
+
return gqlArgs;
|
|
881
|
+
}
|
|
882
|
+
var Args = createParamDecorator(argsExtractor);
|
|
883
|
+
function Complexity(value) {
|
|
884
|
+
return SetMetadata2(COMPLEXITY_KEY, value);
|
|
885
|
+
}
|
|
886
|
+
function Deprecated(reason) {
|
|
887
|
+
return SetMetadata2(DEPRECATED_KEY, reason);
|
|
888
|
+
}
|
|
889
|
+
function Description(text) {
|
|
890
|
+
return SetMetadata2(DESCRIPTION_KEY, text);
|
|
891
|
+
}
|
|
892
|
+
function CacheHint(options) {
|
|
893
|
+
return SetMetadata2(CACHE_KEY, options);
|
|
894
|
+
}
|
|
895
|
+
async function* filterAsyncIterator(source, predicate) {
|
|
896
|
+
for await (const payload of source) {
|
|
897
|
+
let accepted;
|
|
898
|
+
try {
|
|
899
|
+
accepted = await predicate(payload);
|
|
900
|
+
} catch (error) {
|
|
901
|
+
console.error("Subscription filter threw; dropping event:", error);
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
if (accepted) {
|
|
905
|
+
yield payload;
|
|
880
906
|
}
|
|
881
|
-
return path;
|
|
882
|
-
}
|
|
883
|
-
getSelectedFields() {
|
|
884
|
-
const info = this.getInfo();
|
|
885
|
-
if (!info?.fieldNodes?.[0]?.selectionSet?.selections)
|
|
886
|
-
return [];
|
|
887
|
-
return info.fieldNodes[0].selectionSet.selections.filter((selection) => selection.kind === "Field").map((field) => field.name.value);
|
|
888
907
|
}
|
|
889
908
|
}
|
|
890
|
-
function
|
|
891
|
-
|
|
909
|
+
function wrapWithFilter(descriptor, filter) {
|
|
910
|
+
const original = descriptor.value;
|
|
911
|
+
if (typeof original !== "function") {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
descriptor.value = function(...args) {
|
|
915
|
+
const source = original.apply(this, args);
|
|
916
|
+
const variables = args[GQL_RESOLVER_ARGS.ARGS];
|
|
917
|
+
const context = args[GQL_RESOLVER_ARGS.CONTEXT];
|
|
918
|
+
const predicate = (payload) => filter(payload, variables, context);
|
|
919
|
+
return source instanceof Promise ? source.then((resolved) => filterAsyncIterator(resolved, predicate)) : filterAsyncIterator(source, predicate);
|
|
920
|
+
};
|
|
892
921
|
}
|
|
893
|
-
function
|
|
894
|
-
return
|
|
922
|
+
function SubscriptionFilter(filter) {
|
|
923
|
+
return (target, propertyKey, descriptor) => {
|
|
924
|
+
wrapWithFilter(descriptor, filter);
|
|
925
|
+
return SetMetadata2(SUBSCRIPTION_FILTER_KEY, filter)(target, propertyKey, descriptor);
|
|
926
|
+
};
|
|
895
927
|
}
|
|
896
|
-
function
|
|
897
|
-
return
|
|
928
|
+
function Decorators(...decorators) {
|
|
929
|
+
return applyDecorators(...decorators);
|
|
898
930
|
}
|
|
899
|
-
function
|
|
900
|
-
|
|
931
|
+
function createContextDecorator(key) {
|
|
932
|
+
const decorator = createParamDecorator((_data, ctx) => {
|
|
933
|
+
const gqlContext = getGqlContext(ctx);
|
|
934
|
+
return gqlContext[key];
|
|
935
|
+
});
|
|
936
|
+
return () => decorator();
|
|
901
937
|
}
|
|
938
|
+
|
|
902
939
|
// src/schema-builder.ts
|
|
903
|
-
import { Injectable as Injectable5, Inject as Inject3 } from "@nestjs/common";
|
|
904
940
|
class SchemaBuilderService {
|
|
905
941
|
options;
|
|
906
942
|
driver;
|
|
@@ -916,72 +952,54 @@ class SchemaBuilderService {
|
|
|
916
952
|
this.driver.setSchema(schema);
|
|
917
953
|
}
|
|
918
954
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
955
|
+
resolveSchemaSource() {
|
|
956
|
+
const { schema, typeDefs, resolvers, autoSchemaFile, buildSchemaOptions } = this.options;
|
|
957
|
+
if (schema) {
|
|
958
|
+
return { type: "provided", schema };
|
|
922
959
|
}
|
|
923
|
-
if (
|
|
924
|
-
return
|
|
960
|
+
if (typeDefs) {
|
|
961
|
+
return { type: "typeDefs", typeDefs, resolvers };
|
|
925
962
|
}
|
|
926
|
-
if (
|
|
927
|
-
return
|
|
963
|
+
if (autoSchemaFile) {
|
|
964
|
+
return { type: "autoSchema", options: buildSchemaOptions ?? {} };
|
|
928
965
|
}
|
|
929
966
|
return null;
|
|
930
967
|
}
|
|
931
|
-
|
|
932
|
-
const
|
|
933
|
-
if (!
|
|
968
|
+
async buildSchema() {
|
|
969
|
+
const source = this.resolveSchemaSource();
|
|
970
|
+
if (!source) {
|
|
934
971
|
return null;
|
|
935
972
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
return
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
const schema = buildSchema(typeof typeDefs === "string" ? typeDefs : this.mergeTypeDefs(typeDefs));
|
|
946
|
-
if (addResolversToSchema) {
|
|
947
|
-
return addResolversToSchema({
|
|
948
|
-
schema,
|
|
949
|
-
resolvers: Array.isArray(resolvers) ? this.mergeResolvers(resolvers) : resolvers
|
|
950
|
-
});
|
|
951
|
-
}
|
|
952
|
-
return schema;
|
|
953
|
-
} catch (error) {
|
|
954
|
-
console.warn("Failed to build schema from typeDefs. Install @graphql-tools/schema for full support.", error);
|
|
955
|
-
return null;
|
|
973
|
+
switch (source.type) {
|
|
974
|
+
case "provided":
|
|
975
|
+
return source.schema;
|
|
976
|
+
case "typeDefs":
|
|
977
|
+
return this.buildFromTypeDefs(source.typeDefs, source.resolvers);
|
|
978
|
+
case "autoSchema":
|
|
979
|
+
throw new Error("Failed to build the GraphQL schema: 'autoSchemaFile' was configured, but code-first " + "schema generation is not implemented. Supply a pre-built executable schema via the " + "'schema' option, or SDL via 'typeDefs' (optionally with 'resolvers').");
|
|
956
980
|
}
|
|
957
981
|
}
|
|
982
|
+
async buildFromTypeDefs(typeDefs, resolvers) {
|
|
983
|
+
const sdl = this.mergeTypeDefs(typeDefs);
|
|
984
|
+
if (!resolvers) {
|
|
985
|
+
return buildSchemaFromSDL(sdl);
|
|
986
|
+
}
|
|
987
|
+
const schema = makeExecutableSchema({
|
|
988
|
+
typeDefs: sdl,
|
|
989
|
+
resolvers
|
|
990
|
+
});
|
|
991
|
+
applyResolverFieldMetadata(schema, resolvers);
|
|
992
|
+
return schema;
|
|
993
|
+
}
|
|
958
994
|
mergeTypeDefs(typeDefs) {
|
|
959
995
|
if (typeof typeDefs === "string") {
|
|
960
996
|
return typeDefs;
|
|
961
997
|
}
|
|
962
998
|
if (Array.isArray(typeDefs)) {
|
|
963
|
-
return typeDefs.map((td) =>
|
|
964
|
-
if (typeof td === "string") {
|
|
965
|
-
return td;
|
|
966
|
-
}
|
|
967
|
-
const { print: print2 } = __require("graphql");
|
|
968
|
-
return print2(td);
|
|
969
|
-
}).join(`
|
|
999
|
+
return typeDefs.map((td) => typeof td === "string" ? td : print2(td)).join(`
|
|
970
1000
|
`);
|
|
971
1001
|
}
|
|
972
|
-
|
|
973
|
-
return print(typeDefs);
|
|
974
|
-
}
|
|
975
|
-
mergeResolvers(resolvers) {
|
|
976
|
-
return resolvers.reduce((merged, resolver) => {
|
|
977
|
-
for (const [typeName, fields] of Object.entries(resolver)) {
|
|
978
|
-
merged[typeName] = {
|
|
979
|
-
...merged[typeName] || {},
|
|
980
|
-
...fields
|
|
981
|
-
};
|
|
982
|
-
}
|
|
983
|
-
return merged;
|
|
984
|
-
}, {});
|
|
1002
|
+
return print2(typeDefs);
|
|
985
1003
|
}
|
|
986
1004
|
getSchema() {
|
|
987
1005
|
return this.builtSchema;
|
|
@@ -995,7 +1013,7 @@ class SchemaBuilderService {
|
|
|
995
1013
|
}
|
|
996
1014
|
}
|
|
997
1015
|
SchemaBuilderService = __legacyDecorateClassTS([
|
|
998
|
-
|
|
1016
|
+
Injectable4(),
|
|
999
1017
|
__legacyDecorateParamTS(0, Inject3(LEAVEN_MODULE_OPTIONS)),
|
|
1000
1018
|
__legacyDecorateParamTS(1, Inject3(LEAVEN_DRIVER)),
|
|
1001
1019
|
__legacyMetadataTS("design:paramtypes", [
|
|
@@ -1003,15 +1021,71 @@ SchemaBuilderService = __legacyDecorateClassTS([
|
|
|
1003
1021
|
typeof LeavenDriver === "undefined" ? Object : LeavenDriver
|
|
1004
1022
|
])
|
|
1005
1023
|
], SchemaBuilderService);
|
|
1024
|
+
function collectResolverFunctions(container) {
|
|
1025
|
+
const functions = new Map;
|
|
1026
|
+
let current = container;
|
|
1027
|
+
while (current && current !== Object.prototype) {
|
|
1028
|
+
for (const key of Object.getOwnPropertyNames(current)) {
|
|
1029
|
+
if (key === "constructor" || functions.has(key))
|
|
1030
|
+
continue;
|
|
1031
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
1032
|
+
if (descriptor && typeof descriptor.value === "function") {
|
|
1033
|
+
functions.set(key, descriptor.value);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
current = Object.getPrototypeOf(current);
|
|
1037
|
+
}
|
|
1038
|
+
return functions;
|
|
1039
|
+
}
|
|
1040
|
+
function applyResolverFieldMetadata(schema, resolvers) {
|
|
1041
|
+
if (typeof Reflect.getMetadata !== "function") {
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
const maps = Array.isArray(resolvers) ? resolvers : [resolvers];
|
|
1045
|
+
for (const map of maps) {
|
|
1046
|
+
for (const [typeName, container] of Object.entries(map ?? {})) {
|
|
1047
|
+
if (!container || typeof container !== "object")
|
|
1048
|
+
continue;
|
|
1049
|
+
const type = schema.getType(typeName);
|
|
1050
|
+
if (!type || typeof type.getFields !== "function") {
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
const fields = type.getFields();
|
|
1054
|
+
for (const [fieldName, fn] of collectResolverFunctions(container)) {
|
|
1055
|
+
const field = fields[fieldName];
|
|
1056
|
+
if (!field)
|
|
1057
|
+
continue;
|
|
1058
|
+
const description = Reflect.getMetadata(DESCRIPTION_KEY, fn);
|
|
1059
|
+
if (typeof description === "string") {
|
|
1060
|
+
field.description = description;
|
|
1061
|
+
}
|
|
1062
|
+
const deprecationReason = Reflect.getMetadata(DEPRECATED_KEY, fn);
|
|
1063
|
+
if (typeof deprecationReason === "string") {
|
|
1064
|
+
field.deprecationReason = deprecationReason;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
function stripDescriptions(sdl) {
|
|
1071
|
+
const strippedAst = visit(parse2(sdl), {
|
|
1072
|
+
enter(node) {
|
|
1073
|
+
if ("description" in node && node.description) {
|
|
1074
|
+
const { description: _description, ...rest } = node;
|
|
1075
|
+
return rest;
|
|
1076
|
+
}
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
return print2(strippedAst);
|
|
1081
|
+
}
|
|
1006
1082
|
async function generateSchemaFile(schema, options) {
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
const sdl = printSchema(schemaToWrite);
|
|
1014
|
-
await writeFile(options.path, sdl, "utf-8");
|
|
1083
|
+
const schemaToWrite = options.sortSchema ? lexicographicSortSchema(schema) : schema;
|
|
1084
|
+
let sdl = printSchema(schemaToWrite);
|
|
1085
|
+
if (options.includeDescriptions === false) {
|
|
1086
|
+
sdl = stripDescriptions(sdl);
|
|
1087
|
+
}
|
|
1088
|
+
await Bun.write(options.path, sdl);
|
|
1015
1089
|
}
|
|
1016
1090
|
var resolverMetadataStorage = [];
|
|
1017
1091
|
function getResolverMetadata() {
|
|
@@ -1023,8 +1097,17 @@ function registerResolverMetadata(metadata) {
|
|
|
1023
1097
|
function clearResolverMetadata() {
|
|
1024
1098
|
resolverMetadataStorage.length = 0;
|
|
1025
1099
|
}
|
|
1100
|
+
|
|
1026
1101
|
// src/subscriptions.ts
|
|
1027
|
-
import {
|
|
1102
|
+
import {
|
|
1103
|
+
Injectable as Injectable5,
|
|
1104
|
+
Inject as Inject4,
|
|
1105
|
+
SetMetadata as SetMetadata3
|
|
1106
|
+
} from "@nestjs/common";
|
|
1107
|
+
import { parse as parse3, getOperationAST } from "graphql";
|
|
1108
|
+
var SOCKET_OPEN = 1;
|
|
1109
|
+
var ITERATOR_TEARDOWN_TIMEOUT_MS = 1000;
|
|
1110
|
+
|
|
1028
1111
|
class SubscriptionManager {
|
|
1029
1112
|
options;
|
|
1030
1113
|
driver;
|
|
@@ -1037,20 +1120,55 @@ class SubscriptionManager {
|
|
|
1037
1120
|
}
|
|
1038
1121
|
async onModuleInit() {}
|
|
1039
1122
|
async onModuleDestroy() {
|
|
1040
|
-
|
|
1123
|
+
const sockets = [...this.connections.keys()];
|
|
1124
|
+
await Promise.all(sockets.map((socket) => this.handleClose(socket)));
|
|
1125
|
+
for (const socket of sockets) {
|
|
1041
1126
|
socket.close(1000, "Server shutdown");
|
|
1042
1127
|
}
|
|
1043
1128
|
this.connections.clear();
|
|
1044
1129
|
}
|
|
1045
1130
|
normalizeConfig() {
|
|
1131
|
+
const userConfig = this.options.subscriptions ?? {};
|
|
1132
|
+
return {
|
|
1133
|
+
...userConfig,
|
|
1134
|
+
path: userConfig.path ?? this.options.path ?? "/graphql",
|
|
1135
|
+
keepAlive: userConfig.keepAlive ?? 12000,
|
|
1136
|
+
connectionInitWaitTimeout: userConfig.connectionInitWaitTimeout ?? 3000,
|
|
1137
|
+
maxSubscriptionsPerConnection: userConfig.maxSubscriptionsPerConnection ?? 100
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
getPath() {
|
|
1141
|
+
return this.config.path ?? "/graphql";
|
|
1142
|
+
}
|
|
1143
|
+
getWebSocketConfig() {
|
|
1046
1144
|
return {
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1145
|
+
open: (socket) => {
|
|
1146
|
+
this.handleOpen(socket);
|
|
1147
|
+
},
|
|
1148
|
+
message: (socket, message) => {
|
|
1149
|
+
this.handleMessage(socket, message).catch((error) => console.error("WebSocket message handling failed:", error));
|
|
1150
|
+
},
|
|
1151
|
+
close: (socket) => {
|
|
1152
|
+
this.handleClose(socket).catch((error) => console.error("WebSocket close handling failed:", error));
|
|
1153
|
+
}
|
|
1051
1154
|
};
|
|
1052
1155
|
}
|
|
1156
|
+
handleOpen(socket, request) {
|
|
1157
|
+
this.openConnection(socket, request);
|
|
1158
|
+
}
|
|
1053
1159
|
async handleConnection(socket, request) {
|
|
1160
|
+
this.openConnection(socket, request);
|
|
1161
|
+
socket.addEventListener("message", (event) => {
|
|
1162
|
+
this.handleMessage(socket, event.data).catch((error) => console.error("WebSocket message handling failed:", error));
|
|
1163
|
+
});
|
|
1164
|
+
socket.addEventListener("close", () => {
|
|
1165
|
+
this.handleClose(socket).catch((e) => console.error("WebSocket close handling failed:", e));
|
|
1166
|
+
});
|
|
1167
|
+
socket.addEventListener("error", (error) => {
|
|
1168
|
+
this.handleError(socket, error);
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
openConnection(socket, request) {
|
|
1054
1172
|
const state = {
|
|
1055
1173
|
socket,
|
|
1056
1174
|
request,
|
|
@@ -1058,30 +1176,36 @@ class SubscriptionManager {
|
|
|
1058
1176
|
initialized: false,
|
|
1059
1177
|
connectionParams: undefined,
|
|
1060
1178
|
context: undefined,
|
|
1061
|
-
keepAliveInterval: undefined
|
|
1179
|
+
keepAliveInterval: undefined,
|
|
1180
|
+
initTimeout: undefined
|
|
1062
1181
|
};
|
|
1063
|
-
this.connections.set(socket, state);
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
});
|
|
1067
|
-
socket.addEventListener("close", () => {
|
|
1068
|
-
this.handleClose(socket);
|
|
1069
|
-
});
|
|
1070
|
-
socket.addEventListener("error", (error) => {
|
|
1071
|
-
this.handleError(socket, error);
|
|
1072
|
-
});
|
|
1073
|
-
setTimeout(() => {
|
|
1182
|
+
this.connections.set(socket, state);
|
|
1183
|
+
state.initTimeout = setTimeout(() => {
|
|
1184
|
+
state.initTimeout = undefined;
|
|
1074
1185
|
if (!state.initialized) {
|
|
1075
1186
|
socket.close(4408, "Connection initialization timeout");
|
|
1076
1187
|
}
|
|
1077
1188
|
}, this.config.connectionInitWaitTimeout);
|
|
1189
|
+
return state;
|
|
1078
1190
|
}
|
|
1079
1191
|
async handleMessage(socket, data) {
|
|
1080
1192
|
const state = this.connections.get(socket);
|
|
1081
1193
|
if (!state)
|
|
1082
1194
|
return;
|
|
1195
|
+
let message;
|
|
1196
|
+
try {
|
|
1197
|
+
const parsed = JSON.parse(typeof data === "string" ? data : new TextDecoder().decode(data));
|
|
1198
|
+
if (parsed === null || typeof parsed !== "object" || typeof parsed.type !== "string") {
|
|
1199
|
+
socket.close(4400, "Bad Request");
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
message = parsed;
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
console.error("Error parsing WebSocket message:", error);
|
|
1205
|
+
socket.close(4400, "Bad Request");
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1083
1208
|
try {
|
|
1084
|
-
const message = JSON.parse(data);
|
|
1085
1209
|
switch (message.type) {
|
|
1086
1210
|
case "connection_init":
|
|
1087
1211
|
await this.handleConnectionInit(socket, state, message);
|
|
@@ -1098,10 +1222,18 @@ class SubscriptionManager {
|
|
|
1098
1222
|
await this.handleComplete(socket, state, message);
|
|
1099
1223
|
break;
|
|
1100
1224
|
default:
|
|
1225
|
+
socket.close(4400, "Bad Request");
|
|
1101
1226
|
break;
|
|
1102
1227
|
}
|
|
1103
1228
|
} catch (error) {
|
|
1104
1229
|
console.error("Error handling WebSocket message:", error);
|
|
1230
|
+
const type = message.type;
|
|
1231
|
+
const id = message.id;
|
|
1232
|
+
if ((type === "subscribe" || type === "complete") && typeof id === "string") {
|
|
1233
|
+
this.sendError(socket, id, "Internal server error");
|
|
1234
|
+
} else {
|
|
1235
|
+
socket.close(1011, "Internal server error");
|
|
1236
|
+
}
|
|
1105
1237
|
}
|
|
1106
1238
|
}
|
|
1107
1239
|
async handleConnectionInit(socket, state, message) {
|
|
@@ -1122,155 +1254,804 @@ class SubscriptionManager {
|
|
|
1122
1254
|
return;
|
|
1123
1255
|
}
|
|
1124
1256
|
}
|
|
1125
|
-
state.initialized = true;
|
|
1126
|
-
state.context = ctx;
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
state.
|
|
1130
|
-
|
|
1131
|
-
|
|
1257
|
+
state.initialized = true;
|
|
1258
|
+
state.context = ctx;
|
|
1259
|
+
if (state.initTimeout) {
|
|
1260
|
+
clearTimeout(state.initTimeout);
|
|
1261
|
+
state.initTimeout = undefined;
|
|
1262
|
+
}
|
|
1263
|
+
this.send(socket, { type: "connection_ack" });
|
|
1264
|
+
if (this.config.keepAlive) {
|
|
1265
|
+
state.keepAliveInterval = setInterval(() => {
|
|
1266
|
+
this.send(socket, { type: "ping" });
|
|
1267
|
+
}, this.config.keepAlive);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
async handleSubscribe(socket, state, message) {
|
|
1271
|
+
if (!state.initialized) {
|
|
1272
|
+
socket.close(4401, "Unauthorized");
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
if (typeof message.id !== "string" || typeof message.payload?.query !== "string") {
|
|
1276
|
+
socket.close(4400, "Bad Request");
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
if (state.subscriptions.has(message.id)) {
|
|
1280
|
+
socket.close(4409, `Subscriber already exists: ${message.id}`);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (state.subscriptions.size >= (this.config.maxSubscriptionsPerConnection ?? 100)) {
|
|
1284
|
+
this.sendError(socket, message.id, "Too many subscriptions");
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
const streaming = this.isSubscriptionOperation(message.payload.query, message.payload.operationName);
|
|
1288
|
+
let registration;
|
|
1289
|
+
if (streaming) {
|
|
1290
|
+
registration = { clientId: message.id };
|
|
1291
|
+
state.subscriptions.set(message.id, registration);
|
|
1292
|
+
}
|
|
1293
|
+
try {
|
|
1294
|
+
if (this.config.onSubscribe && state.context) {
|
|
1295
|
+
await this.config.onSubscribe(state.context, message);
|
|
1296
|
+
}
|
|
1297
|
+
const schema = this.driver.getSchema();
|
|
1298
|
+
if (!schema) {
|
|
1299
|
+
if (registration && state.subscriptions.get(message.id) === registration) {
|
|
1300
|
+
state.subscriptions.delete(message.id);
|
|
1301
|
+
}
|
|
1302
|
+
this.sendError(socket, message.id, "Schema not available");
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
let context = {
|
|
1306
|
+
req: state.request,
|
|
1307
|
+
res: {},
|
|
1308
|
+
connectionParams: state.connectionParams
|
|
1309
|
+
};
|
|
1310
|
+
if (this.config.context && state.context) {
|
|
1311
|
+
context = await this.config.context(state.context);
|
|
1312
|
+
}
|
|
1313
|
+
let args = {
|
|
1314
|
+
schema,
|
|
1315
|
+
document: message.payload.query,
|
|
1316
|
+
contextValue: context,
|
|
1317
|
+
variableValues: message.payload.variables,
|
|
1318
|
+
operationName: message.payload.operationName
|
|
1319
|
+
};
|
|
1320
|
+
if (this.config.onOperation && state.context) {
|
|
1321
|
+
args = await this.config.onOperation(state.context, message, args);
|
|
1322
|
+
}
|
|
1323
|
+
if (registration) {
|
|
1324
|
+
await this.startStream(socket, state, message.id, args, registration);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
const result = await this.driver.execute(args.document, args.variableValues, args.contextValue, args.operationName);
|
|
1328
|
+
this.send(socket, {
|
|
1329
|
+
id: message.id,
|
|
1330
|
+
type: "next",
|
|
1331
|
+
payload: result
|
|
1332
|
+
});
|
|
1333
|
+
this.send(socket, { id: message.id, type: "complete" });
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
if (registration && state.subscriptions.get(message.id) === registration) {
|
|
1336
|
+
state.subscriptions.delete(message.id);
|
|
1337
|
+
}
|
|
1338
|
+
const errorMessage = error instanceof Error ? error.message : "Subscription failed";
|
|
1339
|
+
this.sendError(socket, message.id, errorMessage);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
async startStream(socket, state, id, args, registration) {
|
|
1343
|
+
const result = await this.driver.subscribe(args.document, args.variableValues, args.contextValue, args.operationName);
|
|
1344
|
+
if (!(Symbol.asyncIterator in result)) {
|
|
1345
|
+
if (state.subscriptions.get(id) === registration) {
|
|
1346
|
+
state.subscriptions.delete(id);
|
|
1347
|
+
}
|
|
1348
|
+
const errors = result.errors ?? [{ message: "Subscription failed" }];
|
|
1349
|
+
this.send(socket, { id, type: "error", payload: [...errors] });
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
const iterator = result;
|
|
1353
|
+
if (state.subscriptions.get(id) !== registration) {
|
|
1354
|
+
await this.closeIterator(iterator);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
registration.iterator = iterator;
|
|
1358
|
+
this.pumpStream(socket, state, id, registration, iterator).catch((error) => {
|
|
1359
|
+
console.error(`Subscription pump failed for operation ${id}:`, error);
|
|
1360
|
+
state.subscriptions.delete(id);
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
async pumpStream(socket, state, id, registration, iterator) {
|
|
1364
|
+
const owns = () => state.subscriptions.get(id) === registration;
|
|
1365
|
+
try {
|
|
1366
|
+
for await (const payload of iterator) {
|
|
1367
|
+
if (!owns()) {
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
this.send(socket, { id, type: "next", payload });
|
|
1371
|
+
}
|
|
1372
|
+
if (owns()) {
|
|
1373
|
+
this.send(socket, { id, type: "complete" });
|
|
1374
|
+
}
|
|
1375
|
+
} catch (error) {
|
|
1376
|
+
if (owns()) {
|
|
1377
|
+
const message = error instanceof Error ? error.message : "Subscription failed";
|
|
1378
|
+
this.sendError(socket, id, message);
|
|
1379
|
+
}
|
|
1380
|
+
} finally {
|
|
1381
|
+
if (owns()) {
|
|
1382
|
+
state.subscriptions.delete(id);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
async closeIterator(iterator) {
|
|
1387
|
+
if (!iterator?.return) {
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
const teardown = Promise.resolve().then(() => iterator.return?.()).then(() => "settled").catch((error) => {
|
|
1391
|
+
console.error("Subscription iterator teardown failed:", error);
|
|
1392
|
+
return "settled";
|
|
1393
|
+
});
|
|
1394
|
+
let timer;
|
|
1395
|
+
const deadline = new Promise((resolve) => {
|
|
1396
|
+
timer = setTimeout(() => resolve("timeout"), ITERATOR_TEARDOWN_TIMEOUT_MS);
|
|
1397
|
+
});
|
|
1398
|
+
try {
|
|
1399
|
+
if (await Promise.race([teardown, deadline]) === "timeout") {
|
|
1400
|
+
console.error(`Subscription iterator did not finish teardown within ${ITERATOR_TEARDOWN_TIMEOUT_MS}ms; ` + "abandoning it. The resolver is most likely an async generator suspended " + "awaiting an event that never arrives, which cancellation cannot resume.");
|
|
1401
|
+
}
|
|
1402
|
+
} finally {
|
|
1403
|
+
clearTimeout(timer);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
isSubscriptionOperation(query, operationName) {
|
|
1407
|
+
try {
|
|
1408
|
+
const document = parse3(query);
|
|
1409
|
+
return getOperationAST(document, operationName)?.operation === "subscription";
|
|
1410
|
+
} catch {
|
|
1411
|
+
return false;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
async handleComplete(socket, state, message) {
|
|
1415
|
+
const subscription = state.subscriptions.get(message.id);
|
|
1416
|
+
if (subscription) {
|
|
1417
|
+
state.subscriptions.delete(message.id);
|
|
1418
|
+
await this.closeIterator(subscription.iterator);
|
|
1419
|
+
if (this.config.onComplete && state.context) {
|
|
1420
|
+
await this.config.onComplete(state.context, message);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
async handleClose(ws) {
|
|
1425
|
+
const state = this.connections.get(ws);
|
|
1426
|
+
if (!state)
|
|
1427
|
+
return;
|
|
1428
|
+
if (state.keepAliveInterval) {
|
|
1429
|
+
clearInterval(state.keepAliveInterval);
|
|
1430
|
+
state.keepAliveInterval = undefined;
|
|
1431
|
+
}
|
|
1432
|
+
if (state.initTimeout) {
|
|
1433
|
+
clearTimeout(state.initTimeout);
|
|
1434
|
+
state.initTimeout = undefined;
|
|
1435
|
+
}
|
|
1436
|
+
const subscriptions = [...state.subscriptions.values()];
|
|
1437
|
+
state.subscriptions.clear();
|
|
1438
|
+
this.connections.delete(ws);
|
|
1439
|
+
await Promise.all(subscriptions.map((s) => this.closeIterator(s.iterator)));
|
|
1440
|
+
if (this.config.onDisconnect && state.context) {
|
|
1441
|
+
try {
|
|
1442
|
+
await this.config.onDisconnect(state.context);
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
console.error("onDisconnect hook failed:", error);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
handleError(ws, _error) {
|
|
1449
|
+
const state = this.connections.get(ws);
|
|
1450
|
+
if (state) {
|
|
1451
|
+
this.handleClose(ws).catch((e) => console.error("WebSocket close handling failed:", e));
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
send(socket, message) {
|
|
1455
|
+
if (socket.readyState === SOCKET_OPEN) {
|
|
1456
|
+
socket.send(JSON.stringify(message));
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
sendError(socket, id, message) {
|
|
1460
|
+
this.send(socket, {
|
|
1461
|
+
id,
|
|
1462
|
+
type: "error",
|
|
1463
|
+
payload: [{ message }]
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
registerSubscription(socket, clientId, topic, context) {
|
|
1467
|
+
const state = this.connections.get(socket);
|
|
1468
|
+
if (!state || !state.initialized) {
|
|
1469
|
+
return false;
|
|
1470
|
+
}
|
|
1471
|
+
if (state.subscriptions.has(clientId)) {
|
|
1472
|
+
return false;
|
|
1473
|
+
}
|
|
1474
|
+
if (state.subscriptions.size >= (this.config.maxSubscriptionsPerConnection ?? 100)) {
|
|
1475
|
+
return false;
|
|
1476
|
+
}
|
|
1477
|
+
state.subscriptions.set(clientId, { clientId, topic, context });
|
|
1478
|
+
return true;
|
|
1479
|
+
}
|
|
1480
|
+
publishToTopic(topic, payload) {
|
|
1481
|
+
for (const [socket, state] of this.connections) {
|
|
1482
|
+
for (const subscription of state.subscriptions.values()) {
|
|
1483
|
+
if (subscription.topic === topic) {
|
|
1484
|
+
this.send(socket, {
|
|
1485
|
+
id: subscription.clientId,
|
|
1486
|
+
type: "next",
|
|
1487
|
+
payload: { data: payload }
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
publish(topic, payload) {
|
|
1494
|
+
this.publishToTopic(topic, payload);
|
|
1495
|
+
}
|
|
1496
|
+
getConnectionCount() {
|
|
1497
|
+
return this.connections.size;
|
|
1498
|
+
}
|
|
1499
|
+
getSubscriptionCount() {
|
|
1500
|
+
let count = 0;
|
|
1501
|
+
for (const [_, state] of this.connections) {
|
|
1502
|
+
count += state.subscriptions.size;
|
|
1503
|
+
}
|
|
1504
|
+
return count;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
SubscriptionManager = __legacyDecorateClassTS([
|
|
1508
|
+
Injectable5(),
|
|
1509
|
+
__legacyDecorateParamTS(0, Inject4(LEAVEN_MODULE_OPTIONS)),
|
|
1510
|
+
__legacyDecorateParamTS(1, Inject4(LEAVEN_DRIVER)),
|
|
1511
|
+
__legacyMetadataTS("design:paramtypes", [
|
|
1512
|
+
typeof LeavenModuleOptions === "undefined" ? Object : LeavenModuleOptions,
|
|
1513
|
+
typeof LeavenDriver === "undefined" ? Object : LeavenDriver
|
|
1514
|
+
])
|
|
1515
|
+
], SubscriptionManager);
|
|
1516
|
+
function Subscription(returns, options) {
|
|
1517
|
+
return (target, propertyKey, descriptor) => {
|
|
1518
|
+
const filter = options?.filter;
|
|
1519
|
+
if (filter) {
|
|
1520
|
+
wrapWithFilter(descriptor, filter);
|
|
1521
|
+
SetMetadata3(SUBSCRIPTION_FILTER_KEY, filter)(target, propertyKey, descriptor);
|
|
1522
|
+
}
|
|
1523
|
+
if (descriptor.value && typeof Reflect.defineMetadata === "function") {
|
|
1524
|
+
Reflect.defineMetadata("leaven:subscription", { returns, options }, descriptor.value);
|
|
1525
|
+
}
|
|
1526
|
+
return descriptor;
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
function InjectPubSub() {
|
|
1530
|
+
return Inject4(LEAVEN_PUBSUB);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// src/module.ts
|
|
1534
|
+
class LeavenModule {
|
|
1535
|
+
options;
|
|
1536
|
+
constructor(options) {
|
|
1537
|
+
this.options = options;
|
|
1538
|
+
}
|
|
1539
|
+
configure(consumer) {
|
|
1540
|
+
if (!this.options) {
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
consumer.apply(GraphQLMiddleware).forRoutes(this.options.path ?? "/graphql");
|
|
1544
|
+
}
|
|
1545
|
+
static forRoot(options = {}) {
|
|
1546
|
+
const optionsProvider = {
|
|
1547
|
+
provide: LEAVEN_MODULE_OPTIONS,
|
|
1548
|
+
useValue: options
|
|
1549
|
+
};
|
|
1550
|
+
const driverProvider = {
|
|
1551
|
+
provide: LEAVEN_DRIVER,
|
|
1552
|
+
useFactory: (opts) => new LeavenDriver(opts),
|
|
1553
|
+
inject: [LEAVEN_MODULE_OPTIONS]
|
|
1554
|
+
};
|
|
1555
|
+
return {
|
|
1556
|
+
module: LeavenModule,
|
|
1557
|
+
providers: [
|
|
1558
|
+
optionsProvider,
|
|
1559
|
+
driverProvider,
|
|
1560
|
+
...LeavenModule.createLimitProviders(),
|
|
1561
|
+
LeavenModule.createPubSubProvider(),
|
|
1562
|
+
SchemaBuilderService,
|
|
1563
|
+
SubscriptionManager,
|
|
1564
|
+
GraphQLMiddleware
|
|
1565
|
+
],
|
|
1566
|
+
exports: [
|
|
1567
|
+
LEAVEN_MODULE_OPTIONS,
|
|
1568
|
+
LEAVEN_DRIVER,
|
|
1569
|
+
LEAVEN_MAX_COMPLEXITY,
|
|
1570
|
+
LEAVEN_MAX_DEPTH,
|
|
1571
|
+
LEAVEN_PUBSUB,
|
|
1572
|
+
SchemaBuilderService,
|
|
1573
|
+
SubscriptionManager
|
|
1574
|
+
]
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
static createLimitProviders() {
|
|
1578
|
+
return [
|
|
1579
|
+
{
|
|
1580
|
+
provide: LEAVEN_MAX_COMPLEXITY,
|
|
1581
|
+
useFactory: (opts) => opts.maxComplexity ?? Infinity,
|
|
1582
|
+
inject: [LEAVEN_MODULE_OPTIONS]
|
|
1583
|
+
},
|
|
1584
|
+
{
|
|
1585
|
+
provide: LEAVEN_MAX_DEPTH,
|
|
1586
|
+
useFactory: (opts) => opts.maxDepth ?? Infinity,
|
|
1587
|
+
inject: [LEAVEN_MODULE_OPTIONS]
|
|
1588
|
+
}
|
|
1589
|
+
];
|
|
1590
|
+
}
|
|
1591
|
+
static createPubSubProvider() {
|
|
1592
|
+
return {
|
|
1593
|
+
provide: LEAVEN_PUBSUB,
|
|
1594
|
+
useFactory: (opts) => opts.pubSub ?? createPubSub(),
|
|
1595
|
+
inject: [LEAVEN_MODULE_OPTIONS]
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
static forRootAsync(options) {
|
|
1599
|
+
const asyncProviders = this.createAsyncProviders(options);
|
|
1600
|
+
const driverProvider = {
|
|
1601
|
+
provide: LEAVEN_DRIVER,
|
|
1602
|
+
useFactory: (opts) => new LeavenDriver(opts),
|
|
1603
|
+
inject: [LEAVEN_MODULE_OPTIONS]
|
|
1604
|
+
};
|
|
1605
|
+
return {
|
|
1606
|
+
module: LeavenModule,
|
|
1607
|
+
imports: options.imports ?? [],
|
|
1608
|
+
providers: [
|
|
1609
|
+
...asyncProviders,
|
|
1610
|
+
driverProvider,
|
|
1611
|
+
...LeavenModule.createLimitProviders(),
|
|
1612
|
+
LeavenModule.createPubSubProvider(),
|
|
1613
|
+
SchemaBuilderService,
|
|
1614
|
+
SubscriptionManager,
|
|
1615
|
+
GraphQLMiddleware
|
|
1616
|
+
],
|
|
1617
|
+
exports: [
|
|
1618
|
+
LEAVEN_MODULE_OPTIONS,
|
|
1619
|
+
LEAVEN_DRIVER,
|
|
1620
|
+
LEAVEN_MAX_COMPLEXITY,
|
|
1621
|
+
LEAVEN_MAX_DEPTH,
|
|
1622
|
+
LEAVEN_PUBSUB,
|
|
1623
|
+
SchemaBuilderService,
|
|
1624
|
+
SubscriptionManager
|
|
1625
|
+
]
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
static createAsyncProviders(options) {
|
|
1629
|
+
if (!options.useFactory && !options.useClass && !options.useExisting) {
|
|
1630
|
+
throw new Error("LeavenModule.forRootAsync requires one of useFactory, useClass, or useExisting");
|
|
1631
|
+
}
|
|
1632
|
+
if (options.useExisting || options.useFactory) {
|
|
1633
|
+
return [this.createAsyncOptionsProvider(options)];
|
|
1634
|
+
}
|
|
1635
|
+
const useClass = options.useClass;
|
|
1636
|
+
return [
|
|
1637
|
+
this.createAsyncOptionsProvider(options),
|
|
1638
|
+
{
|
|
1639
|
+
provide: useClass,
|
|
1640
|
+
useClass
|
|
1641
|
+
}
|
|
1642
|
+
];
|
|
1643
|
+
}
|
|
1644
|
+
static createAsyncOptionsProvider(options) {
|
|
1645
|
+
if (options.useFactory) {
|
|
1646
|
+
return {
|
|
1647
|
+
provide: LEAVEN_MODULE_OPTIONS,
|
|
1648
|
+
useFactory: options.useFactory,
|
|
1649
|
+
inject: options.inject ?? []
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
const inject = [
|
|
1653
|
+
options.useClass ?? options.useExisting
|
|
1654
|
+
];
|
|
1655
|
+
return {
|
|
1656
|
+
provide: LEAVEN_MODULE_OPTIONS,
|
|
1657
|
+
useFactory: async (optionsFactory) => optionsFactory.createLeavenOptions(),
|
|
1658
|
+
inject
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
LeavenModule = __legacyDecorateClassTS([
|
|
1663
|
+
Global(),
|
|
1664
|
+
Module({}),
|
|
1665
|
+
__legacyDecorateParamTS(0, Optional()),
|
|
1666
|
+
__legacyDecorateParamTS(0, Inject5(LEAVEN_MODULE_OPTIONS)),
|
|
1667
|
+
__legacyMetadataTS("design:paramtypes", [
|
|
1668
|
+
typeof LeavenModuleOptions === "undefined" ? Object : LeavenModuleOptions
|
|
1669
|
+
])
|
|
1670
|
+
], LeavenModule);
|
|
1671
|
+
// src/graphql-driver.ts
|
|
1672
|
+
import { HttpException } from "@nestjs/common";
|
|
1673
|
+
import { AbstractGraphQLDriver } from "@nestjs/graphql";
|
|
1674
|
+
import { LeavenExecutor as LeavenExecutor2 } from "@leaven-graphql/core";
|
|
1675
|
+
import {
|
|
1676
|
+
ERROR_CODES,
|
|
1677
|
+
ErrorCode,
|
|
1678
|
+
formatError as formatError3,
|
|
1679
|
+
getErrorCode,
|
|
1680
|
+
isLeavenError as isLeavenError3
|
|
1681
|
+
} from "@leaven-graphql/errors";
|
|
1682
|
+
import { renderGraphiQL as renderGraphiQL2 } from "@leaven-graphql/playground";
|
|
1683
|
+
var HTTP_STATUS_TO_ERROR_CODE = {
|
|
1684
|
+
400: ErrorCode.BAD_REQUEST,
|
|
1685
|
+
401: ErrorCode.UNAUTHENTICATED,
|
|
1686
|
+
403: ErrorCode.FORBIDDEN,
|
|
1687
|
+
404: ErrorCode.NOT_FOUND,
|
|
1688
|
+
409: ErrorCode.ALREADY_EXISTS,
|
|
1689
|
+
413: ErrorCode.PAYLOAD_TOO_LARGE,
|
|
1690
|
+
429: ErrorCode.RATE_LIMITED
|
|
1691
|
+
};
|
|
1692
|
+
|
|
1693
|
+
class LeavenGraphQLDriver extends AbstractGraphQLDriver {
|
|
1694
|
+
executor = null;
|
|
1695
|
+
options = null;
|
|
1696
|
+
async start(options) {
|
|
1697
|
+
if (!options.schema) {
|
|
1698
|
+
throw new Error("LeavenGraphQLDriver: no schema was provided to start(). This is normally supplied by GraphQLModule.");
|
|
1699
|
+
}
|
|
1700
|
+
this.options = options;
|
|
1701
|
+
this.executor = new LeavenExecutor2({
|
|
1702
|
+
schema: options.schema,
|
|
1703
|
+
cache: options.cache,
|
|
1704
|
+
metrics: options.metrics,
|
|
1705
|
+
maxComplexity: options.maxComplexity,
|
|
1706
|
+
maxDepth: options.maxDepth,
|
|
1707
|
+
introspection: options.introspection ?? true,
|
|
1708
|
+
formatExecutionError: LeavenGraphQLDriver.formatExecutionError
|
|
1709
|
+
});
|
|
1710
|
+
const path = this.getNormalizedPath(options);
|
|
1711
|
+
const httpAdapter = this.httpAdapterHost.httpAdapter;
|
|
1712
|
+
httpAdapter.post(path, (req, res) => this.handleOperation(req, res));
|
|
1713
|
+
if (options.playground) {
|
|
1714
|
+
httpAdapter.get(path, (_req, res) => {
|
|
1715
|
+
this.servePlayground(res, path);
|
|
1716
|
+
});
|
|
1132
1717
|
}
|
|
1133
1718
|
}
|
|
1134
|
-
async
|
|
1135
|
-
if (
|
|
1136
|
-
|
|
1719
|
+
async stop() {
|
|
1720
|
+
if (this.executor) {
|
|
1721
|
+
await this.executor.clearCaches();
|
|
1722
|
+
this.executor = null;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
getExecutor() {
|
|
1726
|
+
return this.executor;
|
|
1727
|
+
}
|
|
1728
|
+
async handleOperation(req, res) {
|
|
1729
|
+
if (!this.executor) {
|
|
1730
|
+
res.status(503).json({
|
|
1731
|
+
errors: [
|
|
1732
|
+
{
|
|
1733
|
+
message: "GraphQL server is not running",
|
|
1734
|
+
extensions: { code: ErrorCode.INTERNAL_ERROR }
|
|
1735
|
+
}
|
|
1736
|
+
]
|
|
1737
|
+
});
|
|
1137
1738
|
return;
|
|
1138
1739
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1740
|
+
let body;
|
|
1741
|
+
try {
|
|
1742
|
+
body = await LeavenGraphQLDriver.readBody(req);
|
|
1743
|
+
} catch {
|
|
1744
|
+
this.respond(res, {
|
|
1745
|
+
errors: [
|
|
1746
|
+
{
|
|
1747
|
+
message: "Malformed request body: expected JSON",
|
|
1748
|
+
extensions: { code: ErrorCode.BAD_REQUEST }
|
|
1749
|
+
}
|
|
1750
|
+
]
|
|
1751
|
+
});
|
|
1141
1752
|
return;
|
|
1142
1753
|
}
|
|
1143
|
-
if (
|
|
1144
|
-
|
|
1754
|
+
if (!body.query) {
|
|
1755
|
+
this.respond(res, {
|
|
1756
|
+
errors: [
|
|
1757
|
+
{
|
|
1758
|
+
message: "Query is required",
|
|
1759
|
+
extensions: { code: ErrorCode.BAD_REQUEST }
|
|
1760
|
+
}
|
|
1761
|
+
]
|
|
1762
|
+
});
|
|
1763
|
+
return;
|
|
1145
1764
|
}
|
|
1765
|
+
const context = await this.buildContext(req, res);
|
|
1146
1766
|
try {
|
|
1147
|
-
const
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
if (this.config.context && state.context) {
|
|
1158
|
-
context = await this.config.context(state.context);
|
|
1159
|
-
}
|
|
1160
|
-
const result = await this.driver.execute(message.payload.query, message.payload.variables, context, message.payload.operationName);
|
|
1161
|
-
if (result.data || result.errors) {
|
|
1162
|
-
this.send(socket, {
|
|
1163
|
-
id: message.id,
|
|
1164
|
-
type: "next",
|
|
1165
|
-
payload: result
|
|
1166
|
-
});
|
|
1167
|
-
this.send(socket, { id: message.id, type: "complete" });
|
|
1168
|
-
return;
|
|
1169
|
-
}
|
|
1170
|
-
state.subscriptions.set(message.id, {
|
|
1171
|
-
message,
|
|
1172
|
-
context
|
|
1767
|
+
const result = await this.executor.execute({
|
|
1768
|
+
query: body.query,
|
|
1769
|
+
variables: body.variables,
|
|
1770
|
+
operationName: body.operationName
|
|
1771
|
+
}, context);
|
|
1772
|
+
const extensions = this.options?.metrics && result.metrics ? { ...result.response.extensions, metrics: result.metrics } : result.response.extensions;
|
|
1773
|
+
this.respond(res, {
|
|
1774
|
+
data: result.response.data,
|
|
1775
|
+
errors: result.response.errors,
|
|
1776
|
+
extensions
|
|
1173
1777
|
});
|
|
1174
1778
|
} catch (error) {
|
|
1175
|
-
|
|
1176
|
-
this.sendError(socket, message.id, errorMessage);
|
|
1779
|
+
this.respond(res, { errors: [this.formatThrownError(error)] });
|
|
1177
1780
|
}
|
|
1178
1781
|
}
|
|
1179
|
-
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
}
|
|
1782
|
+
servePlayground(res, path) {
|
|
1783
|
+
const html = renderGraphiQL2({
|
|
1784
|
+
endpoint: path,
|
|
1785
|
+
subscriptionEndpoint: this.options?.subscriptionEndpoint
|
|
1786
|
+
});
|
|
1787
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
1788
|
+
res.send(html);
|
|
1187
1789
|
}
|
|
1188
|
-
async
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1790
|
+
async buildContext(req, res) {
|
|
1791
|
+
const base = { req, res };
|
|
1792
|
+
const factory = this.options?.context;
|
|
1793
|
+
if (typeof factory === "function") {
|
|
1794
|
+
const produced = await factory(base);
|
|
1795
|
+
return { ...base, ...produced };
|
|
1796
|
+
}
|
|
1797
|
+
if (factory && typeof factory === "object") {
|
|
1798
|
+
return { ...base, ...factory };
|
|
1799
|
+
}
|
|
1800
|
+
return base;
|
|
1801
|
+
}
|
|
1802
|
+
respond(res, response) {
|
|
1803
|
+
const errors = response.errors?.map((error) => this.finalizeError(error));
|
|
1804
|
+
const hasErrors = errors !== undefined && errors.length > 0;
|
|
1805
|
+
const totalFailure = response.data === undefined || response.data === null;
|
|
1806
|
+
let status = 200;
|
|
1807
|
+
if (hasErrors && totalFailure) {
|
|
1808
|
+
const code = errors[0]?.extensions?.code;
|
|
1809
|
+
const known = typeof code === "string" ? getErrorCode(code) : null;
|
|
1810
|
+
status = known ? ERROR_CODES[known].status : 500;
|
|
1811
|
+
}
|
|
1812
|
+
const body = {};
|
|
1813
|
+
if (response.data !== undefined) {
|
|
1814
|
+
body.data = response.data;
|
|
1815
|
+
}
|
|
1816
|
+
if (hasErrors) {
|
|
1817
|
+
body.errors = errors;
|
|
1818
|
+
}
|
|
1819
|
+
if (response.extensions !== undefined) {
|
|
1820
|
+
body.extensions = response.extensions;
|
|
1821
|
+
}
|
|
1822
|
+
res.status(status).json(body);
|
|
1823
|
+
}
|
|
1824
|
+
finalizeError(error) {
|
|
1825
|
+
const rawCode = error.extensions?.code;
|
|
1826
|
+
const code = typeof rawCode === "string" ? getErrorCode(rawCode) : null;
|
|
1827
|
+
const resolved = code ?? ErrorCode.INTERNAL_ERROR;
|
|
1828
|
+
const includeStack = this.options?.includeStacktraceInErrorResponses ?? false;
|
|
1829
|
+
const mask = resolved === ErrorCode.INTERNAL_ERROR && false;
|
|
1830
|
+
const finalized = mask ? {
|
|
1831
|
+
message: "An unexpected error occurred",
|
|
1832
|
+
extensions: { code: ErrorCode.INTERNAL_ERROR }
|
|
1833
|
+
} : {
|
|
1834
|
+
...error,
|
|
1835
|
+
extensions: { ...error.extensions, code: resolved }
|
|
1836
|
+
};
|
|
1837
|
+
return this.options?.formatError ? this.options.formatError(finalized) : finalized;
|
|
1838
|
+
}
|
|
1839
|
+
formatThrownError(error) {
|
|
1840
|
+
const includeStack = this.options?.includeStacktraceInErrorResponses ?? false;
|
|
1841
|
+
const formatted = formatError3(error, {
|
|
1842
|
+
maskErrors: !isLeavenError3(error) && false,
|
|
1843
|
+
includeStackTrace: includeStack
|
|
1844
|
+
});
|
|
1845
|
+
return this.options?.formatError ? this.options.formatError(formatted) : formatted;
|
|
1846
|
+
}
|
|
1847
|
+
static async readBody(req) {
|
|
1848
|
+
const raw = req.body;
|
|
1849
|
+
if (typeof raw === "string") {
|
|
1850
|
+
return JSON.parse(raw);
|
|
1194
1851
|
}
|
|
1195
|
-
if (
|
|
1196
|
-
|
|
1852
|
+
if (raw !== null && typeof raw === "object" && !(raw instanceof ArrayBuffer)) {
|
|
1853
|
+
return raw;
|
|
1197
1854
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
handleError(ws, _error) {
|
|
1202
|
-
const state = this.connections.get(ws);
|
|
1203
|
-
if (state) {
|
|
1204
|
-
this.handleClose(ws);
|
|
1855
|
+
const json = req.raw?.json;
|
|
1856
|
+
if (json) {
|
|
1857
|
+
return await json.call(req.raw);
|
|
1205
1858
|
}
|
|
1859
|
+
return {};
|
|
1206
1860
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1861
|
+
static formatExecutionError(error) {
|
|
1862
|
+
const formatted = error.toJSON();
|
|
1863
|
+
const original = error.originalError;
|
|
1864
|
+
if (!(original instanceof HttpException)) {
|
|
1865
|
+
return formatted;
|
|
1210
1866
|
}
|
|
1867
|
+
const status = original.getStatus();
|
|
1868
|
+
const code = HTTP_STATUS_TO_ERROR_CODE[status] ?? (status >= 400 && status < 500 ? ErrorCode.BAD_REQUEST : ErrorCode.INTERNAL_ERROR);
|
|
1869
|
+
return {
|
|
1870
|
+
...formatted,
|
|
1871
|
+
extensions: { ...formatted.extensions, code, statusCode: status }
|
|
1872
|
+
};
|
|
1211
1873
|
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1874
|
+
}
|
|
1875
|
+
// src/interceptors.ts
|
|
1876
|
+
import {
|
|
1877
|
+
Injectable as Injectable6
|
|
1878
|
+
} from "@nestjs/common";
|
|
1879
|
+
import { tap, catchError, throwError } from "rxjs";
|
|
1880
|
+
import { Reflector as Reflector2 } from "@nestjs/core";
|
|
1881
|
+
import { GraphQLError as GraphQLError2 } from "graphql";
|
|
1882
|
+
import { errorToGraphQL, maskError } from "@leaven-graphql/errors";
|
|
1883
|
+
class LoggingInterceptor {
|
|
1884
|
+
intercept(context, next) {
|
|
1885
|
+
const className = context.getClass().name;
|
|
1886
|
+
const methodName = context.getHandler().name;
|
|
1887
|
+
const start = performance.now();
|
|
1888
|
+
return next.handle().pipe(tap(() => {
|
|
1889
|
+
const duration = (performance.now() - start).toFixed(2);
|
|
1890
|
+
console.log(`[GraphQL] ${className}.${methodName} - ${duration}ms`);
|
|
1891
|
+
}), catchError((error) => {
|
|
1892
|
+
const duration = (performance.now() - start).toFixed(2);
|
|
1893
|
+
console.error(`[GraphQL] ${className}.${methodName} - ${duration}ms - failed:`, error);
|
|
1894
|
+
return throwError(() => error);
|
|
1895
|
+
}));
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
LoggingInterceptor = __legacyDecorateClassTS([
|
|
1899
|
+
Injectable6()
|
|
1900
|
+
], LoggingInterceptor);
|
|
1901
|
+
|
|
1902
|
+
class ErrorFormattingInterceptor {
|
|
1903
|
+
intercept(_context, next) {
|
|
1904
|
+
return next.handle().pipe(catchError((error) => {
|
|
1905
|
+
const formattedError = this.formatError(error);
|
|
1906
|
+
return throwError(() => formattedError);
|
|
1907
|
+
}));
|
|
1908
|
+
}
|
|
1909
|
+
formatError(error) {
|
|
1910
|
+
const isProduction = false;
|
|
1911
|
+
const source = errorToGraphQL(error);
|
|
1912
|
+
const formatted = maskError(source, {
|
|
1913
|
+
maskErrors: isProduction,
|
|
1914
|
+
includeStackTrace: !isProduction
|
|
1915
|
+
});
|
|
1916
|
+
const keepLocations = formatted.locations !== undefined;
|
|
1917
|
+
return new GraphQLError2(formatted.message, {
|
|
1918
|
+
nodes: keepLocations ? source.nodes : undefined,
|
|
1919
|
+
source: keepLocations ? source.source : undefined,
|
|
1920
|
+
positions: keepLocations ? source.positions : undefined,
|
|
1921
|
+
path: formatted.path,
|
|
1922
|
+
extensions: formatted.extensions
|
|
1217
1923
|
});
|
|
1218
1924
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1925
|
+
}
|
|
1926
|
+
ErrorFormattingInterceptor = __legacyDecorateClassTS([
|
|
1927
|
+
Injectable6()
|
|
1928
|
+
], ErrorFormattingInterceptor);
|
|
1929
|
+
|
|
1930
|
+
class ComplexityInterceptor {
|
|
1931
|
+
reflector;
|
|
1932
|
+
constructor(reflector) {
|
|
1933
|
+
this.reflector = reflector;
|
|
1934
|
+
}
|
|
1935
|
+
intercept(context, next) {
|
|
1936
|
+
const complexity = this.reflector.get(COMPLEXITY_KEY, context.getHandler());
|
|
1937
|
+
if (complexity !== undefined) {
|
|
1938
|
+
const gqlContext = getGqlContext(context);
|
|
1939
|
+
if (gqlContext) {
|
|
1940
|
+
const current = typeof gqlContext._resolvedFieldComplexity === "number" ? gqlContext._resolvedFieldComplexity : 0;
|
|
1941
|
+
gqlContext._resolvedFieldComplexity = current + complexity;
|
|
1228
1942
|
}
|
|
1229
1943
|
}
|
|
1944
|
+
return next.handle();
|
|
1230
1945
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1946
|
+
}
|
|
1947
|
+
ComplexityInterceptor = __legacyDecorateClassTS([
|
|
1948
|
+
Injectable6(),
|
|
1949
|
+
__legacyMetadataTS("design:paramtypes", [
|
|
1950
|
+
typeof Reflector2 === "undefined" ? Object : Reflector2
|
|
1951
|
+
])
|
|
1952
|
+
], ComplexityInterceptor);
|
|
1953
|
+
|
|
1954
|
+
class CachingInterceptor {
|
|
1955
|
+
reflector;
|
|
1956
|
+
constructor(reflector) {
|
|
1957
|
+
this.reflector = reflector;
|
|
1233
1958
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1959
|
+
intercept(context, next) {
|
|
1960
|
+
const cacheHint = this.reflector.get(CACHE_KEY, context.getHandler());
|
|
1961
|
+
return next.handle().pipe(tap(() => {
|
|
1962
|
+
if (cacheHint) {
|
|
1963
|
+
const gqlInfo = getGqlInfo(context);
|
|
1964
|
+
if (gqlInfo?.cacheControl) {
|
|
1965
|
+
gqlInfo.cacheControl.setCacheHint({
|
|
1966
|
+
maxAge: cacheHint.maxAge,
|
|
1967
|
+
scope: cacheHint.scope
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
}));
|
|
1240
1972
|
}
|
|
1241
1973
|
}
|
|
1242
|
-
|
|
1974
|
+
CachingInterceptor = __legacyDecorateClassTS([
|
|
1243
1975
|
Injectable6(),
|
|
1244
|
-
__legacyDecorateParamTS(0, Inject4(LEAVEN_MODULE_OPTIONS)),
|
|
1245
|
-
__legacyDecorateParamTS(1, Inject4(LEAVEN_DRIVER)),
|
|
1246
1976
|
__legacyMetadataTS("design:paramtypes", [
|
|
1247
|
-
typeof
|
|
1248
|
-
typeof LeavenDriver === "undefined" ? Object : LeavenDriver
|
|
1977
|
+
typeof Reflector2 === "undefined" ? Object : Reflector2
|
|
1249
1978
|
])
|
|
1250
|
-
],
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1979
|
+
], CachingInterceptor);
|
|
1980
|
+
|
|
1981
|
+
class MetricsInterceptor {
|
|
1982
|
+
metrics = new Map;
|
|
1983
|
+
intercept(context, next) {
|
|
1984
|
+
const className = context.getClass().name;
|
|
1985
|
+
const methodName = context.getHandler().name;
|
|
1986
|
+
const key = `${className}.${methodName}`;
|
|
1987
|
+
const start = performance.now();
|
|
1988
|
+
return next.handle().pipe(tap(() => {
|
|
1989
|
+
const duration = performance.now() - start;
|
|
1990
|
+
this.recordSuccess(key, duration);
|
|
1991
|
+
}), catchError((error) => {
|
|
1992
|
+
const duration = performance.now() - start;
|
|
1993
|
+
this.recordError(key, duration);
|
|
1994
|
+
return throwError(() => error);
|
|
1995
|
+
}));
|
|
1996
|
+
}
|
|
1997
|
+
recordSuccess(key, duration) {
|
|
1998
|
+
const metrics = this.getOrCreateMetrics(key);
|
|
1999
|
+
metrics.totalCount++;
|
|
2000
|
+
metrics.successCount++;
|
|
2001
|
+
metrics.totalDuration += duration;
|
|
2002
|
+
metrics.lastDuration = duration;
|
|
2003
|
+
metrics.minDuration = Math.min(metrics.minDuration, duration);
|
|
2004
|
+
metrics.maxDuration = Math.max(metrics.maxDuration, duration);
|
|
2005
|
+
}
|
|
2006
|
+
recordError(key, duration) {
|
|
2007
|
+
const metrics = this.getOrCreateMetrics(key);
|
|
2008
|
+
metrics.totalCount++;
|
|
2009
|
+
metrics.errorCount++;
|
|
2010
|
+
metrics.totalDuration += duration;
|
|
2011
|
+
metrics.lastDuration = duration;
|
|
2012
|
+
metrics.minDuration = Math.min(metrics.minDuration, duration);
|
|
2013
|
+
metrics.maxDuration = Math.max(metrics.maxDuration, duration);
|
|
2014
|
+
}
|
|
2015
|
+
getOrCreateMetrics(key) {
|
|
2016
|
+
let metrics = this.metrics.get(key);
|
|
2017
|
+
if (!metrics) {
|
|
2018
|
+
metrics = {
|
|
2019
|
+
totalCount: 0,
|
|
2020
|
+
successCount: 0,
|
|
2021
|
+
errorCount: 0,
|
|
2022
|
+
totalDuration: 0,
|
|
2023
|
+
lastDuration: 0,
|
|
2024
|
+
minDuration: Infinity,
|
|
2025
|
+
maxDuration: 0
|
|
2026
|
+
};
|
|
2027
|
+
this.metrics.set(key, metrics);
|
|
1265
2028
|
}
|
|
1266
|
-
|
|
2029
|
+
return metrics;
|
|
2030
|
+
}
|
|
2031
|
+
getMetrics() {
|
|
2032
|
+
return new Map(this.metrics);
|
|
2033
|
+
}
|
|
2034
|
+
getSummary() {
|
|
2035
|
+
return Array.from(this.metrics.entries()).map(([operation, metrics]) => ({
|
|
2036
|
+
operation,
|
|
2037
|
+
...metrics,
|
|
2038
|
+
averageDuration: metrics.totalCount > 0 ? metrics.totalDuration / metrics.totalCount : 0,
|
|
2039
|
+
errorRate: metrics.totalCount > 0 ? metrics.errorCount / metrics.totalCount : 0
|
|
2040
|
+
}));
|
|
2041
|
+
}
|
|
2042
|
+
reset() {
|
|
2043
|
+
this.metrics.clear();
|
|
2044
|
+
}
|
|
1267
2045
|
}
|
|
2046
|
+
MetricsInterceptor = __legacyDecorateClassTS([
|
|
2047
|
+
Injectable6()
|
|
2048
|
+
], MetricsInterceptor);
|
|
1268
2049
|
export {
|
|
1269
2050
|
registerResolverMetadata,
|
|
1270
2051
|
getResolverMetadata,
|
|
1271
2052
|
getGqlRoot,
|
|
1272
2053
|
getGqlInfo,
|
|
1273
|
-
|
|
2054
|
+
getGqlContext,
|
|
1274
2055
|
getGqlArgs,
|
|
1275
2056
|
generateSchemaFile,
|
|
1276
2057
|
createGraphQLMiddleware,
|
|
@@ -1293,8 +2074,12 @@ export {
|
|
|
1293
2074
|
MetricsInterceptor,
|
|
1294
2075
|
LoggingInterceptor,
|
|
1295
2076
|
LeavenModule,
|
|
2077
|
+
LeavenGraphQLDriver,
|
|
1296
2078
|
LeavenDriver,
|
|
2079
|
+
LEAVEN_PUBSUB,
|
|
1297
2080
|
LEAVEN_MODULE_OPTIONS,
|
|
2081
|
+
LEAVEN_MAX_DEPTH,
|
|
2082
|
+
LEAVEN_MAX_COMPLEXITY,
|
|
1298
2083
|
LEAVEN_DRIVER,
|
|
1299
2084
|
InjectPubSub,
|
|
1300
2085
|
Info,
|