@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.
- package/CHANGELOG.md +15 -0
- package/package.json +1 -1
- package/src/cache/cache.interceptor.ts +4 -2
- package/src/cache/decorators.ts +4 -0
- package/src/cache/index.ts +1 -0
- 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/core/container.ts +559 -140
- package/src/core/index.ts +2 -0
- package/src/core/lazy-module-loader.ts +29 -0
- package/src/core/metadata.ts +6 -1
- package/src/core/testing-module.ts +123 -0
- package/src/cqrs/cqrs.ts +264 -0
- 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/decorators.ts +16 -0
- package/src/graphql/graphql.module.ts +16 -0
- package/src/http/application.ts +261 -21
- package/src/http/decorators.ts +25 -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 +3 -0
- package/src/microservices/clients.module.ts +47 -0
- package/src/microservices/exceptions.ts +10 -0
- package/src/microservices/index.ts +2 -0
- package/src/microservices/microservice.ts +1 -1
- package/src/queue/queue.module.ts +73 -5
- package/src/schedule/decorators.ts +10 -6
- package/src/schedule/index.ts +1 -0
- package/src/schedule/schedule.module.ts +3 -2
- package/src/schedule/scheduler-registry.ts +50 -0
- package/src/security/index.ts +1 -0
- package/src/security/throttler.module.ts +108 -0
- package/src/terminus/terminus.ts +134 -0
- package/src/validation/compiler.ts +133 -10
- package/src/validation/decorators.ts +164 -2
- package/src/validation/http-pipes.ts +128 -0
- package/src/validation/index.ts +1 -0
- package/src/websockets/decorators.ts +12 -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 +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
|
|
package/src/http/application.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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] =
|
|
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] =
|
|
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
|
-
|
|
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
|
}
|
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
|
|
|
@@ -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);
|
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
|
+
|