@rabstack/rab-api 1.11.1 → 1.12.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 +123 -7
- package/package.json +1 -1
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
|
|
422
|
+
purge: ['/products'], // Purge the list endpoint
|
|
366
423
|
}
|
|
367
424
|
})
|
|
368
425
|
export class CreateProduct {}
|
|
426
|
+
```
|
|
369
427
|
|
|
370
|
-
|
|
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
|
|
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
|
-
|
|
442
|
+
**Function patterns** - Full control with access to the request object:
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
382
445
|
@Delete('/products/:id', {
|
|
383
446
|
cache: {
|
|
384
|
-
purge: [
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rabstack/rab-api",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.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",
|