@rabstack/rab-api 1.8.3 → 1.9.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 +108 -0
- package/index.cjs.js +16 -16
- package/index.esm.d.ts +15 -16
- package/index.esm.js +15 -15
- 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
|
@@ -519,7 +519,7 @@ var utils = /*#__PURE__*/Object.freeze({
|
|
|
519
519
|
});
|
|
520
520
|
|
|
521
521
|
const DEFAULT_TTL = 900; // 15 minutes
|
|
522
|
-
const DEFAULT_STRATEGY = 'url-
|
|
522
|
+
const DEFAULT_STRATEGY = 'url-query';
|
|
523
523
|
/**
|
|
524
524
|
* Normalize cache config - handles `cache: true` shorthand
|
|
525
525
|
*/ function normalizeCacheConfig(cache, defaultTtl) {
|
|
@@ -534,7 +534,7 @@ const DEFAULT_STRATEGY = 'url-params';
|
|
|
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-params';
|
|
|
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-params';
|
|
|
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;
|
|
@@ -643,12 +643,12 @@ const controllerHandler = (controller, config)=>{
|
|
|
643
643
|
// Silently ignore cache errors
|
|
644
644
|
});
|
|
645
645
|
}
|
|
646
|
-
// Handle
|
|
647
|
-
if (
|
|
648
|
-
const
|
|
649
|
-
//
|
|
650
|
-
Promise.all(
|
|
651
|
-
// Silently ignore
|
|
646
|
+
// Handle purge after successful mutation
|
|
647
|
+
if (hasPurge && cacheAdapter) {
|
|
648
|
+
const keys = resolvePurgeKeys(cacheConfig.purge, req);
|
|
649
|
+
// Purge in background no await for promises you could use setImmediate if needed
|
|
650
|
+
Promise.all(keys.map((key)=>cacheAdapter.del(key))).catch(()=>{
|
|
651
|
+
// Silently ignore purge errors
|
|
652
652
|
});
|
|
653
653
|
}
|
|
654
654
|
var _response_statusCode1;
|
|
@@ -1218,10 +1218,10 @@ exports.buildCacheKey = buildCacheKey;
|
|
|
1218
1218
|
exports.controllerHandler = controllerHandler;
|
|
1219
1219
|
exports.errorHandler = errorHandler;
|
|
1220
1220
|
exports.extractTokenFromHeader = extractTokenFromHeader;
|
|
1221
|
-
exports.
|
|
1221
|
+
exports.hasPurgeKeys = hasPurgeKeys;
|
|
1222
1222
|
exports.isCallBackPipe = isCallBackPipe;
|
|
1223
1223
|
exports.isRouteAController = isRouteAController;
|
|
1224
1224
|
exports.normalizeCacheConfig = normalizeCacheConfig;
|
|
1225
|
-
exports.resolveInvalidationPatterns = resolveInvalidationPatterns;
|
|
1226
1225
|
exports.resolvePattern = resolvePattern;
|
|
1226
|
+
exports.resolvePurgeKeys = resolvePurgeKeys;
|
|
1227
1227
|
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
|
@@ -517,7 +517,7 @@ var utils = /*#__PURE__*/Object.freeze({
|
|
|
517
517
|
});
|
|
518
518
|
|
|
519
519
|
const DEFAULT_TTL = 900; // 15 minutes
|
|
520
|
-
const DEFAULT_STRATEGY = 'url-
|
|
520
|
+
const DEFAULT_STRATEGY = 'url-query';
|
|
521
521
|
/**
|
|
522
522
|
* Normalize cache config - handles `cache: true` shorthand
|
|
523
523
|
*/ function normalizeCacheConfig(cache, defaultTtl) {
|
|
@@ -532,7 +532,7 @@ const DEFAULT_STRATEGY = 'url-params';
|
|
|
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-params';
|
|
|
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-params';
|
|
|
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;
|
|
@@ -641,12 +641,12 @@ const controllerHandler = (controller, config)=>{
|
|
|
641
641
|
// Silently ignore cache errors
|
|
642
642
|
});
|
|
643
643
|
}
|
|
644
|
-
// Handle
|
|
645
|
-
if (
|
|
646
|
-
const
|
|
647
|
-
//
|
|
648
|
-
Promise.all(
|
|
649
|
-
// Silently ignore
|
|
644
|
+
// Handle purge after successful mutation
|
|
645
|
+
if (hasPurge && cacheAdapter) {
|
|
646
|
+
const keys = resolvePurgeKeys(cacheConfig.purge, req);
|
|
647
|
+
// Purge in background no await for promises you could use setImmediate if needed
|
|
648
|
+
Promise.all(keys.map((key)=>cacheAdapter.del(key))).catch(()=>{
|
|
649
|
+
// Silently ignore purge errors
|
|
650
650
|
});
|
|
651
651
|
}
|
|
652
652
|
var _response_statusCode1;
|
|
@@ -1183,4 +1183,4 @@ class AtomExpressApp {
|
|
|
1183
1183
|
}
|
|
1184
1184
|
}
|
|
1185
1185
|
|
|
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,
|
|
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, hasPurgeKeys, isCallBackPipe, isRouteAController, normalizeCacheConfig, resolvePattern, resolvePurgeKeys, shouldCache };
|
package/package.json
CHANGED