@rabstack/rab-api 1.9.0 → 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 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
- invalidates: cache.invalidates
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 invalidation patterns for a request
574
- */ function resolveInvalidationPatterns(patterns, req) {
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 invalidation patterns
597
- */ function hasInvalidations(config) {
598
- return Array.isArray(config.invalidates) && config.invalidates.length > 0;
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 hasInvalidationPatterns = cacheConfig && hasInvalidations(cacheConfig);
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 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
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.hasInvalidations = hasInvalidations;
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-params') */
174
+ /** Key derivation strategy (default: 'url-query') */
175
175
  strategy?: CacheStrategy;
176
- /** URL patterns to invalidate after successful response */
177
- invalidates?: InvalidationPattern[];
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 invalidation patterns
457
+ * Check if there are purge patterns
458
458
  */
459
- export declare function hasInvalidations(config: CacheConfig): boolean;
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 all invalidation patterns for a request
946
+ * Resolve :param placeholders in a pattern using request params
948
947
  */
949
- export declare function resolveInvalidationPatterns(patterns: InvalidationPattern[], req: any): string[];
948
+ export declare function resolvePattern(pattern: string, params: Record<string, string>): string;
950
949
 
951
950
  /**
952
- * Resolve :param placeholders in a pattern using request params
951
+ * Resolve all purge keys for a request
953
952
  */
954
- export declare function resolvePattern(pattern: string, params: Record<string, string>): string;
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
- invalidates: cache.invalidates
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 invalidation patterns for a request
572
- */ function resolveInvalidationPatterns(patterns, req) {
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 invalidation patterns
595
- */ function hasInvalidations(config) {
596
- return Array.isArray(config.invalidates) && config.invalidates.length > 0;
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 hasInvalidationPatterns = cacheConfig && hasInvalidations(cacheConfig);
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 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
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, hasInvalidations, isCallBackPipe, isRouteAController, normalizeCacheConfig, resolveInvalidationPatterns, resolvePattern, shouldCache };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rabstack/rab-api",
3
- "version": "1.9.0",
3
+ "version": "1.9.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",