@rabstack/rab-api 1.11.1 → 1.13.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 CHANGED
@@ -355,38 +355,154 @@ export class ListProducts {}
355
355
  - **`url-params`**: Cache key is just the resolved path
356
356
  - `/products?page=1&limit=10` → key: `/products`
357
357
 
358
+ ### How Cache Keys Work
359
+
360
+ Cache keys are deterministically generated from the request URL to ensure consistent cache hits. Understanding how keys are built helps you design effective caching and purge strategies.
361
+
362
+ #### Key Generation Process
363
+
364
+ 1. **Extract the path**: The resolved path (with route params filled in) is extracted from the request
365
+ 2. **Apply strategy**: Based on the configured strategy, query parameters may be included
366
+ 3. **Sort & encode**: Query params are sorted alphabetically and URL-encoded to prevent collisions
367
+
368
+ ```
369
+ Request: GET /stores/123/products?page=2&limit=10&sort=name
370
+
371
+ url-params strategy → /stores/123/products
372
+ url-query strategy → /stores/123/products?limit=10&page=2&sort=name
373
+ ↑ params sorted alphabetically
374
+ ```
375
+
376
+ #### Why Sorting Matters
377
+
378
+ Query parameters are sorted alphabetically to ensure the same cache key regardless of parameter order:
379
+
380
+ ```typescript
381
+ // These requests produce the SAME cache key:
382
+ GET /products?limit=10&page=1
383
+ GET /products?page=1&limit=10
384
+ // Both → /products?limit=10&page=1
385
+ ```
386
+
387
+ #### URL Encoding for Safety
388
+
389
+ Special characters in query values are URL-encoded to prevent cache key collisions:
390
+
391
+ ```typescript
392
+ // Different requests, different cache keys:
393
+ GET /search?q=a&b=2 → /search?b=2&q=a
394
+ GET /search?q=a%26b=2 → /search?q=a%26b%3D2
395
+ ```
396
+
397
+ #### Strategy Selection Guide
398
+
399
+ | Strategy | Use When | Cache Key Example |
400
+ |----------|----------|-------------------|
401
+ | `url-query` | Response varies by query params (pagination, filters) | `/products?limit=10&page=1` |
402
+ | `url-params` | Response is the same regardless of query params | `/products/123` |
403
+
358
404
  ### Cache Purge
359
405
 
360
- Purge cache keys after mutations:
406
+ Purge (invalidate) cache keys after mutations to keep cached data fresh.
407
+
408
+ #### How Purge Works
409
+
410
+ 1. **After successful response**: Purge runs only after the handler returns successfully
411
+ 2. **Pattern resolution**: `:param` placeholders are replaced with actual request param values
412
+ 3. **Background execution**: Purge operations run asynchronously (non-blocking)
413
+ 4. **Silent failures**: Purge errors are caught and ignored to avoid breaking the main response
414
+
415
+ #### Purge Patterns
416
+
417
+ **Static patterns** - Exact cache keys to invalidate:
361
418
 
362
419
  ```typescript
363
420
  @Post('/products', {
364
421
  cache: {
365
- purge: ['/products'], // Purge this key after successful response
422
+ purge: ['/products'], // Purge the list endpoint
366
423
  }
367
424
  })
368
425
  export class CreateProduct {}
426
+ ```
369
427
 
370
- // With dynamic params
428
+ **Dynamic patterns** - Use `:param` placeholders resolved from request params:
429
+
430
+ ```typescript
371
431
  @Put('/products/:id', {
372
432
  cache: {
373
433
  purge: [
374
- '/products',
375
- '/products/:id', // :id is resolved from request params
434
+ '/products', // Purge the list
435
+ '/products/:id', // :id resolved to actual value (e.g., /products/123)
376
436
  ]
377
437
  }
378
438
  })
379
439
  export class UpdateProduct {}
440
+ ```
380
441
 
