@rabstack/rab-api 1.8.0 → 1.8.1-next.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/index.cjs.js +125 -6
- package/index.esm.d.ts +87 -1
- package/index.esm.js +120 -7
- package/package.json +1 -1
package/index.cjs.js
CHANGED
|
@@ -518,7 +518,92 @@ var utils = /*#__PURE__*/Object.freeze({
|
|
|
518
518
|
validateJoiSchema: validateJoiSchema
|
|
519
519
|
});
|
|
520
520
|
|
|
521
|
+
const DEFAULT_TTL = 900; // 15 minutes
|
|
522
|
+
const DEFAULT_STRATEGY = 'url-params';
|
|
523
|
+
/**
|
|
524
|
+
* Normalize cache config - handles `cache: true` shorthand
|
|
525
|
+
*/ function normalizeCacheConfig(cache, defaultTtl) {
|
|
526
|
+
if (!cache) return null;
|
|
527
|
+
if (cache === true) {
|
|
528
|
+
return {
|
|
529
|
+
ttl: defaultTtl != null ? defaultTtl : DEFAULT_TTL,
|
|
530
|
+
strategy: DEFAULT_STRATEGY
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
var _cache_ttl, _ref, _cache_strategy;
|
|
534
|
+
return {
|
|
535
|
+
ttl: (_ref = (_cache_ttl = cache.ttl) != null ? _cache_ttl : defaultTtl) != null ? _ref : DEFAULT_TTL,
|
|
536
|
+
strategy: (_cache_strategy = cache.strategy) != null ? _cache_strategy : DEFAULT_STRATEGY,
|
|
537
|
+
invalidates: cache.invalidates
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Build cache key from request based on strategy
|
|
542
|
+
* - url-params: resolved path only (e.g., /stores/123/categories)
|
|
543
|
+
* - url-query: resolved path + sorted query (e.g., /stores/123/categories?lang=en&page=1)
|
|
544
|
+
*/ function buildCacheKey(req, strategy) {
|
|
545
|
+
var _req_originalUrl;
|
|
546
|
+
// Get the resolved path (with params filled in)
|
|
547
|
+
const path = ((_req_originalUrl = req.originalUrl) == null ? void 0 : _req_originalUrl.split('?')[0]) || req.path;
|
|
548
|
+
// url-params: just the path
|
|
549
|
+
if (strategy === 'url-params') {
|
|
550
|
+
return path;
|
|
551
|
+
}
|
|
552
|
+
// url-query: path + sorted query string
|
|
553
|
+
const query = req.query;
|
|
554
|
+
if (query && Object.keys(query).length > 0) {
|
|
555
|
+
const sortedQuery = Object.keys(query).sort().map((key)=>`${key}=${query[key]}`).join('&');
|
|
556
|
+
return `${path}?${sortedQuery}`;
|
|
557
|
+
}
|
|
558
|
+
return path;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Resolve :param placeholders in a pattern using request params
|
|
562
|
+
*/ function resolvePattern(pattern, params) {
|
|
563
|
+
return pattern.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, paramName)=>{
|
|
564
|
+
const value = params[paramName];
|
|
565
|
+
if (value === undefined) {
|
|
566
|
+
// Keep original if param not found (might be a wildcard pattern)
|
|
567
|
+
return `:${paramName}`;
|
|
568
|
+
}
|
|
569
|
+
return value;
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Resolve all invalidation patterns for a request
|
|
574
|
+
*/ function resolveInvalidationPatterns(patterns, req) {
|
|
575
|
+
const resolved = [];
|
|
576
|
+
for (const pattern of patterns){
|
|
577
|
+
if (typeof pattern === 'function') {
|
|
578
|
+
const result = pattern(req);
|
|
579
|
+
if (Array.isArray(result)) {
|
|
580
|
+
resolved.push(...result);
|
|
581
|
+
} else {
|
|
582
|
+
resolved.push(result);
|
|
583
|
+
}
|
|
584
|
+
} else {
|
|
585
|
+
resolved.push(resolvePattern(pattern, req.params || {}));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return resolved;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Check if cache should be applied (has ttl, not just invalidation)
|
|
592
|
+
*/ function shouldCache(config) {
|
|
593
|
+
return config.ttl !== undefined && config.ttl > 0;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Check if there are invalidation patterns
|
|
597
|
+
*/ function hasInvalidations(config) {
|
|
598
|
+
return Array.isArray(config.invalidates) && config.invalidates.length > 0;
|
|
599
|
+
}
|
|
600
|
+
|
|
521
601
|
const controllerHandler = (controller, config)=>{
|
|
602
|
+
// Normalize cache config once at setup time
|
|
603
|
+
const cacheConfig = normalizeCacheConfig(config.cache, config.cacheDefaultTtl);
|
|
604
|
+
const cacheAdapter = config.cacheAdapter;
|
|
605
|
+
const isCacheEnabled = cacheConfig && cacheAdapter && shouldCache(cacheConfig);
|
|
606
|
+
const hasInvalidationPatterns = cacheConfig && hasInvalidations(cacheConfig);
|
|
522
607
|
return async (req, res, next)=>{
|
|
523
608
|
try {
|
|
524
609
|
let query = req.query;
|
|
@@ -529,19 +614,45 @@ const controllerHandler = (controller, config)=>{
|
|
|
529
614
|
query = await config.validateQuery(req.query);
|
|
530
615
|
}
|
|
531
616
|
}
|
|
617
|
+
// Check cache before executing handler
|
|
618
|
+
if (isCacheEnabled) {
|
|
619
|
+
const cacheKey = buildCacheKey(req, cacheConfig.strategy);
|
|
620
|
+
const cached = await cacheAdapter.get(cacheKey);
|
|
621
|
+
if (cached) {
|
|
622
|
+
return res.status(200).json(cached);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
532
625
|
const response = await controller.handler(_extends({}, req, {
|
|
533
626
|
query,
|
|
534
627
|
body: req.body
|
|
535
628
|
}));
|
|
536
|
-
var _response_statusCode
|
|
537
|
-
|
|
629
|
+
var _response_statusCode;
|
|
630
|
+
const jsonResponse = response.excludeMetaData ? response.data : _extends({
|
|
538
631
|
message: 'successful',
|
|
539
|
-
statusCode: (
|
|
632
|
+
statusCode: (_response_statusCode = response.statusCode) != null ? _response_statusCode : 200
|
|
540
633
|
}, response.data ? {
|
|
541
634
|
data: response.data
|
|
542
635
|
} : {
|
|
543
636
|
data: response
|
|
544
|
-
})
|
|
637
|
+
});
|
|
638
|
+
// Cache the response after successful execution
|
|
639
|
+
if (isCacheEnabled) {
|
|
640
|
+
const cacheKey = buildCacheKey(req, cacheConfig.strategy);
|
|
641
|
+
// Don't await - cache in background
|
|
642
|
+
cacheAdapter.set(cacheKey, jsonResponse, cacheConfig.ttl).catch(()=>{
|
|
643
|
+
// Silently ignore cache errors
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
// Handle invalidations after successful mutation
|
|
647
|
+
if (hasInvalidationPatterns && cacheAdapter) {
|
|
648
|
+
const patterns = resolveInvalidationPatterns(cacheConfig.invalidates, req);
|
|
649
|
+
// Invalidate in background
|
|
650
|
+
Promise.all(patterns.map((pattern)=>pattern.includes('*') ? cacheAdapter.delPattern(pattern) : cacheAdapter.del(pattern))).catch(()=>{
|
|
651
|
+
// Silently ignore invalidation errors
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
var _response_statusCode1;
|
|
655
|
+
return res.status((_response_statusCode1 = response.statusCode) != null ? _response_statusCode1 : 200).json(jsonResponse);
|
|
545
656
|
} catch (error) {
|
|
546
657
|
if (error instanceof RabApiError) {
|
|
547
658
|
return next(new RabApiError(error.message, error.statusCode, error.errorCode, error.errors));
|
|
@@ -984,11 +1095,13 @@ class AtomExpressApp {
|
|
|
984
1095
|
var _options_auth;
|
|
985
1096
|
this.collectedRoutes = [];
|
|
986
1097
|
this.setupRouteController = (expressRouter, route, parentOptions)=>{
|
|
987
|
-
var _config_docs;
|
|
1098
|
+
var _this_options_cache, _this_options_cache1, _config_docs;
|
|
988
1099
|
const metaData = retrieveRouteMetaData(route);
|
|
989
1100
|
const { method, path, options } = metaData;
|
|
990
1101
|
const config = _extends({}, parentOptions, options, {
|
|
991
|
-
enforceBodyValidation: this.options.enforceBodyValidation
|
|
1102
|
+
enforceBodyValidation: this.options.enforceBodyValidation,
|
|
1103
|
+
cacheAdapter: (_this_options_cache = this.options.cache) == null ? void 0 : _this_options_cache.adapter,
|
|
1104
|
+
cacheDefaultTtl: (_this_options_cache1 = this.options.cache) == null ? void 0 : _this_options_cache1.defaultTtl
|
|
992
1105
|
});
|
|
993
1106
|
if (method == 'get' && config.querySchema && !config.validateQuery) {
|
|
994
1107
|
const querySchema = config.querySchema;
|
|
@@ -1101,8 +1214,14 @@ exports.UnauthorizedException = UnauthorizedException;
|
|
|
1101
1214
|
exports.UnprocessableEntityException = UnprocessableEntityException;
|
|
1102
1215
|
exports.authHandler = authHandler;
|
|
1103
1216
|
exports.bodyValidatorWithContext = bodyValidatorWithContext;
|
|
1217
|
+
exports.buildCacheKey = buildCacheKey;
|
|
1104
1218
|
exports.controllerHandler = controllerHandler;
|
|
1105
1219
|
exports.errorHandler = errorHandler;
|
|
1106
1220
|
exports.extractTokenFromHeader = extractTokenFromHeader;
|
|
1221
|
+
exports.hasInvalidations = hasInvalidations;
|
|
1107
1222
|
exports.isCallBackPipe = isCallBackPipe;
|
|
1108
1223
|
exports.isRouteAController = isRouteAController;
|
|
1224
|
+
exports.normalizeCacheConfig = normalizeCacheConfig;
|
|
1225
|
+
exports.resolveInvalidationPatterns = resolveInvalidationPatterns;
|
|
1226
|
+
exports.resolvePattern = resolvePattern;
|
|
1227
|
+
exports.shouldCache = shouldCache;
|
package/index.esm.d.ts
CHANGED
|
@@ -97,6 +97,8 @@ export declare type AtomExpressOptions = {
|
|
|
97
97
|
}>;
|
|
98
98
|
}>;
|
|
99
99
|
};
|
|
100
|
+
/** Global cache configuration */
|
|
101
|
+
cache?: RabApiCacheOptions;
|
|
100
102
|
};
|
|
101
103
|
|
|
102
104
|
export declare namespace AtomHelpers {
|
|
@@ -152,10 +154,36 @@ export declare class BadRequestException extends RabApiError {
|
|
|
152
154
|
|
|
153
155
|
export declare const bodyValidatorWithContext: (config: RouteHandleOptions) => (req: any, res: Response_2, next: NextFunction) => Promise<void>;
|
|
154
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Build cache key from request based on strategy
|
|
159
|
+
* - url-params: resolved path only (e.g., /stores/123/categories)
|
|
160
|
+
* - url-query: resolved path + sorted query (e.g., /stores/123/categories?lang=en&page=1)
|
|
161
|
+
*/
|
|
162
|
+
export declare function buildCacheKey(req: any, strategy: CacheStrategy): string;
|
|
163
|
+
|
|
155
164
|
export declare type BuildRouterProps = CreateRouterProps & {
|
|
156
165
|
fullPath?: string;
|
|
157
166
|
};
|
|
158
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Cache configuration for route decorators
|
|
170
|
+
*/
|
|
171
|
+
export declare interface CacheConfig {
|
|
172
|
+
/** Time-to-live in seconds (default: 900 = 15 minutes) */
|
|
173
|
+
ttl?: number;
|
|
174
|
+
/** Key derivation strategy (default: 'url-params') */
|
|
175
|
+
strategy?: CacheStrategy;
|
|
176
|
+
/** URL patterns to invalidate after successful response */
|
|
177
|
+
invalidates?: InvalidationPattern[];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Cache key strategy
|
|
182
|
+
* - 'url-params': Uses resolved URL path (e.g., /stores/123/categories)
|
|
183
|
+
* - 'url-query': Uses resolved URL path + sorted query string
|
|
184
|
+
*/
|
|
185
|
+
export declare type CacheStrategy = 'url-params' | 'url-query';
|
|
186
|
+
|
|
159
187
|
export declare interface CollectedRoute {
|
|
160
188
|
method: string;
|
|
161
189
|
path: string;
|
|
@@ -425,11 +453,26 @@ export declare function Get(path: string, options?: ControllerRouteDefinitionOpt
|
|
|
425
453
|
*/
|
|
426
454
|
export declare type GetController<TResponse, TQuery = any, TParams = any, TUser = any> = IControllerClass<TQuery, any, TResponse, TParams, TUser>;
|
|
427
455
|
|
|
456
|
+
/**
|
|
457
|
+
* Check if there are invalidation patterns
|
|
458
|
+
*/
|
|
459
|
+
export declare function hasInvalidations(config: CacheConfig): boolean;
|
|
460
|
+
|
|
428
461
|
/**
|
|
429
462
|
* Supported HTTP methods for route decorators.
|
|
430
463
|
*/
|
|
431
464
|
export declare type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
432
465
|
|
|
466
|
+
/**
|
|
467
|
+
* Cache adapter interface - implement this to provide your own cache backend (Redis, Memory, etc.)
|
|
468
|
+
*/
|
|
469
|
+
export declare interface ICacheAdapter {
|
|
470
|
+
get<T>(key: string): Promise<T | null>;
|
|
471
|
+
set<T>(key: string, data: T, ttlSeconds: number): Promise<void>;
|
|
472
|
+
del(key: string): Promise<void>;
|
|
473
|
+
delPattern(pattern: string): Promise<void>;
|
|
474
|
+
}
|
|
475
|
+
|
|
433
476
|
/**
|
|
434
477
|
* Controller class type definition.
|
|
435
478
|
* Used to define the type shape for controller implementations.
|
|
@@ -495,6 +538,8 @@ export declare type InjectorsMetadata<TQuery = any, TBody = any, TResponse = any
|
|
|
495
538
|
querySchema?: Joi.ObjectSchema<TParams>;
|
|
496
539
|
excludeFromDocs?: boolean;
|
|
497
540
|
docs?: OpenApiDocsMetadata;
|
|
541
|
+
/** Cache configuration - use `true` for defaults or provide config object */
|
|
542
|
+
cache?: true | CacheConfig;
|
|
498
543
|
};
|
|
499
544
|
|
|
500
545
|
/**
|
|
@@ -514,6 +559,12 @@ export declare class InternalServerErrorException extends RabApiError {
|
|
|
514
559
|
constructor(message?: string, errorCode?: string);
|
|
515
560
|
}
|
|
516
561
|
|
|
562
|
+
/**
|
|
563
|
+
* Invalidation pattern - either a URL pattern string or a function that returns pattern(s)
|
|
564
|
+
* URL patterns with :param placeholders are auto-resolved from request params
|
|
565
|
+
*/
|
|
566
|
+
export declare type InvalidationPattern = string | ((req: any) => string | string[]);
|
|
567
|
+
|
|
517
568
|
export declare const isCallBackPipe: (pipe: PipeFn | PipeFnCallBack) => pipe is PipeFnCallBack;
|
|
518
569
|
|
|
519
570
|
export declare function isRouteAController(value: AppRoute): value is ControllerClassType;
|
|
@@ -553,6 +604,11 @@ export declare class MethodNotAllowedException extends RabApiError {
|
|
|
553
604
|
constructor(message?: string, errorCode?: string);
|
|
554
605
|
}
|
|
555
606
|
|
|
607
|
+
/**
|
|
608
|
+
* Normalize cache config - handles `cache: true` shorthand
|
|
609
|
+
*/
|
|
610
|
+
export declare function normalizeCacheConfig(cache: true | CacheConfig | undefined, defaultTtl?: number): CacheConfig | null;
|
|
611
|
+
|
|
556
612
|
/**
|
|
557
613
|
* Represents a 404 Not Found error.
|
|
558
614
|
* Use this when a requested resource does not exist.
|
|
@@ -775,6 +831,16 @@ export declare class RabApi {
|
|
|
775
831
|
static createApp(props: AtomExpressOptions): AtomExpressApp;
|
|
776
832
|
}
|
|
777
833
|
|
|
834
|
+
/**
|
|
835
|
+
* Global cache configuration for RabApi
|
|
836
|
+
*/
|
|
837
|
+
export declare interface RabApiCacheOptions {
|
|
838
|
+
/** Cache adapter implementation (Redis, Memory, etc.) */
|
|
839
|
+
adapter: ICacheAdapter;
|
|
840
|
+
/** Default TTL in seconds (default: 900 = 15 minutes) */
|
|
841
|
+
defaultTtl?: number;
|
|
842
|
+
}
|
|
843
|
+
|
|
778
844
|
/**
|
|
779
845
|
* Base controller interface that all HTTP method controllers implement.
|
|
780
846
|
* Defines the contract for request handlers.
|
|
@@ -877,9 +943,24 @@ export declare class RequestTimeoutException extends RabApiError {
|
|
|
877
943
|
constructor(message?: string, errorCode?: string);
|
|
878
944
|
}
|
|
879
945
|
|
|
946
|
+
/**
|
|
947
|
+
* Resolve all invalidation patterns for a request
|
|
948
|
+
*/
|
|
949
|
+
export declare function resolveInvalidationPatterns(patterns: InvalidationPattern[], req: any): string[];
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Resolve :param placeholders in a pattern using request params
|
|
953
|
+
*/
|
|
954
|
+
export declare function resolvePattern(pattern: string, params: Record<string, string>): string;
|
|
955
|
+
|
|
880
956
|
declare function retrieveRouteMetaData(route: ControllerClassType): ControllerRouteDefinition;
|
|
881
957
|
|
|
882
|
-
export declare type RouteHandleOptions = InjectorsMetadata & Pick<AtomExpressOptions, 'enforceBodyValidation'
|
|
958
|
+
export declare type RouteHandleOptions = InjectorsMetadata & Pick<AtomExpressOptions, 'enforceBodyValidation'> & {
|
|
959
|
+
/** Resolved cache adapter from global options */
|
|
960
|
+
cacheAdapter?: ICacheAdapter;
|
|
961
|
+
/** Default TTL from global options */
|
|
962
|
+
cacheDefaultTtl?: number;
|
|
963
|
+
};
|
|
883
964
|
|
|
884
965
|
/**
|
|
885
966
|
* Represents a 503 Service Unavailable error.
|
|
@@ -898,6 +979,11 @@ export declare class ServiceUnavailableException extends RabApiError {
|
|
|
898
979
|
constructor(message?: string, errorCode?: string);
|
|
899
980
|
}
|
|
900
981
|
|
|
982
|
+
/**
|
|
983
|
+
* Check if cache should be applied (has ttl, not just invalidation)
|
|
984
|
+
*/
|
|
985
|
+
export declare function shouldCache(config: CacheConfig): boolean;
|
|
986
|
+
|
|
901
987
|
/**
|
|
902
988
|
* Represents a 429 Too Many Requests error.
|
|
903
989
|
* Use this when rate limiting is exceeded.
|
package/index.esm.js
CHANGED
|
@@ -516,7 +516,92 @@ var utils = /*#__PURE__*/Object.freeze({
|
|
|
516
516
|
validateJoiSchema: validateJoiSchema
|
|
517
517
|
});
|
|
518
518
|
|
|
519
|
+
const DEFAULT_TTL = 900; // 15 minutes
|
|
520
|
+
const DEFAULT_STRATEGY = 'url-params';
|
|
521
|
+
/**
|
|
522
|
+
* Normalize cache config - handles `cache: true` shorthand
|
|
523
|
+
*/ function normalizeCacheConfig(cache, defaultTtl) {
|
|
524
|
+
if (!cache) return null;
|
|
525
|
+
if (cache === true) {
|
|
526
|
+
return {
|
|
527
|
+
ttl: defaultTtl != null ? defaultTtl : DEFAULT_TTL,
|
|
528
|
+
strategy: DEFAULT_STRATEGY
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
var _cache_ttl, _ref, _cache_strategy;
|
|
532
|
+
return {
|
|
533
|
+
ttl: (_ref = (_cache_ttl = cache.ttl) != null ? _cache_ttl : defaultTtl) != null ? _ref : DEFAULT_TTL,
|
|
534
|
+
strategy: (_cache_strategy = cache.strategy) != null ? _cache_strategy : DEFAULT_STRATEGY,
|
|
535
|
+
invalidates: cache.invalidates
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Build cache key from request based on strategy
|
|
540
|
+
* - url-params: resolved path only (e.g., /stores/123/categories)
|
|
541
|
+
* - url-query: resolved path + sorted query (e.g., /stores/123/categories?lang=en&page=1)
|
|
542
|
+
*/ function buildCacheKey(req, strategy) {
|
|
543
|
+
var _req_originalUrl;
|
|
544
|
+
// Get the resolved path (with params filled in)
|
|
545
|
+
const path = ((_req_originalUrl = req.originalUrl) == null ? void 0 : _req_originalUrl.split('?')[0]) || req.path;
|
|
546
|
+
// url-params: just the path
|
|
547
|
+
if (strategy === 'url-params') {
|
|
548
|
+
return path;
|
|
549
|
+
}
|
|
550
|
+
// url-query: path + sorted query string
|
|
551
|
+
const query = req.query;
|
|
552
|
+
if (query && Object.keys(query).length > 0) {
|
|
553
|
+
const sortedQuery = Object.keys(query).sort().map((key)=>`${key}=${query[key]}`).join('&');
|
|
554
|
+
return `${path}?${sortedQuery}`;
|
|
555
|
+
}
|
|
556
|
+
return path;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Resolve :param placeholders in a pattern using request params
|
|
560
|
+
*/ function resolvePattern(pattern, params) {
|
|
561
|
+
return pattern.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, paramName)=>{
|
|
562
|
+
const value = params[paramName];
|
|
563
|
+
if (value === undefined) {
|
|
564
|
+
// Keep original if param not found (might be a wildcard pattern)
|
|
565
|
+
return `:${paramName}`;
|
|
566
|
+
}
|
|
567
|
+
return value;
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Resolve all invalidation patterns for a request
|
|
572
|
+
*/ function resolveInvalidationPatterns(patterns, req) {
|
|
573
|
+
const resolved = [];
|
|
574
|
+
for (const pattern of patterns){
|
|
575
|
+
if (typeof pattern === 'function') {
|
|
576
|
+
const result = pattern(req);
|
|
577
|
+
if (Array.isArray(result)) {
|
|
578
|
+
resolved.push(...result);
|
|
579
|
+
} else {
|
|
580
|
+
resolved.push(result);
|
|
581
|
+
}
|
|
582
|
+
} else {
|
|
583
|
+
resolved.push(resolvePattern(pattern, req.params || {}));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return resolved;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Check if cache should be applied (has ttl, not just invalidation)
|
|
590
|
+
*/ function shouldCache(config) {
|
|
591
|
+
return config.ttl !== undefined && config.ttl > 0;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Check if there are invalidation patterns
|
|
595
|
+
*/ function hasInvalidations(config) {
|
|
596
|
+
return Array.isArray(config.invalidates) && config.invalidates.length > 0;
|
|
597
|
+
}
|
|
598
|
+
|
|
519
599
|
const controllerHandler = (controller, config)=>{
|
|
600
|
+
// Normalize cache config once at setup time
|
|
601
|
+
const cacheConfig = normalizeCacheConfig(config.cache, config.cacheDefaultTtl);
|
|
602
|
+
const cacheAdapter = config.cacheAdapter;
|
|
603
|
+
const isCacheEnabled = cacheConfig && cacheAdapter && shouldCache(cacheConfig);
|
|
604
|
+
const hasInvalidationPatterns = cacheConfig && hasInvalidations(cacheConfig);
|
|
520
605
|
return async (req, res, next)=>{
|
|
521
606
|
try {
|
|
522
607
|
let query = req.query;
|
|
@@ -527,19 +612,45 @@ const controllerHandler = (controller, config)=>{
|
|
|
527
612
|
query = await config.validateQuery(req.query);
|
|
528
613
|
}
|
|
529
614
|
}
|
|
615
|
+
// Check cache before executing handler
|
|
616
|
+
if (isCacheEnabled) {
|
|
617
|
+
const cacheKey = buildCacheKey(req, cacheConfig.strategy);
|
|
618
|
+
const cached = await cacheAdapter.get(cacheKey);
|
|
619
|
+
if (cached) {
|
|
620
|
+
return res.status(200).json(cached);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
530
623
|
const response = await controller.handler(_extends({}, req, {
|
|
531
624
|
query,
|
|
532
625
|
body: req.body
|
|
533
626
|
}));
|
|
534
|
-
var _response_statusCode
|
|
535
|
-
|
|
627
|
+
var _response_statusCode;
|
|
628
|
+
const jsonResponse = response.excludeMetaData ? response.data : _extends({
|
|
536
629
|
message: 'successful',
|
|
537
|
-
statusCode: (
|
|
630
|
+
statusCode: (_response_statusCode = response.statusCode) != null ? _response_statusCode : 200
|
|
538
631
|
}, response.data ? {
|
|
539
632
|
data: response.data
|
|
540
633
|
} : {
|
|
541
634
|
data: response
|
|
542
|
-
})
|
|
635
|
+
});
|
|
636
|
+
// Cache the response after successful execution
|
|
637
|
+
if (isCacheEnabled) {
|
|
638
|
+
const cacheKey = buildCacheKey(req, cacheConfig.strategy);
|
|
639
|
+
// Don't await - cache in background
|
|
640
|
+
cacheAdapter.set(cacheKey, jsonResponse, cacheConfig.ttl).catch(()=>{
|
|
641
|
+
// Silently ignore cache errors
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
// Handle invalidations after successful mutation
|
|
645
|
+
if (hasInvalidationPatterns && cacheAdapter) {
|
|
646
|
+
const patterns = resolveInvalidationPatterns(cacheConfig.invalidates, req);
|
|
647
|
+
// Invalidate in background
|
|
648
|
+
Promise.all(patterns.map((pattern)=>pattern.includes('*') ? cacheAdapter.delPattern(pattern) : cacheAdapter.del(pattern))).catch(()=>{
|
|
649
|
+
// Silently ignore invalidation errors
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
var _response_statusCode1;
|
|
653
|
+
return res.status((_response_statusCode1 = response.statusCode) != null ? _response_statusCode1 : 200).json(jsonResponse);
|
|
543
654
|
} catch (error) {
|
|
544
655
|
if (error instanceof RabApiError) {
|
|
545
656
|
return next(new RabApiError(error.message, error.statusCode, error.errorCode, error.errors));
|
|
@@ -982,11 +1093,13 @@ class AtomExpressApp {
|
|
|
982
1093
|
var _options_auth;
|
|
983
1094
|
this.collectedRoutes = [];
|
|
984
1095
|
this.setupRouteController = (expressRouter, route, parentOptions)=>{
|
|
985
|
-
var _config_docs;
|
|
1096
|
+
var _this_options_cache, _this_options_cache1, _config_docs;
|
|
986
1097
|
const metaData = retrieveRouteMetaData(route);
|
|
987
1098
|
const { method, path, options } = metaData;
|
|
988
1099
|
const config = _extends({}, parentOptions, options, {
|
|
989
|
-
enforceBodyValidation: this.options.enforceBodyValidation
|
|
1100
|
+
enforceBodyValidation: this.options.enforceBodyValidation,
|
|
1101
|
+
cacheAdapter: (_this_options_cache = this.options.cache) == null ? void 0 : _this_options_cache.adapter,
|
|
1102
|
+
cacheDefaultTtl: (_this_options_cache1 = this.options.cache) == null ? void 0 : _this_options_cache1.defaultTtl
|
|
990
1103
|
});
|
|
991
1104
|
if (method == 'get' && config.querySchema && !config.validateQuery) {
|
|
992
1105
|
const querySchema = config.querySchema;
|
|
@@ -1070,4 +1183,4 @@ class AtomExpressApp {
|
|
|
1070
1183
|
}
|
|
1071
1184
|
}
|
|
1072
1185
|
|
|
1073
|
-
export { AtomExpressApp, utils as AtomHelpers, AtomRoute, BadRequestException, CONTROLLER_ROUTE_KEY, ConflictException, Controller, Delete, DiContainer, ForbiddenException, Get, Injectable, InternalServerErrorException, MethodNotAllowedException, NotFoundException, OpenApiGenerator, Patch, PayloadTooLargeException, Post, Put, RabApi, RabApiError, RequestTimeoutException, ServiceUnavailableException, TooManyRequestsException, UnauthorizedException, UnprocessableEntityException, authHandler, bodyValidatorWithContext, controllerHandler, errorHandler, extractTokenFromHeader, isCallBackPipe, isRouteAController };
|
|
1186
|
+
export { AtomExpressApp, utils as AtomHelpers, AtomRoute, BadRequestException, CONTROLLER_ROUTE_KEY, ConflictException, Controller, Delete, DiContainer, ForbiddenException, Get, Injectable, InternalServerErrorException, MethodNotAllowedException, NotFoundException, OpenApiGenerator, Patch, PayloadTooLargeException, Post, Put, RabApi, RabApiError, RequestTimeoutException, ServiceUnavailableException, TooManyRequestsException, UnauthorizedException, UnprocessableEntityException, authHandler, bodyValidatorWithContext, buildCacheKey, controllerHandler, errorHandler, extractTokenFromHeader, hasInvalidations, isCallBackPipe, isRouteAController, normalizeCacheConfig, resolveInvalidationPatterns, resolvePattern, shouldCache };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rabstack/rab-api",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.1-next.1",
|
|
4
4
|
"description": "A TypeScript REST API framework built on Express.js with decorator-based routing, dependency injection, and built-in validation",
|
|
5
5
|
"author": "Softin",
|
|
6
6
|
"license": "MIT",
|