@leaven-graphql/nestjs 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +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 +1577 -792
- 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 +19 -12
package/dist/index.js
CHANGED
|
@@ -14,358 +14,219 @@ 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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
85
|
+
getType() {
|
|
86
|
+
return this.host.getType?.() ?? "unknown";
|
|
87
|
+
}
|
|
88
|
+
isGraphQL() {
|
|
89
|
+
return this.getType() === "graphql";
|
|
90
|
+
}
|
|
91
|
+
getHandler() {
|
|
92
|
+
return this.host.getHandler?.();
|
|
93
|
+
}
|
|
94
|
+
getClass() {
|
|
95
|
+
return this.host.getClass?.();
|
|
96
|
+
}
|
|
97
|
+
getFieldName() {
|
|
98
|
+
const info = this.getInfo();
|
|
99
|
+
return info?.fieldName;
|
|
100
|
+
}
|
|
101
|
+
getParentTypeName() {
|
|
102
|
+
const info = this.getInfo();
|
|
103
|
+
return info?.parentType?.name;
|
|
104
|
+
}
|
|
105
|
+
getReturnTypeName() {
|
|
106
|
+
const info = this.getInfo();
|
|
107
|
+
return info?.returnType?.toString();
|
|
108
|
+
}
|
|
109
|
+
getOperationName() {
|
|
110
|
+
const info = this.getInfo();
|
|
111
|
+
return info?.operation?.name?.value;
|
|
112
|
+
}
|
|
113
|
+
getOperationType() {
|
|
114
|
+
const info = this.getInfo();
|
|
115
|
+
return info?.operation?.operation;
|
|
116
|
+
}
|
|
117
|
+
getVariables() {
|
|
118
|
+
const info = this.getInfo();
|
|
119
|
+
return info?.variableValues ?? {};
|
|
120
|
+
}
|
|
121
|
+
getPath() {
|
|
122
|
+
return this.getFullPath().filter((segment) => typeof segment === "string");
|
|
123
|
+
}
|
|
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;
|
|
135
133
|
}
|
|
136
|
-
return
|
|
137
|
-
query,
|
|
138
|
-
variables: variables ? JSON.parse(variables) : undefined,
|
|
139
|
-
operationName: operationName ?? undefined
|
|
140
|
-
};
|
|
134
|
+
return path;
|
|
141
135
|
}
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
}
|
|
146
158
|
};
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return { ...baseContext, ...customContext };
|
|
159
|
+
for (const fieldNode of info.fieldNodes) {
|
|
160
|
+
collect(fieldNode?.selectionSet);
|
|
150
161
|
}
|
|
151
|
-
return
|
|
162
|
+
return [...fields];
|
|
152
163
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
164
|
+
}
|
|
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);
|
|
156
171
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
172
|
+
return gqlContext;
|
|
173
|
+
}
|
|
174
|
+
function getGqlContext(context) {
|
|
175
|
+
return getCachedGqlExecutionContext(context).getContext();
|
|
176
|
+
}
|
|
177
|
+
function getGqlArgs(context) {
|
|
178
|
+
return getCachedGqlExecutionContext(context).getArgs();
|
|
179
|
+
}
|
|
180
|
+
function getGqlInfo(context) {
|
|
181
|
+
return getCachedGqlExecutionContext(context).getInfo();
|
|
182
|
+
}
|
|
183
|
+
function getGqlRoot(context) {
|
|
184
|
+
return getCachedGqlExecutionContext(context).getRoot();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/guards.ts
|
|
188
|
+
var IS_PUBLIC_KEY = "leaven:isPublic";
|
|
189
|
+
var ROLES_KEY = "leaven:roles";
|
|
190
|
+
var PERMISSIONS_KEY = "leaven:permissions";
|
|
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);
|
|
196
|
+
|
|
197
|
+
class AuthGuard {
|
|
198
|
+
reflector;
|
|
199
|
+
constructor(reflector) {
|
|
200
|
+
this.reflector = reflector;
|
|
162
201
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
202
|
+
canActivate(context) {
|
|
203
|
+
const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [
|
|
204
|
+
context.getHandler(),
|
|
205
|
+
context.getClass()
|
|
206
|
+
]);
|
|
207
|
+
if (isPublic) {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
const gqlContext = this.getGqlContext(context);
|
|
211
|
+
const user = this.extractUser(gqlContext);
|
|
212
|
+
if (!user) {
|
|
213
|
+
throw new UnauthorizedException("Authentication required");
|
|
166
214
|
}
|
|
167
215
|
return true;
|
|
168
216
|
}
|
|
169
|
-
|
|
170
|
-
return
|
|
171
|
-
}
|
|
172
|
-
getCacheStats() {
|
|
173
|
-
return this.executor?.getCacheStats() ?? null;
|
|
217
|
+
getGqlContext(context) {
|
|
218
|
+
return getGqlContext(context);
|
|
174
219
|
}
|
|
175
|
-
|
|
176
|
-
|
|
220
|
+
extractUser(context) {
|
|
221
|
+
return context.user ?? context.req?.user ?? null;
|
|
177
222
|
}
|
|
178
223
|
}
|
|
179
|
-
|
|
224
|
+
AuthGuard = __legacyDecorateClassTS([
|
|
180
225
|
Injectable(),
|
|
181
226
|
__legacyMetadataTS("design:paramtypes", [
|
|
182
|
-
typeof
|
|
227
|
+
typeof Reflector === "undefined" ? Object : Reflector
|
|
183
228
|
])
|
|
184
|
-
],
|
|
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
|
-
};
|
|
206
|
-
}
|
|
207
|
-
static forRootAsync(options) {
|
|
208
|
-
const asyncProviders = this.createAsyncProviders(options);
|
|
209
|
-
const driverProvider = {
|
|
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
|
-
};
|
|
220
|
-
}
|
|
221
|
-
static createAsyncProviders(options) {
|
|
222
|
-
if (options.useExisting || options.useFactory) {
|
|
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
|
-
];
|
|
233
|
-
}
|
|
234
|
-
static createAsyncOptionsProvider(options) {
|
|
235
|
-
if (options.useFactory) {
|
|
236
|
-
return {
|
|
237
|
-
provide: LEAVEN_MODULE_OPTIONS,
|
|
238
|
-
useFactory: options.useFactory,
|
|
239
|
-
inject: options.inject ?? []
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
const inject = [
|
|
243
|
-
options.useClass ?? options.useExisting
|
|
244
|
-
];
|
|
245
|
-
return {
|
|
246
|
-
provide: LEAVEN_MODULE_OPTIONS,
|
|
247
|
-
useFactory: async (optionsFactory) => optionsFactory.createLeavenOptions(),
|
|
248
|
-
inject
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
LeavenModule = __legacyDecorateClassTS([
|
|
253
|
-
Global(),
|
|
254
|
-
Module({})
|
|
255
|
-
], LeavenModule);
|
|
256
|
-
// src/decorators.ts
|
|
257
|
-
import {
|
|
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];
|
|
271
|
-
}
|
|
272
|
-
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
|
-
}
|
|
303
|
-
function SubscriptionFilter(filter) {
|
|
304
|
-
return SetMetadata(SUBSCRIPTION_FILTER_KEY, filter);
|
|
305
|
-
}
|
|
306
|
-
function Decorators(...decorators) {
|
|
307
|
-
return applyDecorators(...decorators);
|
|
308
|
-
}
|
|
309
|
-
function getGqlContext(ctx) {
|
|
310
|
-
const args = ctx.getArgs();
|
|
311
|
-
return args[2];
|
|
312
|
-
}
|
|
313
|
-
function createContextDecorator(key) {
|
|
314
|
-
return () => createParamDecorator((_data, ctx) => {
|
|
315
|
-
const gqlContext = getGqlContext(ctx);
|
|
316
|
-
return gqlContext[key];
|
|
317
|
-
})();
|
|
318
|
-
}
|
|
319
|
-
// 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
|
-
var IS_PUBLIC_KEY = "leaven:isPublic";
|
|
329
|
-
var ROLES_KEY = "leaven:roles";
|
|
330
|
-
var PERMISSIONS_KEY = "leaven:permissions";
|
|
331
|
-
var Public = () => SetMetadata2(IS_PUBLIC_KEY, true);
|
|
332
|
-
var Roles = (...roles) => SetMetadata2(ROLES_KEY, roles);
|
|
333
|
-
var Permissions = (...permissions) => SetMetadata2(PERMISSIONS_KEY, permissions);
|
|
334
|
-
|
|
335
|
-
class AuthGuard {
|
|
336
|
-
reflector;
|
|
337
|
-
constructor(reflector) {
|
|
338
|
-
this.reflector = reflector;
|
|
339
|
-
}
|
|
340
|
-
canActivate(context) {
|
|
341
|
-
const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [
|
|
342
|
-
context.getHandler(),
|
|
343
|
-
context.getClass()
|
|
344
|
-
]);
|
|
345
|
-
if (isPublic) {
|
|
346
|
-
return true;
|
|
347
|
-
}
|
|
348
|
-
const gqlContext = this.getGqlContext(context);
|
|
349
|
-
const user = this.extractUser(gqlContext);
|
|
350
|
-
if (!user) {
|
|
351
|
-
throw new UnauthorizedException("Authentication required");
|
|
352
|
-
}
|
|
353
|
-
return true;
|
|
354
|
-
}
|
|
355
|
-
getGqlContext(context) {
|
|
356
|
-
const args = context.getArgs();
|
|
357
|
-
return args[2];
|
|
358
|
-
}
|
|
359
|
-
extractUser(context) {
|
|
360
|
-
return context.user ?? context.req?.user ?? null;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
AuthGuard = __legacyDecorateClassTS([
|
|
364
|
-
Injectable2(),
|
|
365
|
-
__legacyMetadataTS("design:paramtypes", [
|
|
366
|
-
typeof Reflector === "undefined" ? Object : Reflector
|
|
367
|
-
])
|
|
368
|
-
], AuthGuard);
|
|
229
|
+
], AuthGuard);
|
|
369
230
|
|
|
370
231
|
class RolesGuard {
|
|
371
232
|
reflector;
|
|
@@ -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
|
-
if (complexity !== undefined) {
|
|
568
|
-
const args = context.getArgs();
|
|
569
|
-
const gqlContext = args[2];
|
|
570
|
-
if (gqlContext) {
|
|
571
|
-
gqlContext._fieldComplexity = complexity;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
return next.handle();
|
|
423
|
+
getSchema() {
|
|
424
|
+
return this.schema;
|
|
575
425
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
Injectable3(),
|
|
579
|
-
__legacyMetadataTS("design:paramtypes", [
|
|
580
|
-
typeof Reflector2 === "undefined" ? Object : Reflector2
|
|
581
|
-
])
|
|
582
|
-
], ComplexityInterceptor);
|
|
583
|
-
|
|
584
|
-
class CachingInterceptor {
|
|
585
|
-
reflector;
|
|
586
|
-
constructor(reflector) {
|
|
587
|
-
this.reflector = reflector;
|
|
426
|
+
getExecutor() {
|
|
427
|
+
return this.executor;
|
|
588
428
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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);
|
|
603
443
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
typeof
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
+
};
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
data: response.data,
|
|
473
|
+
errors: formattedErrors,
|
|
474
|
+
extensions
|
|
475
|
+
};
|
|
476
|
+
} catch (error) {
|
|
477
|
+
return { errors: [this.formatThrownError(error)] };
|
|
478
|
+
}
|
|
627
479
|
}
|
|
628
|
-
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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;
|
|
636
488
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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));
|
|
504
|
+
}
|
|
505
|
+
return { errors };
|
|
506
|
+
}
|
|
507
|
+
return result;
|
|
508
|
+
} catch (error) {
|
|
509
|
+
return { errors: [this.formatThrownError(error)] };
|
|
510
|
+
}
|
|
643
511
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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);
|
|
516
|
+
}
|
|
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 query = await request.text();
|
|
530
|
+
if (!query) {
|
|
531
|
+
throw new ValidationError("Query is required");
|
|
532
|
+
}
|
|
533
|
+
return { query };
|
|
534
|
+
}
|
|
535
|
+
throw new ValidationError(`Unsupported content type "${contentType || "(none)"}": use application/json or application/graphql`);
|
|
657
536
|
}
|
|
658
|
-
|
|
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
|
+
};
|
|
659
557
|
}
|
|
660
|
-
|
|
661
|
-
|
|
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;
|
|
662
568
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
+
}
|
|
670
607
|
}
|
|
671
|
-
|
|
672
|
-
this.
|
|
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;
|
|
631
|
+
}
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
getPath() {
|
|
635
|
+
return this.options.path ?? "/graphql";
|
|
636
|
+
}
|
|
637
|
+
getCacheStats() {
|
|
638
|
+
return this.executor?.getCacheStats() ?? null;
|
|
639
|
+
}
|
|
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
|
-
|
|
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];
|
|
809
862
|
}
|
|
810
|
-
|
|
811
|
-
|
|
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];
|
|
812
879
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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;
|
|
864
|
-
}
|
|
865
|
-
getVariables() {
|
|
866
|
-
const info = this.getInfo();
|
|
867
|
-
return info?.variableValues ?? {};
|
|
868
|
-
}
|
|
869
|
-
getPath() {
|
|
870
|
-
const info = this.getInfo();
|
|
871
|
-
if (!info?.path)
|
|
872
|
-
return [];
|
|
873
|
-
const path = [];
|
|
874
|
-
let current = info.path;
|
|
875
|
-
while (current) {
|
|
876
|
-
if (typeof current.key === "string") {
|
|
877
|
-
path.unshift(current.key);
|
|
878
|
-
}
|
|
879
|
-
current = current.prev;
|
|
880
|
-
}
|
|
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);
|
|
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;
|
|
906
|
+
}
|
|
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
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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').");
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
async buildFromTypeDefs(typeDefs, resolvers) {
|
|
983
|
+
const sdl = this.mergeTypeDefs(typeDefs);
|
|
984
|
+
if (!resolvers) {
|
|
985
|
+
return buildSchemaFromSDL(sdl);
|
|
956
986
|
}
|
|
987
|
+
const schema = makeExecutableSchema({
|
|
988
|
+
typeDefs: sdl,
|
|
989
|
+
resolvers
|
|
990
|
+
});
|
|
991
|
+
applyResolverFieldMetadata(schema, resolvers);
|
|
992
|
+
return schema;
|
|
957
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
1182
|
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(() => {
|
|
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) {
|
|
@@ -1124,6 +1256,10 @@ class SubscriptionManager {
|
|
|
1124
1256
|
}
|
|
1125
1257
|
state.initialized = true;
|
|
1126
1258
|
state.context = ctx;
|
|
1259
|
+
if (state.initTimeout) {
|
|
1260
|
+
clearTimeout(state.initTimeout);
|
|
1261
|
+
state.initTimeout = undefined;
|
|
1262
|
+
}
|
|
1127
1263
|
this.send(socket, { type: "connection_ack" });
|
|
1128
1264
|
if (this.config.keepAlive) {
|
|
1129
1265
|
state.keepAliveInterval = setInterval(() => {
|
|
@@ -1136,50 +1272,150 @@ class SubscriptionManager {
|
|
|
1136
1272
|
socket.close(4401, "Unauthorized");
|
|
1137
1273
|
return;
|
|
1138
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
|
+
}
|
|
1139
1283
|
if (state.subscriptions.size >= (this.config.maxSubscriptionsPerConnection ?? 100)) {
|
|
1140
1284
|
this.sendError(socket, message.id, "Too many subscriptions");
|
|
1141
1285
|
return;
|
|
1142
1286
|
}
|
|
1143
|
-
|
|
1144
|
-
|
|
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);
|
|
1145
1292
|
}
|
|
1146
1293
|
try {
|
|
1294
|
+
if (this.config.onSubscribe && state.context) {
|
|
1295
|
+
await this.config.onSubscribe(state.context, message);
|
|
1296
|
+
}
|
|
1147
1297
|
const schema = this.driver.getSchema();
|
|
1148
1298
|
if (!schema) {
|
|
1299
|
+
if (registration && state.subscriptions.get(message.id) === registration) {
|
|
1300
|
+
state.subscriptions.delete(message.id);
|
|
1301
|
+
}
|
|
1149
1302
|
this.sendError(socket, message.id, "Schema not available");
|
|
1150
1303
|
return;
|
|
1151
1304
|
}
|
|
1152
1305
|
let context = {
|
|
1153
1306
|
req: state.request,
|
|
1154
1307
|
res: {},
|
|
1155
|
-
|
|
1308
|
+
connectionParams: state.connectionParams
|
|
1156
1309
|
};
|
|
1157
1310
|
if (this.config.context && state.context) {
|
|
1158
1311
|
context = await this.config.context(state.context);
|
|
1159
1312
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
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);
|
|
1168
1325
|
return;
|
|
1169
1326
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
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
|
|
1173
1332
|
});
|
|
1333
|
+
this.send(socket, { id: message.id, type: "complete" });
|
|
1174
1334
|
} catch (error) {
|
|
1335
|
+
if (registration && state.subscriptions.get(message.id) === registration) {
|
|
1336
|
+
state.subscriptions.delete(message.id);
|
|
1337
|
+
}
|
|
1175
1338
|
const errorMessage = error instanceof Error ? error.message : "Subscription failed";
|
|
1176
1339
|
this.sendError(socket, message.id, errorMessage);
|
|
1177
1340
|
}
|
|
1178
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
|
+
}
|
|
1179
1414
|
async handleComplete(socket, state, message) {
|
|
1180
1415
|
const subscription = state.subscriptions.get(message.id);
|
|
1181
1416
|
if (subscription) {
|
|
1182
1417
|
state.subscriptions.delete(message.id);
|
|
1418
|
+
await this.closeIterator(subscription.iterator);
|
|
1183
1419
|
if (this.config.onComplete && state.context) {
|
|
1184
1420
|
await this.config.onComplete(state.context, message);
|
|
1185
1421
|
}
|
|
@@ -1191,21 +1427,32 @@ class SubscriptionManager {
|
|
|
1191
1427
|
return;
|
|
1192
1428
|
if (state.keepAliveInterval) {
|
|
1193
1429
|
clearInterval(state.keepAliveInterval);
|
|
1430
|
+
state.keepAliveInterval = undefined;
|
|
1194
1431
|
}
|
|
1195
|
-
if (
|
|
1196
|
-
|
|
1432
|
+
if (state.initTimeout) {
|
|
1433
|
+
clearTimeout(state.initTimeout);
|
|
1434
|
+
state.initTimeout = undefined;
|
|
1197
1435
|
}
|
|
1436
|
+
const subscriptions = [...state.subscriptions.values()];
|
|
1198
1437
|
state.subscriptions.clear();
|
|
1199
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
|
+
}
|
|
1200
1447
|
}
|
|
1201
1448
|
handleError(ws, _error) {
|
|
1202
1449
|
const state = this.connections.get(ws);
|
|
1203
1450
|
if (state) {
|
|
1204
|
-
this.handleClose(ws);
|
|
1451
|
+
this.handleClose(ws).catch((e) => console.error("WebSocket close handling failed:", e));
|
|
1205
1452
|
}
|
|
1206
1453
|
}
|
|
1207
1454
|
send(socket, message) {
|
|
1208
|
-
if (socket.readyState ===
|
|
1455
|
+
if (socket.readyState === SOCKET_OPEN) {
|
|
1209
1456
|
socket.send(JSON.stringify(message));
|
|
1210
1457
|
}
|
|
1211
1458
|
}
|
|
@@ -1216,18 +1463,36 @@ class SubscriptionManager {
|
|
|
1216
1463
|
payload: [{ message }]
|
|
1217
1464
|
});
|
|
1218
1465
|
}
|
|
1219
|
-
|
|
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) {
|
|
1220
1481
|
for (const [socket, state] of this.connections) {
|
|
1221
|
-
const subscription
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
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
|
+
}
|
|
1228
1490
|
}
|
|
1229
1491
|
}
|
|
1230
1492
|
}
|
|
1493
|
+
publish(topic, payload) {
|
|
1494
|
+
this.publishToTopic(topic, payload);
|
|
1495
|
+
}
|
|
1231
1496
|
getConnectionCount() {
|
|
1232
1497
|
return this.connections.size;
|
|
1233
1498
|
}
|
|
@@ -1240,7 +1505,7 @@ class SubscriptionManager {
|
|
|
1240
1505
|
}
|
|
1241
1506
|
}
|
|
1242
1507
|
SubscriptionManager = __legacyDecorateClassTS([
|
|
1243
|
-
|
|
1508
|
+
Injectable5(),
|
|
1244
1509
|
__legacyDecorateParamTS(0, Inject4(LEAVEN_MODULE_OPTIONS)),
|
|
1245
1510
|
__legacyDecorateParamTS(1, Inject4(LEAVEN_DRIVER)),
|
|
1246
1511
|
__legacyMetadataTS("design:paramtypes", [
|
|
@@ -1249,73 +1514,593 @@ SubscriptionManager = __legacyDecorateClassTS([
|
|
|
1249
1514
|
])
|
|
1250
1515
|
], SubscriptionManager);
|
|
1251
1516
|
function Subscription(returns, options) {
|
|
1252
|
-
return (
|
|
1253
|
-
|
|
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") {
|
|
1254
1524
|
Reflect.defineMetadata("leaven:subscription", { returns, options }, descriptor.value);
|
|
1255
1525
|
}
|
|
1256
1526
|
return descriptor;
|
|
1257
1527
|
};
|
|
1258
1528
|
}
|
|
1259
1529
|
function InjectPubSub() {
|
|
1260
|
-
return (
|
|
1261
|
-
if (_target && propertyKey !== undefined) {
|
|
1262
|
-
const existingParams = Reflect.getMetadata("leaven:inject:pubsub", _target, propertyKey) || [];
|
|
1263
|
-
existingParams.push(parameterIndex);
|
|
1264
|
-
Reflect.defineMetadata("leaven:inject:pubsub", existingParams, _target, propertyKey);
|
|
1265
|
-
}
|
|
1266
|
-
};
|
|
1530
|
+
return Inject4(LEAVEN_PUBSUB);
|
|
1267
1531
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
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
|
+
});
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
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
|
+
});
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
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;
|
|
1764
|
+
}
|
|
1765
|
+
const context = await this.buildContext(req, res);
|
|
1766
|
+
try {
|
|
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
|
|
1777
|
+
});
|
|
1778
|
+
} catch (error) {
|
|
1779
|
+
this.respond(res, { errors: [this.formatThrownError(error)] });
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
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);
|
|
1789
|
+
}
|
|
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);
|
|
1851
|
+
}
|
|
1852
|
+
if (raw !== null && typeof raw === "object" && !(raw instanceof ArrayBuffer)) {
|
|
1853
|
+
return raw;
|
|
1854
|
+
}
|
|
1855
|
+
const json = req.raw?.json;
|
|
1856
|
+
if (json) {
|
|
1857
|
+
return await json.call(req.raw);
|
|
1858
|
+
}
|
|
1859
|
+
return {};
|
|
1860
|
+
}
|
|
1861
|
+
static formatExecutionError(error) {
|
|
1862
|
+
const formatted = error.toJSON();
|
|
1863
|
+
const original = error.originalError;
|
|
1864
|
+
if (!(original instanceof HttpException)) {
|
|
1865
|
+
return formatted;
|
|
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
|
+
};
|
|
1873
|
+
}
|
|
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
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
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;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
return next.handle();
|
|
1945
|
+
}
|
|
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;
|
|
1958
|
+
}
|
|
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
|
+
}));
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
CachingInterceptor = __legacyDecorateClassTS([
|
|
1975
|
+
Injectable6(),
|
|
1976
|
+
__legacyMetadataTS("design:paramtypes", [
|
|
1977
|
+
typeof Reflector2 === "undefined" ? Object : Reflector2
|
|
1978
|
+
])
|
|
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);
|
|
2028
|
+
}
|
|
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
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
MetricsInterceptor = __legacyDecorateClassTS([
|
|
2047
|
+
Injectable6()
|
|
2048
|
+
], MetricsInterceptor);
|
|
2049
|
+
export {
|
|
2050
|
+
Args,
|
|
2051
|
+
AuthGuard,
|
|
2052
|
+
CACHE_KEY,
|
|
2053
|
+
COMPLEXITY_KEY,
|
|
2054
|
+
CacheHint,
|
|
2055
|
+
CachingInterceptor,
|
|
2056
|
+
Complexity,
|
|
2057
|
+
ComplexityGuard,
|
|
2058
|
+
ComplexityInterceptor,
|
|
2059
|
+
Context,
|
|
2060
|
+
DEPRECATED_KEY,
|
|
2061
|
+
DESCRIPTION_KEY,
|
|
2062
|
+
Decorators,
|
|
2063
|
+
Deprecated,
|
|
2064
|
+
DepthGuard,
|
|
2065
|
+
Description,
|
|
2066
|
+
ErrorFormattingInterceptor,
|
|
2067
|
+
GqlExecutionContext,
|
|
2068
|
+
GraphQLMiddleware,
|
|
2069
|
+
IS_PUBLIC_KEY,
|
|
2070
|
+
Info,
|
|
2071
|
+
InjectPubSub,
|
|
2072
|
+
LEAVEN_DRIVER,
|
|
2073
|
+
LEAVEN_MAX_COMPLEXITY,
|
|
2074
|
+
LEAVEN_MAX_DEPTH,
|
|
2075
|
+
LEAVEN_MODULE_OPTIONS,
|
|
2076
|
+
LEAVEN_PUBSUB,
|
|
2077
|
+
LeavenDriver,
|
|
2078
|
+
LeavenGraphQLDriver,
|
|
2079
|
+
LeavenModule,
|
|
2080
|
+
LoggingInterceptor,
|
|
2081
|
+
MetricsInterceptor,
|
|
2082
|
+
PERMISSIONS_KEY,
|
|
2083
|
+
Parent,
|
|
2084
|
+
Permissions,
|
|
2085
|
+
PermissionsGuard,
|
|
2086
|
+
Public,
|
|
2087
|
+
ROLES_KEY,
|
|
2088
|
+
Roles,
|
|
2089
|
+
RolesGuard,
|
|
2090
|
+
Root,
|
|
2091
|
+
SUBSCRIPTION_FILTER_KEY,
|
|
2092
|
+
SchemaBuilderService,
|
|
2093
|
+
Subscription,
|
|
2094
|
+
SubscriptionFilter,
|
|
2095
|
+
SubscriptionManager,
|
|
2096
|
+
clearResolverMetadata,
|
|
2097
|
+
createContextDecorator,
|
|
2098
|
+
createGraphQLMiddleware,
|
|
2099
|
+
generateSchemaFile,
|
|
2100
|
+
getGqlArgs,
|
|
2101
|
+
getGqlContext,
|
|
2102
|
+
getGqlInfo,
|
|
2103
|
+
getGqlRoot,
|
|
2104
|
+
getResolverMetadata,
|
|
2105
|
+
registerResolverMetadata
|
|
1321
2106
|
};
|