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