381
- // Function-based purge
442
+ **Function patterns** - Full control with access to the request object:
443
+
444
+ ```typescript
382
445
  @Delete('/products/:id', {
383
446
  cache: {
384
- purge: [(req) => `/products/${req.params.id}`]
447
+ purge: [
448
+ (req) => `/products/${req.params.id}`,
449
+ (req) => `/categories/${req.body.categoryId}/products`, // Access body
450
+ (req) => [ // Return multiple keys
451
+ `/products/${req.params.id}`,
452
+ `/products/${req.params.id}/reviews`,
453
+ ],
454
+ ]
385
455
  }
386
456
  })
387
457
  export class DeleteProduct {}
388
458
  ```
389
459
 
460
+ #### Purge Flow Diagram
461
+
462
+ ```
463
+ Request: DELETE /stores/123/products/456
464
+
465
+ 1. Handler executes successfully
466
+ 2. Purge patterns resolved:
467
+ - '/stores/:storeId/products' → '/stores/123/products'
468
+ - '/stores/:storeId/products/:id' → '/stores/123/products/456'
469
+ 3. cacheAdapter.del() called for each key (async, non-blocking)
470
+ 4. Response returned to client immediately
471
+ ```
472
+
473
+ #### Common Purge Patterns
474
+
475
+ ```typescript
476
+ // List + detail invalidation
477
+ @Put('/products/:id', {
478
+ cache: {
479
+ purge: ['/products', '/products/:id']
480
+ }
481
+ })
482
+
483
+ // Hierarchical invalidation
484
+ @Delete('/stores/:storeId/products/:id', {
485
+ cache: {
486
+ purge: [
487
+ '/stores/:storeId/products', // List
488
+ '/stores/:storeId/products/:id', // Detail
489
+ '/stores/:storeId', // Parent
490
+ ]
491
+ }
492
+ })
493
+
494
+ // Cross-entity invalidation
495
+ @Post('/orders', {
496
+ cache: {
497
+ purge: [
498
+ '/orders',
499
+ (req) => `/users/${req.auth.userId}/orders`, // User's orders
500
+ (req) => req.body.items.map(i => `/products/${i.productId}/stock`),
501
+ ]
502
+ }
503
+ })
504
+ ```
505
+
390
506
  ### Example: Redis Adapter
391
507
 
