@catbee/utils 0.0.8-rc.0 → 0.0.8-rc.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/README.md +25 -1
- package/build/esm/utils/decorators.utils.d.ts +196 -3
- package/build/esm/utils/decorators.utils.js +447 -56
- package/build/esm/utils/decorators.utils.js.map +1 -1
- package/build/esnext/utils/decorators.utils.d.ts +196 -3
- package/build/esnext/utils/decorators.utils.js +389 -16
- package/build/esnext/utils/decorators.utils.js.map +1 -1
- package/build/src/utils/decorators.utils.d.ts +196 -3
- package/build/src/utils/decorators.utils.js +397 -17
- package/build/src/utils/decorators.utils.js.map +1 -1
- package/package.json +2 -2
|
@@ -22,8 +22,11 @@
|
|
|
22
22
|
* SOFTWARE.
|
|
23
23
|
*/
|
|
24
24
|
import 'reflect-metadata';
|
|
25
|
+
import { getLogger } from './logger.utils';
|
|
26
|
+
import { createFinalErrorResponse } from './response.utils';
|
|
25
27
|
import { HttpStatusCodes } from './http-status-codes';
|
|
26
|
-
import {
|
|
28
|
+
import { rateLimit } from 'express-rate-limit';
|
|
29
|
+
import { TTLCache } from './cache.utils';
|
|
27
30
|
// Metadata keys
|
|
28
31
|
const ROUTES_KEY = Symbol('routes');
|
|
29
32
|
const MIDDLEWARE_KEY = Symbol('middlewares');
|
|
@@ -34,6 +37,65 @@ const BEFORE_KEY = Symbol('before');
|
|
|
34
37
|
const AFTER_KEY = Symbol('after');
|
|
35
38
|
const ROLES_KEY = Symbol('roles');
|
|
36
39
|
const REDIRECT_KEY = Symbol('redirect');
|
|
40
|
+
const CACHE_KEY = Symbol('cache');
|
|
41
|
+
const RATE_LIMIT_KEY = Symbol('rateLimit');
|
|
42
|
+
const CONTENT_TYPE_KEY = Symbol('contentType');
|
|
43
|
+
const VERSION_KEY = Symbol('version');
|
|
44
|
+
const TIMEOUT_KEY = Symbol('timeout');
|
|
45
|
+
const LOG_KEY = Symbol('log');
|
|
46
|
+
/**
|
|
47
|
+
* RateLimiter cache that uses TTLCache for automatic TTL and LRU handling.
|
|
48
|
+
*/
|
|
49
|
+
export class RateLimiterCache {
|
|
50
|
+
constructor(maxSize = 100, ttlMs = 5 * 60 * 1000) {
|
|
51
|
+
this.cache = new TTLCache({
|
|
52
|
+
maxSize,
|
|
53
|
+
ttlMs
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
generateKey(options) {
|
|
57
|
+
return `${options.max}:${options.windowMs}:${options.standardHeaders}:${options.legacyHeaders}`;
|
|
58
|
+
}
|
|
59
|
+
get(options) {
|
|
60
|
+
const key = this.generateKey(options);
|
|
61
|
+
const cached = this.cache.get(key);
|
|
62
|
+
if (cached) {
|
|
63
|
+
return cached.limiter;
|
|
64
|
+
}
|
|
65
|
+
// Create new limiter
|
|
66
|
+
const limiter = rateLimit(Object.assign(Object.assign({}, options), { handler: (req, res) => {
|
|
67
|
+
const errorResponse = createFinalErrorResponse(req, HttpStatusCodes.TOO_MANY_REQUESTS, 'Too Many Requests');
|
|
68
|
+
res.status(HttpStatusCodes.TOO_MANY_REQUESTS).json(errorResponse);
|
|
69
|
+
} }));
|
|
70
|
+
// Store in cache
|
|
71
|
+
this.cache.set(key, {
|
|
72
|
+
limiter,
|
|
73
|
+
config: key
|
|
74
|
+
});
|
|
75
|
+
return limiter;
|
|
76
|
+
}
|
|
77
|
+
clear() {
|
|
78
|
+
this.cache.clear();
|
|
79
|
+
}
|
|
80
|
+
size() {
|
|
81
|
+
return this.cache.size();
|
|
82
|
+
}
|
|
83
|
+
destroy() {
|
|
84
|
+
this.cache.destroy();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Global cache instance
|
|
88
|
+
const rateLimiterCache = new RateLimiterCache();
|
|
89
|
+
function normalizeHeaderValue(value) {
|
|
90
|
+
if (typeof value === 'undefined')
|
|
91
|
+
return undefined;
|
|
92
|
+
if (typeof value === 'string')
|
|
93
|
+
return value;
|
|
94
|
+
if (Array.isArray(value) && value.every(item => typeof item === 'string')) {
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
return String(value);
|
|
98
|
+
}
|
|
37
99
|
/**
|
|
38
100
|
* Factory function that creates HTTP method decorators.
|
|
39
101
|
*
|
|
@@ -296,8 +358,8 @@ export function HttpCode(status) {
|
|
|
296
358
|
/**
|
|
297
359
|
* Decorator that adds a custom HTTP header to the response.
|
|
298
360
|
*
|
|
299
|
-
* @param
|
|
300
|
-
* @param value - Header value
|
|
361
|
+
* @param header - Header name-value pairs or a single header name and value
|
|
362
|
+
* @param value - Header value if a single header name is provided
|
|
301
363
|
* @returns Method decorator
|
|
302
364
|
*
|
|
303
365
|
* @example
|
|
@@ -308,13 +370,44 @@ export function HttpCode(status) {
|
|
|
308
370
|
* // Response will include the Cache-Control header
|
|
309
371
|
* return { data: '...' };
|
|
310
372
|
* }
|
|
311
|
-
* ```
|
|
312
373
|
*/
|
|
313
374
|
export function Header(name, value) {
|
|
314
|
-
return (
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
375
|
+
return Headers(name, value);
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Decorator that adds a custom HTTP headers to the response.
|
|
379
|
+
*
|
|
380
|
+
* @param headers - Header name-value pairs or a single header name and value
|
|
381
|
+
* @param value - Header value if a single header name is provided
|
|
382
|
+
* @returns Method decorator
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* ```ts
|
|
386
|
+
* @Get('/data')
|
|
387
|
+
* @Headers('Cache-Control', 'max-age=60')
|
|
388
|
+
* getData() {
|
|
389
|
+
* // Response will include the Cache-Control header
|
|
390
|
+
* return { data: '...' };
|
|
391
|
+
* }
|
|
392
|
+
*
|
|
393
|
+
* @Get('/data/:id')
|
|
394
|
+
* @Headers({
|
|
395
|
+
* 'Cache-Control': 'max-age=60',
|
|
396
|
+
* 'X-Custom-Header': 'custom-value',
|
|
397
|
+
* 'Content-Security-Policy': "default-src 'self'"
|
|
398
|
+
* })
|
|
399
|
+
* getData() {
|
|
400
|
+
* // Response will include all specified headers
|
|
401
|
+
* return { data: '...' };
|
|
402
|
+
* }
|
|
403
|
+
*
|
|
404
|
+
* ```
|
|
405
|
+
*/
|
|
406
|
+
export function Headers(headers, value) {
|
|
407
|
+
return (target, propertyKey) => {
|
|
408
|
+
const existing = Reflect.getMetadata(HEADER_KEY, target, propertyKey) || {};
|
|
409
|
+
const newHeaders = typeof headers === 'string' ? { [headers]: value } : headers;
|
|
410
|
+
Reflect.defineMetadata(HEADER_KEY, Object.assign(Object.assign({}, existing), newHeaders), target, propertyKey);
|
|
318
411
|
};
|
|
319
412
|
}
|
|
320
413
|
/**
|
|
@@ -416,6 +509,162 @@ export function Redirect(url, statusCode = 302) {
|
|
|
416
509
|
return descriptor;
|
|
417
510
|
};
|
|
418
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* Decorator that adds caching to a route response.
|
|
514
|
+
*
|
|
515
|
+
* @param ttlSeconds - Time to live in seconds for the cache
|
|
516
|
+
* @returns Method decorator
|
|
517
|
+
*
|
|
518
|
+
* @example
|
|
519
|
+
* ```ts
|
|
520
|
+
* @Get('/data')
|
|
521
|
+
* @Cache(300) // Cache for 5 minutes
|
|
522
|
+
* getData() {
|
|
523
|
+
* return { data: 'expensive operation result' };
|
|
524
|
+
* }
|
|
525
|
+
* ```
|
|
526
|
+
*/
|
|
527
|
+
export function Cache(ttlSeconds) {
|
|
528
|
+
return (target, propertyKey, _descriptor) => {
|
|
529
|
+
Reflect.defineMetadata(CACHE_KEY, { ttlSeconds }, target, propertyKey);
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Decorator that applies rate limiting to a route.
|
|
534
|
+
* Note: Requires 'express-rate-limit' package to be installed.
|
|
535
|
+
*
|
|
536
|
+
* @param limit - Maximum number of requests allowed in the window
|
|
537
|
+
* @param windowMs - Time window in milliseconds
|
|
538
|
+
* @returns Method decorator
|
|
539
|
+
*
|
|
540
|
+
* Default Options:
|
|
541
|
+
* - standardHeaders: true
|
|
542
|
+
* - legacyHeaders: false
|
|
543
|
+
*
|
|
544
|
+
* @example
|
|
545
|
+
* ```ts
|
|
546
|
+
* @Post('/login')
|
|
547
|
+
* @RateLimit({ max: 5, windowMs: 60000, standardHeaders: true, legacyHeaders: false }) // 5 requests per minute
|
|
548
|
+
* login(@Body() credentials: LoginDto) {
|
|
549
|
+
* return this.authService.login(credentials);
|
|
550
|
+
* }
|
|
551
|
+
* ```
|
|
552
|
+
*/
|
|
553
|
+
export function RateLimit(options) {
|
|
554
|
+
const opts = Object.assign({ standardHeaders: true, legacyHeaders: false }, options);
|
|
555
|
+
return (target, propertyKey, _descriptor) => {
|
|
556
|
+
Reflect.defineMetadata(RATE_LIMIT_KEY, opts, target, propertyKey);
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Decorator that sets the content type for the response.
|
|
561
|
+
*
|
|
562
|
+
* @param type - MIME type for the response
|
|
563
|
+
* @returns Method decorator
|
|
564
|
+
*
|
|
565
|
+
* @example
|
|
566
|
+
* ```ts
|
|
567
|
+
* @Get('/download')
|
|
568
|
+
* @ContentType('application/pdf')
|
|
569
|
+
* downloadPdf() {
|
|
570
|
+
* return this.fileService.generatePdf();
|
|
571
|
+
* }
|
|
572
|
+
* ```
|
|
573
|
+
*/
|
|
574
|
+
export function ContentType(type) {
|
|
575
|
+
return (target, propertyKey, _descriptor) => {
|
|
576
|
+
Reflect.defineMetadata(CONTENT_TYPE_KEY, { type }, target, propertyKey);
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Decorator that adds API versioning to a route.
|
|
581
|
+
*
|
|
582
|
+
* @param version - Version string for the API endpoint
|
|
583
|
+
* @param options - Versioning options
|
|
584
|
+
* @returns Method decorator
|
|
585
|
+
*
|
|
586
|
+
* Default Options:
|
|
587
|
+
* - addPrefix: true
|
|
588
|
+
* - addHeader: true
|
|
589
|
+
* - headerName: 'X-API-Version'
|
|
590
|
+
*
|
|
591
|
+
* @example
|
|
592
|
+
* ```ts
|
|
593
|
+
*
|
|
594
|
+
* @Get('/users')
|
|
595
|
+
* @Version('v2')
|
|
596
|
+
* getUsersV2() {
|
|
597
|
+
* return this.userService.findAllV2();
|
|
598
|
+
* }
|
|
599
|
+
*
|
|
600
|
+
* @Get('/users')
|
|
601
|
+
* @Version('v2', { addPrefix: true, addHeader: true, headerName: 'X-API-Version' })
|
|
602
|
+
* getUsersV2() {
|
|
603
|
+
* // Route becomes /v2/users
|
|
604
|
+
* return this.userService.findAllV2();
|
|
605
|
+
* }
|
|
606
|
+
* ```
|
|
607
|
+
*/
|
|
608
|
+
export function Version(version, options) {
|
|
609
|
+
const opts = Object.assign({ addPrefix: true, addHeader: true, headerName: 'X-API-Version' }, options);
|
|
610
|
+
return (target, propertyKey, _descriptor) => {
|
|
611
|
+
Reflect.defineMetadata(VERSION_KEY, { version, options: opts }, target, propertyKey);
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Decorator that sets a timeout for route execution.
|
|
616
|
+
*
|
|
617
|
+
* @param ms - Timeout in milliseconds
|
|
618
|
+
* @returns Method decorator
|
|
619
|
+
*
|
|
620
|
+
* @example
|
|
621
|
+
* ```ts
|
|
622
|
+
* @Get('/slow-operation')
|
|
623
|
+
* @Timeout(30000) // 30 second timeout
|
|
624
|
+
* slowOperation() {
|
|
625
|
+
* return this.heavyService.processData();
|
|
626
|
+
* }
|
|
627
|
+
* ```
|
|
628
|
+
*/
|
|
629
|
+
export function Timeout(ms) {
|
|
630
|
+
return (target, propertyKey, _descriptor) => {
|
|
631
|
+
Reflect.defineMetadata(TIMEOUT_KEY, { ms }, target, propertyKey);
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Decorator that adds comprehensive logging to a route.
|
|
636
|
+
*
|
|
637
|
+
* @param options - Logging configuration options
|
|
638
|
+
* @returns Method decorator
|
|
639
|
+
*
|
|
640
|
+
* Default Options:
|
|
641
|
+
* - logEntry: true
|
|
642
|
+
* - logExit: true
|
|
643
|
+
* - logBody: false
|
|
644
|
+
* - logParams: false
|
|
645
|
+
* - logResponse: false
|
|
646
|
+
*
|
|
647
|
+
* @example
|
|
648
|
+
* ```ts
|
|
649
|
+
* @Post('/users')
|
|
650
|
+
* @Log({
|
|
651
|
+
* logEntry: true,
|
|
652
|
+
* logExit: true,
|
|
653
|
+
* logBody: true,
|
|
654
|
+
* logParams: true,
|
|
655
|
+
* logResponse: false
|
|
656
|
+
* })
|
|
657
|
+
* createUser(@Body() userData: any) {
|
|
658
|
+
* return this.userService.create(userData);
|
|
659
|
+
* }
|
|
660
|
+
* ```
|
|
661
|
+
*/
|
|
662
|
+
export function Log(options) {
|
|
663
|
+
return (target, propertyKey, _descriptor) => {
|
|
664
|
+
const config = Object.assign({ logEntry: true, logExit: true, logBody: false, logParams: false, logResponse: false }, options);
|
|
665
|
+
Reflect.defineMetadata(LOG_KEY, config, target, propertyKey);
|
|
666
|
+
};
|
|
667
|
+
}
|
|
419
668
|
/**
|
|
420
669
|
* Registers all controller classes with the provided router.
|
|
421
670
|
* This function processes all decorators and sets up the Express routes.
|
|
@@ -428,25 +677,121 @@ export function registerControllers(router, controllers) {
|
|
|
428
677
|
const instance = new ControllerClass();
|
|
429
678
|
const basePath = Reflect.getMetadata('basePath', ControllerClass) || '';
|
|
430
679
|
const routes = Reflect.getMetadata(ROUTES_KEY, ControllerClass) || [];
|
|
680
|
+
// Get controller-level decorators (fallback values)
|
|
681
|
+
const controllerRateLimit = Reflect.getMetadata(RATE_LIMIT_KEY, ControllerClass);
|
|
682
|
+
const controllerCache = Reflect.getMetadata(CACHE_KEY, ControllerClass);
|
|
683
|
+
const controllerTimeout = Reflect.getMetadata(TIMEOUT_KEY, ControllerClass);
|
|
684
|
+
const controllerVersion = Reflect.getMetadata(VERSION_KEY, ControllerClass);
|
|
685
|
+
const controllerRoles = Reflect.getMetadata(ROLES_KEY, ControllerClass);
|
|
686
|
+
const controllerLogConfig = Reflect.getMetadata(LOG_KEY, ControllerClass);
|
|
687
|
+
const controllerHeaders = Reflect.getMetadata(HEADER_KEY, ControllerClass) || {};
|
|
431
688
|
routes.forEach(({ path, method, handlerName }) => {
|
|
689
|
+
var _a;
|
|
432
690
|
const middlewares = Reflect.getMetadata(MIDDLEWARE_KEY, instance, handlerName) || [];
|
|
433
691
|
const params = Reflect.getMetadata(PARAMS_KEY, instance, handlerName) || [];
|
|
434
692
|
const httpCode = Reflect.getMetadata(HTTP_CODE_KEY, instance, handlerName);
|
|
435
|
-
|
|
693
|
+
// Merge controller-level and method-level headers
|
|
694
|
+
const methodHeaders = Reflect.getMetadata(HEADER_KEY, instance, handlerName) || {};
|
|
695
|
+
const headers = Object.assign(Object.assign({}, controllerHeaders), methodHeaders);
|
|
436
696
|
const beforeHooks = Reflect.getMetadata(BEFORE_KEY, instance, handlerName) || [];
|
|
437
697
|
const afterHooks = Reflect.getMetadata(AFTER_KEY, instance, handlerName) || [];
|
|
438
|
-
const roles = Reflect.getMetadata(ROLES_KEY, instance, handlerName) || [];
|
|
439
698
|
const redirect = Reflect.getMetadata(REDIRECT_KEY, instance, handlerName);
|
|
699
|
+
const contentType = Reflect.getMetadata(CONTENT_TYPE_KEY, instance, handlerName);
|
|
700
|
+
// Use method-level decorators if present, otherwise fall back to controller-level
|
|
701
|
+
const roles = Reflect.getMetadata(ROLES_KEY, instance, handlerName) || controllerRoles || [];
|
|
702
|
+
const cache = Reflect.getMetadata(CACHE_KEY, instance, handlerName) || controllerCache;
|
|
703
|
+
const rateLimitOptions = Reflect.getMetadata(RATE_LIMIT_KEY, instance, handlerName) || controllerRateLimit;
|
|
704
|
+
const version = Reflect.getMetadata(VERSION_KEY, instance, handlerName) || controllerVersion;
|
|
705
|
+
const timeout = Reflect.getMetadata(TIMEOUT_KEY, instance, handlerName) || controllerTimeout;
|
|
706
|
+
const logConfig = Reflect.getMetadata(LOG_KEY, instance, handlerName) || controllerLogConfig;
|
|
707
|
+
// Create rate limiter for this specific route if needed
|
|
708
|
+
let rateLimiter = null;
|
|
709
|
+
if (rateLimitOptions) {
|
|
710
|
+
try {
|
|
711
|
+
rateLimiter = rateLimiterCache.get(rateLimitOptions);
|
|
712
|
+
}
|
|
713
|
+
catch (err) {
|
|
714
|
+
getLogger().warn({ err }, 'express-rate-limit not available, skipping rate limiting for this route');
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
let finalPath = path;
|
|
718
|
+
if ((_a = version === null || version === void 0 ? void 0 : version.options) === null || _a === void 0 ? void 0 : _a.addPrefix) {
|
|
719
|
+
finalPath = `/${version.version}${path}`;
|
|
720
|
+
}
|
|
440
721
|
const handler = async (req, res, next) => {
|
|
441
722
|
var _a, _b, _c, _d;
|
|
723
|
+
// Set start time for duration tracking
|
|
724
|
+
req['startTime'] = Date.now();
|
|
725
|
+
let timeoutId;
|
|
726
|
+
let timedOut = false;
|
|
442
727
|
try {
|
|
728
|
+
// Handle timeout setup
|
|
729
|
+
if (timeout) {
|
|
730
|
+
timeoutId = setTimeout(() => {
|
|
731
|
+
if (!res.headersSent && !timedOut) {
|
|
732
|
+
timedOut = true;
|
|
733
|
+
const errorResponse = createFinalErrorResponse(req, HttpStatusCodes.REQUEST_TIMEOUT, 'Request timed out');
|
|
734
|
+
res.status(HttpStatusCodes.REQUEST_TIMEOUT).json(errorResponse);
|
|
735
|
+
}
|
|
736
|
+
}, timeout.ms);
|
|
737
|
+
}
|
|
738
|
+
if (timedOut)
|
|
739
|
+
return;
|
|
740
|
+
// Handle rate limiting
|
|
741
|
+
if (rateLimiter) {
|
|
742
|
+
await new Promise((resolve, reject) => {
|
|
743
|
+
rateLimiter(req, res, (err) => {
|
|
744
|
+
if (err)
|
|
745
|
+
reject(err);
|
|
746
|
+
else
|
|
747
|
+
resolve();
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
// Handle content type
|
|
752
|
+
if (!res.headersSent && contentType) {
|
|
753
|
+
res.setHeader('Content-Type', contentType.type);
|
|
754
|
+
}
|
|
755
|
+
// Handle versioning header
|
|
756
|
+
if (((_a = version === null || version === void 0 ? void 0 : version.options) === null || _a === void 0 ? void 0 : _a.addHeader) && ((_b = version === null || version === void 0 ? void 0 : version.options) === null || _b === void 0 ? void 0 : _b.headerName) && (version === null || version === void 0 ? void 0 : version.version)) {
|
|
757
|
+
if (!res.headersSent) {
|
|
758
|
+
res.setHeader(version.options.headerName, version.version);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// Handle caching
|
|
762
|
+
if (cache && !res.headersSent) {
|
|
763
|
+
res.setHeader('Cache-Control', `public, max-age=${cache.ttlSeconds}`);
|
|
764
|
+
}
|
|
765
|
+
// Handle logging - entry
|
|
766
|
+
if (logConfig === null || logConfig === void 0 ? void 0 : logConfig.logEntry) {
|
|
767
|
+
const logger = getLogger();
|
|
768
|
+
const logData = {
|
|
769
|
+
method: req.method,
|
|
770
|
+
url: req.originalUrl || req.url,
|
|
771
|
+
userAgent: req.get('User-Agent')
|
|
772
|
+
};
|
|
773
|
+
if (logConfig.logParams) {
|
|
774
|
+
logData.params = req.params;
|
|
775
|
+
logData.query = req.query;
|
|
776
|
+
}
|
|
777
|
+
if (logConfig.logBody)
|
|
778
|
+
logData.body = req.body;
|
|
779
|
+
logger.info({ entry: logData }, 'Route Entry:');
|
|
780
|
+
}
|
|
443
781
|
// Handle roles-based access control
|
|
444
|
-
if (roles.length && !((
|
|
445
|
-
|
|
782
|
+
if (roles.length && !((_d = (_c = req === null || req === void 0 ? void 0 : req.user) === null || _c === void 0 ? void 0 : _c.roles) === null || _d === void 0 ? void 0 : _d.some((role) => roles.includes(role)))) {
|
|
783
|
+
const errorResponse = createFinalErrorResponse(req, HttpStatusCodes.FORBIDDEN, 'Forbidden Insufficient Roles');
|
|
784
|
+
res.status(HttpStatusCodes.FORBIDDEN).json(errorResponse);
|
|
785
|
+
if (timeoutId) {
|
|
786
|
+
clearTimeout(timeoutId);
|
|
787
|
+
}
|
|
446
788
|
return;
|
|
447
789
|
}
|
|
448
790
|
// Process static redirect if configured
|
|
449
791
|
if (redirect && redirect.url) {
|
|
792
|
+
if (timeoutId) {
|
|
793
|
+
clearTimeout(timeoutId);
|
|
794
|
+
}
|
|
450
795
|
return res.redirect(redirect.statusCode, redirect.url);
|
|
451
796
|
}
|
|
452
797
|
for (const fn of beforeHooks)
|
|
@@ -477,6 +822,12 @@ export function registerControllers(router, controllers) {
|
|
|
477
822
|
const result = instance[handlerName](...args);
|
|
478
823
|
// Support both sync and async handlers
|
|
479
824
|
const awaited = result instanceof Promise ? await result : result;
|
|
825
|
+
// Clear timeout if operation completed
|
|
826
|
+
if (timeoutId) {
|
|
827
|
+
clearTimeout(timeoutId);
|
|
828
|
+
}
|
|
829
|
+
if (timedOut)
|
|
830
|
+
return;
|
|
480
831
|
// Handle dynamic redirects
|
|
481
832
|
if (redirect && awaited && typeof awaited === 'object' && 'url' in awaited) {
|
|
482
833
|
const redirectUrl = awaited.url;
|
|
@@ -485,20 +836,42 @@ export function registerControllers(router, controllers) {
|
|
|
485
836
|
}
|
|
486
837
|
if (!res.headersSent && typeof awaited !== 'undefined') {
|
|
487
838
|
if (httpCode)
|
|
488
|
-
|
|
489
|
-
for (const [k, v] of Object.entries(headers))
|
|
490
|
-
|
|
839
|
+
res.status(httpCode);
|
|
840
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
841
|
+
const normalized = normalizeHeaderValue(v);
|
|
842
|
+
if (typeof normalized !== 'undefined') {
|
|
843
|
+
res.set(k, normalized);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
491
846
|
res.json(awaited);
|
|
492
847
|
}
|
|
848
|
+
// Handle logging - exit
|
|
849
|
+
if (logConfig === null || logConfig === void 0 ? void 0 : logConfig.logExit) {
|
|
850
|
+
const logger = getLogger();
|
|
851
|
+
const logData = {
|
|
852
|
+
method: req.method,
|
|
853
|
+
url: req.originalUrl || req.url,
|
|
854
|
+
statusCode: res.statusCode,
|
|
855
|
+
duration: `${Date.now() - req.startTime}ms`
|
|
856
|
+
};
|
|
857
|
+
if (logConfig.logResponse)
|
|
858
|
+
logData.response = awaited;
|
|
859
|
+
logger.info({ exit: logData }, 'Route Exit:');
|
|
860
|
+
}
|
|
493
861
|
for (const fn of afterHooks)
|
|
494
862
|
await fn(req, res, awaited);
|
|
495
863
|
}
|
|
496
864
|
catch (err) {
|
|
865
|
+
if (timeoutId) {
|
|
866
|
+
clearTimeout(timeoutId);
|
|
867
|
+
}
|
|
497
868
|
next(err);
|
|
498
869
|
}
|
|
499
870
|
};
|
|
500
|
-
router[method](basePath +
|
|
871
|
+
router[method](basePath + finalPath, ...middlewares, handler);
|
|
501
872
|
});
|
|
502
873
|
});
|
|
503
874
|
}
|
|
875
|
+
// Export for testing purposes
|
|
876
|
+
export { rateLimiterCache };
|
|
504
877
|
//# sourceMappingURL=decorators.utils.js.map
|