@martel/calyx 0.1.0 → 1.1.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/.github/workflows/release.yml +6 -2
- package/CHANGELOG.md +13 -0
- package/README.md +130 -0
- package/benchmarks/di-benchmark.ts +15 -15
- package/benchmarks/run-calyx-lifecycle.ts +2 -2
- package/benchmarks/run-calyx.ts +2 -2
- package/bun.lock +1261 -0
- package/docs/controllers.md +2 -2
- package/docs/dependency-injection.md +1 -1
- package/docs/lifecycle.md +2 -2
- package/docs/migration.md +5 -5
- package/package.json +9 -5
- package/src/cli/index.ts +323 -0
- package/src/core/container.ts +252 -27
- package/src/core/decorators.ts +64 -10
- package/src/core/index.ts +1 -0
- package/src/core/metadata.ts +13 -0
- package/src/core/module-ref.ts +3 -1
- package/src/core/reflector.ts +32 -0
- package/src/http/application.ts +323 -154
- package/src/http/decorators.ts +29 -8
- package/src/http/factory.ts +4 -4
- package/src/http/router.ts +12 -0
- package/src/lifecycle/context.ts +2 -2
- package/src/lifecycle/interfaces.ts +20 -0
- package/tests/cli.test.ts +93 -0
- package/tests/di.test.ts +11 -11
- package/tests/dynamic-module.test.ts +2 -2
- package/tests/lifecycle.test.ts +4 -4
- package/tests/phase1.test.ts +143 -0
- package/tests/phase2.test.ts +107 -0
- package/tests/phase3.test.ts +203 -0
- package/tests/phase5.test.ts +73 -0
- package/tests/routing.test.ts +4 -4
package/src/http/application.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { METADATA_KEYS } from '../core/metadata.ts';
|
|
1
|
+
import { CalyxContainer } from '../core/container.ts';
|
|
2
|
+
import { METADATA_KEYS, Scope, REQUEST } from '../core/metadata.ts';
|
|
3
3
|
import { RadixRouter } from './router.ts';
|
|
4
4
|
import { ParameterConfig } from './decorators.ts';
|
|
5
5
|
import { HttpException, NotFoundException } from './exceptions.ts';
|
|
6
6
|
import { ArgumentsHost, ExecutionContext } from '../lifecycle/interfaces.ts';
|
|
7
|
-
import {
|
|
7
|
+
import { CalyxArgumentsHost, CalyxExecutionContext } from '../lifecycle/context.ts';
|
|
8
8
|
|
|
9
|
-
export class
|
|
9
|
+
export class CalyxResponse {
|
|
10
10
|
statusCode = 200;
|
|
11
|
-
headers =
|
|
11
|
+
headers: Record<string, string> = {};
|
|
12
12
|
body: any = null;
|
|
13
13
|
sent = false;
|
|
14
14
|
|
|
@@ -24,33 +24,51 @@ export class calyxResponse {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
json(body: any) {
|
|
27
|
-
this.headers
|
|
27
|
+
this.headers['content-type'] = 'application/json';
|
|
28
28
|
this.body = JSON.stringify(body);
|
|
29
29
|
this.sent = true;
|
|
30
30
|
return this;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
set(name: string, value: string) {
|
|
34
|
-
this.headers.
|
|
34
|
+
this.headers[name.toLowerCase()] = value;
|
|
35
35
|
return this;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
interface CompiledLifecycleItem {
|
|
40
|
+
isReqScoped: boolean;
|
|
41
|
+
token: any;
|
|
42
|
+
instance: any;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface CompiledParameterConfig extends ParameterConfig {
|
|
46
|
+
pipesList: CompiledLifecycleItem[];
|
|
47
|
+
paramType: any;
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
interface HandlerConfig {
|
|
51
|
+
controllerClass: any;
|
|
52
|
+
moduleClass: any;
|
|
40
53
|
instance: any;
|
|
41
54
|
methodName: string;
|
|
42
|
-
paramsConfig:
|
|
55
|
+
paramsConfig: CompiledParameterConfig[];
|
|
43
56
|
httpCode?: number;
|
|
44
57
|
headers: { name: string; value: string }[];
|
|
45
58
|
redirect?: { url: string; statusCode?: number };
|
|
46
59
|
hasBodyParam: boolean;
|
|
47
60
|
hasQueryParam: boolean;
|
|
48
61
|
hasResParam: boolean;
|
|
62
|
+
isPassthrough: boolean;
|
|
63
|
+
isRequestScoped: boolean;
|
|
49
64
|
hasLifecycleMetadata: boolean;
|
|
65
|
+
guardsList: CompiledLifecycleItem[];
|
|
66
|
+
interceptorsList: CompiledLifecycleItem[];
|
|
67
|
+
filtersList: CompiledLifecycleItem[];
|
|
50
68
|
}
|
|
51
69
|
|
|
52
|
-
export class
|
|
53
|
-
private container = new
|
|
70
|
+
export class CalyxApplication {
|
|
71
|
+
private container = new CalyxContainer();
|
|
54
72
|
private router = new RadixRouter<HandlerConfig>();
|
|
55
73
|
private server: any;
|
|
56
74
|
|
|
@@ -83,6 +101,12 @@ export class calyxApplication {
|
|
|
83
101
|
|
|
84
102
|
// Build the routing table from registered controllers
|
|
85
103
|
this.buildRoutes();
|
|
104
|
+
|
|
105
|
+
// Call OnModuleInit hooks
|
|
106
|
+
await this.runOnModuleInit();
|
|
107
|
+
|
|
108
|
+
// Call OnApplicationBootstrap hooks
|
|
109
|
+
await this.runOnApplicationBootstrap();
|
|
86
110
|
}
|
|
87
111
|
|
|
88
112
|
private buildRoutes() {
|
|
@@ -90,10 +114,11 @@ export class calyxApplication {
|
|
|
90
114
|
for (const [moduleClass, record] of modules.entries()) {
|
|
91
115
|
for (const controllerClass of record.controllers) {
|
|
92
116
|
const prefix = Reflect.getMetadata(METADATA_KEYS.CONTROLLER, controllerClass) ?? '';
|
|
93
|
-
const instance = record.instances.get(controllerClass);
|
|
94
|
-
if (!instance) continue;
|
|
95
|
-
|
|
96
117
|
const methods = this.getMethods(controllerClass.prototype);
|
|
118
|
+
const controllerScope = this.container.getControllerScope(controllerClass);
|
|
119
|
+
const isRequestScoped = controllerScope === Scope.REQUEST;
|
|
120
|
+
const instance = isRequestScoped ? null : record.instances.get(controllerClass);
|
|
121
|
+
|
|
97
122
|
for (const method of methods) {
|
|
98
123
|
const routeMeta = Reflect.getMetadata(METADATA_KEYS.HTTP_METHOD, controllerClass.prototype, method);
|
|
99
124
|
if (!routeMeta) continue;
|
|
@@ -114,29 +139,59 @@ export class calyxApplication {
|
|
|
114
139
|
const hasBodyParam = paramsConfig.some((p) => p.type === 'body');
|
|
115
140
|
const hasQueryParam = paramsConfig.some((p) => p.type === 'query');
|
|
116
141
|
const hasResParam = paramsConfig.some((p) => p.type === 'res');
|
|
142
|
+
const resParamConfig = paramsConfig.find((p) => p.type === 'res');
|
|
143
|
+
const isPassthrough = resParamConfig?.name === 'passthrough';
|
|
144
|
+
|
|
145
|
+
// Compile lifecycle lists
|
|
146
|
+
const classGuards = Reflect.getMetadata(METADATA_KEYS.GUARDS, controllerClass) || [];
|
|
147
|
+
const methodGuards = Reflect.getMetadata(METADATA_KEYS.GUARDS, controllerClass.prototype, method) || [];
|
|
148
|
+
const guardsList = this.compileLifecycleItems(moduleClass, [...this.globalGuards, ...classGuards, ...methodGuards]);
|
|
149
|
+
|
|
150
|
+
const classInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, controllerClass) || [];
|
|
151
|
+
const methodInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, controllerClass.prototype, method) || [];
|
|
152
|
+
const interceptorsList = this.compileLifecycleItems(moduleClass, [...this.globalInterceptors, ...classInterceptors, ...methodInterceptors]);
|
|
153
|
+
|
|
154
|
+
const classFilters = Reflect.getMetadata(METADATA_KEYS.FILTERS, controllerClass) || [];
|
|
155
|
+
const methodFilters = Reflect.getMetadata(METADATA_KEYS.FILTERS, controllerClass.prototype, method) || [];
|
|
156
|
+
const filtersList = this.compileLifecycleItems(moduleClass, [...this.globalFilters, ...classFilters, ...methodFilters]);
|
|
157
|
+
|
|
158
|
+
const paramTypes = Reflect.getMetadata('design:paramtypes', controllerClass.prototype, method) || [];
|
|
159
|
+
const compiledParamsConfig: CompiledParameterConfig[] = paramsConfig.map((p) => {
|
|
160
|
+
const classPipes = Reflect.getMetadata(METADATA_KEYS.PIPES, controllerClass) || [];
|
|
161
|
+
const methodPipes = Reflect.getMetadata(METADATA_KEYS.PIPES, controllerClass.prototype, method) || [];
|
|
162
|
+
const paramPipes = p.pipes || [];
|
|
163
|
+
const allPipes = [...this.globalPipes, ...classPipes, ...methodPipes, ...paramPipes];
|
|
164
|
+
return {
|
|
165
|
+
...p,
|
|
166
|
+
pipesList: this.compileLifecycleItems(moduleClass, allPipes),
|
|
167
|
+
paramType: paramTypes[p.index],
|
|
168
|
+
};
|
|
169
|
+
});
|
|
117
170
|
|
|
118
171
|
const hasLifecycleMetadata =
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
Reflect.hasMetadata(METADATA_KEYS.PIPES, controllerClass) ||
|
|
124
|
-
Reflect.hasMetadata(METADATA_KEYS.PIPES, controllerClass.prototype, method) ||
|
|
125
|
-
Reflect.hasMetadata(METADATA_KEYS.FILTERS, controllerClass) ||
|
|
126
|
-
Reflect.hasMetadata(METADATA_KEYS.FILTERS, controllerClass.prototype, method) ||
|
|
127
|
-
paramsConfig.some((p) => p.pipes && p.pipes.length > 0);
|
|
172
|
+
guardsList.length > 0 ||
|
|
173
|
+
interceptorsList.length > 0 ||
|
|
174
|
+
filtersList.length > 0 ||
|
|
175
|
+
compiledParamsConfig.some((p) => p.pipesList.length > 0);
|
|
128
176
|
|
|
129
177
|
this.router.insert(routeMeta.method, fullPath, {
|
|
178
|
+
controllerClass,
|
|
179
|
+
moduleClass,
|
|
130
180
|
instance,
|
|
131
181
|
methodName: method,
|
|
132
|
-
paramsConfig,
|
|
182
|
+
paramsConfig: compiledParamsConfig,
|
|
133
183
|
httpCode,
|
|
134
184
|
headers,
|
|
135
185
|
redirect,
|
|
136
186
|
hasBodyParam,
|
|
137
187
|
hasQueryParam,
|
|
138
188
|
hasResParam,
|
|
189
|
+
isPassthrough,
|
|
190
|
+
isRequestScoped,
|
|
139
191
|
hasLifecycleMetadata,
|
|
192
|
+
guardsList,
|
|
193
|
+
interceptorsList,
|
|
194
|
+
filtersList,
|
|
140
195
|
});
|
|
141
196
|
}
|
|
142
197
|
}
|
|
@@ -188,21 +243,30 @@ export class calyxApplication {
|
|
|
188
243
|
|
|
189
244
|
const { handler, params } = matched;
|
|
190
245
|
|
|
246
|
+
let instance = handler.instance;
|
|
247
|
+
let requestContext: Map<any, any> | undefined = undefined;
|
|
248
|
+
|
|
249
|
+
if (handler.isRequestScoped) {
|
|
250
|
+
requestContext = new Map<any, any>();
|
|
251
|
+
requestContext.set(REQUEST, req);
|
|
252
|
+
instance = this.container.resolveControllerInRequestContext(handler.moduleClass, handler.controllerClass, requestContext);
|
|
253
|
+
}
|
|
254
|
+
|
|
191
255
|
// Check if we need to run lifecycle hooks
|
|
192
256
|
const hasLifecycle = handler.hasLifecycleMetadata || this.hasGlobalLifecycle();
|
|
193
257
|
if (hasLifecycle) {
|
|
194
258
|
if (handler.hasBodyParam && req.body && req.method !== 'GET' && req.method !== 'HEAD') {
|
|
195
|
-
return this.handleRequestPipelineAsync(req, handler, params, search);
|
|
259
|
+
return this.handleRequestPipelineAsync(req, handler, params, search, instance, requestContext);
|
|
196
260
|
}
|
|
197
|
-
return this.handleRequestPipeline(req, handler, params, search, null);
|
|
261
|
+
return this.handleRequestPipeline(req, handler, params, search, null, instance, requestContext);
|
|
198
262
|
}
|
|
199
263
|
|
|
200
264
|
// Check if we need to parse body (which makes it async)
|
|
201
265
|
if (handler.hasBodyParam && req.body && req.method !== 'GET' && req.method !== 'HEAD') {
|
|
202
|
-
return this.handleRequestAsync(req, handler, params, search);
|
|
266
|
+
return this.handleRequestAsync(req, handler, params, search, instance, requestContext);
|
|
203
267
|
}
|
|
204
268
|
|
|
205
|
-
return this.executeHandlerSync(req, handler, params, search, null);
|
|
269
|
+
return this.executeHandlerSync(req, handler, params, search, null, instance, requestContext);
|
|
206
270
|
} catch (err: any) {
|
|
207
271
|
return this.handleError(err);
|
|
208
272
|
}
|
|
@@ -212,7 +276,9 @@ export class calyxApplication {
|
|
|
212
276
|
req: Request,
|
|
213
277
|
handler: HandlerConfig,
|
|
214
278
|
params: Record<string, string>,
|
|
215
|
-
search: string
|
|
279
|
+
search: string,
|
|
280
|
+
instance: any,
|
|
281
|
+
requestContext?: Map<any, any>
|
|
216
282
|
): Promise<Response> {
|
|
217
283
|
try {
|
|
218
284
|
let body: any = null;
|
|
@@ -238,11 +304,11 @@ export class calyxApplication {
|
|
|
238
304
|
}
|
|
239
305
|
}
|
|
240
306
|
|
|
241
|
-
return this.handleRequestPipeline(req, handler, params, search, body);
|
|
307
|
+
return this.handleRequestPipeline(req, handler, params, search, body, instance, requestContext);
|
|
242
308
|
} catch (err: any) {
|
|
243
|
-
const resWrapper = new
|
|
244
|
-
const host = new
|
|
245
|
-
return this.handleLifecycleError(err, host, handler.
|
|
309
|
+
const resWrapper = new CalyxResponse();
|
|
310
|
+
const host = new CalyxArgumentsHost(req, resWrapper);
|
|
311
|
+
return this.handleLifecycleError(err, host, handler.controllerClass, handler.methodName, requestContext);
|
|
246
312
|
}
|
|
247
313
|
}
|
|
248
314
|
|
|
@@ -251,34 +317,32 @@ export class calyxApplication {
|
|
|
251
317
|
handler: HandlerConfig,
|
|
252
318
|
params: Record<string, string>,
|
|
253
319
|
search: string,
|
|
254
|
-
body: any
|
|
320
|
+
body: any,
|
|
321
|
+
instance: any,
|
|
322
|
+
requestContext?: Map<any, any>
|
|
255
323
|
): Promise<Response> {
|
|
256
|
-
const {
|
|
257
|
-
const controllerClass =
|
|
258
|
-
const moduleClass =
|
|
259
|
-
const resWrapper = new
|
|
260
|
-
const host = new
|
|
261
|
-
const context = new
|
|
324
|
+
const { methodName, paramsConfig, httpCode, headers, redirect, hasQueryParam } = handler;
|
|
325
|
+
const controllerClass = handler.controllerClass;
|
|
326
|
+
const moduleClass = handler.moduleClass;
|
|
327
|
+
const resWrapper = new CalyxResponse();
|
|
328
|
+
const host = new CalyxArgumentsHost(req, resWrapper);
|
|
329
|
+
const context = new CalyxExecutionContext(req, resWrapper, controllerClass, instance[methodName]);
|
|
262
330
|
|
|
263
331
|
try {
|
|
264
332
|
// 1. Run Guards
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
333
|
+
for (const guard of handler.guardsList) {
|
|
334
|
+
const guardInstance = guard.isReqScoped
|
|
335
|
+
? this.container.resolveTokenInModuleContext(moduleClass, guard.token, requestContext)
|
|
336
|
+
: guard.instance;
|
|
337
|
+
const canActivate = guardInstance.canActivate(context);
|
|
338
|
+
if (canActivate instanceof Promise) {
|
|
339
|
+
const resolved = await canActivate;
|
|
340
|
+
if (!resolved) throw new HttpException('Forbidden resource', 403);
|
|
341
|
+
} else if (!canActivate) {
|
|
273
342
|
throw new HttpException('Forbidden resource', 403);
|
|
274
343
|
}
|
|
275
344
|
}
|
|
276
345
|
|
|
277
|
-
// 2. Run Interceptors & Pipes & Handler
|
|
278
|
-
const classInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, controllerClass) || [];
|
|
279
|
-
const methodInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, controllerClass.prototype, methodName) || [];
|
|
280
|
-
const interceptors = [...this.globalInterceptors, ...classInterceptors, ...methodInterceptors];
|
|
281
|
-
|
|
282
346
|
let query: any = null;
|
|
283
347
|
if (hasQueryParam && search) {
|
|
284
348
|
query = Object.fromEntries(new URLSearchParams(search).entries());
|
|
@@ -286,83 +350,83 @@ export class calyxApplication {
|
|
|
286
350
|
query = {};
|
|
287
351
|
}
|
|
288
352
|
|
|
289
|
-
const paramTypes = Reflect.getMetadata('design:paramtypes', controllerClass.prototype, methodName) || [];
|
|
290
|
-
|
|
291
353
|
// Execute pipes for each argument
|
|
292
354
|
const args: any[] = [];
|
|
293
355
|
for (const config of paramsConfig) {
|
|
294
356
|
let val: any;
|
|
295
357
|
switch (config.type) {
|
|
296
|
-
case 'req':
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
case '
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
case '
|
|
303
|
-
val = config.name ? params[config.name] : params;
|
|
304
|
-
break;
|
|
305
|
-
case 'query':
|
|
306
|
-
val = config.name ? query?.[config.name] : query;
|
|
307
|
-
break;
|
|
308
|
-
case 'body':
|
|
309
|
-
val = config.name ? body?.[config.name] : body;
|
|
310
|
-
break;
|
|
311
|
-
case 'headers':
|
|
312
|
-
val = config.name ? req.headers.get(config.name) : Object.fromEntries(req.headers.entries());
|
|
313
|
-
break;
|
|
358
|
+
case 'req': val = req; break;
|
|
359
|
+
case 'res': val = resWrapper; break;
|
|
360
|
+
case 'param': val = config.name ? params[config.name] : params; break;
|
|
361
|
+
case 'query': val = config.name ? query?.[config.name] : query; break;
|
|
362
|
+
case 'body': val = config.name ? body?.[config.name] : body; break;
|
|
363
|
+
case 'headers': val = config.name ? req.headers.get(config.name) : Object.fromEntries(req.headers.entries()); break;
|
|
364
|
+
case 'custom': val = config.factory ? config.factory(config.name, context) : undefined; break;
|
|
314
365
|
}
|
|
315
366
|
|
|
316
367
|
// Apply pipes
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
for (const pipe of pipes) {
|
|
323
|
-
const pipeInstance = this.resolveLifecycleInstance(moduleClass, pipe);
|
|
324
|
-
val = await pipeInstance.transform(val, {
|
|
368
|
+
for (const pipe of config.pipesList) {
|
|
369
|
+
const pipeInstance = pipe.isReqScoped
|
|
370
|
+
? this.container.resolveTokenInModuleContext(moduleClass, pipe.token, requestContext)
|
|
371
|
+
: pipe.instance;
|
|
372
|
+
const transformed = pipeInstance.transform(val, {
|
|
325
373
|
type: config.type,
|
|
326
|
-
metatype:
|
|
374
|
+
metatype: config.paramType,
|
|
327
375
|
data: config.name,
|
|
328
376
|
});
|
|
377
|
+
if (transformed instanceof Promise) {
|
|
378
|
+
val = await transformed;
|
|
379
|
+
} else {
|
|
380
|
+
val = transformed;
|
|
381
|
+
}
|
|
329
382
|
}
|
|
330
383
|
|
|
331
384
|
args[config.index] = val;
|
|
332
385
|
}
|
|
333
386
|
|
|
334
|
-
let
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
387
|
+
let result: any;
|
|
388
|
+
if (handler.interceptorsList.length > 0) {
|
|
389
|
+
let interceptorIndex = 0;
|
|
390
|
+
const executeHandler = async (): Promise<any> => {
|
|
391
|
+
if (interceptorIndex < handler.interceptorsList.length) {
|
|
392
|
+
const interceptor = handler.interceptorsList[interceptorIndex++];
|
|
393
|
+
const interceptorInstance = interceptor.isReqScoped
|
|
394
|
+
? this.container.resolveTokenInModuleContext(moduleClass, interceptor.token, requestContext)
|
|
395
|
+
: interceptor.instance;
|
|
396
|
+
return interceptorInstance.intercept(context, { handle: () => executeHandler() });
|
|
397
|
+
}
|
|
398
|
+
return instance[methodName](...args);
|
|
399
|
+
};
|
|
400
|
+
result = await executeHandler();
|
|
401
|
+
} else {
|
|
402
|
+
result = instance[methodName](...args);
|
|
403
|
+
if (result instanceof Promise) {
|
|
404
|
+
result = await result;
|
|
340
405
|
}
|
|
341
|
-
|
|
342
|
-
};
|
|
406
|
+
}
|
|
343
407
|
|
|
344
|
-
|
|
408
|
+
if (this.isObservable(result)) {
|
|
409
|
+
result = await this.resolveObservable(result);
|
|
410
|
+
}
|
|
345
411
|
|
|
346
|
-
return this.processResult(req, result, resWrapper, httpCode, headers, redirect);
|
|
412
|
+
return this.processResult(req, result, resWrapper, httpCode, headers, redirect, handler.isPassthrough, handler.hasResParam);
|
|
347
413
|
} catch (err: any) {
|
|
348
|
-
return this.handleLifecycleError(err, host,
|
|
414
|
+
return this.handleLifecycleError(err, host, handler, requestContext);
|
|
349
415
|
}
|
|
350
416
|
}
|
|
351
417
|
|
|
352
418
|
private async handleLifecycleError(
|
|
353
419
|
err: any,
|
|
354
420
|
host: ArgumentsHost,
|
|
355
|
-
|
|
356
|
-
|
|
421
|
+
handler: HandlerConfig,
|
|
422
|
+
requestContext?: Map<any, any>
|
|
357
423
|
): Promise<Response> {
|
|
358
|
-
const
|
|
359
|
-
const methodFilters = Reflect.getMetadata(METADATA_KEYS.FILTERS, controllerClass.prototype, methodName) || [];
|
|
360
|
-
const filters = [...this.globalFilters, ...classFilters, ...methodFilters];
|
|
361
|
-
|
|
362
|
-
const moduleClass = this.getControllerModule(controllerClass);
|
|
424
|
+
const moduleClass = handler.moduleClass;
|
|
363
425
|
|
|
364
|
-
for (const filter of
|
|
365
|
-
const filterInstance =
|
|
426
|
+
for (const filter of handler.filtersList) {
|
|
427
|
+
const filterInstance = filter.isReqScoped
|
|
428
|
+
? this.container.resolveTokenInModuleContext(moduleClass, filter.token, requestContext)
|
|
429
|
+
: filter.instance;
|
|
366
430
|
const catchExceptions = Reflect.getMetadata(METADATA_KEYS.CATCH, filterInstance.constructor) || [];
|
|
367
431
|
|
|
368
432
|
const catchesException =
|
|
@@ -370,8 +434,11 @@ export class calyxApplication {
|
|
|
370
434
|
catchExceptions.some((excClass: any) => err instanceof excClass);
|
|
371
435
|
|
|
372
436
|
if (catchesException) {
|
|
373
|
-
|
|
374
|
-
|
|
437
|
+
const catchResult = filterInstance.catch(err, host);
|
|
438
|
+
if (catchResult instanceof Promise) {
|
|
439
|
+
await catchResult;
|
|
440
|
+
}
|
|
441
|
+
const resWrapper = host.switchToHttp().getResponse<CalyxResponse>();
|
|
375
442
|
if (resWrapper.sent) {
|
|
376
443
|
return new Response(resWrapper.body, {
|
|
377
444
|
status: resWrapper.statusCode,
|
|
@@ -384,32 +451,33 @@ export class calyxApplication {
|
|
|
384
451
|
return this.handleError(err);
|
|
385
452
|
}
|
|
386
453
|
|
|
387
|
-
private
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
return new item();
|
|
454
|
+
private compileLifecycleItems(moduleClass: any, items: any[]): CompiledLifecycleItem[] {
|
|
455
|
+
return items.map((item) => {
|
|
456
|
+
if (typeof item === 'function') {
|
|
457
|
+
const scope = this.container.getProviderScope(item);
|
|
458
|
+
if (scope === Scope.REQUEST) {
|
|
459
|
+
return { isReqScoped: true, token: item, instance: null };
|
|
460
|
+
} else {
|
|
461
|
+
let instance: any;
|
|
462
|
+
try {
|
|
463
|
+
instance = this.container.resolveTokenInModuleContext(moduleClass, item);
|
|
464
|
+
} catch {
|
|
465
|
+
instance = new item();
|
|
466
|
+
}
|
|
467
|
+
return { isReqScoped: false, token: item, instance };
|
|
468
|
+
}
|
|
403
469
|
}
|
|
404
|
-
|
|
405
|
-
|
|
470
|
+
return { isReqScoped: false, token: item.constructor, instance: item };
|
|
471
|
+
});
|
|
406
472
|
}
|
|
407
473
|
|
|
408
474
|
private async handleRequestAsync(
|
|
409
475
|
req: Request,
|
|
410
476
|
handler: HandlerConfig,
|
|
411
477
|
params: Record<string, string>,
|
|
412
|
-
search: string
|
|
478
|
+
search: string,
|
|
479
|
+
instance: any,
|
|
480
|
+
requestContext?: Map<any, any>
|
|
413
481
|
): Promise<Response> {
|
|
414
482
|
try {
|
|
415
483
|
let body: any = null;
|
|
@@ -435,7 +503,7 @@ export class calyxApplication {
|
|
|
435
503
|
}
|
|
436
504
|
}
|
|
437
505
|
|
|
438
|
-
return this.executeHandlerSync(req, handler, params, search, body);
|
|
506
|
+
return this.executeHandlerSync(req, handler, params, search, body, instance, requestContext);
|
|
439
507
|
} catch (err: any) {
|
|
440
508
|
return this.handleError(err);
|
|
441
509
|
}
|
|
@@ -446,9 +514,11 @@ export class calyxApplication {
|
|
|
446
514
|
handler: HandlerConfig,
|
|
447
515
|
params: Record<string, string>,
|
|
448
516
|
search: string,
|
|
449
|
-
body: any
|
|
517
|
+
body: any,
|
|
518
|
+
instance: any,
|
|
519
|
+
requestContext?: Map<any, any>
|
|
450
520
|
): Response | Promise<Response> {
|
|
451
|
-
const {
|
|
521
|
+
const { methodName, paramsConfig, httpCode, headers, redirect, hasQueryParam, hasResParam } = handler;
|
|
452
522
|
|
|
453
523
|
// Parse query on-demand
|
|
454
524
|
let query: any = null;
|
|
@@ -458,9 +528,12 @@ export class calyxApplication {
|
|
|
458
528
|
query = {};
|
|
459
529
|
}
|
|
460
530
|
|
|
461
|
-
const resWrapper = hasResParam ? new
|
|
531
|
+
const resWrapper = hasResParam ? new CalyxResponse() : null;
|
|
462
532
|
const args: any[] = [];
|
|
463
533
|
|
|
534
|
+
const hasCustomParam = paramsConfig.some((p) => p.type === 'custom');
|
|
535
|
+
const context = hasCustomParam ? new CalyxExecutionContext(req, resWrapper, instance.constructor, instance[methodName]) : null;
|
|
536
|
+
|
|
464
537
|
for (const config of paramsConfig) {
|
|
465
538
|
let val: any;
|
|
466
539
|
switch (config.type) {
|
|
@@ -482,47 +555,66 @@ export class calyxApplication {
|
|
|
482
555
|
case 'headers':
|
|
483
556
|
val = config.name ? req.headers.get(config.name) : Object.fromEntries(req.headers.entries());
|
|
484
557
|
break;
|
|
558
|
+
case 'custom':
|
|
559
|
+
val = config.factory ? config.factory(config.name, context!) : undefined;
|
|
560
|
+
break;
|
|
485
561
|
}
|
|
486
562
|
args[config.index] = val;
|
|
487
563
|
}
|
|
488
564
|
|
|
489
|
-
|
|
565
|
+
let result = instance[methodName](...args);
|
|
566
|
+
|
|
567
|
+
if (this.isObservable(result)) {
|
|
568
|
+
result = this.resolveObservable(result);
|
|
569
|
+
}
|
|
490
570
|
|
|
491
571
|
if (result instanceof Promise) {
|
|
492
|
-
return result.then((resolvedResult) => this.processResult(req, resolvedResult, resWrapper, httpCode, headers, redirect));
|
|
572
|
+
return result.then((resolvedResult) => this.processResult(req, resolvedResult, resWrapper, httpCode, headers, redirect, handler.isPassthrough, handler.hasResParam));
|
|
493
573
|
}
|
|
494
574
|
|
|
495
|
-
return this.processResult(req, result, resWrapper, httpCode, headers, redirect);
|
|
575
|
+
return this.processResult(req, result, resWrapper, httpCode, headers, redirect, handler.isPassthrough, handler.hasResParam);
|
|
496
576
|
}
|
|
497
577
|
|
|
498
578
|
private processResult(
|
|
499
579
|
req: Request,
|
|
500
580
|
result: any,
|
|
501
|
-
resWrapper:
|
|
581
|
+
resWrapper: CalyxResponse | null,
|
|
502
582
|
httpCode: number | undefined,
|
|
503
583
|
headers: { name: string; value: string }[],
|
|
504
|
-
redirect: { url: string; statusCode?: number } | undefined
|
|
584
|
+
redirect: { url: string; statusCode?: number } | undefined,
|
|
585
|
+
isPassthrough = false,
|
|
586
|
+
hasResParam = false
|
|
505
587
|
): Response {
|
|
506
588
|
if (redirect) {
|
|
507
589
|
return Response.redirect(redirect.url, redirect.statusCode || 302);
|
|
508
590
|
}
|
|
509
591
|
|
|
510
|
-
if (resWrapper
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
592
|
+
if (resWrapper) {
|
|
593
|
+
if (resWrapper.sent && !isPassthrough) {
|
|
594
|
+
return new Response(resWrapper.body, {
|
|
595
|
+
status: resWrapper.statusCode,
|
|
596
|
+
headers: resWrapper.headers,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
if (hasResParam && !isPassthrough) {
|
|
600
|
+
return new Response(resWrapper.body, {
|
|
601
|
+
status: resWrapper.statusCode,
|
|
602
|
+
headers: resWrapper.headers,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
515
605
|
}
|
|
516
606
|
|
|
517
|
-
const status =
|
|
607
|
+
const status = resWrapper && isPassthrough
|
|
608
|
+
? resWrapper.statusCode
|
|
609
|
+
: (httpCode ?? (req.method === 'POST' ? 201 : 200));
|
|
518
610
|
|
|
519
|
-
// Build headers
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
611
|
+
// Build headers as a plain object Record<string, string>
|
|
612
|
+
const responseHeaders: Record<string, string> = resWrapper && isPassthrough
|
|
613
|
+
? { ...resWrapper.headers }
|
|
614
|
+
: {};
|
|
615
|
+
|
|
616
|
+
for (const header of headers) {
|
|
617
|
+
responseHeaders[header.name.toLowerCase()] = header.value;
|
|
526
618
|
}
|
|
527
619
|
|
|
528
620
|
if (result === undefined || result === null) {
|
|
@@ -534,17 +626,26 @@ export class calyxApplication {
|
|
|
534
626
|
}
|
|
535
627
|
|
|
536
628
|
if (typeof result === 'object') {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
return new Response(JSON.stringify(result), { status, headers: responseHeaders });
|
|
540
|
-
}
|
|
541
|
-
return Response.json(result, { status });
|
|
629
|
+
responseHeaders['content-type'] = 'application/json';
|
|
630
|
+
return new Response(JSON.stringify(result), { status, headers: responseHeaders });
|
|
542
631
|
}
|
|
543
632
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
633
|
+
return new Response(String(result), { status, headers: responseHeaders });
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private isObservable(obj: any): boolean {
|
|
637
|
+
return obj && typeof obj === 'object' && typeof obj.subscribe === 'function';
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
private resolveObservable(obs: any): Promise<any> {
|
|
641
|
+
return new Promise((resolve, reject) => {
|
|
642
|
+
let lastVal: any = undefined;
|
|
643
|
+
obs.subscribe({
|
|
644
|
+
next: (val: any) => { lastVal = val; },
|
|
645
|
+
error: (err: any) => reject(err),
|
|
646
|
+
complete: () => resolve(lastVal),
|
|
647
|
+
});
|
|
648
|
+
});
|
|
548
649
|
}
|
|
549
650
|
|
|
550
651
|
private handleError(err: any): Response {
|
|
@@ -580,9 +681,77 @@ export class calyxApplication {
|
|
|
580
681
|
return this.server;
|
|
581
682
|
}
|
|
582
683
|
|
|
583
|
-
|
|
684
|
+
private cleanupListeners: (() => void)[] = [];
|
|
685
|
+
|
|
686
|
+
enableShutdownHooks(signals: string[] = ['SIGTERM', 'SIGINT']) {
|
|
687
|
+
const handler = async (signal: string) => {
|
|
688
|
+
await this.close(signal);
|
|
689
|
+
process.exit(0);
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
for (const signal of signals) {
|
|
693
|
+
const listener = () => handler(signal);
|
|
694
|
+
process.on(signal as any, listener);
|
|
695
|
+
this.cleanupListeners.push(() => {
|
|
696
|
+
process.off(signal as any, listener);
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async close(signal?: string) {
|
|
702
|
+
// Run shutdown hooks
|
|
703
|
+
await this.runShutdownHooks(signal);
|
|
704
|
+
|
|
705
|
+
for (const cleanup of this.cleanupListeners) {
|
|
706
|
+
cleanup();
|
|
707
|
+
}
|
|
708
|
+
this.cleanupListeners = [];
|
|
709
|
+
|
|
584
710
|
if (this.server) {
|
|
585
711
|
this.server.stop();
|
|
586
712
|
}
|
|
587
713
|
}
|
|
714
|
+
|
|
715
|
+
private async runOnModuleInit() {
|
|
716
|
+
const instances = this.container.getProviderAndControllerInstances();
|
|
717
|
+
for (const instance of instances) {
|
|
718
|
+
if (instance && typeof instance.onModuleInit === 'function') {
|
|
719
|
+
await instance.onModuleInit();
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
private async runOnApplicationBootstrap() {
|
|
725
|
+
const instances = this.container.getProviderAndControllerInstances();
|
|
726
|
+
for (const instance of instances) {
|
|
727
|
+
if (instance && typeof instance.onApplicationBootstrap === 'function') {
|
|
728
|
+
await instance.onApplicationBootstrap();
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
private async runShutdownHooks(signal?: string) {
|
|
734
|
+
const instances = this.container.getProviderAndControllerInstances();
|
|
735
|
+
|
|
736
|
+
// 1. Run OnModuleDestroy
|
|
737
|
+
for (const instance of instances) {
|
|
738
|
+
if (instance && typeof instance.onModuleDestroy === 'function') {
|
|
739
|
+
await instance.onModuleDestroy();
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// 2. Run BeforeApplicationShutdown
|
|
744
|
+
for (const instance of instances) {
|
|
745
|
+
if (instance && typeof instance.beforeApplicationShutdown === 'function') {
|
|
746
|
+
await instance.beforeApplicationShutdown(signal);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// 3. Run OnApplicationShutdown
|
|
751
|
+
for (const instance of instances) {
|
|
752
|
+
if (instance && typeof instance.onApplicationShutdown === 'function') {
|
|
753
|
+
await instance.onApplicationShutdown(signal);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
588
757
|
}
|