@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +5 -3
  3. package/benchmarks/graphql-benchmark.ts +4 -1
  4. package/benchmarks/serialization-benchmark.ts +46 -6
  5. package/benchmarks/techniques-benchmark.ts +12 -0
  6. package/benchmarks/validation-benchmark.ts +8 -1
  7. package/docs/controllers.md +3 -3
  8. package/docs/dependency-injection.md +3 -3
  9. package/docs/lifecycle.md +3 -3
  10. package/package.json +1 -1
  11. package/src/cli/index.ts +7 -1
  12. package/src/config/config.module.ts +16 -2
  13. package/src/config/config.service.ts +20 -6
  14. package/src/cookies/cookies.ts +45 -8
  15. package/src/core/container.ts +340 -154
  16. package/src/core/testing-module.ts +4 -0
  17. package/src/cqrs/cqrs.ts +93 -4
  18. package/src/database/sequelize.module.ts +239 -0
  19. package/src/event-emitter/decorators.ts +2 -2
  20. package/src/event-emitter/event-emitter.ts +3 -0
  21. package/src/graphql/graphql.module.ts +2 -4
  22. package/src/http/application.ts +140 -10
  23. package/src/http/decorators.ts +21 -1
  24. package/src/http/exceptions.ts +97 -0
  25. package/src/http/factory.ts +3 -0
  26. package/src/http/router.ts +27 -4
  27. package/src/index.ts +1 -0
  28. package/src/microservices/exceptions.ts +10 -0
  29. package/src/microservices/index.ts +1 -0
  30. package/src/queue/queue.module.ts +73 -5
  31. package/src/terminus/terminus.ts +75 -2
  32. package/src/validation/compiler.ts +137 -17
  33. package/src/validation/decorators.ts +164 -2
  34. package/src/websockets/exceptions.ts +10 -0
  35. package/src/websockets/index.ts +1 -0
  36. package/tests/circular-di.test.ts +151 -0
  37. package/tests/di.test.ts +10 -2
  38. package/tests/nestjs-parity.test.ts +255 -0
@@ -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 fullPath = '/' + [normalizedPrefix, normalizedPath].filter(Boolean).join('/');
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
- this.router.insert(methodStr, pathStr, {
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
- const versionedPath = '/' + [versionPrefix.replace(/^\/|\/$/g, ''), normalizedPrefix, normalizedPath].filter(Boolean).join('/');
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
- matched = this.router.match(`${req.method}:v${version}`.toUpperCase(), pathname);
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 ? req.headers.get('host') : req.headers.get('host'); break;
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 ? req.headers.get('host') : req.headers.get('host');
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
  }
@@ -1,9 +1,29 @@
1
1
  import 'reflect-metadata';
2
2
  import { METADATA_KEYS } from '../core/metadata.ts';
3
3
 
4
- export function Controller(prefix = ''): ClassDecorator {
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
 
@@ -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
+
@@ -14,3 +14,6 @@ export class CalyxFactory {
14
14
  return app;
15
15
  }
16
16
  }
17
+
18
+ export const NestFactory = CalyxFactory;
19
+
@@ -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') ?? node.wildcardChild.handlers.values().next().value;
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') ?? node.handlers.values().next().value;
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';
@@ -0,0 +1,10 @@
1
+ export class RpcException extends Error {
2
+ constructor(private readonly error: string | object) {
3
+ super(typeof error === 'string' ? error : JSON.stringify(error));
4
+ this.name = 'RpcException';
5
+ }
6
+
7
+ getError() {
8
+ return this.error;
9
+ }
10
+ }
@@ -5,3 +5,4 @@ export * from './decorators.ts';
5
5
  export * from './server-tcp.ts';
6
6
  export * from './microservice.ts';
7
7
  export * from './clients.module.ts';
8
+ export * from './exceptions.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 registerQueue(...configs: { name: string }[]): DynamicModule {
154
- const providers = configs.map((c) => {
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_${c.name}`,
221
+ provide: `Queue_${name}`,
157
222
  useFactory: () => {
158
- return QueueManager.getOrCreateQueue(c.name);
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
+
@@ -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: [HealthCheckService, HttpHealthIndicator],
59
- exports: [HealthCheckService, HttpHealthIndicator],
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
+