@nestjs-pipeline/cache 0.2.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.
Files changed (44) hide show
  1. package/COMMERCIAL_LICENSE.txt +34 -0
  2. package/LICENSE +661 -0
  3. package/README.md +572 -0
  4. package/dist/adapters/cache-manager.adapter.d.ts +21 -0
  5. package/dist/adapters/cache-manager.adapter.d.ts.map +1 -0
  6. package/dist/adapters/cache-manager.adapter.js +56 -0
  7. package/dist/adapters/cache-manager.adapter.js.map +1 -0
  8. package/dist/cache.behavior.d.ts +84 -0
  9. package/dist/cache.behavior.d.ts.map +1 -0
  10. package/dist/cache.behavior.js +239 -0
  11. package/dist/cache.behavior.js.map +1 -0
  12. package/dist/cache.module.d.ts +85 -0
  13. package/dist/cache.module.d.ts.map +1 -0
  14. package/dist/cache.module.js +189 -0
  15. package/dist/cache.module.js.map +1 -0
  16. package/dist/constants/tokens.d.ts +13 -0
  17. package/dist/constants/tokens.d.ts.map +1 -0
  18. package/dist/constants/tokens.js +17 -0
  19. package/dist/constants/tokens.js.map +1 -0
  20. package/dist/errors/missing-partition.error.d.ts +18 -0
  21. package/dist/errors/missing-partition.error.d.ts.map +1 -0
  22. package/dist/errors/missing-partition.error.js +23 -0
  23. package/dist/errors/missing-partition.error.js.map +1 -0
  24. package/dist/helpers/cache-factory.d.ts +39 -0
  25. package/dist/helpers/cache-factory.d.ts.map +1 -0
  26. package/dist/helpers/cache-factory.js +152 -0
  27. package/dist/helpers/cache-factory.js.map +1 -0
  28. package/dist/helpers/cache-key.d.ts +79 -0
  29. package/dist/helpers/cache-key.d.ts.map +1 -0
  30. package/dist/helpers/cache-key.js +92 -0
  31. package/dist/helpers/cache-key.js.map +1 -0
  32. package/dist/helpers/cache.intent.d.ts +26 -0
  33. package/dist/helpers/cache.intent.d.ts.map +1 -0
  34. package/dist/helpers/cache.intent.js +24 -0
  35. package/dist/helpers/cache.intent.js.map +1 -0
  36. package/dist/index.d.ts +10 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +27 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/interfaces/cache-options.interface.d.ts +120 -0
  41. package/dist/interfaces/cache-options.interface.d.ts.map +1 -0
  42. package/dist/interfaces/cache-options.interface.js +4 -0
  43. package/dist/interfaces/cache-options.interface.js.map +1 -0
  44. package/package.json +86 -0