392
508
  ```typescript
package/index.cjs.js CHANGED
@@ -688,22 +688,28 @@ const controllerHandler = (controller, config)=>{
688
688
  };
689
689
 
690
690
  const authHandler = (isProtected, config)=>(req, res, next)=>{
691
- console.log('authHandler:', req.path, ':isProtected:', isProtected);
691
+ var _config_debug;
692
+ const debug = (_config_debug = config.debug) != null ? _config_debug : false;
693
+ if (debug) console.log('authHandler:', req.path, ':isProtected:', isProtected);
692
694
  const token = extractTokenFromHeader(req);
693
695
  // If not protected and no token, just continue
694
696
  if (!isProtected && !token) return next();
695
697
  // If no token but route is protected, throw error
696
698
  if (!token) {
697
- console.log('authHandler:UnauthorizedException:Token Not Found');
699
+ if (debug) console.log('authHandler:UnauthorizedException:Token Not Found');
698
700
  throw new UnauthorizedException('Unauthorized', config.errorCode);
699
701
  }
700
- // Token exists - verify it (must be valid regardless of protection)
702
+ // Token exists - verify it
701
703
  try {
702
- const payload = jwt.verify(token, config.jwt.secret_key);
704
+ const payload = jwt.verify(token, config.jwt.secret_key, {
705
+ algorithms: config.jwt.algorithms
706
+ });
703
707
  req['auth'] = payload;
704
708
  return next();
705
709
  } catch (err) {
706
- console.error('authHandler:JWT Error:', err.message);
710
+ // If route is not protected, silently continue without auth
711
+ if (!isProtected) return next();
712
+ if (debug) console.error('authHandler:JWT Error:', err.message);
707
713
  throw new UnauthorizedException('Unauthorized', config.errorCode);
708
714
  }
709
715
  };
@@ -1131,7 +1137,9 @@ class AtomExpressApp {
1131
1137
  //auth middleware
1132
1138
  if (this.options.auth) {
1133
1139
  var _config_isProtected;
1134
- allPipes.unshift(authHandler((_config_isProtected = config.isProtected) != null ? _config_isProtected : this.options.enforceRouteProtection, this.options.auth));
1140
+ allPipes.unshift(authHandler((_config_isProtected = config.isProtected) != null ? _config_isProtected : this.options.enforceRouteProtection, _extends({}, this.options.auth, {
1141
+ debug: this.options.debug
1142
+ })));
1135
1143
  }
1136
1144
  //add body validation to validate the schema and inject request context
1137
1145
  if (config.bodySchema && !config.disableBodyValidation) {
package/index.esm.d.ts CHANGED
@@ -78,6 +78,7 @@ export declare type AtomExpressOptions = {
78
78
  errorHandler?: (err: any, req: Request_2, res: Response_2, next: NextFunction) => any;
79
79
  enforceBodyValidation?: boolean;
80
80
  enforceRouteProtection?: boolean;
81
+ debug?: boolean;
81
82
  auth?: AuthHandlerOptions;
82
83
  openapi?: {
83
84
  enabled?: boolean;
@@ -124,6 +125,7 @@ export declare const authHandler: (isProtected: boolean, config: AuthHandlerOpti
124
125
 
125
126
  export declare type AuthHandlerOptions = {
126
127
  errorCode?: string;
128
+ debug?: boolean;
127
129
  jwt: {
128
130
  secret_key: string;
129
131
  algorithms: any;
package/index.esm.js CHANGED
@@ -686,22 +686,28 @@ const controllerHandler = (controller, config)=>{
686
686
  };
687
687
 
688
688
  const authHandler = (isProtected, config)=>(req, res, next)=>{
689
- console.log('authHandler:', req.path, ':isProtected:', isProtected);
689
+ var _config_debug;
690
+ const debug = (_config_debug = config.debug) != null ? _config_debug : false;
691
+ if (debug) console.log('authHandler:', req.path, ':isProtected:', isProtected);
690
692
  const token = extractTokenFromHeader(req);
691
693
  // If not protected and no token, just continue
692
694
  if (!isProtected && !token) return next();
693
695
  // If no token but route is protected, throw error
694
696
  if (!token) {
695
- console.log('authHandler:UnauthorizedException:Token Not Found');
697
+ if (debug) console.log('authHandler:UnauthorizedException:Token Not Found');
696
698
  throw new UnauthorizedException('Unauthorized', config.errorCode);
697
699
  }
698
- // Token exists - verify it (must be valid regardless of protection)
700
+ // Token exists - verify it
699
701
  try {
700
- const payload = jwt.verify(token, config.jwt.secret_key);
702
+ const payload = jwt.verify(token, config.jwt.secret_key, {
703
+ algorithms: config.jwt.algorithms
704
+ });
701
705
  req['auth'] = payload;
702
706
  return next();
703
707
  } catch (err) {
704
- console.error('authHandler:JWT Error:', err.message);
708
+ // If route is not protected, silently continue without auth
709
+ if (!isProtected) return next();
710
+ if (debug) console.error('authHandler:JWT Error:', err.message);
705
711
  throw new UnauthorizedException('Unauthorized', config.errorCode);
706
712
  }
707
713
  };
@@ -1129,7 +1135,9 @@ class AtomExpressApp {
1129
1135
  //auth middleware
1130
1136
  if (this.options.auth) {
1131
1137
  var _config_isProtected;
1132
- allPipes.unshift(authHandler((_config_isProtected = config.isProtected) != null ? _config_isProtected : this.options.enforceRouteProtection, this.options.auth));
1138
+ allPipes.unshift(authHandler((_config_isProtected = config.isProtected) != null ? _config_isProtected : this.options.enforceRouteProtection, _extends({}, this.options.auth, {
1139
+ debug: this.options.debug
1140
+ })));
1133
1141
  }
1134
1142
  //add body validation to validate the schema and inject request context
1135
1143
  if (config.bodySchema && !config.disableBodyValidation) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rabstack/rab-api",
3
- "version": "1.11.1",
3
+ "version": "1.13.0",
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",