@martel/calyx 1.11.0 → 1.13.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +1 -1
  3. package/src/cache/cache.interceptor.ts +4 -2
  4. package/src/cache/decorators.ts +4 -0
  5. package/src/cache/index.ts +1 -0
  6. package/src/cli/index.ts +7 -1
  7. package/src/config/config.module.ts +16 -2
  8. package/src/config/config.service.ts +20 -6
  9. package/src/core/container.ts +559 -140
  10. package/src/core/index.ts +2 -0
  11. package/src/core/lazy-module-loader.ts +29 -0
  12. package/src/core/metadata.ts +6 -1
  13. package/src/core/testing-module.ts +123 -0
  14. package/src/cqrs/cqrs.ts +264 -0
  15. package/src/database/sequelize.module.ts +239 -0
  16. package/src/event-emitter/decorators.ts +2 -2
  17. package/src/event-emitter/event-emitter.ts +3 -0
  18. package/src/graphql/decorators.ts +16 -0
  19. package/src/graphql/graphql.module.ts +16 -0
  20. package/src/http/application.ts +261 -21
  21. package/src/http/decorators.ts +25 -1
  22. package/src/http/exceptions.ts +97 -0
  23. package/src/http/factory.ts +3 -0
  24. package/src/http/router.ts +27 -4
  25. package/src/index.ts +3 -0
  26. package/src/microservices/clients.module.ts +47 -0
  27. package/src/microservices/exceptions.ts +10 -0
  28. package/src/microservices/index.ts +2 -0
  29. package/src/microservices/microservice.ts +1 -1
  30. package/src/queue/queue.module.ts +73 -5
  31. package/src/schedule/decorators.ts +10 -6
  32. package/src/schedule/index.ts +1 -0
  33. package/src/schedule/schedule.module.ts +3 -2
  34. package/src/schedule/scheduler-registry.ts +50 -0
  35. package/src/security/index.ts +1 -0
  36. package/src/security/throttler.module.ts +108 -0
  37. package/src/terminus/terminus.ts +134 -0
  38. package/src/validation/compiler.ts +133 -10
  39. package/src/validation/decorators.ts +164 -2
  40. package/src/validation/http-pipes.ts +128 -0
  41. package/src/validation/index.ts +1 -0
  42. package/src/websockets/decorators.ts +12 -2
  43. package/src/websockets/exceptions.ts +10 -0
  44. package/src/websockets/index.ts +1 -0
  45. package/tests/circular-di.test.ts +151 -0
  46. package/tests/di.test.ts +10 -2
  47. package/tests/nestjs-parity.test.ts +527 -0
@@ -47,6 +47,22 @@ export function Parent(): ParameterDecorator {
47
47
  };
48
48
  }
49
49
 
