@cqrs-ddd/pipeline-cache 0.5.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/COMMERCIAL_LICENSE.txt +34 -0
- package/LICENSE +661 -0
- package/README.md +37 -0
- package/dist/adapters/cache-manager.adapter.d.ts +21 -0
- package/dist/adapters/cache-manager.adapter.d.ts.map +1 -0
- package/dist/adapters/cache-manager.adapter.js +52 -0
- package/dist/adapters/cache-manager.adapter.js.map +1 -0
- package/dist/cache.behavior.d.ts +83 -0
- package/dist/cache.behavior.d.ts.map +1 -0
- package/dist/cache.behavior.js +213 -0
- package/dist/cache.behavior.js.map +1 -0
- package/dist/errors/missing-partition.error.d.ts +18 -0
- package/dist/errors/missing-partition.error.d.ts.map +1 -0
- package/dist/errors/missing-partition.error.js +19 -0
- package/dist/errors/missing-partition.error.js.map +1 -0
- package/dist/helpers/build-attributes.d.ts +37 -0
- package/dist/helpers/build-attributes.d.ts.map +1 -0
- package/dist/helpers/build-attributes.js +43 -0
- package/dist/helpers/build-attributes.js.map +1 -0
- package/dist/helpers/cache-factory.d.ts +36 -0
- package/dist/helpers/cache-factory.d.ts.map +1 -0
- package/dist/helpers/cache-factory.js +147 -0
- package/dist/helpers/cache-factory.js.map +1 -0
- package/dist/helpers/cache-key.d.ts +81 -0
- package/dist/helpers/cache-key.d.ts.map +1 -0
- package/dist/helpers/cache-key.js +91 -0
- package/dist/helpers/cache-key.js.map +1 -0
- package/dist/helpers/cache.intent.d.ts +31 -0
- package/dist/helpers/cache.intent.d.ts.map +1 -0
- package/dist/helpers/cache.intent.js +26 -0
- package/dist/helpers/cache.intent.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/cache-options.interface.d.ts +111 -0
- package/dist/interfaces/cache-options.interface.d.ts.map +1 -0
- package/dist/interfaces/cache-options.interface.js +3 -0
- package/dist/interfaces/cache-options.interface.js.map +1 -0
- package/package.json +89 -0
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @cqrs-ddd/pipeline-cache
|
|
2
|
+
|
|
3
|
+
Result caching for `@cqrs-ddd/pipeline` on [cache-manager](https://github.com/jaredwray/cacheable)
|
|
4
|
+
and Keyv stores (memory, Redis, Memcache, SQLite, PostgreSQL, tiered). `buildCache()`
|
|
5
|
+
creates the cache from a store configuration; `createPartitionedCacheKeyFactory()`
|
|
6
|
+
builds keys partitioned by tenant, principal and permission scope, because a cache hit
|
|
7
|
+
skips the function and every check inside it.
|
|
8
|
+
|
|
9
|
+
**Documentation:** [guide](https://aristoteliss.github.io/ddd-cqrs/packages/pipeline-cache/) · [API reference](https://aristoteliss.github.io/ddd-cqrs/api/cqrs-ddd/pipeline-cache/) · [all packages](https://aristoteliss.github.io/ddd-cqrs/)
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @cqrs-ddd/pipeline-cache @cqrs-ddd/pipeline cache-manager keyv
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Add `@keyv/redis`, `@keyv/memcache`, `@keyv/sqlite` or `@keyv/postgres` for those stores.
|
|
18
|
+
Requires Node.js 22.12 or later. Published as an ES module; a CommonJS application loads
|
|
19
|
+
it with `require()`.
|
|
20
|
+
|
|
21
|
+
## Example
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
const pipeline = createPipeline({
|
|
25
|
+
behaviors: [new CacheBehavior(buildCache({ store: { type: 'redis', url: redisUrl } }), { ttl: 30_000 })],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const getPrice = pipeline.wrap(
|
|
29
|
+
{ name: 'getPrice', kind: 'query' },
|
|
30
|
+
cache({ key: createPartitionedCacheKeyFactory({ principal: currentUserId, requireScope: false }) }),
|
|
31
|
+
)(async (sku: string) => prices.find(sku));
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
|
|
36
|
+
Dual-licensed under **AGPLv3** and a **Commercial License**. See `LICENSE` and
|
|
37
|
+
`COMMERCIAL_LICENSE.txt`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Cache } from 'cache-manager';
|
|
2
|
+
/**
|
|
3
|
+
* Common interface for cache interactions within the pipeline.
|
|
4
|
+
*/
|
|
5
|
+
export interface IPipelineCache {
|
|
6
|
+
get(key: string): Promise<unknown>;
|
|
7
|
+
set(key: string, value: unknown, ttl?: number): Promise<unknown>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Adapter wrapping a cache-manager Cache instance to reliably expose backend
|
|
11
|
+
* store errors that cache-manager would otherwise swallow in get().
|
|
12
|
+
*/
|
|
13
|
+
export declare class CacheManagerAdapter {
|
|
14
|
+
private readonly cache;
|
|
15
|
+
private readonly operation;
|
|
16
|
+
constructor(cache: Cache);
|
|
17
|
+
private captureError;
|
|
18
|
+
get(key: string): Promise<unknown>;
|
|
19
|
+
set(key: string, value: unknown, ttl?: number): Promise<unknown>;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=cache-manager.adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-manager.adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/cache-manager.adapter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAE3C;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAClE;AAED;;;GAGG;AACH,qBAAa,mBAAmB;IASlB,OAAO,CAAC,QAAQ,CAAC,KAAK;IANlC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAIrB;IAEL,YAA6B,KAAK,EAAE,KAAK,EASxC;IAED,OAAO,CAAC,YAAY;IAcd,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOvC;IAEK,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOrE;CACF"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
+
/**
|
|
4
|
+
* Adapter wrapping a cache-manager Cache instance to reliably expose backend
|
|
5
|
+
* store errors that cache-manager would otherwise swallow in get().
|
|
6
|
+
*/
|
|
7
|
+
export class CacheManagerAdapter {
|
|
8
|
+
cache;
|
|
9
|
+
// cache-manager emits errors in the originating operation's async context.
|
|
10
|
+
// A key alone cannot identify concurrent reads, writes, or fallback lookups.
|
|
11
|
+
operation = new AsyncLocalStorage();
|
|
12
|
+
constructor(cache) {
|
|
13
|
+
this.cache = cache;
|
|
14
|
+
// A caller-supplied store may swallow backend errors, which then read as a
|
|
15
|
+
// miss; these listeners surface what it reports.
|
|
16
|
+
const emitter = cache;
|
|
17
|
+
emitter.on?.('get', (event) => this.captureError('get', event));
|
|
18
|
+
emitter.on?.('set', (event) => this.captureError('set', event));
|
|
19
|
+
emitter.on?.('error', (event) => this.captureError(undefined, event));
|
|
20
|
+
}
|
|
21
|
+
captureError(kind, event) {
|
|
22
|
+
const operation = this.operation.getStore();
|
|
23
|
+
if (!operation || (kind && operation.kind !== kind))
|
|
24
|
+
return;
|
|
25
|
+
if (event &&
|
|
26
|
+
typeof event === 'object' &&
|
|
27
|
+
'key' in event &&
|
|
28
|
+
event.key === operation.key &&
|
|
29
|
+
'error' in event) {
|
|
30
|
+
operation.error = event.error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async get(key) {
|
|
34
|
+
return this.operation.run({ key, kind: 'get' }, async () => {
|
|
35
|
+
const value = await this.cache.get(key);
|
|
36
|
+
const error = this.operation.getStore()?.error;
|
|
37
|
+
if (value === undefined && error !== undefined)
|
|
38
|
+
throw error;
|
|
39
|
+
return value;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async set(key, value, ttl) {
|
|
43
|
+
return this.operation.run({ key, kind: 'set' }, async () => {
|
|
44
|
+
const result = await this.cache.set(key, value, ttl);
|
|
45
|
+
const error = this.operation.getStore()?.error;
|
|
46
|
+
if (error !== undefined)
|
|
47
|
+
throw error;
|
|
48
|
+
return result;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=cache-manager.adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-manager.adapter.js","sourceRoot":"","sources":["../../src/adapters/cache-manager.adapter.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAWrD;;;GAGG;AACH,MAAM,OAAO,mBAAmB;IASD,KAAK;IARlC,2EAA2E;IAC3E,6EAA6E;IAC5D,SAAS,GAAG,IAAI,iBAAiB,EAI9C,CAAC;IAEL,YAA6B,KAAY;qBAAZ,KAAK;QAChC,2EAA2E;QAC3E,iDAAiD;QACjD,MAAM,OAAO,GAAG,KAEf,CAAC;QACF,OAAO,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;QAChE,OAAO,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;QAChE,OAAO,CAAC,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC;IACxE,CAAC;IAEO,YAAY,CAAC,IAA+B,EAAE,KAAc;QAClE,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC;QAC5C,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,IAAI,SAAS,CAAC,IAAI,KAAK,IAAI,CAAC;YAAE,OAAO;QAC5D,IACE,KAAK;YACL,OAAO,KAAK,KAAK,QAAQ;YACzB,KAAK,IAAI,KAAK;YACd,KAAK,CAAC,GAAG,KAAK,SAAS,CAAC,GAAG;YAC3B,OAAO,IAAI,KAAK,EAChB,CAAC;YACD,SAAS,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QAChC,CAAC;IACH,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,KAAK,IAAI,EAAE;YACzD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACxC,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,KAAK,CAAC;YAC/C,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS;gBAAE,MAAM,KAAK,CAAC;YAC5D,OAAO,KAAK,CAAC;QACf,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,KAAc,EAAE,GAAY;QACjD,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,KAAK,IAAI,EAAE;YACzD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;YACrD,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,KAAK,CAAC;YAC/C,IAAI,KAAK,KAAK,SAAS;gBAAE,MAAM,KAAK,CAAC;YACrC,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type IPipelineBehavior, type IPipelineBehaviorContract, type IPipelineBehaviorOptionsResolver, type IPipelineContext, type NextDelegate, PIPELINE_BEHAVIOR_CONTRACT, type PipelineItemToken, type PipelineLogger } from '@cqrs-ddd/pipeline';
|
|
2
|
+
import type { Cache } from 'cache-manager';
|
|
3
|
+
import { type IPipelineCache } from './adapters/cache-manager.adapter.js';
|
|
4
|
+
import type { CacheBehaviorOptions } from './interfaces/cache-options.interface.js';
|
|
5
|
+
/**
|
|
6
|
+
* Unique symbol key set on `context.items` recording whether the request was served from cache (`true` on hit).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* const isHit = context.items.get(CACHE_HIT_ITEM) === true;
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare const CACHE_HIT_ITEM: unique symbol;
|
|
14
|
+
/**
|
|
15
|
+
* Typed token for {@link CACHE_HIT_ITEM}: the cache hit flag. Reads and writes the same
|
|
16
|
+
* `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
|
|
17
|
+
*/
|
|
18
|
+
export declare const CACHE_HIT_ITEM_TOKEN: PipelineItemToken<boolean>;
|
|
19
|
+
/**
|
|
20
|
+
* Unique symbol key set on `context.items` recording the resolved cache key string.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const key = context.items.get(CACHE_KEY_ITEM) as string | undefined;
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare const CACHE_KEY_ITEM: unique symbol;
|
|
28
|
+
/**
|
|
29
|
+
* Typed token for {@link CACHE_KEY_ITEM}: the resolved cache key. Reads and writes the same
|
|
30
|
+
* `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
|
|
31
|
+
*/
|
|
32
|
+
export declare const CACHE_KEY_ITEM_TOKEN: PipelineItemToken<string>;
|
|
33
|
+
/**
|
|
34
|
+
* Pipeline behavior that transparently caches handler results with
|
|
35
|
+
* `cache-manager` (v7) on top of Keyv.
|
|
36
|
+
*
|
|
37
|
+
* Resolution of the effective options for a handler:
|
|
38
|
+
* 1. Application-wide defaults, the second constructor argument.
|
|
39
|
+
* 2. Per-operation options from the `cache({ ... })` entry,
|
|
40
|
+
* shallow-merged on top of the defaults (handler keys win).
|
|
41
|
+
*
|
|
42
|
+
* A `key` factory is required — see {@link createPartitionedCacheKeyFactory}. Only
|
|
43
|
+
* `query` requests are cached by default; commands and events pass through
|
|
44
|
+
* untouched. On a cache miss, `null` / `undefined` results are not written.
|
|
45
|
+
* Cache hits return the value from the single explicit lookup. This behavior
|
|
46
|
+
* intentionally does not use `cache-manager.wrap()` because its background
|
|
47
|
+
* refresh callback would re-enter every downstream pipeline behavior.
|
|
48
|
+
* Cached results are JSON values. A miss returns the JSON form of the handler
|
|
49
|
+
* result that it stores (a `Date` becomes an ISO string and a class instance a
|
|
50
|
+
* plain object), so a hit and a miss return the same shape. A result that cannot
|
|
51
|
+
* be JSON-serialized is returned unchanged and not cached.
|
|
52
|
+
*
|
|
53
|
+
* Store errors fail open by default: read failures bypass caching for that
|
|
54
|
+
* execution, and write failures return the successful handler result. Set
|
|
55
|
+
* `failOpen: false` to propagate store failures instead.
|
|
56
|
+
*
|
|
57
|
+
* @example Principal/permission-scoped query cache
|
|
58
|
+
* ```ts
|
|
59
|
+
* const userCacheKey = createPartitionedCacheKeyFactory({
|
|
60
|
+
* principal: (ctx) => ctx.items.get('userId') as string | undefined,
|
|
61
|
+
* scope: (ctx) => ctx.items.get('capabilityVersion') as string | undefined,
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* class GetUsersHandler {
|
|
65
|
+
* @pipeline.wrap({ kind: 'query' }, [CacheBehavior, { key: userCacheKey, ttl: 30_000 }])
|
|
66
|
+
* async handle(query: GetUsersQuery) {}
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export declare class CacheBehavior implements IPipelineBehavior, IPipelineBehaviorOptionsResolver<CacheBehaviorOptions> {
|
|
71
|
+
static readonly [PIPELINE_BEHAVIOR_CONTRACT]: IPipelineBehaviorContract;
|
|
72
|
+
private readonly logger;
|
|
73
|
+
private readonly defaults;
|
|
74
|
+
private readonly cacheAdapter;
|
|
75
|
+
constructor(cache: Cache | IPipelineCache, defaults?: CacheBehaviorOptions, logger?: PipelineLogger);
|
|
76
|
+
handle(context: IPipelineContext, next: NextDelegate): Promise<unknown>;
|
|
77
|
+
/** Logs a store failure and either bypasses it or propagates it. */
|
|
78
|
+
private handleStoreError;
|
|
79
|
+
private diagnose;
|
|
80
|
+
/** Shallow-merges pipeline-level options over application defaults. */
|
|
81
|
+
resolveEffectiveOptions(options?: CacheBehaviorOptions): CacheBehaviorOptions;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=cache.behavior.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.behavior.d.ts","sourceRoot":"","sources":["../src/cache.behavior.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,gCAAgC,EACrC,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,0BAA0B,EAI1B,KAAK,iBAAiB,EACtB,KAAK,cAAc,EAEpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yCAAyC,CAAC;AAEpF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,eAA2B,CAAC;AAEvD;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,iBAAiB,CAAC,OAAO,CACG,CAAC;AAEhE;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,eAA2B,CAAC;AAEvD;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,iBAAiB,CAAC,MAAM,CACG,CAAC;AAY/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,qBAAa,aACX,YACE,iBAAiB,EACjB,gCAAgC,CAAC,oBAAoB,CAAC;IAExD,MAAM,CAAC,QAAQ,CAAC,CAAC,0BAA0B,CAAC,EAAE,yBAAyB,CAkDrE;IAEF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuB;IAChD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAiB;IAE9C,YACE,KAAK,EAAE,KAAK,GAAG,cAAc,EAC7B,QAAQ,CAAC,EAAE,oBAAoB,EAC/B,MAAM,CAAC,EAAE,cAAc,EAcxB;IAEK,MAAM,CACV,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,OAAO,CAAC,CAyDlB;IAED,oEAAoE;IACpE,OAAO,CAAC,gBAAgB;IA0BxB,OAAO,CAAC,QAAQ;IAQhB,uEAAuE;IACvE,uBAAuB,CACrB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,oBAAoB,CAGtB;CACF"}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
import { createPipelineItem, PIPELINE_BEHAVIOR_CONTRACT, setPipelineItem, } from '@cqrs-ddd/pipeline';
|
|
3
|
+
import { CacheManagerAdapter, } from './adapters/cache-manager.adapter.js';
|
|
4
|
+
/**
|
|
5
|
+
* Unique symbol key set on `context.items` recording whether the request was served from cache (`true` on hit).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const isHit = context.items.get(CACHE_HIT_ITEM) === true;
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export const CACHE_HIT_ITEM = Symbol('CACHE_HIT_ITEM');
|
|
13
|
+
/**
|
|
14
|
+
* Typed token for {@link CACHE_HIT_ITEM}: the cache hit flag. Reads and writes the same
|
|
15
|
+
* `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
|
|
16
|
+
*/
|
|
17
|
+
export const CACHE_HIT_ITEM_TOKEN = createPipelineItem('CACHE_HIT_ITEM', CACHE_HIT_ITEM);
|
|
18
|
+
/**
|
|
19
|
+
* Unique symbol key set on `context.items` recording the resolved cache key string.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const key = context.items.get(CACHE_KEY_ITEM) as string | undefined;
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const CACHE_KEY_ITEM = Symbol('CACHE_KEY_ITEM');
|
|
27
|
+
/**
|
|
28
|
+
* Typed token for {@link CACHE_KEY_ITEM}: the resolved cache key. Reads and writes the same
|
|
29
|
+
* `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
|
|
30
|
+
*/
|
|
31
|
+
export const CACHE_KEY_ITEM_TOKEN = createPipelineItem('CACHE_KEY_ITEM', CACHE_KEY_ITEM);
|
|
32
|
+
const DEFAULT_KINDS = ['query'];
|
|
33
|
+
/** Whether this request kind is configured for caching. */
|
|
34
|
+
function inScope(options, requestKind) {
|
|
35
|
+
return (options?.kinds ?? DEFAULT_KINDS).includes(requestKind);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Pipeline behavior that transparently caches handler results with
|
|
39
|
+
* `cache-manager` (v7) on top of Keyv.
|
|
40
|
+
*
|
|
41
|
+
* Resolution of the effective options for a handler:
|
|
42
|
+
* 1. Application-wide defaults, the second constructor argument.
|
|
43
|
+
* 2. Per-operation options from the `cache({ ... })` entry,
|
|
44
|
+
* shallow-merged on top of the defaults (handler keys win).
|
|
45
|
+
*
|
|
46
|
+
* A `key` factory is required — see {@link createPartitionedCacheKeyFactory}. Only
|
|
47
|
+
* `query` requests are cached by default; commands and events pass through
|
|
48
|
+
* untouched. On a cache miss, `null` / `undefined` results are not written.
|
|
49
|
+
* Cache hits return the value from the single explicit lookup. This behavior
|
|
50
|
+
* intentionally does not use `cache-manager.wrap()` because its background
|
|
51
|
+
* refresh callback would re-enter every downstream pipeline behavior.
|
|
52
|
+
* Cached results are JSON values. A miss returns the JSON form of the handler
|
|
53
|
+
* result that it stores (a `Date` becomes an ISO string and a class instance a
|
|
54
|
+
* plain object), so a hit and a miss return the same shape. A result that cannot
|
|
55
|
+
* be JSON-serialized is returned unchanged and not cached.
|
|
56
|
+
*
|
|
57
|
+
* Store errors fail open by default: read failures bypass caching for that
|
|
58
|
+
* execution, and write failures return the successful handler result. Set
|
|
59
|
+
* `failOpen: false` to propagate store failures instead.
|
|
60
|
+
*
|
|
61
|
+
* @example Principal/permission-scoped query cache
|
|
62
|
+
* ```ts
|
|
63
|
+
* const userCacheKey = createPartitionedCacheKeyFactory({
|
|
64
|
+
* principal: (ctx) => ctx.items.get('userId') as string | undefined,
|
|
65
|
+
* scope: (ctx) => ctx.items.get('capabilityVersion') as string | undefined,
|
|
66
|
+
* });
|
|
67
|
+
*
|
|
68
|
+
* class GetUsersHandler {
|
|
69
|
+
* @pipeline.wrap({ kind: 'query' }, [CacheBehavior, { key: userCacheKey, ttl: 30_000 }])
|
|
70
|
+
* async handle(query: GetUsersQuery) {}
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export class CacheBehavior {
|
|
75
|
+
static [PIPELINE_BEHAVIOR_CONTRACT] = {
|
|
76
|
+
order: (context) => {
|
|
77
|
+
const options = context.effectiveOptions;
|
|
78
|
+
if (!inScope(options, context.requestKind)) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
after: ['@cqrs-ddd/pipeline-casl:CaslBehavior', 'CaslBehavior'],
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
validate: (context) => {
|
|
86
|
+
const options = context.effectiveOptions;
|
|
87
|
+
if (!inScope(options, context.requestKind)) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
if (!options?.key) {
|
|
91
|
+
return [
|
|
92
|
+
{
|
|
93
|
+
handlerName: context.handlerName,
|
|
94
|
+
behaviorName: CacheBehavior.name,
|
|
95
|
+
message: 'Active CacheBehavior requires an explicit `key` factory',
|
|
96
|
+
fix: 'Provide a key factory via createPartitionedCacheKeyFactory(...) in ' +
|
|
97
|
+
'the cache({ key }) entry or the CacheBehavior constructor defaults.',
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
if (typeof options.key !== 'function') {
|
|
102
|
+
return [
|
|
103
|
+
{
|
|
104
|
+
handlerName: context.handlerName,
|
|
105
|
+
behaviorName: CacheBehavior.name,
|
|
106
|
+
message: `CacheBehavior key factory must be a callable function, received ${typeof options.key}`,
|
|
107
|
+
fix: 'Pass a valid function (ctx) => string as the key option of the cache({ key }) entry.',
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
logger;
|
|
115
|
+
defaults;
|
|
116
|
+
cacheAdapter;
|
|
117
|
+
constructor(cache, defaults, logger) {
|
|
118
|
+
if (!cache || typeof cache.get !== 'function') {
|
|
119
|
+
throw new TypeError('CacheBehavior requires a cache, such as buildCache({ store }) of @cqrs-ddd/pipeline-cache.');
|
|
120
|
+
}
|
|
121
|
+
this.defaults = defaults ?? {};
|
|
122
|
+
this.cacheAdapter =
|
|
123
|
+
cache instanceof CacheManagerAdapter
|
|
124
|
+
? cache
|
|
125
|
+
: new CacheManagerAdapter(cache);
|
|
126
|
+
this.logger = logger ?? console;
|
|
127
|
+
}
|
|
128
|
+
async handle(context, next) {
|
|
129
|
+
const options = this.resolveEffectiveOptions(context.getBehaviorOptions(CacheBehavior));
|
|
130
|
+
if (!inScope(options, context.requestKind))
|
|
131
|
+
return next();
|
|
132
|
+
if (options.condition && !options.condition(context))
|
|
133
|
+
return next();
|
|
134
|
+
if (!options.key) {
|
|
135
|
+
throw new TypeError(`CacheBehavior on ${context.handlerName} requires an explicit \`key\` factory. ` +
|
|
136
|
+
'A cache hit returns without running the handler, so the key must partition ' +
|
|
137
|
+
'every dimension that can change the authorized response: tenant, principal, ' +
|
|
138
|
+
'permission scope and request payload. Use createPartitionedCacheKeyFactory(...).');
|
|
139
|
+
}
|
|
140
|
+
const key = options.key(context);
|
|
141
|
+
setPipelineItem(context, CACHE_KEY_ITEM_TOKEN, key);
|
|
142
|
+
let cached;
|
|
143
|
+
let canWrite = true;
|
|
144
|
+
try {
|
|
145
|
+
cached = await this.cacheAdapter.get(key);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
this.handleStoreError('read', context, key, error, options);
|
|
149
|
+
canWrite = false;
|
|
150
|
+
}
|
|
151
|
+
if (cached !== undefined && cached !== null) {
|
|
152
|
+
setPipelineItem(context, CACHE_HIT_ITEM_TOKEN, true);
|
|
153
|
+
this.diagnose('debug', `Cache hit for ${context.requestName} (${key})`);
|
|
154
|
+
return cached;
|
|
155
|
+
}
|
|
156
|
+
setPipelineItem(context, CACHE_HIT_ITEM_TOKEN, false);
|
|
157
|
+
this.diagnose('debug', `Cache miss for ${context.requestName} (${key})`);
|
|
158
|
+
const result = await next();
|
|
159
|
+
if (result === undefined || result === null)
|
|
160
|
+
return result;
|
|
161
|
+
const cacheable = toJsonForm(result);
|
|
162
|
+
if (cacheable === undefined) {
|
|
163
|
+
this.diagnose('warn', `Result of ${context.requestName} is not JSON-serializable; not cached (key: ${key})`);
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
if (canWrite) {
|
|
167
|
+
try {
|
|
168
|
+
await this.cacheAdapter.set(key, cacheable, options.ttl);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
this.handleStoreError('write', context, key, error, options);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return cacheable;
|
|
175
|
+
}
|
|
176
|
+
/** Logs a store failure and either bypasses it or propagates it. */
|
|
177
|
+
handleStoreError(operation, context, key, error, options) {
|
|
178
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
179
|
+
if (options.failOpen ?? true) {
|
|
180
|
+
this.diagnose('warn', `Cache ${operation} error for ${context.requestName} ` +
|
|
181
|
+
`(key: ${key}); failing open: ${message}`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
this.diagnose('error', `Cache ${operation} error for ${context.requestName} ` +
|
|
185
|
+
`(key: ${key}); failing closed: ${message}`);
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
diagnose(level, message) {
|
|
189
|
+
try {
|
|
190
|
+
this.logger[level]?.(message, CacheBehavior.name);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// Diagnostics must not replace the handler result or cache-store error.
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** Shallow-merges pipeline-level options over application defaults. */
|
|
197
|
+
resolveEffectiveOptions(options) {
|
|
198
|
+
if (!options)
|
|
199
|
+
return this.defaults;
|
|
200
|
+
return { ...this.defaults, ...options };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** JSON round-trip of a cacheable result, or `undefined` when it has no JSON form. */
|
|
204
|
+
function toJsonForm(value) {
|
|
205
|
+
try {
|
|
206
|
+
const json = JSON.stringify(value);
|
|
207
|
+
return json === undefined ? undefined : JSON.parse(json);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=cache.behavior.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.behavior.js","sourceRoot":"","sources":["../src/cache.behavior.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EACL,kBAAkB,EAMlB,0BAA0B,EAM1B,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,mBAAmB,GAEpB,MAAM,qCAAqC,CAAC;AAG7C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAEvD;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAC/B,kBAAkB,CAAU,gBAAgB,EAAE,cAAc,CAAC,CAAC;AAEhE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAEvD;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAC/B,kBAAkB,CAAS,gBAAgB,EAAE,cAAc,CAAC,CAAC;AAE/D,MAAM,aAAa,GAA2C,CAAC,OAAO,CAAC,CAAC;AAExE,2DAA2D;AAC3D,SAAS,OAAO,CACd,OAAyC,EACzC,WAA4C;IAE5C,OAAO,CAAC,OAAO,EAAE,KAAK,IAAI,aAAa,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AACjE,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,OAAO,aAAa;IAKxB,MAAM,CAAU,CAAC,0BAA0B,CAAC,GAA8B;QACxE,KAAK,EAAE,CACL,OAA0C,EACH,EAAE;YACzC,MAAM,OAAO,GAAG,OAAO,CAAC,gBAEX,CAAC;YACd,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;gBAC3C,OAAO,SAAS,CAAC;YACnB,CAAC;YACD,OAAO;gBACL,KAAK,EAAE,CAAC,sCAAsC,EAAE,cAAc,CAAC;aAChE,CAAC;QACJ,CAAC;QACD,QAAQ,EAAE,CACR,OAA0C,EACA,EAAE;YAC5C,MAAM,OAAO,GAAG,OAAO,CAAC,gBAEX,CAAC;YACd,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;gBAC3C,OAAO,SAAS,CAAC;YACnB,CAAC;YAED,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC;gBAClB,OAAO;oBACL;wBACE,WAAW,EAAE,OAAO,CAAC,WAAW;wBAChC,YAAY,EAAE,aAAa,CAAC,IAAI;wBAChC,OAAO,EAAE,yDAAyD;wBAClE,GAAG,EACD,qEAAqE;4BACrE,qEAAqE;qBACxE;iBACF,CAAC;YACJ,CAAC;YAED,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,UAAU,EAAE,CAAC;gBACtC,OAAO;oBACL;wBACE,WAAW,EAAE,OAAO,CAAC,WAAW;wBAChC,YAAY,EAAE,aAAa,CAAC,IAAI;wBAChC,OAAO,EAAE,mEAAmE,OAAO,OAAO,CAAC,GAAG,EAAE;wBAChG,GAAG,EAAE,sFAAsF;qBAC5F;iBACF,CAAC;YACJ,CAAC;YAED,OAAO,SAAS,CAAC;QACnB,CAAC;KACF,CAAC;IAEe,MAAM,CAAiB;IACvB,QAAQ,CAAuB;IAC/B,YAAY,CAAiB;IAE9C,YACE,KAA6B,EAC7B,QAA+B,EAC/B,MAAuB;QAEvB,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,CAAC,GAAG,KAAK,UAAU,EAAE,CAAC;YAC9C,MAAM,IAAI,SAAS,CACjB,4FAA4F,CAC7F,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,QAAQ,IAAI,EAAE,CAAC;QAC/B,IAAI,CAAC,YAAY;YACf,KAAK,YAAY,mBAAmB;gBAClC,CAAC,CAAC,KAAK;gBACP,CAAC,CAAC,IAAI,mBAAmB,CAAC,KAAc,CAAC,CAAC;QAE9C,IAAI,CAAC,MAAM,GAAG,MAAM,IAAI,OAAO,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,MAAM,CACV,OAAyB,EACzB,IAAkB;QAElB,MAAM,OAAO,GAAG,IAAI,CAAC,uBAAuB,CAC1C,OAAO,CAAC,kBAAkB,CAAuB,aAAa,CAAC,CAChE,CAAC;QAEF,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC;YAAE,OAAO,IAAI,EAAE,CAAC;QAC1D,IAAI,OAAO,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,EAAE,CAAC;QAEpE,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;YACjB,MAAM,IAAI,SAAS,CACjB,oBAAoB,OAAO,CAAC,WAAW,yCAAyC;gBAC9E,6EAA6E;gBAC7E,8EAA8E;gBAC9E,kFAAkF,CACrF,CAAC;QACJ,CAAC;QAED,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACjC,eAAe,CAAC,OAAO,EAAE,oBAAoB,EAAE,GAAG,CAAC,CAAC;QAEpD,IAAI,MAAe,CAAC;QACpB,IAAI,QAAQ,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,gBAAgB,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YAC5D,QAAQ,GAAG,KAAK,CAAC;QACnB,CAAC;QACD,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YAC5C,eAAe,CAAC,OAAO,EAAE,oBAAoB,EAAE,IAAI,CAAC,CAAC;YACrD,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,iBAAiB,OAAO,CAAC,WAAW,KAAK,GAAG,GAAG,CAAC,CAAC;YACxE,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,eAAe,CAAC,OAAO,EAAE,oBAAoB,EAAE,KAAK,CAAC,CAAC;QACtD,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,OAAO,CAAC,WAAW,KAAK,GAAG,GAAG,CAAC,CAAC;QAEzE,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;QAC5B,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,MAAM,CAAC;QAE3D,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;QACrC,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,IAAI,CAAC,QAAQ,CACX,MAAM,EACN,aAAa,OAAO,CAAC,WAAW,+CAA+C,GAAG,GAAG,CACtF,CAAC;YACF,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;YAC3D,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YAC/D,CAAC;QACH,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,oEAAoE;IAC5D,gBAAgB,CACtB,SAA2B,EAC3B,OAAyB,EACzB,GAAW,EACX,KAAc,EACd,OAA6B;QAE7B,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAEvE,IAAI,OAAO,CAAC,QAAQ,IAAI,IAAI,EAAE,CAAC;YAC7B,IAAI,CAAC,QAAQ,CACX,MAAM,EACN,SAAS,SAAS,cAAc,OAAO,CAAC,WAAW,GAAG;gBACpD,SAAS,GAAG,oBAAoB,OAAO,EAAE,CAC5C,CAAC;YACF,OAAO;QACT,CAAC;QAED,IAAI,CAAC,QAAQ,CACX,OAAO,EACP,SAAS,SAAS,cAAc,OAAO,CAAC,WAAW,GAAG;YACpD,SAAS,GAAG,sBAAsB,OAAO,EAAE,CAC9C,CAAC;QACF,MAAM,KAAK,CAAC;IACd,CAAC;IAEO,QAAQ,CAAC,KAAiC,EAAE,OAAe;QACjE,IAAI,CAAC;YACH,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC;QACpD,CAAC;QAAC,MAAM,CAAC;YACP,wEAAwE;QAC1E,CAAC;IACH,CAAC;IAED,uEAAuE;IACvE,uBAAuB,CACrB,OAA8B;QAE9B,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAC;QACnC,OAAO,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,OAAO,EAAE,CAAC;IAC1C,CAAC;CACF;AAED,sFAAsF;AACtF,SAAS,UAAU,CAAC,KAAc;IAChC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACnC,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { MissingPartitionError } from '@cqrs-ddd/pipeline';
|
|
2
|
+
/** Which dimension of the cache key could not be resolved. */
|
|
3
|
+
export type CachePartitionDimension = 'tenant' | 'principal' | 'scope';
|
|
4
|
+
/**
|
|
5
|
+
* Raised before the cache is consulted when a dimension required by the
|
|
6
|
+
* configured key factory cannot be resolved.
|
|
7
|
+
*
|
|
8
|
+
* A cache hit returns without executing the handler,
|
|
9
|
+
* so it also skips whatever entity-level authorization and field filtering that
|
|
10
|
+
* handler performs. A key missing its tenant or principal segment therefore does
|
|
11
|
+
* not merely lose isolation — it can replay one caller's authorized response to
|
|
12
|
+
* another.
|
|
13
|
+
*/
|
|
14
|
+
export declare class MissingCachePartitionError extends MissingPartitionError<CachePartitionDimension> {
|
|
15
|
+
readonly name = "MissingCachePartitionError";
|
|
16
|
+
constructor(requestName: string, dimension: CachePartitionDimension, remedy: string);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=missing-partition.error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"missing-partition.error.d.ts","sourceRoot":"","sources":["../../src/errors/missing-partition.error.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,8DAA8D;AAC9D,MAAM,MAAM,uBAAuB,GAAG,QAAQ,GAAG,WAAW,GAAG,OAAO,CAAC;AAEvE;;;;;;;;;GASG;AACH,qBAAa,0BAA2B,SAAQ,qBAAqB,CAAC,uBAAuB,CAAC;IAC5F,SAAkB,IAAI,gCAAgC;IAEtD,YACE,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,uBAAuB,EAClC,MAAM,EAAE,MAAM,EAGf;CACF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
import { MissingPartitionError } from '@cqrs-ddd/pipeline';
|
|
3
|
+
/**
|
|
4
|
+
* Raised before the cache is consulted when a dimension required by the
|
|
5
|
+
* configured key factory cannot be resolved.
|
|
6
|
+
*
|
|
7
|
+
* A cache hit returns without executing the handler,
|
|
8
|
+
* so it also skips whatever entity-level authorization and field filtering that
|
|
9
|
+
* handler performs. A key missing its tenant or principal segment therefore does
|
|
10
|
+
* not merely lose isolation — it can replay one caller's authorized response to
|
|
11
|
+
* another.
|
|
12
|
+
*/
|
|
13
|
+
export class MissingCachePartitionError extends MissingPartitionError {
|
|
14
|
+
name = 'MissingCachePartitionError';
|
|
15
|
+
constructor(requestName, dimension, remedy) {
|
|
16
|
+
super('Cache', requestName, dimension, remedy);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=missing-partition.error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"missing-partition.error.js","sourceRoot":"","sources":["../../src/errors/missing-partition.error.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAK3D;;;;;;;;;GASG;AACH,MAAM,OAAO,0BAA2B,SAAQ,qBAA8C;IAC1E,IAAI,GAAG,4BAA4B,CAAC;IAEtD,YACE,WAAmB,EACnB,SAAkC,EAClC,MAAc;QAEd,KAAK,CAAC,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;IACjD,CAAC;CACF"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type IPipelineContext } from '@cqrs-ddd/pipeline';
|
|
2
|
+
/**
|
|
3
|
+
* The cache decision of one execution as flat attributes:
|
|
4
|
+
* `{ 'cache.hit': boolean }` once `CacheBehavior` looked the response up, and
|
|
5
|
+
* `{}` when it did not run, so a request without caching never reports a
|
|
6
|
+
* fabricated miss. The cache key is never included: it carries the tenant and
|
|
7
|
+
* the principal, and is unbounded as a value.
|
|
8
|
+
*
|
|
9
|
+
* The result is plain data and needs no telemetry package: put it on a span, a
|
|
10
|
+
* log line, an audit record or a metric label (bounded values only).
|
|
11
|
+
*
|
|
12
|
+
* @param context - The pipeline execution, read after `CacheBehavior` ran.
|
|
13
|
+
* @returns The attributes; empty when the behavior did not run.
|
|
14
|
+
*
|
|
15
|
+
* @example Span attributes
|
|
16
|
+
* With `AttributesBehavior` of `@cqrs-ddd/pipeline-opentelemetry`:
|
|
17
|
+
* ```ts
|
|
18
|
+
* createPipeline({
|
|
19
|
+
* globalBehaviors: {
|
|
20
|
+
* before: [TraceBehavior, [AttributesBehavior, { factories: [buildCacheAttributes] }]],
|
|
21
|
+
* },
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @example A log line from a custom behavior, after the chain ran
|
|
26
|
+
* ```ts
|
|
27
|
+
* async handle(context: IPipelineContext, next: NextDelegate) {
|
|
28
|
+
* try {
|
|
29
|
+
* return await next();
|
|
30
|
+
* } finally {
|
|
31
|
+
* this.logger.log({ request: context.requestName, ...buildCacheAttributes(context) });
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildCacheAttributes(context: IPipelineContext): Record<string, boolean>;
|
|
37
|
+
//# sourceMappingURL=build-attributes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-attributes.d.ts","sourceRoot":"","sources":["../../src/helpers/build-attributes.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAG5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,gBAAgB,GACxB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIzB"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
import { getPipelineItem, hasPipelineItem, } from '@cqrs-ddd/pipeline';
|
|
3
|
+
import { CACHE_HIT_ITEM_TOKEN } from '../cache.behavior.js';
|
|
4
|
+
/**
|
|
5
|
+
* The cache decision of one execution as flat attributes:
|
|
6
|
+
* `{ 'cache.hit': boolean }` once `CacheBehavior` looked the response up, and
|
|
7
|
+
* `{}` when it did not run, so a request without caching never reports a
|
|
8
|
+
* fabricated miss. The cache key is never included: it carries the tenant and
|
|
9
|
+
* the principal, and is unbounded as a value.
|
|
10
|
+
*
|
|
11
|
+
* The result is plain data and needs no telemetry package: put it on a span, a
|
|
12
|
+
* log line, an audit record or a metric label (bounded values only).
|
|
13
|
+
*
|
|
14
|
+
* @param context - The pipeline execution, read after `CacheBehavior` ran.
|
|
15
|
+
* @returns The attributes; empty when the behavior did not run.
|
|
16
|
+
*
|
|
17
|
+
* @example Span attributes
|
|
18
|
+
* With `AttributesBehavior` of `@cqrs-ddd/pipeline-opentelemetry`:
|
|
19
|
+
* ```ts
|
|
20
|
+
* createPipeline({
|
|
21
|
+
* globalBehaviors: {
|
|
22
|
+
* before: [TraceBehavior, [AttributesBehavior, { factories: [buildCacheAttributes] }]],
|
|
23
|
+
* },
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @example A log line from a custom behavior, after the chain ran
|
|
28
|
+
* ```ts
|
|
29
|
+
* async handle(context: IPipelineContext, next: NextDelegate) {
|
|
30
|
+
* try {
|
|
31
|
+
* return await next();
|
|
32
|
+
* } finally {
|
|
33
|
+
* this.logger.log({ request: context.requestName, ...buildCacheAttributes(context) });
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function buildCacheAttributes(context) {
|
|
39
|
+
return hasPipelineItem(context, CACHE_HIT_ITEM_TOKEN)
|
|
40
|
+
? { 'cache.hit': getPipelineItem(context, CACHE_HIT_ITEM_TOKEN) === true }
|
|
41
|
+
: {};
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=build-attributes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-attributes.js","sourceRoot":"","sources":["../../src/helpers/build-attributes.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EACL,eAAe,EACf,eAAe,GAEhB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAyB;IAEzB,OAAO,eAAe,CAAC,OAAO,EAAE,oBAAoB,CAAC;QACnD,CAAC,CAAC,EAAE,WAAW,EAAE,eAAe,CAAC,OAAO,EAAE,oBAAoB,CAAC,KAAK,IAAI,EAAE;QAC1E,CAAC,CAAC,EAAE,CAAC;AACT,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Cache } from 'cache-manager';
|
|
2
|
+
import { Keyv } from 'keyv';
|
|
3
|
+
import type { BuildCacheOptions, CacheStoreConfig } from '../interfaces/cache-options.interface.js';
|
|
4
|
+
/**
|
|
5
|
+
* Build a single package-owned `Keyv` instance from a declarative store
|
|
6
|
+
* configuration. Package-owned stores enable `throwOnErrors` so
|
|
7
|
+
* {@link CacheBehavior} can apply its own `failOpen` / fail-closed policy rather
|
|
8
|
+
* than having Keyv silently consume backend failures first.
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildKeyv(config: CacheStoreConfig): Keyv;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the {@link BuildCacheOptions} into a ready-to-use `cache-manager`
|
|
13
|
+
* {@link Cache}. A pre-built `cache` wins, followed by pre-built `stores`,
|
|
14
|
+
* followed by declarative `store` configuration, falling back to an in-memory
|
|
15
|
+
* store when nothing is provided.
|
|
16
|
+
*
|
|
17
|
+
* Package-created stores use `throwOnErrors: true` so cache failures reach
|
|
18
|
+
* `CacheBehavior` and its configured failure policy. Caller-owned `cache` and
|
|
19
|
+
* `stores` are **not mutated**: if an application intentionally shares a Keyv
|
|
20
|
+
* instance with another subsystem, this package must not change that object's
|
|
21
|
+
* error semantics globally.
|
|
22
|
+
*
|
|
23
|
+
* @example Fully package-owned Redis cache
|
|
24
|
+
* ```ts
|
|
25
|
+
* const cache = buildCache({ store: { type: 'redis', url: redisUrl } });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @example Caller-owned shared store — configuration remains caller-controlled
|
|
29
|
+
* ```ts
|
|
30
|
+
* const shared = new Keyv({ store: sharedRedis, throwOnErrors: false });
|
|
31
|
+
* const cache = buildCache({ stores: [shared] });
|
|
32
|
+
* // buildCache() will not mutate shared.throwOnErrors.
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildCache(options: BuildCacheOptions): Cache;
|
|
36
|
+
//# sourceMappingURL=cache-factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-factory.d.ts","sourceRoot":"","sources":["../../src/helpers/cache-factory.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,KAAK,EAAe,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,IAAI,EAAyB,MAAM,MAAM,CAAC;AACnD,OAAO,KAAK,EACV,iBAAiB,EACjB,gBAAgB,EAEjB,MAAM,0CAA0C,CAAC;AAqGlD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAexD;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,KAAK,CAsB5D"}
|