package/README.md ADDED
@@ -0,0 +1,572 @@
1
+ # @nestjs-pipeline/cache
2
+
3
+ ## Architectural role
4
+
5
+ This reusable package caches final application query results, including expensive
6
+ aggregations, read models combining repositories and external-service composition.
7
+ It complements repository snapshot/read-through caches; it is not replaced by
8
+ them just because a particular example currently uses repository caching. Choose
9
+ the layer that owns the result and define its dependencies, security scope and
10
+ freshness. Using both layers is optional, and invalidation is not automatically
11
+ shared.
12
+
13
+ [![npm version](https://img.shields.io/npm/v/@nestjs-pipeline/cache.svg)](https://www.npmjs.com/package/@nestjs-pipeline/cache)
14
+ [![License](https://img.shields.io/npm/l/@nestjs-pipeline/cache.svg)](https://www.npmjs.com/package/@nestjs-pipeline/cache)
15
+
16
+ Caching behavior for `@nestjs-pipeline/core`, powered by [cache-manager](https://www.npmjs.com/package/cache-manager) v7 on top of [Keyv](https://keyv.org/). Transparently cache query results — declaratively, with zero changes to your handler code — and choose any backend: **memory** (default), **redis**, **memcache**, **sqlite**, or **postgres**.
17
+
18
+ > **New in 0.2.0:** this is the first published release of the package.
19
+
20
+ ---
21
+
22
+ ## Table of Contents
23
+
24
+ - [Why](#why)
25
+ - [Installation](#installation)
26
+ - [Quick Start](#quick-start)
27
+ - [1. Register the module](#1-register-the-module)
28
+ - [2. Attach the behavior](#2-attach-the-behavior)
29
+ - [3. Configure per handler](#3-configure-per-handler)
30
+ - [Choosing a Store](#choosing-a-store)
31
+ - [Memory (default)](#memory-default)
32
+ - [Redis](#redis)
33
+ - [Memcache](#memcache)
34
+ - [SQLite](#sqlite)
35
+ - [Postgres](#postgres)
36
+ - [Tiered (multi-layer) caches](#tiered-multi-layer-caches)
37
+ - [Escape hatches](#escape-hatches)
38
+ - [How It Works](#how-it-works)
39
+ - [What gets cached](#what-gets-cached)
40
+ - [Cache keys](#cache-keys)
41
+ - [Tenant partitioning](#tenant-partitioning)
42
+ - [Options resolution](#options-resolution)
43
+ - [Context items](#context-items)
44
+ - [Configuration](#configuration)
45
+ - [Behavior Contract & Bootstrap Diagnostics](#behavior-contract--bootstrap-diagnostics)
46
+ - [Custom Logger](#custom-logger)
47
+ - [API Reference](#api-reference)
48
+ - [License](#license)
49
+
50
+ ---
51
+
52
+ ## Why
53
+
54
+ Read-heavy queries often hit the same data repeatedly. `@nestjs-pipeline/cache` adds a transparent caching layer to your CQRS pipeline without coupling the caching logic to your business code. It is a thin, type-safe behavior over [cache-manager](https://github.com/jaredwray/cacheable) v7 + [Keyv](https://keyv.org/), so you get tiered caches and a consistent interface across every supported backend.
55
+
56
+ ---
57
+
58
+ ## Installation
59
+
60
+ ```bash
61
+ pnpm add @nestjs-pipeline/cache cache-manager keyv
62
+ ```
63
+
64
+ **Peer dependencies:**
65
+
66
+ ```bash
67
+ pnpm add @nestjs-pipeline/core @nestjs/common reflect-metadata
68
+ ```
69
+
70
+ Requires `@nestjs/common` `^11.0.0` and `@nestjs-pipeline/core` `^0.2.0`.
71
+
72
+ **Optional store adapters** — install only the one(s) you use:
73
+
74
+ ```bash
75
+ pnpm add @keyv/redis # type: 'redis'
76
+ pnpm add @keyv/memcache # type: 'memcache'
77
+ pnpm add @keyv/sqlite # type: 'sqlite'
78
+ pnpm add @keyv/postgres # type: 'postgres'
79
+ ```
80
+
81
+ > The `memory` store needs no adapter — it ships with Keyv. The other backends are loaded lazily; you only need the matching `@keyv/*` package when you actually select that store type.
82
+
83
+ ---
84
+
85
+ ## Quick Start
86
+
87
+ ### 1. Register the module
88
+
89
+ ```ts
90
+ import { Module } from '@nestjs/common';
91
+ import { PipelineModule } from '@nestjs-pipeline/core';
92
+ import { CacheModule, CacheBehavior } from '@nestjs-pipeline/cache';
93
+
94
+ @Module({
95
+ imports: [
96
+ // In-memory cache with a 30s default TTL
97
+ CacheModule.forRoot({ ttl: 30_000 }),
98
+ // Make CacheBehavior available to @UsePipeline/globalBehaviors.
99
+ PipelineModule.forRoot({ behaviors: [CacheBehavior] }),
100
+ ],
101
+ })
102
+ export class AppModule {}
103
+ ```
104
+
105
+ The cache is built when the application instantiates its providers, once per
106
+ application. To derive store settings from injected configuration, use
107
+ `forRootAsync`:
108
+
109
+ ```ts
110
+ CacheModule.forRootAsync({
111
+ inject: [ConfigService],
112
+ useFactory: (config: ConfigService) => ({
113
+ store: { type: 'redis', url: config.getOrThrow('REDIS_URL') },
114
+ ttl: 30_000,
115
+ }),
116
+ });
117
+ ```
118
+
119
+ A cache built from `store` (or the default memory store) is disconnected on
120
+ application shutdown (`app.close()` or Nest shutdown hooks). A supplied `cache` or
121
+ `stores` belongs to the caller, who closes it.
122
+
123
+ ### 2. Attach the behavior
124
+
125
+ The `behaviors` option above registers `CacheBehavior` with Nest DI; it does not execute it globally. Attach it per handler with `@UsePipeline`, or put it in `globalBehaviors` if you want it to run for a global scope.
126
+
127
+ ### 3. Configure per handler
128
+
129
+ This protected-query fragment assumes the pipeline has a tenant (`context.tenantId`,
130
+ see [Tenant partitioning](#tenant-partitioning)) and that an upstream behavior sets
131
+ `currentUserId` and `capabilityVersion` on `context.items` before caching runs.
132
+ The handler still performs entity and field authorization on cache misses.
133
+
134
+ ```ts
135
+ import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
136
+ import { UsePipeline } from '@nestjs-pipeline/core';
137
+ import { cache, createPartitionedCacheKeyFactory } from '@nestjs-pipeline/cache';
138
+
139
+ @QueryHandler(GetUserQuery)
140
+ @UsePipeline(cache({
141
+ ttl: 60_000,
142
+ key: createPartitionedCacheKeyFactory({
143
+ principal: (ctx) => ctx.items.get('currentUserId') as string | undefined,
144
+ scope: (ctx) => {
145
+ const scope = ctx.items.get('capabilityVersion');
146
+ if (typeof scope !== 'string' || !scope.trim()) {
147
+ throw new Error('Missing capability version');
148
+ }
149
+ return scope;
150
+ },
151
+ }),
152
+ }))
153
+ export class GetUserHandler implements IQueryHandler<GetUserQuery> {
154
+ async execute(query: GetUserQuery) {
155
+ // ...expensive read; result cached for 60s
156
+ }
157
+ }
158
+ ```
159
+
160
+ > Use `cache({ inheritModuleKey: true })` only when the module supplies the key factory.
161
+ > The raw tuple form `@UsePipeline([CacheBehavior, { ... }])` remains supported as an escape hatch.
162
+
163
+ ---
164
+
165
+ ## Choosing a Store
166
+
167
+ The backend is selected once, when registering the module. Per-handler options
168
+ (`ttl`, `key`, `condition`, `kinds`) are independent of the store you pick.
169
+
170
+ ### Memory (default)
171
+
172
+ ```ts
173
+ CacheModule.forRoot({ ttl: 30_000 });
174
+ // equivalent to:
175
+ CacheModule.forRoot({ store: { type: 'memory' }, ttl: 30_000 });
176
+ ```
177
+
178
+ ### Redis
179
+
180
+ ```ts
181
+ CacheModule.forRoot({
182
+ store: { type: 'redis', url: 'redis://localhost:6379' },
183
+ ttl: 60_000,
184
+ });
185
+ ```
186
+
187
+ ### Memcache
188
+
189
+ ```ts
190
+ CacheModule.forRoot({
191
+ store: { type: 'memcache', url: 'localhost:11211' },
192
+ });
193
+ ```
194
+
195
+ ### SQLite
196
+
197
+ ```ts
198
+ CacheModule.forRoot({
199
+ store: { type: 'sqlite', url: 'sqlite://./cache.sqlite' },
200
+ });
201
+ ```
202
+
203
+ ### Postgres
204
+
205
+ ```ts
206
+ CacheModule.forRoot({
207
+ store: {
208
+ type: 'postgres',
209
+ url: 'postgresql://user:pass@localhost:5432/db',
210
+ options: { table: 'cache' },
211
+ },
212
+ });
213
+ ```
214
+
215
+ ### Tiered (multi-layer) caches
216
+
217
+ Provide an array of stores — they are checked in order (fastest first) and
218
+ writes fan out to every layer:
219
+
220
+ ```ts
221
+ CacheModule.forRoot({
222
+ store: [
223
+ { type: 'memory', ttl: 5_000 }, // L1: in-process
224
+ { type: 'redis', url: 'redis://localhost:6379' }, // L2: shared
225
+ ],
226
+ });
227
+ ```
228
+
229
+ ### Escape hatches
230
+
231
+ For full control, pass a pre-built `cache-manager` instance or your own `Keyv`
232
+ stores:
233
+
234
+ ```ts
235
+ import { createCache } from 'cache-manager';
236
+ import { Keyv } from 'keyv';
237
+ import KeyvRedis from '@keyv/redis';
238
+
239
+ // Pre-built Keyv stores
240
+ CacheModule.forRoot({
241
+ stores: [new Keyv({ store: new KeyvRedis('redis://localhost:6379') })],
242
+ });
243
+
244
+ // Fully pre-built cache
245
+ CacheModule.forRoot({ cache: createCache({ stores: [new Keyv()] }) });
246
+ ```
247
+
248
+ | Option | Precedence | Description |
249
+ | ------ | ---------- | ----------- |
250
+ | `cache` | 1 (highest) | A ready-made `cache-manager` instance. |
251
+ | `stores` | 2 | Pre-built `Keyv[]` (tiered, highest priority first). |
252
+ | `store` | 3 | Declarative config — a single store or an array. |
253
+ | _(none)_ | 4 (fallback) | In-memory `Keyv`. |
254
+
255
+ ---
256
+
257
+ ## How It Works
258
+
259
+ ### What gets cached
260
+
261
+ Only **query** requests are cached by default — commands and events always pass
262
+ through untouched. Override this with the `kinds` option. On a cache miss,
263
+ `null` and `undefined` results are not written. A hit returns the value from
264
+ that lookup directly. `CacheBehavior` does not use `cache-manager.wrap()` or
265
+ background refresh because a refresh callback would re-run every behavior and
266
+ side effect nested after the cache behavior.
267
+
268
+ Cached results are JSON values. On a miss the behavior stores the JSON form of
269
+ the handler result and returns that same form, so a miss and a later hit have
270
+ the same shape: a `Date` is an ISO string and a class instance a plain object on
271
+ both. Return plain JSON data (a snapshot or read model) from cached handlers. A
272
+ result without a JSON form (a `bigint`, a cycle) is returned unchanged, not
273
+ cached, and logged as a warning.
274
+
275
+ ### Store errors
276
+
277
+ Declaratively created stores use `throwOnErrors: true`. Pre-built `cache` and
278
+ `stores` remain caller-owned and are passed through without changing their error
279
+ settings. The behavior can handle only failures those implementations surface;
280
+ it cannot detect backend errors they swallow.
281
+
282
+ `CacheBehavior` owns a consistent failure policy independently of the injected
283
+ `cache-manager` or custom cache implementation. By default, `failOpen: true`:
284
+
285
+ - a thrown cache read is logged, recorded as `cache.hit = false`, and bypasses
286
+ the cache write for that execution; the handler result still uses the same JSON conversion as a normal miss;
287
+ - a thrown cache write is logged and the successful handler result is returned.
288
+
289
+ Set `failOpen: false` to log and propagate either store error. This strict mode
290
+ can turn a successful downstream handler execution into a rejected request when
291
+ the subsequent cache write fails, so it is best suited to cases where cache
292
+ availability is part of the operation's contract. Errors from the condition,
293
+ key factory, or downstream handler are always propagated unchanged.
294
+
295
+ ### Cache keys
296
+
297
+ The helper emits `cache:v3:<tenant>:<principal>:<scope>:<requestName>:<sha256>`,
298
+ with escaped segments and the absent-segment encoding supplied by core
299
+ `joinKeySegments`. Treat the generated key as opaque. Changing from older
300
+ formats causes a cold cache; let old entries expire or remove their namespace.
301
+
302
+ `key` is **required**. There is deliberately no default.
303
+
304
+ The previous default embedded `context.correlationId`, which is unique per
305
+ request. That made the cache write an entry for every query and never read one
306
+ back — two extra round-trips and unbounded store growth for a zero percent hit
307
+ rate. It was not an authorization boundary either: a client can send its own
308
+ correlation ID, and nested executions deliberately inherit one. Correlation metadata
309
+ does not establish principal or permission isolation, as correlation IDs may be supplied
310
+ or reused. Cache hits skip the handler, including entity/field checks. For protected
311
+ results, provide an explicit `key` covering tenant, principal type/ID, effective
312
+ permission scope and response dependencies; fail closed if required context is
313
+ missing. Configure invalidation/freshness for that result separately. A type-level
314
+ authorization behavior outside the cache does not reproduce every entity check.
315
+
316
+ Use `createPartitionedCacheKeyFactory`. It partitions every dimension that can
317
+ change an authorized response, escapes each segment so `a:b` + `c` cannot collide
318
+ with `a` + `b:c`, and fails closed with `MissingCachePartitionError` when a
319
+ required dimension is absent:
320
+
321
+ ```typescript
322
+ import { createPartitionedCacheKeyFactory } from '@nestjs-pipeline/cache';
323
+
324
+ @UsePipeline([CacheBehavior, {
325
+ key: createPartitionedCacheKeyFactory({
326
+ principal: (ctx) => ctx.items.get('currentUserId') as string | undefined,
327
+ // Include a role-set hash or capability version, otherwise a principal whose
328
+ // permissions were revoked keeps reading the old response until it expires.
329
+ scope: (ctx) => ctx.items.get('capabilityVersion') as string | undefined,
330
+ }),
331
+ }])
332
+ export class GetUsersHandler {}
333
+ ```
334
+
335
+ For genuinely public responses that are identical for every caller:
336
+
337
+ ```typescript
338
+ createPartitionedCacheKeyFactory({
339
+ principal: () => 'public',
340
+ requirePrincipal: false,
341
+ requireTenant: false,
342
+ requireScope: false,
343
+ });
344
+ ```
345
+
346
+ The permission scope is required by default: creating a factory without a
347
+ `scope` resolver throws unless `requireScope: false` declares that responses do
348
+ not depend on the caller's permissions, and a resolver that returns nothing
349
+ throws `MissingCachePartitionError` at request time.
350
+
351
+ The tenant options are the same for the cache, idempotency and rate-limit key factories
352
+ (`TenantPartitionOptions` of `@nestjs-pipeline/core`): `includeTenant` (default `true`)
353
+ puts the tenant in the key, and `requireTenant` (default: `includeTenant`) refuses a
354
+ missing one. The three packages' partition errors extend `MissingPartitionError`.
355
+
356
+ The request payload is included as a SHA-256 digest, so secrets and search terms
357
+ stay out of Redis key listings. The digest is built with `stableStringify` from `@cqrs-ddd/safe-stringify` (not re-exported by this package), which
358
+ sorts object keys recursively so structurally equal payloads map to the same
359
+ entry. It accepts `null`, booleans, finite numbers, strings, arrays, record-like
360
+ objects, and valid dates (converted to ISO strings). Lossy native JSON cases such
361
+ as `Map`, `Set`, `RegExp`, `Error`, binary values, non-finite numbers,
362
+ `undefined`, bigint, functions, symbols, and cycles are rejected instead of
363
+ risking a collision.
364
+
365
+ Because a cache hit returns before the handler runs, it also skips whatever
366
+ entity-level authorization and field filtering the handler performs. That is why
367
+ tenant, principal and scope are all required by the helper by default. Opt out
368
+ of each explicitly with `requireTenant: false`, `requirePrincipal: false` or
369
+ `requireScope: false`.
370
+
371
+ A caught `MissingCachePartitionError` names the missing dimension:
372
+
373
+ ```typescript
374
+ import { MissingCachePartitionError } from '@nestjs-pipeline/cache';
375
+
376
+ if (error instanceof MissingCachePartitionError) {
377
+ // error.name === 'MissingCachePartitionError'; map it to 401/403 in your filter
378
+ }
379
+ ```
380
+
381
+ ### Tenant partitioning
382
+
383
+ The tenant segment of the key is `context.tenantId`, not a value on
384
+ `context.items`. The pipeline takes it from the tenant source configured on
385
+ `PipelineModule.forRoot()`; the cache package does not import
386
+ `@nestjs-pipeline/tenant`:
387
+
388
+ ```typescript
389
+ import { Module } from '@nestjs/common';
390
+ import { PipelineModule } from '@nestjs-pipeline/core';
391
+ import { tenantSource } from '@nestjs-pipeline/tenant';
392
+ import { CacheBehavior, CacheModule } from '@nestjs-pipeline/cache';
393
+
394
+ @Module({
395
+ imports: [
396
+ CacheModule.forRoot({ ttl: 30_000 }),
397
+ PipelineModule.forRoot({
398
+ sources: { tenantId: tenantSource },
399
+ behaviors: [CacheBehavior],
400
+ }),
401
+ ],
402
+ })
403
+ export class AppModule {}
404
+ ```
405
+
406
+ For a single-tenant deployment, leave the tenant out of the key:
407
+
408
+ ```typescript
409
+ createPartitionedCacheKeyFactory({
410
+ includeTenant: false,
411
+ principal: (ctx) => ctx.items.get('currentUserId') as string | undefined,
412
+ scope: (ctx) => ctx.items.get('capabilityVersion') as string | undefined,
413
+ });
414
+ ```
415
+
416
+ The correlation id is never part of the key (see above).
417
+
418
+ ### Sharing a key factory through module defaults
419
+
420
+ Set the factory once in `defaults` and opt in per handler with
421
+ `inheritModuleKey: true`; `cache()` rejects a call with neither `key` nor
422
+ `inheritModuleKey` at compile time:
423
+
424
+ ```typescript
425
+ CacheModule.forRoot({
426
+ ttl: 30_000,
427
+ defaults: {
428
+ key: createPartitionedCacheKeyFactory({
429
+ principal: (ctx) => ctx.items.get('currentUserId') as string | undefined,
430
+ scope: (ctx) => ctx.items.get('capabilityVersion') as string | undefined,
431
+ }),
432
+ },
433
+ });
434
+
435
+ @QueryHandler(ListOrdersQuery)
436
+ @UsePipeline(cache({ inheritModuleKey: true, ttl: 10_000 }))
437
+ export class ListOrdersHandler {}
438
+ ```
439
+
440
+ ### Conditional caching
441
+
442
+ `condition` returns `false` to bypass the cache for one request:
443
+
444
+ ```typescript
445
+ @UsePipeline(cache({
446
+ inheritModuleKey: true,
447
+ condition: (ctx) => (ctx.request as { fresh?: boolean }).fresh !== true,
448
+ }))
449
+ export class GetReportHandler {}
450
+ ```
451
+
452
+ ### Options resolution
453
+
454
+ Effective options for a handler are resolved as:
455
+
456
+ 1. Module-wide defaults bound via `CacheModule.forRoot({ ttl, defaults })`.
457
+ 2. Per-handler options from `@UsePipeline([CacheBehavior, { ... }])`,
458
+ shallow-merged on top (handler keys win).
459
+
460
+ ### Context items
461
+
462
+ The behavior records diagnostics on `context.items`:
463
+
464
+ | Item Token | Type | Meaning |
465
+ | ---------- | ---- | ------- |
466
+ | `CACHE_HIT_ITEM` | `boolean` | Whether the request was served from cache. |
467
+ | `CACHE_KEY_ITEM` | `string` | The resolved cache key. |
468
+
469
+ Exported as unique `Symbol` constants (`CACHE_HIT_ITEM` and `CACHE_KEY_ITEM`) to prevent key collisions in `context.items`. `CACHE_HIT_ITEM_TOKEN` and `CACHE_KEY_ITEM_TOKEN` are typed tokens over the same keys for `getPipelineItem` / `requirePipelineItem` from `@nestjs-pipeline/core`.
470
+
471
+
472
+ ---
473
+
474
+ ## Configuration
475
+
476
+ `CacheModuleOptions` (passed to `CacheModule.forRoot`):
477
+
478
+ | Field | Type | Description |
479
+ | ----- | ---- | ----------- |
480
+ | `cache` | `Cache` | Pre-built `cache-manager` instance (escape hatch). |
481
+ | `stores` | `Keyv[]` | Pre-built Keyv stores (tiered). |
482
+ | `store` | `CacheStoreConfig \| CacheStoreConfig[]` | Declarative store(s). |
483
+ | `ttl` | `number` | Default TTL (ms) for stores and handlers. |
484
+ | `nonBlocking` | `boolean` | Optimize multi-store reads/writes. |
485
+ | `defaults` | `CacheBehaviorOptions` | Default per-handler options. |
486
+
487
+ `CacheBehaviorOptions` (per handler and/or module `defaults`):
488
+
489
+ | Field | Type | Default | Description |
490
+ | ----- | ---- | ------- | ----------- |
491
+ | `kinds` | `Array<'command' \| 'query' \| 'event' \| 'unknown'>` | `['query']` | Request kinds eligible for caching. |
492
+ | `ttl` | `number` | module `ttl` | TTL (ms) for entries written by this handler. |
493
+ | `key` | `(context) => string` | Required | Explicit cache-key factory; use `createPartitionedCacheKeyFactory` for protected responses. |
494
+ | `condition` | `(context) => boolean` | _always_ | Gate whether a request is cached. |
495
+ | `failOpen` | `boolean` | `true` | Log and bypass thrown cache read/write errors; set `false` to propagate them. |
496
+
497
+ `CacheStoreConfig` (declarative store):
498
+
499
+ | Field | Type | Description |
500
+ | ----- | ---- | ----------- |
501
+ | `type` | `'memory' \| 'redis' \| 'memcache' \| 'sqlite' \| 'postgres'` | Backend to build. |
502
+ | `url` | `string` | Connection string / URI (ignored for `memory`). |
503
+ | `namespace` | `string` | Key prefix for this store. |
504
+ | `ttl` | `number` | Default TTL (ms) for this store. |
505
+ | `options` | `Record<string, unknown>` | Adapter-specific options passed through. |
506
+
507
+ ---
508
+
509
+ ## Behavior Contract & Bootstrap Diagnostics
510
+
511
+ `CacheBehavior` implements `@nestjs-pipeline/core` behavior contract diagnostics:
512
+
513
+ ### Ordering Constraints
514
+
515
+ - **Execution order**: Cache lookup must execute **after** CASL authorization (`@nestjs-pipeline/casl:CaslBehavior`) for all active cache request kinds (`kinds: ['query']` by default). This guarantees unauthenticated or unauthorized callers never receive cached responses.
516
+ - **Dynamic evaluation**: The ordering rule evaluates dynamically per handler. For inactive request kinds (e.g. a command handler where cache defaults to queries only), ordering constraints are not enforced.
517
+
518
+ ### Validation Invariants
519
+
520
+ - **Callable key factory**: Whenever caching is active for a handler's request kind, `key` must be a callable function (`typeof === 'function'`). Strings or non-callable values are rejected fast at bootstrap.
521
+ - **Module defaults**: Application-wide defaults supplied to `CacheModule.forRoot({ defaults: { ... } })` are resolved by `CacheBehavior.resolveEffectiveOptions` and evaluated alongside handler options during bootstrap.
522
+
523
+ ---
524
+
525
+ ## Custom Logger
526
+
527
+ Diagnostic logger failures do not replace handler results or cache-store errors.
528
+
529
+ `CacheBehavior` emits `debug` cache hit/miss lines and `warn`/`error` store-failure lines through the logger injected with `LOGGING_BEHAVIOR_LOGGER`, falling back to a standard NestJS `Logger` when that token is not bound. `CacheModule` logs the store-initialization message through a NestJS `Logger` when the cache is built.
530
+
531
+ ---
532
+
533
+ ## API Reference
534
+
535
+ ```ts
536
+ import {
537
+ CacheModule,
538
+ CacheBehavior,
539
+ cache,
540
+ CACHE_DEFAULT_OPTIONS,
541
+ PIPELINE_CACHE,
542
+ CACHE_HIT_ITEM,
543
+ CACHE_HIT_ITEM_TOKEN,
544
+ CACHE_KEY_ITEM,
545
+ CACHE_KEY_ITEM_TOKEN,
546
+ CacheManagerAdapter,
547
+ buildCache,
548
+ buildKeyv,
549
+ createPartitionedCacheKeyFactory,
550
+ MissingCachePartitionError,
551
+ type CachePartitionDimension,
552
+ type IPipelineCache,
553
+ type CacheModuleOptions,
554
+ type CacheModuleAsyncOptions,
555
+ type CacheBehaviorOptions,
556
+ type CacheIntentOptions,
557
+ type CacheStoreConfig,
558
+ type CacheStoreType,
559
+ type CacheKeyFactory,
560
+ type CacheCondition,
561
+ type PartitionedCacheKeyOptions,
562
+ } from '@nestjs-pipeline/cache';
563
+ ```
564
+
565
+ ---
566
+
567
+ ## License
568
+
569
+ Distributed under a dual license: **AGPLv3** (open source) or a **Commercial
570
+ License**. See [`LICENSE`](https://github.com/aristoteliss/nestjs-pipeline/blob/master/LICENSE) and
571
+ [`COMMERCIAL_LICENSE.txt`](https://github.com/aristoteliss/nestjs-pipeline/blob/master/COMMERCIAL_LICENSE.txt), or contact
572
+ aristotelis@ik.me.
@@ -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;gBAEwB,KAAK,EAAE,KAAK;IAWzC,OAAO,CAAC,YAAY;IAcd,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASlC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAQvE"}
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.CacheManagerAdapter = void 0;
5
+ const node_async_hooks_1 = require("node:async_hooks");
6
+ /**
7
+ * Adapter wrapping a cache-manager Cache instance to reliably expose backend
8
+ * store errors that cache-manager would otherwise swallow in get().
9
+ */
10
+ class CacheManagerAdapter {
11
+ cache;
12
+ // cache-manager emits errors in the originating operation's async context.
13
+ // A key alone cannot identify concurrent reads, writes, or fallback lookups.
14
+ operation = new node_async_hooks_1.AsyncLocalStorage();
15
+ constructor(cache) {
16
+ this.cache = cache;
17
+ // A caller-supplied store may swallow backend errors, which then read as a
18
+ // miss; these listeners surface what it reports.
19
+ const emitter = cache;
20
+ emitter.on?.('get', (event) => this.captureError('get', event));
21
+ emitter.on?.('set', (event) => this.captureError('set', event));
22
+ emitter.on?.('error', (event) => this.captureError(undefined, event));
23
+ }
24
+ captureError(kind, event) {
25
+ const operation = this.operation.getStore();
26
+ if (!operation || (kind && operation.kind !== kind))
27
+ return;
28
+ if (event &&
29
+ typeof event === 'object' &&
30
+ 'key' in event &&
31
+ event.key === operation.key &&
32
+ 'error' in event) {
33
+ operation.error = event.error;
34
+ }
35
+ }
36
+ async get(key) {
37
+ return this.operation.run({ key, kind: 'get' }, async () => {
38
+ const value = await this.cache.get(key);
39
+ const error = this.operation.getStore()?.error;
40
+ if (value === undefined && error !== undefined)
41
+ throw error;
42
+ return value;
43
+ });
44
+ }
45
+ async set(key, value, ttl) {
46
+ return this.operation.run({ key, kind: 'set' }, async () => {
47
+ const result = await this.cache.set(key, value, ttl);
48
+ const error = this.operation.getStore()?.error;
49
+ if (error !== undefined)
50
+ throw error;
51
+ return result;
52
+ });
53
+ }
54
+ }
55
+ exports.CacheManagerAdapter = CacheManagerAdapter;
56
+ //# 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,uDAAqD;AAWrD;;;GAGG;AACH,MAAa,mBAAmB;IASD;IAR7B,2EAA2E;IAC3E,6EAA6E;IAC5D,SAAS,GAAG,IAAI,oCAAiB,EAI9C,CAAC;IAEL,YAA6B,KAAY;QAAZ,UAAK,GAAL,KAAK,CAAO;QACvC,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;AAnDD,kDAmDC"}