50
+ export function Context(name?: string): ParameterDecorator {
51
+ return (target, propertyKey, parameterIndex) => {
52
+ const contextParams = Reflect.getOwnMetadata('calyx:context', target, propertyKey!) || [];
53
+ contextParams.push({ parameterIndex, name });
54
+ Reflect.defineMetadata('calyx:context', contextParams, target, propertyKey!);
55
+ };
56
+ }
57
+
58
+ export function Info(): ParameterDecorator {
59
+ return (target, propertyKey, parameterIndex) => {
60
+ const infoParams = Reflect.getOwnMetadata('calyx:info', target, propertyKey!) || [];
61
+ infoParams.push(parameterIndex);
62
+ Reflect.defineMetadata('calyx:info', infoParams, target, propertyKey!);
63
+ };
64
+ }
65
+
50
66
  export function ObjectType(): ClassDecorator {
51
67
  return (target) => {
52
68
  Reflect.defineMetadata('calyx:object_type', true, target);
@@ -313,6 +313,14 @@ export class GraphQLModule {
313
313
  for (const idx of parentParams) {
314
314
  params[idx] = parent;
315
315
  }
316
+ const contextParams = Reflect.getMetadata('calyx:context', resolverInstance, fieldMeta.propertyKey) || [];
317
+ for (const item of contextParams) {
318
+ params[item.parameterIndex] = item.name ? context?.[item.name] : context;
319
+ }
320
+ const infoParams = Reflect.getMetadata('calyx:info', resolverInstance, fieldMeta.propertyKey) || [];
321
+ for (const idx of infoParams) {
322
+ params[idx] = info;
323
+ }
316
324
  return resolverInstance[fieldMeta.propertyKey](...params);
317
325
  };
318
326
 
@@ -473,6 +481,14 @@ export class GraphQLModule {
473
481
  for (const idx of parentParams) {
474
482
  params[idx] = parent;
475
483
  }
484
+ const contextParams = Reflect.getMetadata('calyx:context', resolverInstance, fieldRes.propertyKey) || [];
485
+ for (const item of contextParams) {
486
+ params[item.parameterIndex] = item.name ? context?.[item.name] : context;
487
+ }
488
+ const infoParams = Reflect.getMetadata('calyx:info', resolverInstance, fieldRes.propertyKey) || [];
489
+ for (const idx of infoParams) {
490
+ params[idx] = info;
491
+ }
476
492
  return resolverInstance[fieldRes.propertyKey](...params);
477
493
  };
478
494
 
@@ -10,6 +10,7 @@ import { EventEmitter } from '../event-emitter/event-emitter.ts';
10
10
  import { cors, CorsOptions } from '../security/cors.middleware.ts';
11
11
  import { helmet, HelmetOptions } from '../security/helmet.middleware.ts';
12
12
  import { CronMatcher } from '../schedule/cron.matcher.ts';
13
+ import { SchedulerRegistry } from '../schedule/scheduler-registry.ts';
13
14
  import { SerializationCompiler } from '../validation/compiler.ts';
14
15
  import { VersioningOptions, VersioningType, VersionExtractor, VERSION_METADATA_KEY } from '../versioning/versioning.ts';
15
16
  import { QueueManager, PROCESSOR_METADATA_KEY } from '../queue/queue.module.ts';
@@ -17,6 +18,8 @@ import { parseCookies, formatCookie } from '../cookies/cookies.ts';
17
18
  import { StreamableFile } from '../streaming/streamable-file.ts';
18
19
  import { defaultRenderEngine, ViewEngine } from '../mvc/mvc.ts';
19
20
  import { join } from 'path';
21
+ import { CalyxMicroservice } from '../microservices/microservice.ts';
22
+
20
23
 
21
24
  class ObjectPool<T> {
22
25
  private pool: T[] = [];
@@ -66,6 +69,41 @@ export class CalyxResponse {
66
69
  this.cookiesList.push(formatCookie(name, value, options));
67
70
  return this;
68
71
  }
72
+
73
+ header(name: string, value: string) {
74
+ return this.set(name, value);
75
+ }
76
+
77
+ type(contentType: string) {
78
+ return this.set('content-type', contentType);
79
+ }
80
+
81
+ redirect(url: string, status?: number) {
82
+ this.statusCode = status ?? 302;
83
+ this.set('location', url);
84
+ this.sent = true;
85
+ return this;
86
+ }
87
+
88
+ end() {
89
+ this.sent = true;
90
+ return this;
91
+ }
92
+
93
+ get(name: string): string | undefined {
94
+ return this.headers[name.toLowerCase()];
95
+ }
96
+
97
+ append(name: string, value: string) {
98
+ const key = name.toLowerCase();
99
+ const existing = this.headers[key];
100
+ this.headers[key] = existing ? `${existing}, ${value}` : value;
101
+ return this;
102
+ }
103
+
104
+ clearCookie(name: string, options?: any) {
105
+ return this.cookie(name, '', { ...options, expires: new Date(0) });
106
+ }
69
107
  }
70
108
 
71
109
  interface CompiledLifecycleItem {
@@ -122,12 +160,54 @@ export class CalyxApplication {
122
160
  private graphqlQueryCache = new Map<string, any>();
123
161
  private isInitialized = false;
124
162
  private versioningOptions?: VersioningOptions;
163
+ private globalPrefix?: string;
164
+ private globalPrefixOptions?: { exclude?: (string | { path: string; method: string })[] };
165
+ private connectedMicroservices: any[] = [];
166
+ private websocketAdapter: any = null;
125
167
 
126
168
  enableVersioning(options: VersioningOptions) {
127
169
  this.versioningOptions = options;
128
170
  return this;
129
171
  }
130
172
 
173
+ setGlobalPrefix(prefix: string, options?: { exclude?: (string | { path: string; method: string })[] }) {
174
+ this.globalPrefix = prefix;
175
+ this.globalPrefixOptions = options;
176
+ return this;
177
+ }
178
+
179
+ getHttpServer() {
180
+ return this.server;
181
+ }
182
+
183
+ getHttpAdapter() {
184
+ return {
185
+ getInstance: () => this.server,
186
+ getHttpServer: () => this.server,
187
+ };
188
+ }
189
+
190
+ async getUrl(): Promise<string> {
191
+ return `http://localhost:${this.serverPort}`;
192
+ }
193
+
194
+ useWebSocketAdapter(adapter: any) {
195
+ this.websocketAdapter = adapter;
196
+ return this;
197
+ }
198
+
199
+ connectMicroservice(options: any) {
200
+ const microservice = new CalyxMicroservice(this.rootModule, options);
201
+ this.connectedMicroservices.push(microservice);
202
+ return microservice;
203
+ }
204
+
205
+ async startAllMicroservices(): Promise<void> {
206
+ for (const ms of this.connectedMicroservices) {
207
+ await ms.listen();
208
+ }
209
+ }
210
+
131
211
  private compressionEnabled = false;
132
212
 
133
213
  enableCompression() {
@@ -186,7 +266,7 @@ export class CalyxApplication {
186
266
  this.isInitialized = true;
187
267
 
188
268
  // Bootstrap the dependency injection container
189
- this.container.bootstrap(this.rootModule);
269
+ await this.container.bootstrap(this.rootModule);
190
270
 
191
271
  // Resolve registered modules middlewares
192
272
  this.resolveMiddleware();
@@ -232,6 +312,7 @@ export class CalyxApplication {
232
312
  const isControllerRequestScoped = controllerScope === Scope.REQUEST;
233
313
  const singletonInstance = isControllerRequestScoped ? null : record.instances.get(controllerClass);
234
314
 
315
+ const controllerHost = Reflect.getMetadata('calyx:controller_host', controllerClass);
235
316
  for (const method of methods) {
236
317
  const routeMeta = Reflect.getMetadata(METADATA_KEYS.HTTP_METHOD, controllerClass.prototype, method);
237
318
  if (!routeMeta) continue;
@@ -239,7 +320,13 @@ export class CalyxApplication {
239
320
  // Normalize prefix and path
240
321
  const normalizedPrefix = prefix.replace(/^\/|\/$/g, '');
241
322
  const normalizedPath = routeMeta.path.replace(/^\/|\/$/g, '');
242
- const fullPath = '/' + [normalizedPrefix, normalizedPath].filter(Boolean).join('/');
323
+ const baseRoutePath = '/' + [normalizedPrefix, normalizedPath].filter(Boolean).join('/');
324
+
325
+ let fullPath = baseRoutePath;
326
+ if (this.globalPrefix && !this.isRouteExcludedFromGlobalPrefix(baseRoutePath, routeMeta.method)) {
327
+ const cleanGlobal = this.globalPrefix.replace(/^\/|\/$/g, '');
328
+ fullPath = '/' + [cleanGlobal, normalizedPrefix, normalizedPath].filter(Boolean).join('/');
329
+ }
243
330
 
244
331
  const paramsConfig: ParameterConfig[] = Reflect.getMetadata(METADATA_KEYS.HTTP_PARAMS, controllerClass.prototype, method) || [];
245
332
  // Sort parameters by index ascending so they map correctly to function arguments
@@ -325,7 +412,14 @@ export class CalyxApplication {
325
412
  Reflect.hasMetadata('calyx:sse', methodFn);
326
413
 
327
414
  const insertRoute = (methodStr: string, pathStr: string) => {
328
- this.router.insert(methodStr, pathStr, {
415
+ let finalMethod = methodStr;
416
+ if (controllerHost) {
417
+ const hasParams = controllerHost.includes(':');
418
+ finalMethod = hasParams
419
+ ? `${methodStr}:host-dynamic:${controllerHost}`
420
+ : `${methodStr}:host:${controllerHost}`;
421
+ }
422
+ this.router.insert(finalMethod, pathStr, {
329
423
  controllerClass,
330
424
  moduleClass,
331
425
  instance: singletonInstance,
@@ -352,7 +446,11 @@ export class CalyxApplication {
352
446
  if (this.versioningOptions && this.versioningOptions.type === VersioningType.URI && versions.length > 0) {
353
447
  for (const version of versions) {
354
448
  const versionPrefix = `/v${version}`;
355
- const versionedPath = '/' + [versionPrefix.replace(/^\/|\/$/g, ''), normalizedPrefix, normalizedPath].filter(Boolean).join('/');
449
+ let versionedPath = '/' + [versionPrefix.replace(/^\/|\/$/g, ''), normalizedPrefix, normalizedPath].filter(Boolean).join('/');
450
+ if (this.globalPrefix && !this.isRouteExcludedFromGlobalPrefix(versionedPath, routeMeta.method)) {
451
+ const cleanGlobal = this.globalPrefix.replace(/^\/|\/$/g, '');
452
+ versionedPath = '/' + [cleanGlobal, versionPrefix.replace(/^\/|\/$/g, ''), normalizedPrefix, normalizedPath].filter(Boolean).join('/');
453
+ }
356
454
  insertRoute(routeMeta.method, versionedPath);
357
455
  }
358
456
  } else if (this.versioningOptions && (this.versioningOptions.type === VersioningType.HEADER || this.versioningOptions.type === VersioningType.MEDIA_TYPE) && versions.length > 0) {
@@ -504,12 +602,27 @@ export class CalyxApplication {
504
602
  }
505
603
 
506
604
  let matched = null;
605
+ const hostHeader = req.headers.get('host') || '';
606
+
507
607
  if (this.versioningOptions && this.versioningOptions.type !== VersioningType.URI) {
508
608
  const version = VersionExtractor.extract(req, this.versioningOptions.type, this.versioningOptions);
509
609
  if (version) {
510
- matched = this.router.match(`${req.method}:v${version}`.toUpperCase(), pathname);
610
+ const vMethod = `${req.method}:v${version}`.toUpperCase();
611
+ matched = this.router.match(`${vMethod}:host:${hostHeader}`.toUpperCase(), pathname);
612
+ if (!matched) {
613
+ matched = this.matchDynamicHost(vMethod, hostHeader, pathname);
614
+ }
615
+ if (!matched) {
616
+ matched = this.router.match(vMethod, pathname);
617
+ }
511
618
  }
512
619
  }
620
+ if (!matched) {
621
+ matched = this.router.match(`${req.method}:host:${hostHeader}`.toUpperCase(), pathname);
622
+ }
623
+ if (!matched) {
624
+ matched = this.matchDynamicHost(req.method.toUpperCase(), hostHeader, pathname);
625
+ }
513
626
  if (!matched) {
514
627
  matched = this.router.match(req.method, pathname);
515
628
  }
@@ -685,6 +798,10 @@ export class CalyxApplication {
685
798
  case 'query': val = config.name ? query?.[config.name] : query; break;
686
799
  case 'body': val = config.name ? body?.[config.name] : body; break;
687
800
  case 'headers': val = config.name ? req.headers.get(config.name) : Object.fromEntries(req.headers.entries()); break;
801
+ case 'session': val = (req as any).session; break;
802
+ case 'ip': val = this.server ? (this.server.requestIP(req)?.address ?? req.headers.get('x-forwarded-for') ?? '') : (req.headers.get('x-forwarded-for') ?? ''); break;
803
+ case 'hostparam': val = config.name ? params[config.name] : params; break;
804
+ case 'next': val = next; break;
688
805
  case 'custom': val = config.factory ? config.factory(config.name, context) : undefined; break;
689
806
  }
690
807
 
@@ -939,6 +1056,18 @@ export class CalyxApplication {
939
1056
  case 'headers':
940
1057
  val = config.name ? req.headers.get(config.name) : Object.fromEntries(req.headers.entries());
941
1058
  break;
1059
+ case 'session':
1060
+ val = (req as any).session;
1061
+ break;
1062
+ case 'ip':
1063
+ val = this.server ? (this.server.requestIP(req)?.address ?? req.headers.get('x-forwarded-for') ?? '') : (req.headers.get('x-forwarded-for') ?? '');
1064
+ break;
1065
+ case 'hostparam':
1066
+ val = config.name ? params[config.name] : params;
1067
+ break;
1068
+ case 'next':
1069
+ val = () => {};
1070
+ break;
942
1071
  case 'custom':
943
1072
  val = config.factory ? config.factory(config.name, context!) : undefined;
944
1073
  break;
@@ -1554,11 +1683,18 @@ export class CalyxApplication {
1554
1683
  return;
1555
1684
  }
1556
1685
 
1686
+ let registry: SchedulerRegistry | null = null;
1687
+ try {
1688
+ registry = this.container.getGlobalOrAnyInstance(SchedulerRegistry);
1689
+ } catch {
1690
+ // ignore
1691
+ }
1692
+
1557
1693
  const instances = this.container.getProviderAndControllerInstances();
1558
1694
  for (const instance of instances) {
1559
1695
  if (!instance || !instance.constructor) continue;
1560
1696
 
1561
- const crons: { expression: string; propertyKey: string | symbol }[] =
1697
+ const crons: { expression: string; propertyKey: string | symbol; name?: string }[] =
1562
1698
  Reflect.getMetadata('calyx:cron', instance.constructor) || [];
1563
1699
  for (const cron of crons) {
1564
1700
  const parts = cron.expression.split(' ');
@@ -1584,9 +1720,16 @@ export class CalyxApplication {
1584
1720
  const intervalMs = isSecondLevel ? 1000 : 20000;
1585
1721
  const timer = setInterval(tick, intervalMs);
1586
1722
  this.cleanupListeners.push(() => clearInterval(timer));
1723
+
1724
+ if (registry && cron.name) {
1725
+ registry.addCronJob(cron.name, {
1726
+ start: () => {},
1727
+ stop: () => clearInterval(timer),
1728
+ });
1729
+ }
1587
1730
  }
1588
1731
 
1589
- const intervals: { ms: number; propertyKey: string | symbol }[] =
1732
+ const intervals: { ms: number; propertyKey: string | symbol; name?: string }[] =
1590
1733
  Reflect.getMetadata('calyx:interval', instance.constructor) || [];
1591
1734
  for (const interval of intervals) {
1592
1735
  const timer = setInterval(() => {
@@ -1597,9 +1740,13 @@ export class CalyxApplication {
1597
1740
  }
1598
1741
  }, interval.ms);
1599
1742
  this.cleanupListeners.push(() => clearInterval(timer));
1743
+
1744
+ if (registry && interval.name) {
1745
+ registry.addInterval(interval.name, timer);
1746
+ }
1600
1747
  }
1601
1748
 
1602
- const timeouts: { ms: number; propertyKey: string | symbol }[] =
1749
+ const timeouts: { ms: number; propertyKey: string | symbol; name?: string }[] =
1603
1750
  Reflect.getMetadata('calyx:timeout', instance.constructor) || [];
1604
1751
  for (const timeout of timeouts) {
1605
1752
  const timer = setTimeout(() => {
@@ -1610,6 +1757,10 @@ export class CalyxApplication {
1610
1757
  }
1611
1758
  }, timeout.ms);
1612
1759
  this.cleanupListeners.push(() => clearTimeout(timer));
1760
+
1761
+ if (registry && timeout.name) {
1762
+ registry.addTimeout(timeout.name, timer);
1763
+ }
1613
1764
  }
1614
1765
  }
1615
1766
  }
@@ -1635,14 +1786,24 @@ export class CalyxApplication {
1635
1786
  for (const sub of subMessages) {
1636
1787
  const paramMapping: any[] = [];
1637
1788
 
1789
+ const classPipes = Reflect.getMetadata(METADATA_KEYS.PIPES, instance.constructor) || [];
1790
+ const methodPipes = Reflect.getMetadata(METADATA_KEYS.PIPES, instance.constructor.prototype, sub.propertyKey) || [];
1791
+ const methodPipesCompiled = this.compileLifecycleItems(this.rootModule, [...this.globalPipes, ...classPipes, ...methodPipes]);
1792
+
1638
1793
  for (const bp of bodyParams) {
1639
1794
  if (bp.propertyKey === sub.propertyKey) {
1640
- paramMapping[bp.parameterIndex] = 'body';
1795
+ paramMapping[bp.parameterIndex] = {
1796
+ type: 'body',
1797
+ name: bp.name,
1798
+ pipes: [...methodPipesCompiled, ...this.compileLifecycleItems(this.rootModule, bp.pipes || [])],
1799
+ };
1641
1800
  }
1642
1801
  }
1643
1802
  for (const sp of socketParams) {
1644
1803
  if (sp.propertyKey === sub.propertyKey) {
1645
- paramMapping[sp.parameterIndex] = 'socket';
1804
+ paramMapping[sp.parameterIndex] = {
1805
+ type: 'socket',
1806
+ };
1646
1807
  }
1647
1808
  }
1648
1809
 
@@ -1654,11 +1815,16 @@ export class CalyxApplication {
1654
1815
  const methodInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, instance.constructor.prototype, sub.propertyKey) || [];
1655
1816
  const interceptors = this.compileLifecycleItems(this.rootModule, [...this.globalInterceptors, ...classInterceptors, ...methodInterceptors]);
1656
1817
 
1818
+ const classFilters = Reflect.getMetadata(METADATA_KEYS.FILTERS, instance.constructor) || [];
1819
+ const methodFilters = Reflect.getMetadata(METADATA_KEYS.FILTERS, instance.constructor.prototype, sub.propertyKey) || [];
1820
+ const filters = this.compileLifecycleItems(this.rootModule, [...this.globalFilters, ...classFilters, ...methodFilters]);
1821
+
1657
1822
  handlers.set(sub.event, {
1658
1823
  propertyKey: sub.propertyKey,
1659
1824
  paramMapping,
1660
1825
  guards,
1661
1826
  interceptors,
1827
+ filters,
1662
1828
  gatewayClass: instance.constructor,
1663
1829
  });
1664
1830
  }
@@ -1738,16 +1904,6 @@ export class CalyxApplication {
1738
1904
 
1739
1905
  const handlerInfo = gateway.handlers.get(event);
1740
1906
  if (handlerInfo) {
1741
- const args: any[] = [];
1742
- for (let i = 0; i < handlerInfo.paramMapping.length; i++) {
1743
- const type = handlerInfo.paramMapping[i];
1744
- if (type === 'body') {
1745
- args[i] = data;
1746
- } else if (type === 'socket') {
1747
- args[i] = ws;
1748
- }
1749
- }
1750
-
1751
1907
  const context = this.contextPool.acquire();
1752
1908
  context.resetContextWs(ws, data, handlerInfo.gatewayClass, gateway.instance[handlerInfo.propertyKey]);
1753
1909
 
@@ -1759,6 +1915,26 @@ export class CalyxApplication {
1759
1915
  }
1760
1916
  }
1761
1917
 
1918
+ const args: any[] = [];
1919
+ for (let i = 0; i < handlerInfo.paramMapping.length; i++) {
1920
+ const mapping = handlerInfo.paramMapping[i];
1921
+ if (!mapping) continue;
1922
+ if (mapping.type === 'body') {
1923
+ let val = mapping.name ? data?.[mapping.name] : data;
1924
+ for (const pipe of mapping.pipes || []) {
1925
+ const transformed = pipe.instance.transform(val, {
1926
+ type: 'body',
1927
+ metatype: undefined,
1928
+ data: mapping.name,
1929
+ });
1930
+ val = transformed instanceof Promise ? await transformed : transformed;
1931
+ }
1932
+ args[i] = val;
1933
+ } else if (mapping.type === 'socket') {
1934
+ args[i] = ws;
1935
+ }
1936
+ }
1937
+
1762
1938
  const nextCall = {
1763
1939
  handle: async () => {
1764
1940
  return gateway.instance[handlerInfo.propertyKey](...args);
@@ -1782,7 +1958,19 @@ export class CalyxApplication {
1782
1958
  ws.send(JSON.stringify(result));
1783
1959
  }
1784
1960
  } catch (err: any) {
1785
- // ignore or log
1961
+ if (handlerInfo.filters && handlerInfo.filters.length > 0) {
1962
+ for (const filter of handlerInfo.filters) {
1963
+ const catchException = Reflect.getMetadata('calyx:catch', filter.token) || [];
1964
+ if (catchException.length === 0 || catchException.some((exc: any) => err instanceof exc)) {
1965
+ const filterResult = filter.instance.catch(err, context);
1966
+ if (filterResult !== undefined) {
1967
+ ws.send(JSON.stringify(filterResult));
1968
+ return;
1969
+ }
1970
+ }
1971
+ }
1972
+ }
1973
+ ws.send(JSON.stringify({ event: 'error', data: err.message || 'Internal error' }));
1786
1974
  } finally {
1787
1975
  context.clearContext();
1788
1976
  this.contextPool.release(context);
@@ -1793,6 +1981,58 @@ export class CalyxApplication {
1793
1981
  }
1794
1982
  }
1795
1983
 
1984
+ private isRouteExcludedFromGlobalPrefix(path: string, method: string): boolean {
1985
+ if (!this.globalPrefixOptions?.exclude) return false;
1986
+ const cleanPath = path.replace(/^\/|\/$/g, '');
1987
+ for (const exclusion of this.globalPrefixOptions.exclude) {
1988
+ if (typeof exclusion === 'string') {
1989
+ if (exclusion.replace(/^\/|\/$/g, '') === cleanPath) return true;
1990
+ } else {
1991
+ const cleanExclPath = exclusion.path.replace(/^\/|\/$/g, '');
1992
+ const exclMethod = String(exclusion.method).toUpperCase();
1993
+ if (cleanExclPath === cleanPath && (exclMethod === 'ALL' || exclMethod === method.toUpperCase())) {
1994
+ return true;
1995
+ }
1996
+ }
1997
+ }
1998
+ return false;
1999
+ }
2000
+
2001
+ private matchDynamicHost(method: string, hostHeader: string, pathname: string): any {
2002
+ const routes = this.router.getRoutes();
2003
+ for (const route of routes) {
2004
+ if (route.method.startsWith(method + ':host-dynamic:')) {
2005
+ const hostPattern = route.method.substring((method + ':host-dynamic:').length);
2006
+ const params = this.matchHostPattern(hostPattern, hostHeader);
2007
+ if (params) {
2008
+ const matched = this.router.match(route.method, pathname);
2009
+ if (matched) {
2010
+ matched.params = { ...matched.params, ...params };
2011
+ return matched;
2012
+ }
2013
+ }
2014
+ }
2015
+ }
2016
+ return null;
2017
+ }
2018
+
2019
+ private matchHostPattern(pattern: string, host: string): Record<string, string> | null {
2020
+ const patternSegments = pattern.split('.');
2021
+ const hostSegments = host.split('.');
2022
+ if (patternSegments.length !== hostSegments.length) return null;
2023
+ const params: Record<string, string> = {};
2024
+ for (let i = 0; i < patternSegments.length; i++) {
2025
+ const pSeg = patternSegments[i];
2026
+ const hSeg = hostSegments[i];
2027
+ if (pSeg.startsWith(':')) {
2028
+ params[pSeg.slice(1)] = hSeg;
2029
+ } else if (pSeg !== hSeg) {
2030
+ return null;
2031
+ }
2032
+ }
2033
+ return params;
2034
+ }
2035
+
1796
2036
  getRoutes() {
1797
2037
  return this.router.getRoutes();
1798
2038
  }
@@ -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
 
@@ -82,6 +102,10 @@ export const Headers = (first?: any, ...pipes: any[]) => {
82
102
  return createHttpParamDecorator('headers', name, parsedPipes);
83
103
  };
84
104
 
105
+ export const Ip = () => createHttpParamDecorator('ip');
106
+ export const HostParam = (name?: string) => createHttpParamDecorator('hostparam', name);
107
+ export const Next = () => createHttpParamDecorator('next');
108
+
85
109
  export function HttpCode(code: number): MethodDecorator {
86
110
  return (target, propertyKey) => {
87
111
  Reflect.defineMetadata(METADATA_KEYS.HTTP_CODE, code, target, propertyKey);
@@ -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
+