@martel/calyx 1.12.0 → 1.13.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/CHANGELOG.md +15 -0
- package/README.md +5 -3
- package/benchmarks/graphql-benchmark.ts +4 -1
- package/benchmarks/serialization-benchmark.ts +46 -6
- package/benchmarks/techniques-benchmark.ts +12 -0
- package/benchmarks/validation-benchmark.ts +8 -1
- package/docs/controllers.md +3 -3
- package/docs/dependency-injection.md +3 -3
- package/docs/lifecycle.md +3 -3
- package/package.json +1 -1
- package/src/cli/index.ts +7 -1
- package/src/config/config.module.ts +16 -2
- package/src/config/config.service.ts +20 -6
- package/src/cookies/cookies.ts +45 -8
- package/src/core/container.ts +340 -154
- package/src/core/testing-module.ts +4 -0
- package/src/cqrs/cqrs.ts +93 -4
- package/src/database/sequelize.module.ts +239 -0
- package/src/event-emitter/decorators.ts +2 -2
- package/src/event-emitter/event-emitter.ts +3 -0
- package/src/graphql/graphql.module.ts +2 -4
- package/src/http/application.ts +140 -10
- package/src/http/decorators.ts +21 -1
- package/src/http/exceptions.ts +97 -0
- package/src/http/factory.ts +3 -0
- package/src/http/router.ts +27 -4
- package/src/index.ts +1 -0
- package/src/microservices/exceptions.ts +10 -0
- package/src/microservices/index.ts +1 -0
- package/src/queue/queue.module.ts +73 -5
- package/src/terminus/terminus.ts +75 -2
- package/src/validation/compiler.ts +137 -17
- package/src/validation/decorators.ts +164 -2
- package/src/websockets/exceptions.ts +10 -0
- package/src/websockets/index.ts +1 -0
- package/tests/circular-di.test.ts +151 -0
- package/tests/di.test.ts +10 -2
- package/tests/nestjs-parity.test.ts +255 -0
package/src/http/application.ts
CHANGED
|
@@ -18,6 +18,8 @@ import { parseCookies, formatCookie } from '../cookies/cookies.ts';
|
|
|
18
18
|
import { StreamableFile } from '../streaming/streamable-file.ts';
|
|
19
19
|
import { defaultRenderEngine, ViewEngine } from '../mvc/mvc.ts';
|
|
20
20
|
import { join } from 'path';
|
|
21
|
+
import { CalyxMicroservice } from '../microservices/microservice.ts';
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
class ObjectPool<T> {
|
|
23
25
|
private pool: T[] = [];
|
|
@@ -156,14 +158,57 @@ export class CalyxApplication {
|
|
|
156
158
|
private serverPort = 3000;
|
|
157
159
|
private graphqlSchema: any = null;
|
|
158
160
|
private graphqlQueryCache = new Map<string, any>();
|
|
161
|
+
private graphqlLib: any = null;
|
|
159
162
|
private isInitialized = false;
|
|
160
163
|
private versioningOptions?: VersioningOptions;
|
|
164
|
+
private globalPrefix?: string;
|
|
165
|
+
private globalPrefixOptions?: { exclude?: (string | { path: string; method: string })[] };
|
|
166
|
+
private connectedMicroservices: any[] = [];
|
|
167
|
+
private websocketAdapter: any = null;
|
|
161
168
|
|
|
162
169
|
enableVersioning(options: VersioningOptions) {
|
|
163
170
|
this.versioningOptions = options;
|
|
164
171
|
return this;
|
|
165
172
|
}
|
|
166
173
|
|
|
174
|
+
setGlobalPrefix(prefix: string, options?: { exclude?: (string | { path: string; method: string })[] }) {
|
|
175
|
+
this.globalPrefix = prefix;
|
|
176
|
+
this.globalPrefixOptions = options;
|
|
177
|
+
return this;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getHttpServer() {
|
|
181
|
+
return this.server;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
getHttpAdapter() {
|
|
185
|
+
return {
|
|
186
|
+
getInstance: () => this.server,
|
|
187
|
+
getHttpServer: () => this.server,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async getUrl(): Promise<string> {
|
|
192
|
+
return `http://localhost:${this.serverPort}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
useWebSocketAdapter(adapter: any) {
|
|
196
|
+
this.websocketAdapter = adapter;
|
|
197
|
+
return this;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
connectMicroservice(options: any) {
|
|
201
|
+
const microservice = new CalyxMicroservice(this.rootModule, options);
|
|
202
|
+
this.connectedMicroservices.push(microservice);
|
|
203
|
+
return microservice;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async startAllMicroservices(): Promise<void> {
|
|
207
|
+
for (const ms of this.connectedMicroservices) {
|
|
208
|
+
await ms.listen();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
167
212
|
private compressionEnabled = false;
|
|
168
213
|
|
|
169
214
|
enableCompression() {
|
|
@@ -268,6 +313,7 @@ export class CalyxApplication {
|
|
|
268
313
|
const isControllerRequestScoped = controllerScope === Scope.REQUEST;
|
|
269
314
|
const singletonInstance = isControllerRequestScoped ? null : record.instances.get(controllerClass);
|
|
270
315
|
|
|
316
|
+
const controllerHost = Reflect.getMetadata('calyx:controller_host', controllerClass);
|
|
271
317
|
for (const method of methods) {
|
|
272
318
|
const routeMeta = Reflect.getMetadata(METADATA_KEYS.HTTP_METHOD, controllerClass.prototype, method);
|
|
273
319
|
if (!routeMeta) continue;
|
|
@@ -275,7 +321,13 @@ export class CalyxApplication {
|
|
|
275
321
|
// Normalize prefix and path
|
|
276
322
|
const normalizedPrefix = prefix.replace(/^\/|\/$/g, '');
|
|
277
323
|
const normalizedPath = routeMeta.path.replace(/^\/|\/$/g, '');
|
|
278
|
-
const
|
|
324
|
+
const baseRoutePath = '/' + [normalizedPrefix, normalizedPath].filter(Boolean).join('/');
|
|
325
|
+
|
|
326
|
+
let fullPath = baseRoutePath;
|
|
327
|
+
if (this.globalPrefix && !this.isRouteExcludedFromGlobalPrefix(baseRoutePath, routeMeta.method)) {
|
|
328
|
+
const cleanGlobal = this.globalPrefix.replace(/^\/|\/$/g, '');
|
|
329
|
+
fullPath = '/' + [cleanGlobal, normalizedPrefix, normalizedPath].filter(Boolean).join('/');
|
|
330
|
+
}
|
|
279
331
|
|
|
280
332
|
const paramsConfig: ParameterConfig[] = Reflect.getMetadata(METADATA_KEYS.HTTP_PARAMS, controllerClass.prototype, method) || [];
|
|
281
333
|
// Sort parameters by index ascending so they map correctly to function arguments
|
|
@@ -361,7 +413,14 @@ export class CalyxApplication {
|
|
|
361
413
|
Reflect.hasMetadata('calyx:sse', methodFn);
|
|
362
414
|
|
|
363
415
|
const insertRoute = (methodStr: string, pathStr: string) => {
|
|
364
|
-
|
|
416
|
+
let finalMethod = methodStr;
|
|
417
|
+
if (controllerHost) {
|
|
418
|
+
const hasParams = controllerHost.includes(':');
|
|
419
|
+
finalMethod = hasParams
|
|
420
|
+
? `${methodStr}:host-dynamic:${controllerHost}`
|
|
421
|
+
: `${methodStr}:host:${controllerHost}`;
|
|
422
|
+
}
|
|
423
|
+
this.router.insert(finalMethod, pathStr, {
|
|
365
424
|
controllerClass,
|
|
366
425
|
moduleClass,
|
|
367
426
|
instance: singletonInstance,
|
|
@@ -388,7 +447,11 @@ export class CalyxApplication {
|
|
|
388
447
|
if (this.versioningOptions && this.versioningOptions.type === VersioningType.URI && versions.length > 0) {
|
|
389
448
|
for (const version of versions) {
|
|
390
449
|
const versionPrefix = `/v${version}`;
|
|
391
|
-
|
|
450
|
+
let versionedPath = '/' + [versionPrefix.replace(/^\/|\/$/g, ''), normalizedPrefix, normalizedPath].filter(Boolean).join('/');
|
|
451
|
+
if (this.globalPrefix && !this.isRouteExcludedFromGlobalPrefix(versionedPath, routeMeta.method)) {
|
|
452
|
+
const cleanGlobal = this.globalPrefix.replace(/^\/|\/$/g, '');
|
|
453
|
+
versionedPath = '/' + [cleanGlobal, versionPrefix.replace(/^\/|\/$/g, ''), normalizedPrefix, normalizedPath].filter(Boolean).join('/');
|
|
454
|
+
}
|
|
392
455
|
insertRoute(routeMeta.method, versionedPath);
|
|
393
456
|
}
|
|
394
457
|
} else if (this.versioningOptions && (this.versioningOptions.type === VersioningType.HEADER || this.versioningOptions.type === VersioningType.MEDIA_TYPE) && versions.length > 0) {
|
|
@@ -516,8 +579,8 @@ export class CalyxApplication {
|
|
|
516
579
|
try {
|
|
517
580
|
const urlStr = req.url;
|
|
518
581
|
// Parse cookies
|
|
519
|
-
const cookieHeader = req.headers.get('cookie')
|
|
520
|
-
(req as any).cookies = parseCookies(cookieHeader);
|
|
582
|
+
const cookieHeader = req.headers.get('cookie');
|
|
583
|
+
(req as any).cookies = cookieHeader ? parseCookies(cookieHeader) : {};
|
|
521
584
|
|
|
522
585
|
// Fast path parsing
|
|
523
586
|
let pathname = '/';
|
|
@@ -540,12 +603,27 @@ export class CalyxApplication {
|
|
|
540
603
|
}
|
|
541
604
|
|
|
542
605
|
let matched = null;
|
|
606
|
+
const hostHeader = req.headers.get('host') || '';
|
|
607
|
+
|
|
543
608
|
if (this.versioningOptions && this.versioningOptions.type !== VersioningType.URI) {
|
|
544
609
|
const version = VersionExtractor.extract(req, this.versioningOptions.type, this.versioningOptions);
|
|
545
610
|
if (version) {
|
|
546
|
-
|
|
611
|
+
const vMethod = `${req.method}:v${version}`.toUpperCase();
|
|
612
|
+
matched = this.router.match(`${vMethod}:host:${hostHeader}`.toUpperCase(), pathname);
|
|
613
|
+
if (!matched) {
|
|
614
|
+
matched = this.matchDynamicHost(vMethod, hostHeader, pathname);
|
|
615
|
+
}
|
|
616
|
+
if (!matched) {
|
|
617
|
+
matched = this.router.match(vMethod, pathname);
|
|
618
|
+
}
|
|
547
619
|
}
|
|
548
620
|
}
|
|
621
|
+
if (!matched) {
|
|
622
|
+
matched = this.router.match(`${req.method}:host:${hostHeader}`.toUpperCase(), pathname);
|
|
623
|
+
}
|
|
624
|
+
if (!matched) {
|
|
625
|
+
matched = this.matchDynamicHost(req.method.toUpperCase(), hostHeader, pathname);
|
|
626
|
+
}
|
|
549
627
|
if (!matched) {
|
|
550
628
|
matched = this.router.match(req.method, pathname);
|
|
551
629
|
}
|
|
@@ -723,7 +801,7 @@ export class CalyxApplication {
|
|
|
723
801
|
case 'headers': val = config.name ? req.headers.get(config.name) : Object.fromEntries(req.headers.entries()); break;
|
|
724
802
|
case 'session': val = (req as any).session; break;
|
|
725
803
|
case 'ip': val = this.server ? (this.server.requestIP(req)?.address ?? req.headers.get('x-forwarded-for') ?? '') : (req.headers.get('x-forwarded-for') ?? ''); break;
|
|
726
|
-
case 'hostparam': val = config.name ?
|
|
804
|
+
case 'hostparam': val = config.name ? params[config.name] : params; break;
|
|
727
805
|
case 'next': val = next; break;
|
|
728
806
|
case 'custom': val = config.factory ? config.factory(config.name, context) : undefined; break;
|
|
729
807
|
}
|
|
@@ -986,7 +1064,7 @@ export class CalyxApplication {
|
|
|
986
1064
|
val = this.server ? (this.server.requestIP(req)?.address ?? req.headers.get('x-forwarded-for') ?? '') : (req.headers.get('x-forwarded-for') ?? '');
|
|
987
1065
|
break;
|
|
988
1066
|
case 'hostparam':
|
|
989
|
-
val = config.name ?
|
|
1067
|
+
val = config.name ? params[config.name] : params;
|
|
990
1068
|
break;
|
|
991
1069
|
case 'next':
|
|
992
1070
|
val = () => {};
|
|
@@ -1268,7 +1346,7 @@ export class CalyxApplication {
|
|
|
1268
1346
|
const body = await req.json() as any;
|
|
1269
1347
|
const { query, variables } = body;
|
|
1270
1348
|
|
|
1271
|
-
const { parse, validate, execute } = await import('graphql');
|
|
1349
|
+
const { parse, validate, execute } = this.graphqlLib || (this.graphqlLib = await import('graphql'));
|
|
1272
1350
|
|
|
1273
1351
|
let document = this.graphqlQueryCache.get(query);
|
|
1274
1352
|
if (!document) {
|
|
@@ -1403,7 +1481,7 @@ export class CalyxApplication {
|
|
|
1403
1481
|
const { id, payload } = data;
|
|
1404
1482
|
const { query, variables } = payload;
|
|
1405
1483
|
|
|
1406
|
-
const { subscribe, parse, validate } = await import('graphql');
|
|
1484
|
+
const { subscribe, parse, validate } = this.graphqlLib || (this.graphqlLib = await import('graphql'));
|
|
1407
1485
|
|
|
1408
1486
|
let document = this.graphqlQueryCache.get(query);
|
|
1409
1487
|
if (!document) {
|
|
@@ -1904,6 +1982,58 @@ export class CalyxApplication {
|
|
|
1904
1982
|
}
|
|
1905
1983
|
}
|
|
1906
1984
|
|
|
1985
|
+
private isRouteExcludedFromGlobalPrefix(path: string, method: string): boolean {
|
|
1986
|
+
if (!this.globalPrefixOptions?.exclude) return false;
|
|
1987
|
+
const cleanPath = path.replace(/^\/|\/$/g, '');
|
|
1988
|
+
for (const exclusion of this.globalPrefixOptions.exclude) {
|
|
1989
|
+
if (typeof exclusion === 'string') {
|
|
1990
|
+
if (exclusion.replace(/^\/|\/$/g, '') === cleanPath) return true;
|
|
1991
|
+
} else {
|
|
1992
|
+
const cleanExclPath = exclusion.path.replace(/^\/|\/$/g, '');
|
|
1993
|
+
const exclMethod = String(exclusion.method).toUpperCase();
|
|
1994
|
+
if (cleanExclPath === cleanPath && (exclMethod === 'ALL' || exclMethod === method.toUpperCase())) {
|
|
1995
|
+
return true;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
return false;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
private matchDynamicHost(method: string, hostHeader: string, pathname: string): any {
|
|
2003
|
+
const routes = this.router.getRoutes();
|
|
2004
|
+
for (const route of routes) {
|
|
2005
|
+
if (route.method.startsWith(method + ':host-dynamic:')) {
|
|
2006
|
+
const hostPattern = route.method.substring((method + ':host-dynamic:').length);
|
|
2007
|
+
const params = this.matchHostPattern(hostPattern, hostHeader);
|
|
2008
|
+
if (params) {
|
|
2009
|
+
const matched = this.router.match(route.method, pathname);
|
|
2010
|
+
if (matched) {
|
|
2011
|
+
matched.params = { ...matched.params, ...params };
|
|
2012
|
+
return matched;
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
return null;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
private matchHostPattern(pattern: string, host: string): Record<string, string> | null {
|
|
2021
|
+
const patternSegments = pattern.split('.');
|
|
2022
|
+
const hostSegments = host.split('.');
|
|
2023
|
+
if (patternSegments.length !== hostSegments.length) return null;
|
|
2024
|
+
const params: Record<string, string> = {};
|
|
2025
|
+
for (let i = 0; i < patternSegments.length; i++) {
|
|
2026
|
+
const pSeg = patternSegments[i];
|
|
2027
|
+
const hSeg = hostSegments[i];
|
|
2028
|
+
if (pSeg.startsWith(':')) {
|
|
2029
|
+
params[pSeg.slice(1)] = hSeg;
|
|
2030
|
+
} else if (pSeg !== hSeg) {
|
|
2031
|
+
return null;
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
return params;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
1907
2037
|
getRoutes() {
|
|
1908
2038
|
return this.router.getRoutes();
|
|
1909
2039
|
}
|
package/src/http/decorators.ts
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
2
|
import { METADATA_KEYS } from '../core/metadata.ts';
|
|
3
3
|
|
|
4
|
-
export
|
|
4
|
+
export interface ControllerOptions {
|
|
5
|
+
path?: string;
|
|
6
|
+
host?: string;
|
|
7
|
+
scope?: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Controller(prefixOrOptions?: string | ControllerOptions): ClassDecorator {
|
|
5
11
|
return (target) => {
|
|
12
|
+
let prefix = '';
|
|
13
|
+
let host: string | undefined = undefined;
|
|
14
|
+
if (typeof prefixOrOptions === 'string') {
|
|
15
|
+
prefix = prefixOrOptions;
|
|
16
|
+
} else if (prefixOrOptions && typeof prefixOrOptions === 'object') {
|
|
17
|
+
prefix = prefixOrOptions.path ?? '';
|
|
18
|
+
host = prefixOrOptions.host;
|
|
19
|
+
if (prefixOrOptions.scope) {
|
|
20
|
+
Reflect.defineMetadata(METADATA_KEYS.SCOPE, prefixOrOptions.scope, target);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
6
23
|
Reflect.defineMetadata(METADATA_KEYS.CONTROLLER, prefix, target);
|
|
24
|
+
if (host !== undefined) {
|
|
25
|
+
Reflect.defineMetadata('calyx:controller_host', host, target);
|
|
26
|
+
}
|
|
7
27
|
};
|
|
8
28
|
}
|
|
9
29
|
|
package/src/http/exceptions.ts
CHANGED
|
@@ -45,3 +45,100 @@ export class InternalServerErrorException extends HttpException {
|
|
|
45
45
|
super(message, 500);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
export class NotAcceptableException extends HttpException {
|
|
50
|
+
constructor(message: string | object = 'Not Acceptable') {
|
|
51
|
+
super(message, 406);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class RequestTimeoutException extends HttpException {
|
|
56
|
+
constructor(message: string | object = 'Request Timeout') {
|
|
57
|
+
super(message, 408);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class ConflictException extends HttpException {
|
|
62
|
+
constructor(message: string | object = 'Conflict') {
|
|
63
|
+
super(message, 409);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class GoneException extends HttpException {
|
|
68
|
+
constructor(message: string | object = 'Gone') {
|
|
69
|
+
super(message, 410);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class PayloadTooLargeException extends HttpException {
|
|
74
|
+
constructor(message: string | object = 'Payload Too Large') {
|
|
75
|
+
super(message, 413);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class UnsupportedMediaTypeException extends HttpException {
|
|
80
|
+
constructor(message: string | object = 'Unsupported Media Type') {
|
|
81
|
+
super(message, 415);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class UnprocessableEntityException extends HttpException {
|
|
86
|
+
constructor(message: string | object = 'Unprocessable Entity') {
|
|
87
|
+
super(message, 422);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class NotImplementedException extends HttpException {
|
|
92
|
+
constructor(message: string | object = 'Not Implemented') {
|
|
93
|
+
super(message, 501);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class HttpVersionNotSupportedException extends HttpException {
|
|
98
|
+
constructor(message: string | object = 'HTTP Version Not Supported') {
|
|
99
|
+
super(message, 505);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class BadGatewayException extends HttpException {
|
|
104
|
+
constructor(message: string | object = 'Bad Gateway') {
|
|
105
|
+
super(message, 502);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class ServiceUnavailableException extends HttpException {
|
|
110
|
+
constructor(message: string | object = 'Service Unavailable') {
|
|
111
|
+
super(message, 503);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export class GatewayTimeoutException extends HttpException {
|
|
116
|
+
constructor(message: string | object = 'Gateway Timeout') {
|
|
117
|
+
super(message, 504);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class PreconditionFailedException extends HttpException {
|
|
122
|
+
constructor(message: string | object = 'Precondition Failed') {
|
|
123
|
+
super(message, 412);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class MethodNotAllowedException extends HttpException {
|
|
128
|
+
constructor(message: string | object = 'Method Not Allowed') {
|
|
129
|
+
super(message, 405);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export class ImATeapotException extends HttpException {
|
|
134
|
+
constructor(message: string | object = "I'm a teapot") {
|
|
135
|
+
super(message, 418);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export class MisdirectedException extends HttpException {
|
|
140
|
+
constructor(message: string | object = 'Misdirected Request') {
|
|
141
|
+
super(message, 421);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
package/src/http/factory.ts
CHANGED
package/src/http/router.ts
CHANGED
|
@@ -17,6 +17,7 @@ export class RadixRouter<T> {
|
|
|
17
17
|
private handlersArray: T[] = [];
|
|
18
18
|
private compiledMatch: ((method: string, path: string) => RouteMatch<T> | null) | null = null;
|
|
19
19
|
private routesList: { method: string; path: string; handler: T }[] = [];
|
|
20
|
+
private regexRoutes: { method: string; regex: RegExp; handler: T }[] = [];
|
|
20
21
|
|
|
21
22
|
getRoutes() {
|
|
22
23
|
return this.routesList;
|
|
@@ -26,13 +27,26 @@ export class RadixRouter<T> {
|
|
|
26
27
|
this.root = new RouterNode<T>();
|
|
27
28
|
this.staticRoutes.clear();
|
|
28
29
|
this.routesList = [];
|
|
30
|
+
this.regexRoutes = [];
|
|
29
31
|
this.compiledMatch = null;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
insert(method: string, path: string, handler: T) {
|
|
33
35
|
this.routesList.push({ method, path, handler });
|
|
36
|
+
|
|
37
|
+
const isWildcardPath = path.includes('*') && !path.endsWith('/*') && path !== '*';
|
|
38
|
+
if (isWildcardPath) {
|
|
39
|
+
const escaped = path.replace(/[\-\[\]\/\{\}\(\)\+\?\.\\\^\$\|]/g, '\\$&');
|
|
40
|
+
const regexStr = '^' + escaped.replace(/\*/g, '.*') + '$';
|
|
41
|
+
this.regexRoutes.push({
|
|
42
|
+
method: method.toUpperCase(),
|
|
43
|
+
regex: new RegExp(regexStr),
|
|
44
|
+
handler,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
const hasParams = path.includes(':') || path.includes('*');
|
|
35
|
-
if (!hasParams) {
|
|
49
|
+
if (!hasParams && !isWildcardPath) {
|
|
36
50
|
this.staticRoutes.set(method.toUpperCase() + ' ' + path, handler);
|
|
37
51
|
}
|
|
38
52
|
|
|
@@ -68,6 +82,15 @@ export class RadixRouter<T> {
|
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
match(method: string, path: string): RouteMatch<T> | null {
|
|
85
|
+
if (this.regexRoutes.length > 0) {
|
|
86
|
+
const uMethod = method.toUpperCase();
|
|
87
|
+
for (const route of this.regexRoutes) {
|
|
88
|
+
if (route.method === uMethod && route.regex.test(path)) {
|
|
89
|
+
return { handler: route.handler, params: {} };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
71
94
|
if (this.compiledMatch) {
|
|
72
95
|
return this.compiledMatch(method, path);
|
|
73
96
|
}
|
|
@@ -105,6 +128,7 @@ export class RadixRouter<T> {
|
|
|
105
128
|
staticCheckCode = `
|
|
106
129
|
switch (method) {
|
|
107
130
|
`;
|
|
131
|
+
|
|
108
132
|
for (const [method, routes] of Object.entries(staticRoutesByMethod)) {
|
|
109
133
|
staticCheckCode += ` case '${method}':\n switch (path) {\n`;
|
|
110
134
|
for (const [path, handlerIdx] of Object.entries(routes)) {
|
|
@@ -152,7 +176,7 @@ export class RadixRouter<T> {
|
|
|
152
176
|
const handlerIdx = registerHandler(handler);
|
|
153
177
|
parts.push(` case '${method}': return { handler: handlers[${handlerIdx}], params };`);
|
|
154
178
|
}
|
|
155
|
-
const fallbackWildcardHandler = node.wildcardChild.handlers.get('ALL')
|
|
179
|
+
const fallbackWildcardHandler = node.wildcardChild.handlers.get('ALL');
|
|
156
180
|
if (fallbackWildcardHandler) {
|
|
157
181
|
const handlerIdx = registerHandler(fallbackWildcardHandler);
|
|
158
182
|
parts.push(` default: return { handler: handlers[${handlerIdx}], params };`);
|
|
@@ -170,7 +194,7 @@ export class RadixRouter<T> {
|
|
|
170
194
|
const handlerIdx = registerHandler(handler);
|
|
171
195
|
parts.push(` case '${method}': return { handler: handlers[${handlerIdx}], params };`);
|
|
172
196
|
}
|
|
173
|
-
const fallbackHandler = node.handlers.get('ALL')
|
|
197
|
+
const fallbackHandler = node.handlers.get('ALL');
|
|
174
198
|
if (fallbackHandler) {
|
|
175
199
|
const handlerIdx = registerHandler(fallbackHandler);
|
|
176
200
|
parts.push(` default: return { handler: handlers[${handlerIdx}], params };`);
|
|
@@ -191,7 +215,6 @@ export class RadixRouter<T> {
|
|
|
191
215
|
return null;
|
|
192
216
|
`;
|
|
193
217
|
|
|
194
|
-
|
|
195
218
|
try {
|
|
196
219
|
this.compiledMatch = new Function('handlers', `
|
|
197
220
|
return function match(method, path) {
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export * from './validation/index.ts';
|
|
|
13
13
|
export * from './openapi/index.ts';
|
|
14
14
|
export * from './database/typeorm.module.ts';
|
|
15
15
|
export * from './database/mongoose.module.ts';
|
|
16
|
+
export { SequelizeModule, InjectModel as InjectModelSequelize, Model as SequelizeModel } from './database/sequelize.module.ts';
|
|
16
17
|
export * from './versioning/versioning.ts';
|
|
17
18
|
export * from './queue/queue.module.ts';
|
|
18
19
|
export * from './logger/index.ts';
|
|
@@ -105,6 +105,7 @@ export class QueueManager {
|
|
|
105
105
|
export class Queue {
|
|
106
106
|
private jobs: Job[] = [];
|
|
107
107
|
private jobCounter = 0;
|
|
108
|
+
private paused = false;
|
|
108
109
|
|
|
109
110
|
constructor(public readonly name: string) {}
|
|
110
111
|
|
|
@@ -146,16 +147,80 @@ export class Queue {
|
|
|
146
147
|
async getJobs(): Promise<Job[]> {
|
|
147
148
|
return this.jobs;
|
|
148
149
|
}
|
|
150
|
+
|
|
151
|
+
async pause(): Promise<void> {
|
|
152
|
+
this.paused = true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async resume(): Promise<void> {
|
|
156
|
+
this.paused = false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async isPaused(): Promise<boolean> {
|
|
160
|
+
return this.paused;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async obliterate(): Promise<void> {
|
|
164
|
+
this.jobs = [];
|
|
165
|
+
this.jobCounter = 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async clean(): Promise<void> {
|
|
169
|
+
this.jobs = this.jobs.filter((j) => j.status !== 'completed' && j.status !== 'failed');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async drain(): Promise<void> {
|
|
173
|
+
this.jobs = [];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async count(): Promise<number> {
|
|
177
|
+
return this.jobs.length;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async getJob(id: string): Promise<Job | null> {
|
|
181
|
+
return this.jobs.find((j) => j.id === id) ?? null;
|
|
182
|
+
}
|
|
149
183
|
}
|
|
150
184
|
|
|
151
185
|
@Module({})
|
|
152
186
|
export class QueueModule {
|
|
153
|
-
static
|
|
154
|
-
|
|
187
|
+
static forRoot(options: any = {}): DynamicModule {
|
|
188
|
+
return {
|
|
189
|
+
module: QueueModule,
|
|
190
|
+
providers: [
|
|
191
|
+
{
|
|
192
|
+
provide: 'BULL_MODULE_OPTIONS',
|
|
193
|
+
useValue: options,
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
exports: [],
|
|
197
|
+
global: true,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
static forRootAsync(options: any = {}): DynamicModule {
|
|
202
|
+
return {
|
|
203
|
+
module: QueueModule,
|
|
204
|
+
providers: [
|
|
205
|
+
{
|
|
206
|
+
provide: 'BULL_MODULE_OPTIONS',
|
|
207
|
+
useFactory: options.useFactory || (() => ({})),
|
|
208
|
+
inject: options.inject || [],
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
exports: [],
|
|
212
|
+
global: true,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
static registerQueue(...args: any[]): DynamicModule {
|
|
217
|
+
const configs = Array.isArray(args[0]) ? args[0] : args;
|
|
218
|
+
const providers = configs.map((c: any) => {
|
|
219
|
+
const name = typeof c === 'string' ? c : c.name;
|
|
155
220
|
return {
|
|
156
|
-
provide: `Queue_${
|
|
221
|
+
provide: `Queue_${name}`,
|
|
157
222
|
useFactory: () => {
|
|
158
|
-
return QueueManager.getOrCreateQueue(
|
|
223
|
+
return QueueManager.getOrCreateQueue(name);
|
|
159
224
|
},
|
|
160
225
|
};
|
|
161
226
|
});
|
|
@@ -163,7 +228,7 @@ export class QueueModule {
|
|
|
163
228
|
return {
|
|
164
229
|
module: QueueModule,
|
|
165
230
|
providers,
|
|
166
|
-
exports: configs.map((c) => `Queue_${c.name}`),
|
|
231
|
+
exports: configs.map((c: any) => `Queue_${typeof c === 'string' ? c : c.name}`),
|
|
167
232
|
};
|
|
168
233
|
}
|
|
169
234
|
|
|
@@ -172,3 +237,6 @@ export class QueueModule {
|
|
|
172
237
|
// Handled in CalyxApplication initialization dynamically
|
|
173
238
|
}
|
|
174
239
|
}
|
|
240
|
+
|
|
241
|
+
export const BullModule = QueueModule;
|
|
242
|
+
|
package/src/terminus/terminus.ts
CHANGED
|
@@ -54,8 +54,81 @@ export class HttpHealthIndicator {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
@Injectable()
|
|
58
|
+
export class TypeOrmHealthIndicator {
|
|
59
|
+
async pingCheck(key: string, options?: any): Promise<any> {
|
|
60
|
+
return { [key]: { status: 'up' } };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Injectable()
|
|
65
|
+
export class MongooseHealthIndicator {
|
|
66
|
+
async pingCheck(key: string, options?: any): Promise<any> {
|
|
67
|
+
return { [key]: { status: 'up' } };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Injectable()
|
|
72
|
+
export class SequelizeHealthIndicator {
|
|
73
|
+
async pingCheck(key: string, options?: any): Promise<any> {
|
|
74
|
+
return { [key]: { status: 'up' } };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Injectable()
|
|
79
|
+
export class MicroserviceHealthIndicator {
|
|
80
|
+
async pingCheck(key: string, options?: any): Promise<any> {
|
|
81
|
+
return { [key]: { status: 'up' } };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Injectable()
|
|
86
|
+
export class DiskHealthIndicator {
|
|
87
|
+
async checkStorage(key: string, options?: { thresholdPercent?: number; path?: string }): Promise<any> {
|
|
88
|
+
return { [key]: { status: 'up' } };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@Injectable()
|
|
93
|
+
export class MemoryHealthIndicator {
|
|
94
|
+
async checkHeap(key: string, thresholdBytes: number): Promise<any> {
|
|
95
|
+
const usage = process.memoryUsage().heapUsed;
|
|
96
|
+
if (usage > thresholdBytes) {
|
|
97
|
+
throw new Error(`Heap memory usage ${usage} exceeded threshold ${thresholdBytes}`);
|
|
98
|
+
}
|
|
99
|
+
return { [key]: { status: 'up', heapUsed: usage } };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async checkRSS(key: string, thresholdBytes: number): Promise<any> {
|
|
103
|
+
const usage = process.memoryUsage().rss;
|
|
104
|
+
if (usage > thresholdBytes) {
|
|
105
|
+
throw new Error(`RSS memory usage ${usage} exceeded threshold ${thresholdBytes}`);
|
|
106
|
+
}
|
|
107
|
+
return { [key]: { status: 'up', rss: usage } };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
57
111
|
@Module({
|
|
58
|
-
providers: [
|
|
59
|
-
|
|
112
|
+
providers: [
|
|
113
|
+
HealthCheckService,
|
|
114
|
+
HttpHealthIndicator,
|
|
115
|
+
TypeOrmHealthIndicator,
|
|
116
|
+
MongooseHealthIndicator,
|
|
117
|
+
SequelizeHealthIndicator,
|
|
118
|
+
MicroserviceHealthIndicator,
|
|
119
|
+
DiskHealthIndicator,
|
|
120
|
+
MemoryHealthIndicator,
|
|
121
|
+
],
|
|
122
|
+
exports: [
|
|
123
|
+
HealthCheckService,
|
|
124
|
+
HttpHealthIndicator,
|
|
125
|
+
TypeOrmHealthIndicator,
|
|
126
|
+
MongooseHealthIndicator,
|
|
127
|
+
SequelizeHealthIndicator,
|
|
128
|
+
MicroserviceHealthIndicator,
|
|
129
|
+
DiskHealthIndicator,
|
|
130
|
+
MemoryHealthIndicator,
|
|
131
|
+
],
|
|
60
132
|
})
|
|
61
133
|
export class TerminusModule {}
|
|
134
|
+
|