@rabstack/rab-api 1.9.0 → 1.10.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/README.md +108 -0
- package/index.cjs.js +20 -19
- package/index.esm.d.ts +15 -16
- package/index.esm.js +19 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ A TypeScript REST API framework built on Express.js with decorator-based routing
|
|
|
9
9
|
- ✅ Request validation with Joi schemas
|
|
10
10
|
- 💉 Dependency injection (TypeDI)
|
|
11
11
|
- 🔐 Role-based access control
|
|
12
|
+
- ⚡ Response caching with purge support
|
|
12
13
|
- 📝 Full TypeScript type safety
|
|
13
14
|
- 🚀 Production-ready
|
|
14
15
|
|
|
@@ -305,6 +306,113 @@ app.route({
|
|
|
305
306
|
});
|
|
306
307
|
```
|
|
307
308
|
|
|
309
|
+
## Caching
|
|
310
|
+
|
|
311
|
+
Built-in response caching with cache invalidation (purge) support.
|
|
312
|
+
|
|
313
|
+
### Setup
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
const app = RabApi.createApp({
|
|
317
|
+
cache: {
|
|
318
|
+
adapter: myCacheAdapter, // Implement ICacheAdapter
|
|
319
|
+
defaultTtl: 900, // 15 minutes default
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Cache Adapter Interface
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
interface ICacheAdapter {
|
|
328
|
+
get<T>(key: string): Promise<T | null>;
|
|
329
|
+
set<T>(key: string, data: T, ttlSeconds: number): Promise<void>;
|
|
330
|
+
del(key: string): Promise<void>;
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Enable Caching
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
// Simple - uses defaults (ttl: 900s, strategy: url-query)
|
|
338
|
+
@Get('/products', { cache: true })
|
|
339
|
+
export class ListProducts {}
|
|
340
|
+
|
|
341
|
+
// With options
|
|
342
|
+
@Get('/products', {
|
|
343
|
+
cache: {
|
|
344
|
+
ttl: 600, // 10 minutes
|
|
345
|
+
strategy: 'url-query', // Include query params in cache key
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
export class ListProducts {}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Cache Strategies
|
|
352
|
+
|
|
353
|
+
- **`url-query`** (default): Cache key includes path + sorted query params
|
|
354
|
+
- `/products?page=1&limit=10` → key: `/products?limit=10&page=1`
|
|
355
|
+
- **`url-params`**: Cache key is just the resolved path
|
|
356
|
+
- `/products?page=1&limit=10` → key: `/products`
|
|
357
|
+
|
|
358
|
+
### Cache Purge
|
|
359
|
+
|
|
360
|
+
Purge cache keys after mutations:
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
@Post('/products', {
|
|
364
|
+
cache: {
|
|
365
|
+
purge: ['/products'], // Purge this key after successful response
|
|
366
|
+
}
|
|
367
|
+
})
|
|
368
|
+
export class CreateProduct {}
|
|
369
|
+
|
|
370
|
+
// With dynamic params
|
|
371
|
+
@Put('/products/:id', {
|
|
372
|
+
cache: {
|
|
373
|
+
purge: [
|
|
374
|
+
'/products',
|
|
375
|
+
'/products/:id', // :id is resolved from request params
|
|
376
|
+
]
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
export class UpdateProduct {}
|
|
380
|
+
|
|
381
|
+
// Function-based purge
|
|
382
|
+
@Delete('/products/:id', {
|
|
383
|
+
cache: {
|
|
384
|
+
purge: [(req) => `/products/${req.params.id}`]
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
export class DeleteProduct {}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Example: Redis Adapter
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
import Redis from 'ioredis';
|
|
394
|
+
import { ICacheAdapter } from 'rab-api';
|
|
395
|
+
|
|
396
|
+
const redis = new Redis();
|
|
397
|
+
|
|
398
|
+
const redisCacheAdapter: ICacheAdapter = {
|
|
399
|
+
async get(key) {
|
|
400
|
+
const data = await redis.get(key);
|
|
401
|
+
return data ? JSON.parse(data) : null;
|
|
402
|
+
},
|
|
403
|
+
async set(key, data, ttl) {
|
|
404
|
+
await redis.setex(key, ttl, JSON.stringify(data));
|
|
405
|
+
},
|
|
406
|
+
async del(key) {
|
|
407
|
+
await redis.del(key);
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const app = RabApi.createApp({
|
|
412
|
+
cache: { adapter: redisCacheAdapter },
|
|
413
|
+
});
|
|
414
|
+
```
|
|
415
|
+
|
|
308
416
|
## Advanced Features
|
|
309
417
|
|
|
310
418
|
### Controller Type Helpers
|
package/index.cjs.js
CHANGED
|
@@ -534,7 +534,7 @@ const DEFAULT_STRATEGY = 'url-query';
|
|
|
534
534
|
return {
|
|
535
535
|
ttl: (_ref = (_cache_ttl = cache.ttl) != null ? _cache_ttl : defaultTtl) != null ? _ref : DEFAULT_TTL,
|
|
536
536
|
strategy: (_cache_strategy = cache.strategy) != null ? _cache_strategy : DEFAULT_STRATEGY,
|
|
537
|
-
|
|
537
|
+
purge: cache.purge
|
|
538
538
|
};
|
|
539
539
|
}
|
|
540
540
|
/**
|
|
@@ -570,8 +570,8 @@ const DEFAULT_STRATEGY = 'url-query';
|
|
|
570
570
|
});
|
|
571
571
|
}
|
|
572
572
|
/**
|
|
573
|
-
* Resolve all
|
|
574
|
-
*/ function
|
|
573
|
+
* Resolve all purge keys for a request
|
|
574
|
+
*/ function resolvePurgeKeys(patterns, req) {
|
|
575
575
|
const resolved = [];
|
|
576
576
|
for (const pattern of patterns){
|
|
577
577
|
if (typeof pattern === 'function') {
|
|
@@ -593,9 +593,9 @@ const DEFAULT_STRATEGY = 'url-query';
|
|
|
593
593
|
return config.ttl !== undefined && config.ttl > 0;
|
|
594
594
|
}
|
|
595
595
|
/**
|
|
596
|
-
* Check if there are
|
|
597
|
-
*/ function
|
|
598
|
-
return Array.isArray(config.
|
|
596
|
+
* Check if there are purge patterns
|
|
597
|
+
*/ function hasPurgeKeys(config) {
|
|
598
|
+
return Array.isArray(config.purge) && config.purge.length > 0;
|
|
599
599
|
}
|
|
600
600
|
|
|
601
601
|
const controllerHandler = (controller, config)=>{
|
|
@@ -603,7 +603,7 @@ const controllerHandler = (controller, config)=>{
|
|
|
603
603
|
const cacheConfig = normalizeCacheConfig(config.cache, config.cacheDefaultTtl);
|
|
604
604
|
const cacheAdapter = config.cacheAdapter;
|
|
605
605
|
const isCacheEnabled = cacheConfig && cacheAdapter && shouldCache(cacheConfig);
|
|
606
|
-
const
|
|
606
|
+
const hasPurge = cacheConfig && hasPurgeKeys(cacheConfig);
|
|
607
607
|
return async (req, res, next)=>{
|
|
608
608
|
try {
|
|
609
609
|
let query = req.query;
|
|
@@ -614,8 +614,9 @@ const controllerHandler = (controller, config)=>{
|
|
|
614
614
|
query = await config.validateQuery(req.query);
|
|
615
615
|
}
|
|
616
616
|
}
|
|
617
|
-
// Check cache before executing handler
|
|
618
|
-
|
|
617
|
+
// Check cache before executing handler (GET only)
|
|
618
|
+
const isGetRequest = req.method === 'GET';
|
|
619
|
+
if (isCacheEnabled && isGetRequest) {
|
|
619
620
|
const cacheKey = buildCacheKey(req, cacheConfig.strategy);
|
|
620
621
|
const cached = await cacheAdapter.get(cacheKey);
|
|
621
622
|
if (cached) {
|
|
@@ -635,20 +636,20 @@ const controllerHandler = (controller, config)=>{
|
|
|
635
636
|
} : {
|
|
636
637
|
data: response
|
|
637
638
|
});
|
|
638
|
-
// Cache the response after successful execution
|
|
639
|
-
if (isCacheEnabled) {
|
|
639
|
+
// Cache the response after successful execution (GET only)
|
|
640
|
+
if (isCacheEnabled && isGetRequest) {
|
|
640
641
|
const cacheKey = buildCacheKey(req, cacheConfig.strategy);
|
|
641
642
|
// Don't await - cache in background
|
|
642
643
|
cacheAdapter.set(cacheKey, jsonResponse, cacheConfig.ttl).catch(()=>{
|
|
643
644
|
// Silently ignore cache errors
|
|
644
645
|
});
|
|
645
646
|
}
|
|
646
|
-
// Handle
|
|
647
|
-
if (
|
|
648
|
-
const
|
|
649
|
-
//
|
|
650
|
-
Promise.all(
|
|
651
|
-
// Silently ignore
|
|
647
|
+
// Handle purge after successful mutation
|
|
648
|
+
if (hasPurge && cacheAdapter) {
|
|
649
|
+
const keys = resolvePurgeKeys(cacheConfig.purge, req);
|
|
650
|
+
// Purge in background no await for promises you could use setImmediate if needed
|
|
651
|
+
Promise.all(keys.map((key)=>cacheAdapter.del(key))).catch(()=>{
|
|
652
|
+
// Silently ignore purge errors
|
|
652
653
|
});
|
|
653
654
|
}
|
|
654
655
|
var _response_statusCode1;
|
|
@@ -1218,10 +1219,10 @@ exports.buildCacheKey = buildCacheKey;
|
|
|
1218
1219
|
exports.controllerHandler = controllerHandler;
|
|
1219
1220
|
exports.errorHandler = errorHandler;
|
|
1220
1221
|
exports.extractTokenFromHeader = extractTokenFromHeader;
|
|
1221
|
-
exports.
|
|
1222
|
+
exports.hasPurgeKeys = hasPurgeKeys;
|
|
1222
1223
|
exports.isCallBackPipe = isCallBackPipe;
|
|
1223
1224
|
exports.isRouteAController = isRouteAController;
|
|
1224
1225
|
exports.normalizeCacheConfig = normalizeCacheConfig;
|
|
1225
|
-
exports.resolveInvalidationPatterns = resolveInvalidationPatterns;
|
|
1226
1226
|
exports.resolvePattern = resolvePattern;
|
|
1227
|
+
exports.resolvePurgeKeys = resolvePurgeKeys;
|
|
1227
1228
|
exports.shouldCache = shouldCache;
|
package/index.esm.d.ts
CHANGED
|
@@ -171,10 +171,10 @@ export declare type BuildRouterProps = CreateRouterProps & {
|
|
|
171
171
|
export declare interface CacheConfig {
|
|
172
172
|
/** Time-to-live in seconds (default: 900 = 15 minutes) */
|
|
173
173
|
ttl?: number;
|
|
174
|
-
/** Key derivation strategy (default: 'url-
|
|
174
|
+
/** Key derivation strategy (default: 'url-query') */
|
|
175
175
|
strategy?: CacheStrategy;
|
|
176
|
-
/**
|
|
177
|
-
|
|
176
|
+
/** Keys to purge after successful response */
|
|
177
|
+
purge?: PurgePattern[];
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
/**
|
|
@@ -454,9 +454,9 @@ export declare function Get(path: string, options?: ControllerRouteDefinitionOpt
|
|
|
454
454
|
export declare type GetController<TResponse, TQuery = any, TParams = any, TUser = any> = IControllerClass<TQuery, any, TResponse, TParams, TUser>;
|
|
455
455
|
|
|
456
456
|
/**
|
|
457
|
-
* Check if there are
|
|
457
|
+
* Check if there are purge patterns
|
|
458
458
|
*/
|
|
459
|
-
export declare function
|
|
459
|
+
export declare function hasPurgeKeys(config: CacheConfig): boolean;
|
|
460
460
|
|
|
461
461
|
/**
|
|
462
462
|
* Supported HTTP methods for route decorators.
|
|
@@ -470,7 +470,6 @@ export declare interface ICacheAdapter {
|
|
|
470
470
|
get<T>(key: string): Promise<T | null>;
|
|
471
471
|
set<T>(key: string, data: T, ttlSeconds: number): Promise<void>;
|
|
472
472
|
del(key: string): Promise<void>;
|
|
473
|
-
delPattern(pattern: string): Promise<void>;
|
|
474
473
|
}
|
|
475
474
|
|
|
476
475
|
/**
|
|
@@ -559,12 +558,6 @@ export declare class InternalServerErrorException extends RabApiError {
|
|
|
559
558
|
constructor(message?: string, errorCode?: string);
|
|
560
559
|
}
|
|
561
560
|
|
|
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
|
-
|
|
568
561
|
export declare const isCallBackPipe: (pipe: PipeFn | PipeFnCallBack) => pipe is PipeFnCallBack;
|
|
569
562
|
|
|
570
563
|
export declare function isRouteAController(value: AppRoute): value is ControllerClassType;
|
|
@@ -796,6 +789,12 @@ export declare function Post(path: string, options?: ControllerRouteDefinitionOp
|
|
|
796
789
|
*/
|
|
797
790
|
export declare type PostController<TBody = any, TResponse = any, TParams = any, TUser = any, TQuery = any> = IControllerClass<TQuery, TBody, TResponse, TParams, TUser>;
|
|
798
791
|
|
|
792
|
+
/**
|
|
793
|
+
* Purge pattern - either a key string or a function that returns key(s)
|
|
794
|
+
* Patterns with :param placeholders are auto-resolved from request params
|
|
795
|
+
*/
|
|
796
|
+
export declare type PurgePattern = string | ((req: any) => string | string[]);
|
|
797
|
+
|
|
799
798
|
/**
|
|
800
799
|
* PUT route decorator.
|
|
801
800
|
* Registers a controller class as a PUT endpoint handler.
|
|
@@ -944,14 +943,14 @@ export declare class RequestTimeoutException extends RabApiError {
|
|
|
944
943
|
}
|
|
945
944
|
|
|
946
945
|
/**
|
|
947
|
-
* Resolve
|
|
946
|
+
* Resolve :param placeholders in a pattern using request params
|
|
948
947
|
*/
|
|
949
|
-
export declare function
|
|
948
|
+
export declare function resolvePattern(pattern: string, params: Record<string, string>): string;
|
|
950
949
|
|
|
951
950
|
/**
|
|
952
|
-
* Resolve
|
|
951
|
+
* Resolve all purge keys for a request
|
|
953
952
|
*/
|
|
954
|
-
export declare function
|
|
953
|
+
export declare function resolvePurgeKeys(patterns: PurgePattern[], req: any): string[];
|
|
955
954
|
|
|
956
955
|
declare function retrieveRouteMetaData(route: ControllerClassType): ControllerRouteDefinition;
|
|
957
956
|
|
package/index.esm.js
CHANGED
|
@@ -532,7 +532,7 @@ const DEFAULT_STRATEGY = 'url-query';
|
|
|
532
532
|
return {
|
|
533
533
|
ttl: (_ref = (_cache_ttl = cache.ttl) != null ? _cache_ttl : defaultTtl) != null ? _ref : DEFAULT_TTL,
|
|
534
534
|
strategy: (_cache_strategy = cache.strategy) != null ? _cache_strategy : DEFAULT_STRATEGY,
|
|
535
|
-
|
|
535
|
+
purge: cache.purge
|
|
536
536
|
};
|
|
537
537
|
}
|
|
538
538
|
/**
|
|
@@ -568,8 +568,8 @@ const DEFAULT_STRATEGY = 'url-query';
|
|
|
568
568
|
});
|
|
569
569
|
}
|
|
570
570
|
/**
|
|
571
|
-
* Resolve all
|
|
572
|
-
*/ function
|
|
571
|
+
* Resolve all purge keys for a request
|
|
572
|
+
*/ function resolvePurgeKeys(patterns, req) {
|
|
573
573
|
const resolved = [];
|
|
574
574
|
for (const pattern of patterns){
|
|
575
575
|
if (typeof pattern === 'function') {
|
|
@@ -591,9 +591,9 @@ const DEFAULT_STRATEGY = 'url-query';
|
|
|
591
591
|
return config.ttl !== undefined && config.ttl > 0;
|
|
592
592
|
}
|
|
593
593
|
/**
|
|
594
|
-
* Check if there are
|
|
595
|
-
*/ function
|
|
596
|
-
return Array.isArray(config.
|
|
594
|
+
* Check if there are purge patterns
|
|
595
|
+
*/ function hasPurgeKeys(config) {
|
|
596
|
+
return Array.isArray(config.purge) && config.purge.length > 0;
|
|
597
597
|
}
|
|
598
598
|
|
|
599
599
|
const controllerHandler = (controller, config)=>{
|
|
@@ -601,7 +601,7 @@ const controllerHandler = (controller, config)=>{
|
|
|
601
601
|
const cacheConfig = normalizeCacheConfig(config.cache, config.cacheDefaultTtl);
|
|
602
602
|
const cacheAdapter = config.cacheAdapter;
|
|
603
603
|
const isCacheEnabled = cacheConfig && cacheAdapter && shouldCache(cacheConfig);
|
|
604
|
-
const
|
|
604
|
+
const hasPurge = cacheConfig && hasPurgeKeys(cacheConfig);
|
|
605
605
|
return async (req, res, next)=>{
|
|
606
606
|
try {
|
|
607
607
|
let query = req.query;
|
|
@@ -612,8 +612,9 @@ const controllerHandler = (controller, config)=>{
|
|
|
612
612
|
query = await config.validateQuery(req.query);
|
|
613
613
|
}
|
|
614
614
|
}
|
|
615
|
-
// Check cache before executing handler
|
|
616
|
-
|
|
615
|
+
// Check cache before executing handler (GET only)
|
|
616
|
+
const isGetRequest = req.method === 'GET';
|
|
617
|
+
if (isCacheEnabled && isGetRequest) {
|
|
617
618
|
const cacheKey = buildCacheKey(req, cacheConfig.strategy);
|
|
618
619
|
const cached = await cacheAdapter.get(cacheKey);
|
|
619
620
|
if (cached) {
|
|
@@ -633,20 +634,20 @@ const controllerHandler = (controller, config)=>{
|
|
|
633
634
|
} : {
|
|
634
635
|
data: response
|
|
635
636
|
});
|
|
636
|
-
// Cache the response after successful execution
|
|
637
|
-
if (isCacheEnabled) {
|
|
637
|
+
// Cache the response after successful execution (GET only)
|
|
638
|
+
if (isCacheEnabled && isGetRequest) {
|
|
638
639
|
const cacheKey = buildCacheKey(req, cacheConfig.strategy);
|
|
639
640
|
// Don't await - cache in background
|
|
640
641
|
cacheAdapter.set(cacheKey, jsonResponse, cacheConfig.ttl).catch(()=>{
|
|
641
642
|
// Silently ignore cache errors
|
|
642
643
|
});
|
|
643
644
|
}
|
|
644
|
-
// Handle
|
|
645
|
-
if (
|
|
646
|
-
const
|
|
647
|
-
//
|
|
648
|
-
Promise.all(
|
|
649
|
-
// Silently ignore
|
|
645
|
+
// Handle purge after successful mutation
|
|
646
|
+
if (hasPurge && cacheAdapter) {
|
|
647
|
+
const keys = resolvePurgeKeys(cacheConfig.purge, req);
|
|
648
|
+
// Purge in background no await for promises you could use setImmediate if needed
|
|
649
|
+
Promise.all(keys.map((key)=>cacheAdapter.del(key))).catch(()=>{
|
|
650
|
+
// Silently ignore purge errors
|
|
650
651
|
});
|
|
651
652
|
}
|
|
652
653
|
var _response_statusCode1;
|
|
@@ -1183,4 +1184,4 @@ class AtomExpressApp {
|
|
|
1183
1184
|
}
|
|
1184
1185
|
}
|
|
1185
1186
|
|
|
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,
|
|
1187
|
+
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, hasPurgeKeys, isCallBackPipe, isRouteAController, normalizeCacheConfig, resolvePattern, resolvePurgeKeys, shouldCache };
|
package/package.json
CHANGED