@martel/calyx 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +71 -27
- package/benchmarks/graphql-benchmark.ts +81 -0
- package/benchmarks/index.ts +32 -0
- package/benchmarks/openapi-benchmark.ts +168 -0
- package/benchmarks/serialization-benchmark.ts +52 -0
- package/benchmarks/techniques-benchmark.ts +84 -0
- package/benchmarks/validation-benchmark.ts +74 -0
- package/bun.lock +11 -0
- package/package.json +7 -6
- package/src/cli/index.ts +19 -3
- package/src/compression/compression.middleware.ts +7 -0
- package/src/cookies/cookies.ts +69 -0
- package/src/database/mongoose.module.ts +250 -0
- package/src/database/typeorm.module.ts +276 -0
- package/src/file-upload/file-upload.interceptor.ts +93 -0
- package/src/file-upload/index.ts +1 -0
- package/src/graphql/decorators.ts +70 -0
- package/src/graphql/graphql.module.ts +401 -57
- package/src/http/application.ts +434 -74
- package/src/http-client/http-client.module.ts +124 -0
- package/src/http-client/index.ts +1 -0
- package/src/index.ts +14 -0
- package/src/logger/index.ts +1 -0
- package/src/logger/logger.service.ts +118 -0
- package/src/mvc/index.ts +1 -0
- package/src/mvc/mvc.ts +22 -0
- package/src/openapi/decorators.ts +154 -0
- package/src/openapi/swagger.module.ts +172 -20
- package/src/queue/queue.module.ts +174 -0
- package/src/session/index.ts +1 -0
- package/src/session/session.middleware.ts +82 -0
- package/src/sse/index.ts +1 -0
- package/src/sse/sse.ts +18 -0
- package/src/streaming/index.ts +1 -0
- package/src/streaming/streamable-file.ts +32 -0
- package/src/validation/pipe.ts +79 -10
- package/src/versioning/versioning.ts +46 -0
- package/tests/graphql.test.ts +245 -6
- package/tests/openapi.test.ts +78 -11
- package/tests/techniques.test.ts +471 -0
|
@@ -3,6 +3,10 @@ import { Module } from '../core/decorators.ts';
|
|
|
3
3
|
import {
|
|
4
4
|
GraphQLSchema,
|
|
5
5
|
GraphQLObjectType,
|
|
6
|
+
GraphQLInputObjectType,
|
|
7
|
+
GraphQLScalarType,
|
|
8
|
+
GraphQLInterfaceType,
|
|
9
|
+
GraphQLUnionType,
|
|
6
10
|
GraphQLString,
|
|
7
11
|
GraphQLInt,
|
|
8
12
|
GraphQLFloat,
|
|
@@ -24,6 +28,66 @@ export class GraphQLModule {
|
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
const typeMap = new Map<any, any>();
|
|
31
|
+
const inputTypeMap = new Map<any, any>();
|
|
32
|
+
|
|
33
|
+
function findModuleClass(resolverClass: any): any {
|
|
34
|
+
for (const [moduleClass, record] of container.getModules().entries()) {
|
|
35
|
+
if (record.providers.has(resolverClass) || record.controllers.has(resolverClass)) {
|
|
36
|
+
return moduleClass;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function compileLifecycleItems(moduleClass: any, items: any[]): any[] {
|
|
43
|
+
return items.map((item) => {
|
|
44
|
+
if (typeof item === 'function') {
|
|
45
|
+
const isClass =
|
|
46
|
+
item.prototype &&
|
|
47
|
+
(typeof item.prototype.canActivate === 'function' ||
|
|
48
|
+
typeof item.prototype.intercept === 'function' ||
|
|
49
|
+
typeof item.prototype.transform === 'function' ||
|
|
50
|
+
typeof item.prototype.catch === 'function');
|
|
51
|
+
if (!isClass) {
|
|
52
|
+
return { isReqScoped: false, token: item, instance: item };
|
|
53
|
+
}
|
|
54
|
+
let instance: any;
|
|
55
|
+
try {
|
|
56
|
+
instance = container.resolveTokenInModuleContext(moduleClass, item);
|
|
57
|
+
} catch {
|
|
58
|
+
instance = new item();
|
|
59
|
+
}
|
|
60
|
+
return { isReqScoped: false, token: item, instance };
|
|
61
|
+
}
|
|
62
|
+
return { isReqScoped: false, token: item.constructor, instance: item };
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildFieldsConfig(typeClass: any, isInput: boolean): any {
|
|
67
|
+
const fieldsMetadata: { propertyKey: string; typeFunc?: any; options?: any }[] =
|
|
68
|
+
Reflect.getMetadata('calyx:fields', typeClass) || [];
|
|
69
|
+
|
|
70
|
+
const fieldsConfig: any = {};
|
|
71
|
+
for (const field of fieldsMetadata) {
|
|
72
|
+
let returnTypeClass = field.typeFunc ? field.typeFunc(null) : undefined;
|
|
73
|
+
if (!returnTypeClass) {
|
|
74
|
+
returnTypeClass = Reflect.getMetadata('design:type', typeClass.prototype, field.propertyKey);
|
|
75
|
+
}
|
|
76
|
+
if (!returnTypeClass) {
|
|
77
|
+
returnTypeClass = String;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let gqlType = isInput ? getGraphQLInputType(returnTypeClass) : getGraphQLType(returnTypeClass);
|
|
81
|
+
if (Array.isArray(returnTypeClass)) {
|
|
82
|
+
gqlType = new GraphQLList(isInput ? getGraphQLInputType(returnTypeClass[0]) : getGraphQLType(returnTypeClass[0]));
|
|
83
|
+
}
|
|
84
|
+
if (!field.options?.nullable) {
|
|
85
|
+
gqlType = new GraphQLNonNull(gqlType);
|
|
86
|
+
}
|
|
87
|
+
fieldsConfig[field.propertyKey] = { type: gqlType };
|
|
88
|
+
}
|
|
89
|
+
return fieldsConfig;
|
|
90
|
+
}
|
|
27
91
|
|
|
28
92
|
function getGraphQLType(typeClass: any): any {
|
|
29
93
|
if (typeClass === String) return GraphQLString;
|
|
@@ -31,34 +95,49 @@ export class GraphQLModule {
|
|
|
31
95
|
if (typeClass === Boolean) return GraphQLBoolean;
|
|
32
96
|
if (typeMap.has(typeClass)) return typeMap.get(typeClass);
|
|
33
97
|
|
|
98
|
+
// Custom Scalar
|
|
99
|
+
if (typeof typeClass === 'function' && Reflect.hasMetadata('calyx:scalar', typeClass)) {
|
|
100
|
+
const scalarMeta = Reflect.getMetadata('calyx:scalar', typeClass);
|
|
101
|
+
const inst = container.get(typeClass) || new typeClass();
|
|
102
|
+
const gqlScalar = new GraphQLScalarType({
|
|
103
|
+
name: scalarMeta.name,
|
|
104
|
+
description: Reflect.getMetadata('calyx:description', typeClass),
|
|
105
|
+
serialize: inst.serialize ? inst.serialize.bind(inst) : (val: any) => val,
|
|
106
|
+
parseValue: inst.parseValue ? inst.parseValue.bind(inst) : (val: any) => val,
|
|
107
|
+
parseLiteral: inst.parseLiteral ? inst.parseLiteral.bind(inst) : undefined,
|
|
108
|
+
});
|
|
109
|
+
typeMap.set(typeClass, gqlScalar);
|
|
110
|
+
return gqlScalar;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Interface
|
|
114
|
+
if (typeof typeClass === 'function' && Reflect.hasMetadata('calyx:interface_type', typeClass)) {
|
|
115
|
+
const gqlInterfaceType = new GraphQLInterfaceType({
|
|
116
|
+
name: typeClass.name,
|
|
117
|
+
description: Reflect.getMetadata('calyx:description', typeClass),
|
|
118
|
+
fields: () => buildFieldsConfig(typeClass, false),
|
|
119
|
+
});
|
|
120
|
+
typeMap.set(typeClass, gqlInterfaceType);
|
|
121
|
+
return gqlInterfaceType;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Union
|
|
125
|
+
if (typeClass && (typeClass.__isUnion || (typeof typeClass === 'function' && Reflect.hasMetadata('calyx:union_type', typeClass)))) {
|
|
126
|
+
const unionMeta = typeClass.__isUnion ? typeClass : Reflect.getMetadata('calyx:union_type', typeClass);
|
|
127
|
+
const gqlUnionType = new GraphQLUnionType({
|
|
128
|
+
name: unionMeta.name,
|
|
129
|
+
types: () => unionMeta.types().map((t: any) => getGraphQLType(t)),
|
|
130
|
+
resolveType: unionMeta.resolveType,
|
|
131
|
+
});
|
|
132
|
+
typeMap.set(typeClass, gqlUnionType);
|
|
133
|
+
return gqlUnionType;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ObjectType
|
|
34
137
|
if (typeof typeClass === 'function' && Reflect.hasMetadata('calyx:object_type', typeClass)) {
|
|
35
138
|
const gqlObjectType = new GraphQLObjectType({
|
|
36
139
|
name: typeClass.name,
|
|
37
|
-
fields: () =>
|
|
38
|
-
const fieldsMetadata: { propertyKey: string; typeFunc?: any; options?: any }[] =
|
|
39
|
-
Reflect.getMetadata('calyx:fields', typeClass) || [];
|
|
40
|
-
|
|
41
|
-
const fieldsConfig: any = {};
|
|
42
|
-
for (const field of fieldsMetadata) {
|
|
43
|
-
let returnTypeClass = field.typeFunc ? field.typeFunc(null) : undefined;
|
|
44
|
-
if (!returnTypeClass) {
|
|
45
|
-
returnTypeClass = Reflect.getMetadata('design:type', typeClass.prototype, field.propertyKey);
|
|
46
|
-
}
|
|
47
|
-
if (!returnTypeClass) {
|
|
48
|
-
returnTypeClass = String;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
let gqlType = getGraphQLType(returnTypeClass);
|
|
52
|
-
if (Array.isArray(returnTypeClass)) {
|
|
53
|
-
gqlType = new GraphQLList(getGraphQLType(returnTypeClass[0]));
|
|
54
|
-
}
|
|
55
|
-
if (!field.options?.nullable) {
|
|
56
|
-
gqlType = new GraphQLNonNull(gqlType);
|
|
57
|
-
}
|
|
58
|
-
fieldsConfig[field.propertyKey] = { type: gqlType };
|
|
59
|
-
}
|
|
60
|
-
return fieldsConfig;
|
|
61
|
-
},
|
|
140
|
+
fields: () => buildFieldsConfig(typeClass, false),
|
|
62
141
|
});
|
|
63
142
|
|
|
64
143
|
typeMap.set(typeClass, gqlObjectType);
|
|
@@ -68,48 +147,216 @@ export class GraphQLModule {
|
|
|
68
147
|
return GraphQLString;
|
|
69
148
|
}
|
|
70
149
|
|
|
150
|
+
function getGraphQLInputType(typeClass: any): any {
|
|
151
|
+
if (typeClass === String) return GraphQLString;
|
|
152
|
+
if (typeClass === Number) return GraphQLFloat;
|
|
153
|
+
if (typeClass === Boolean) return GraphQLBoolean;
|
|
154
|
+
if (inputTypeMap.has(typeClass)) return inputTypeMap.get(typeClass);
|
|
155
|
+
|
|
156
|
+
if (typeof typeClass === 'function' && Reflect.hasMetadata('calyx:input_type', typeClass)) {
|
|
157
|
+
const gqlInputObjectType = new GraphQLInputObjectType({
|
|
158
|
+
name: typeClass.name,
|
|
159
|
+
fields: () => buildFieldsConfig(typeClass, true),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
inputTypeMap.set(typeClass, gqlInputObjectType);
|
|
163
|
+
return gqlInputObjectType;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return GraphQLString;
|
|
167
|
+
}
|
|
168
|
+
|
|
71
169
|
const queryFields: any = {};
|
|
72
170
|
const mutationFields: any = {};
|
|
171
|
+
const subscriptionFields: any = {};
|
|
73
172
|
|
|
74
173
|
for (const resolverInstance of resolverInstances) {
|
|
75
174
|
const resolverClass = resolverInstance.constructor;
|
|
175
|
+
const moduleClass = findModuleClass(resolverClass);
|
|
76
176
|
|
|
77
|
-
//
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
for (const query of queries) {
|
|
81
|
-
const returnTypeClass = query.typeFunc ? query.typeFunc(null) : String;
|
|
177
|
+
// Compile helper for queries, mutations, subscriptions
|
|
178
|
+
const compileField = (fieldMeta: { propertyKey: string | symbol; typeFunc?: any; options?: any }, list: any) => {
|
|
179
|
+
const returnTypeClass = fieldMeta.typeFunc ? fieldMeta.typeFunc(null) : String;
|
|
82
180
|
let gqlType = getGraphQLType(returnTypeClass);
|
|
83
181
|
if (Array.isArray(returnTypeClass)) {
|
|
84
182
|
gqlType = new GraphQLList(getGraphQLType(returnTypeClass[0]));
|
|
85
183
|
}
|
|
86
184
|
|
|
87
185
|
const argsMetadata: { parameterIndex: number; name: string }[] =
|
|
88
|
-
Reflect.getMetadata('calyx:args', resolverInstance,
|
|
186
|
+
Reflect.getMetadata('calyx:args', resolverInstance, fieldMeta.propertyKey) || [];
|
|
89
187
|
const argsConfig: any = {};
|
|
90
188
|
|
|
91
|
-
const paramTypes = Reflect.getMetadata('design:paramtypes', resolverInstance,
|
|
189
|
+
const paramTypes = Reflect.getMetadata('design:paramtypes', resolverInstance, fieldMeta.propertyKey) || [];
|
|
92
190
|
for (const arg of argsMetadata) {
|
|
93
191
|
const paramType = paramTypes[arg.parameterIndex] || String;
|
|
94
|
-
|
|
192
|
+
if (!arg.name && typeof paramType === 'function' && Reflect.hasMetadata('calyx:args_type', paramType)) {
|
|
193
|
+
const fieldsMetadata = Reflect.getMetadata('calyx:fields', paramType) || [];
|
|
194
|
+
for (const f of fieldsMetadata) {
|
|
195
|
+
let fType = f.typeFunc ? f.typeFunc(null) : undefined;
|
|
196
|
+
if (!fType) {
|
|
197
|
+
fType = Reflect.getMetadata('design:type', paramType.prototype, f.propertyKey) || String;
|
|
198
|
+
}
|
|
199
|
+
let gqlFType = getGraphQLInputType(fType);
|
|
200
|
+
if (Array.isArray(fType)) {
|
|
201
|
+
gqlFType = new GraphQLList(getGraphQLInputType(fType[0]));
|
|
202
|
+
}
|
|
203
|
+
if (!f.options?.nullable) {
|
|
204
|
+
gqlFType = new GraphQLNonNull(gqlFType);
|
|
205
|
+
}
|
|
206
|
+
argsConfig[f.propertyKey] = { type: gqlFType };
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
const argName = arg.name || 'args';
|
|
210
|
+
argsConfig[argName] = { type: getGraphQLInputType(paramType) };
|
|
211
|
+
}
|
|
95
212
|
}
|
|
96
213
|
|
|
97
|
-
|
|
214
|
+
// Enhancers metadata resolution
|
|
215
|
+
const classGuards = Reflect.getMetadata('calyx:guards', resolverClass) || [];
|
|
216
|
+
const methodGuards = Reflect.getMetadata('calyx:guards', resolverClass.prototype, fieldMeta.propertyKey) || [];
|
|
217
|
+
const compiledGuards = compileLifecycleItems(moduleClass, [...classGuards, ...methodGuards]);
|
|
98
218
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
219
|
+
const classInterceptors = Reflect.getMetadata('calyx:interceptors', resolverClass) || [];
|
|
220
|
+
const methodInterceptors = Reflect.getMetadata('calyx:interceptors', resolverClass.prototype, fieldMeta.propertyKey) || [];
|
|
221
|
+
const compiledInterceptors = compileLifecycleItems(moduleClass, [...classInterceptors, ...methodInterceptors]);
|
|
222
|
+
|
|
223
|
+
const classFilters = Reflect.getMetadata('calyx:filters', resolverClass) || [];
|
|
224
|
+
const methodFilters = Reflect.getMetadata('calyx:filters', resolverClass.prototype, fieldMeta.propertyKey) || [];
|
|
225
|
+
const compiledFilters = compileLifecycleItems(moduleClass, [...classFilters, ...methodFilters]);
|
|
226
|
+
|
|
227
|
+
const classPipes = Reflect.getMetadata('calyx:pipes', resolverClass) || [];
|
|
228
|
+
const methodPipes = Reflect.getMetadata('calyx:pipes', resolverClass.prototype, fieldMeta.propertyKey) || [];
|
|
229
|
+
const compiledPipes = compileLifecycleItems(moduleClass, [...classPipes, ...methodPipes]);
|
|
230
|
+
|
|
231
|
+
const fieldName = fieldMeta.options?.name || String(fieldMeta.propertyKey);
|
|
232
|
+
|
|
233
|
+
const resolveFn = async (parent: any, args: any, context: any, info: any) => {
|
|
234
|
+
const req = context?.req;
|
|
235
|
+
const { CalyxExecutionContext } = await import('../lifecycle/context.ts');
|
|
236
|
+
const execContext = new CalyxExecutionContext(req, null, resolverClass, resolverInstance[fieldMeta.propertyKey]);
|
|
237
|
+
(execContext as any).type = 'graphql';
|
|
238
|
+
(execContext as any).data = args;
|
|
239
|
+
|
|
240
|
+
const gqlArgs = [parent, args, context, info];
|
|
241
|
+
execContext.getArgs = () => gqlArgs as any;
|
|
242
|
+
execContext.getArgByIndex = (index: number) => gqlArgs[index];
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
// 1. Guards
|
|
246
|
+
for (const guard of compiledGuards) {
|
|
247
|
+
const canActivate = guard.instance.canActivate(execContext);
|
|
248
|
+
const resolved = canActivate instanceof Promise ? await canActivate : canActivate;
|
|
249
|
+
if (!resolved) {
|
|
250
|
+
const { HttpException } = await import('../http/exceptions.ts');
|
|
251
|
+
throw new HttpException('Forbidden resource', 403);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 2. Pipes
|
|
256
|
+
let processedArgs = args;
|
|
257
|
+
for (const pipe of compiledPipes) {
|
|
258
|
+
const transformed = pipe.instance.transform(processedArgs, {
|
|
259
|
+
type: 'custom',
|
|
260
|
+
metatype: undefined,
|
|
261
|
+
data: undefined,
|
|
262
|
+
});
|
|
263
|
+
processedArgs = transformed instanceof Promise ? await transformed : transformed;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const executeHandler = async () => {
|
|
267
|
+
const params: any[] = [];
|
|
268
|
+
for (const arg of argsMetadata) {
|
|
269
|
+
const paramType = paramTypes[arg.parameterIndex] || String;
|
|
270
|
+
if (!arg.name && typeof paramType === 'function' && Reflect.hasMetadata('calyx:args_type', paramType)) {
|
|
271
|
+
const argInst = new paramType();
|
|
272
|
+
const fieldsMetadata = Reflect.getMetadata('calyx:fields', paramType) || [];
|
|
273
|
+
for (const f of fieldsMetadata) {
|
|
274
|
+
argInst[f.propertyKey] = processedArgs[f.propertyKey];
|
|
275
|
+
}
|
|
276
|
+
params[arg.parameterIndex] = argInst;
|
|
277
|
+
} else {
|
|
278
|
+
const argName = arg.name || 'args';
|
|
279
|
+
params[arg.parameterIndex] = processedArgs[argName];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const parentParams = Reflect.getMetadata('calyx:parent', resolverInstance, fieldMeta.propertyKey) || [];
|
|
283
|
+
for (const idx of parentParams) {
|
|
284
|
+
params[idx] = parent;
|
|
285
|
+
}
|
|
286
|
+
return resolverInstance[fieldMeta.propertyKey](...params);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// 3. Interceptors
|
|
290
|
+
if (compiledInterceptors.length > 0) {
|
|
291
|
+
let interceptorIndex = 0;
|
|
292
|
+
const nextHandler = async (): Promise<any> => {
|
|
293
|
+
if (interceptorIndex < compiledInterceptors.length) {
|
|
294
|
+
const interceptor = compiledInterceptors[interceptorIndex++];
|
|
295
|
+
return interceptor.instance.intercept(execContext, { handle: () => nextHandler() });
|
|
296
|
+
}
|
|
297
|
+
return executeHandler();
|
|
298
|
+
};
|
|
299
|
+
return await nextHandler();
|
|
300
|
+
} else {
|
|
301
|
+
return await executeHandler();
|
|
106
302
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
303
|
+
} catch (err: any) {
|
|
304
|
+
// 4. Exception Filters
|
|
305
|
+
if (compiledFilters.length > 0) {
|
|
306
|
+
for (const filter of compiledFilters) {
|
|
307
|
+
const catchException = Reflect.getMetadata('calyx:catch', filter.token) || [];
|
|
308
|
+
if (catchException.length === 0 || catchException.some((exc: any) => err instanceof exc)) {
|
|
309
|
+
const filterResult = filter.instance.catch(err, execContext);
|
|
310
|
+
if (filterResult !== undefined) {
|
|
311
|
+
return filterResult;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
110
315
|
}
|
|
111
|
-
|
|
112
|
-
}
|
|
316
|
+
throw err;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
list[fieldName] = {
|
|
321
|
+
type: gqlType,
|
|
322
|
+
args: argsConfig,
|
|
323
|
+
resolve: resolveFn,
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return { argsMetadata, paramTypes };
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Queries
|
|
330
|
+
const queries: { propertyKey: string | symbol; typeFunc?: any; options?: any }[] =
|
|
331
|
+
Reflect.getMetadata('calyx:queries', resolverClass) || [];
|
|
332
|
+
for (const query of queries) {
|
|
333
|
+
compileField(query, queryFields);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Mutations
|
|
337
|
+
const mutations: { propertyKey: string | symbol; typeFunc?: any; options?: any }[] =
|
|
338
|
+
Reflect.getMetadata('calyx:mutations', resolverClass) || [];
|
|
339
|
+
for (const mutation of mutations) {
|
|
340
|
+
compileField(mutation, mutationFields);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Subscriptions
|
|
344
|
+
const subscriptions: { propertyKey: string | symbol; typeFunc?: any; options?: any }[] =
|
|
345
|
+
Reflect.getMetadata('calyx:subscriptions', resolverClass) || [];
|
|
346
|
+
for (const sub of subscriptions) {
|
|
347
|
+
const { argsMetadata, paramTypes } = compileField(sub, subscriptionFields);
|
|
348
|
+
const subName = sub.options?.name || String(sub.propertyKey);
|
|
349
|
+
|
|
350
|
+
const originalResolve = subscriptionFields[subName].resolve;
|
|
351
|
+
subscriptionFields[subName].subscribe = originalResolve;
|
|
352
|
+
subscriptionFields[subName].resolve = (payload: any) => {
|
|
353
|
+
if (sub.options?.resolve) {
|
|
354
|
+
return sub.options.resolve(payload);
|
|
355
|
+
}
|
|
356
|
+
if (payload && typeof payload === 'object' && subName in payload) {
|
|
357
|
+
return payload[subName];
|
|
358
|
+
}
|
|
359
|
+
return payload;
|
|
113
360
|
};
|
|
114
361
|
}
|
|
115
362
|
|
|
@@ -125,18 +372,107 @@ export class GraphQLModule {
|
|
|
125
372
|
for (const fieldRes of fieldResolvers) {
|
|
126
373
|
const fieldName = fieldRes.options?.name || String(fieldRes.propertyKey);
|
|
127
374
|
if (targetFields[fieldName]) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
375
|
+
// Resolve field enhancers resolution
|
|
376
|
+
const classGuards = Reflect.getMetadata('calyx:guards', resolverClass) || [];
|
|
377
|
+
const methodGuards = Reflect.getMetadata('calyx:guards', resolverClass.prototype, fieldRes.propertyKey) || [];
|
|
378
|
+
const compiledGuards = compileLifecycleItems(moduleClass, [...classGuards, ...methodGuards]);
|
|
379
|
+
|
|
380
|
+
const classInterceptors = Reflect.getMetadata('calyx:interceptors', resolverClass) || [];
|
|
381
|
+
const methodInterceptors = Reflect.getMetadata('calyx:interceptors', resolverClass.prototype, fieldRes.propertyKey) || [];
|
|
382
|
+
const compiledInterceptors = compileLifecycleItems(moduleClass, [...classInterceptors, ...methodInterceptors]);
|
|
383
|
+
|
|
384
|
+
const classFilters = Reflect.getMetadata('calyx:filters', resolverClass) || [];
|
|
385
|
+
const methodFilters = Reflect.getMetadata('calyx:filters', resolverClass.prototype, fieldRes.propertyKey) || [];
|
|
386
|
+
const compiledFilters = compileLifecycleItems(moduleClass, [...classFilters, ...methodFilters]);
|
|
387
|
+
|
|
388
|
+
const classPipes = Reflect.getMetadata('calyx:pipes', resolverClass) || [];
|
|
389
|
+
const methodPipes = Reflect.getMetadata('calyx:pipes', resolverClass.prototype, fieldRes.propertyKey) || [];
|
|
390
|
+
const compiledPipes = compileLifecycleItems(moduleClass, [...classPipes, ...methodPipes]);
|
|
391
|
+
|
|
392
|
+
targetFields[fieldName].resolve = async (parent: any, args: any, context: any, info: any) => {
|
|
393
|
+
const req = context?.req;
|
|
394
|
+
const { CalyxExecutionContext } = await import('../lifecycle/context.ts');
|
|
395
|
+
const execContext = new CalyxExecutionContext(req, null, resolverClass, resolverInstance[fieldRes.propertyKey]);
|
|
396
|
+
(execContext as any).type = 'graphql';
|
|
397
|
+
(execContext as any).data = args;
|
|
398
|
+
|
|
399
|
+
const gqlArgs = [parent, args, context, info];
|
|
400
|
+
execContext.getArgs = () => gqlArgs as any;
|
|
401
|
+
execContext.getArgByIndex = (index: number) => gqlArgs[index];
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
for (const guard of compiledGuards) {
|
|
405
|
+
const canActivate = guard.instance.canActivate(execContext);
|
|
406
|
+
const resolved = canActivate instanceof Promise ? await canActivate : canActivate;
|
|
407
|
+
if (!resolved) {
|
|
408
|
+
const { HttpException } = await import('../http/exceptions.ts');
|
|
409
|
+
throw new HttpException('Forbidden resource', 403);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
let processedArgs = args;
|
|
414
|
+
for (const pipe of compiledPipes) {
|
|
415
|
+
const transformed = pipe.instance.transform(processedArgs, {
|
|
416
|
+
type: 'custom',
|
|
417
|
+
metatype: undefined,
|
|
418
|
+
data: undefined,
|
|
419
|
+
});
|
|
420
|
+
processedArgs = transformed instanceof Promise ? await transformed : transformed;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const executeHandler = async () => {
|
|
424
|
+
const argsMetadata: { parameterIndex: number; name: string }[] =
|
|
425
|
+
Reflect.getMetadata('calyx:args', resolverInstance, fieldRes.propertyKey) || [];
|
|
426
|
+
const params: any[] = [];
|
|
427
|
+
const paramTypes = Reflect.getMetadata('design:paramtypes', resolverInstance, fieldRes.propertyKey) || [];
|
|
428
|
+
for (const arg of argsMetadata) {
|
|
429
|
+
const paramType = paramTypes[arg.parameterIndex] || String;
|
|
430
|
+
if (!arg.name && typeof paramType === 'function' && Reflect.hasMetadata('calyx:args_type', paramType)) {
|
|
431
|
+
const argInst = new paramType();
|
|
432
|
+
const fieldsMetadata = Reflect.getMetadata('calyx:fields', paramType) || [];
|
|
433
|
+
for (const f of fieldsMetadata) {
|
|
434
|
+
argInst[f.propertyKey] = processedArgs[f.propertyKey];
|
|
435
|
+
}
|
|
436
|
+
params[arg.parameterIndex] = argInst;
|
|
437
|
+
} else {
|
|
438
|
+
const argName = arg.name || 'args';
|
|
439
|
+
params[arg.parameterIndex] = processedArgs[argName];
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const parentParams = Reflect.getMetadata('calyx:parent', resolverInstance, fieldRes.propertyKey) || [];
|
|
443
|
+
for (const idx of parentParams) {
|
|
444
|
+
params[idx] = parent;
|
|
445
|
+
}
|
|
446
|
+
return resolverInstance[fieldRes.propertyKey](...params);
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
if (compiledInterceptors.length > 0) {
|
|
450
|
+
let interceptorIndex = 0;
|
|
451
|
+
const nextHandler = async (): Promise<any> => {
|
|
452
|
+
if (interceptorIndex < compiledInterceptors.length) {
|
|
453
|
+
const interceptor = compiledInterceptors[interceptorIndex++];
|
|
454
|
+
return interceptor.instance.intercept(execContext, { handle: () => nextHandler() });
|
|
455
|
+
}
|
|
456
|
+
return executeHandler();
|
|
457
|
+
};
|
|
458
|
+
return await nextHandler();
|
|
459
|
+
} else {
|
|
460
|
+
return await executeHandler();
|
|
461
|
+
}
|
|
462
|
+
} catch (err: any) {
|
|
463
|
+
if (compiledFilters.length > 0) {
|
|
464
|
+
for (const filter of compiledFilters) {
|
|
465
|
+
const catchException = Reflect.getMetadata('calyx:catch', filter.token) || [];
|
|
466
|
+
if (catchException.length === 0 || catchException.some((exc: any) => err instanceof exc)) {
|
|
467
|
+
const filterResult = filter.instance.catch(err, execContext);
|
|
468
|
+
if (filterResult !== undefined) {
|
|
469
|
+
return filterResult;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
throw err;
|
|
138
475
|
}
|
|
139
|
-
return resolverInstance[fieldRes.propertyKey](...params);
|
|
140
476
|
};
|
|
141
477
|
}
|
|
142
478
|
}
|
|
@@ -161,6 +497,14 @@ export class GraphQLModule {
|
|
|
161
497
|
}),
|
|
162
498
|
}
|
|
163
499
|
: {}),
|
|
500
|
+
...(Object.keys(subscriptionFields).length > 0
|
|
501
|
+
? {
|
|
502
|
+
subscription: new GraphQLObjectType({
|
|
503
|
+
name: 'Subscription',
|
|
504
|
+
fields: subscriptionFields,
|
|
505
|
+
}),
|
|
506
|
+
}
|
|
507
|
+
: {}),
|
|
164
508
|
});
|
|
165
509
|
}
|
|
166
510
|
}
|