@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
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { createCache } from 'cache-manager';
|
|
4
|
+
import { Keyv } from 'keyv';
|
|
5
|
+
const load = createRequire(import.meta.url);
|
|
6
|
+
/** Maps declarative store types to their optional `@keyv/*` adapter package. */
|
|
7
|
+
const ADAPTER_PACKAGES = {
|
|
8
|
+
redis: '@keyv/redis',
|
|
9
|
+
memcache: '@keyv/memcache',
|
|
10
|
+
sqlite: '@keyv/sqlite',
|
|
11
|
+
postgres: '@keyv/postgres',
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Whether the requested adapter package itself could not be resolved.
|
|
15
|
+
*
|
|
16
|
+
* Only a resolution failure *for that exact package* counts. A `MODULE_NOT_FOUND`
|
|
17
|
+
* naming some other module means the adapter is installed but one of its own
|
|
18
|
+
* dependencies is not, and a native binding failure means it is installed but
|
|
19
|
+
* did not build.
|
|
20
|
+
*/
|
|
21
|
+
function isRequestedModuleMissing(error, pkg) {
|
|
22
|
+
if (!(error instanceof Error))
|
|
23
|
+
return false;
|
|
24
|
+
if (error.code !== 'MODULE_NOT_FOUND') {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return (error.message.includes(`Cannot find module '${pkg}'`) ||
|
|
28
|
+
error.message.includes(`Cannot find module "${pkg}"`));
|
|
29
|
+
}
|
|
30
|
+
/** Whether the adapter is present but its native binary is missing or unusable. */
|
|
31
|
+
function isNativeBindingFailure(error) {
|
|
32
|
+
if (!(error instanceof Error))
|
|
33
|
+
return false;
|
|
34
|
+
return (error.message.includes('bindings file') ||
|
|
35
|
+
error.message.includes('was compiled against a different Node.js version') ||
|
|
36
|
+
error.message.includes('invalid ELF header'));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Lazily resolve an optional `@keyv/*` adapter. The adapters are declared as
|
|
40
|
+
* optional peer dependencies, so they are only required when the matching store
|
|
41
|
+
* type is actually requested.
|
|
42
|
+
*
|
|
43
|
+
* Each failure keeps its own diagnosis, and every wrapper preserves `cause`:
|
|
44
|
+
*
|
|
45
|
+
* - the package cannot be resolved → install it;
|
|
46
|
+
* - the package loaded but its native binary did not → rebuild it;
|
|
47
|
+
* - anything else → rethrown untouched, so the real root cause survives.
|
|
48
|
+
*/
|
|
49
|
+
function requireAdapter(pkg) {
|
|
50
|
+
let mod;
|
|
51
|
+
try {
|
|
52
|
+
mod = load(pkg);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (isRequestedModuleMissing(error, pkg)) {
|
|
56
|
+
throw new Error(`[pipeline-cache] The optional '${pkg}' package is required for this store type. Install it with: pnpm add ${pkg}`, { cause: error });
|
|
57
|
+
}
|
|
58
|
+
if (isNativeBindingFailure(error)) {
|
|
59
|
+
throw new Error(`[pipeline-cache] '${pkg}' is installed but its native binding could not be loaded. ` +
|
|
60
|
+
`Rebuild it for this Node.js version (for example: pnpm rebuild ${pkg}); reinstalling the package alone will not help.`, { cause: error });
|
|
61
|
+
}
|
|
62
|
+
// An installed adapter that failed for any other reason — a missing
|
|
63
|
+
// transitive dependency, a broken export, an initialization error. Its own
|
|
64
|
+
// message is the accurate one.
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
return (mod.default ??
|
|
68
|
+
mod);
|
|
69
|
+
}
|
|
70
|
+
/** Construct the backing `Keyv` store adapter for a declarative config. */
|
|
71
|
+
function createAdapterStore(config) {
|
|
72
|
+
const { type, url, options } = config;
|
|
73
|
+
const Adapter = requireAdapter(ADAPTER_PACKAGES[type]);
|
|
74
|
+
if (type === 'postgres') {
|
|
75
|
+
return new Adapter({ uri: url, ...options });
|
|
76
|
+
}
|
|
77
|
+
return new Adapter(url, options);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build a single package-owned `Keyv` instance from a declarative store
|
|
81
|
+
* configuration. Package-owned stores enable `throwOnErrors` so
|
|
82
|
+
* {@link CacheBehavior} can apply its own `failOpen` / fail-closed policy rather
|
|
83
|
+
* than having Keyv silently consume backend failures first.
|
|
84
|
+
*/
|
|
85
|
+
export function buildKeyv(config) {
|
|
86
|
+
if (config.type === 'memory') {
|
|
87
|
+
return new Keyv({
|
|
88
|
+
namespace: config.namespace,
|
|
89
|
+
ttl: config.ttl,
|
|
90
|
+
throwOnErrors: true,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return new Keyv({
|
|
94
|
+
store: createAdapterStore(config),
|
|
95
|
+
namespace: config.namespace,
|
|
96
|
+
ttl: config.ttl,
|
|
97
|
+
throwOnErrors: true,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Resolve the {@link BuildCacheOptions} into a ready-to-use `cache-manager`
|
|
102
|
+
* {@link Cache}. A pre-built `cache` wins, followed by pre-built `stores`,
|
|
103
|
+
* followed by declarative `store` configuration, falling back to an in-memory
|
|
104
|
+
* store when nothing is provided.
|
|
105
|
+
*
|
|
106
|
+
* Package-created stores use `throwOnErrors: true` so cache failures reach
|
|
107
|
+
* `CacheBehavior` and its configured failure policy. Caller-owned `cache` and
|
|
108
|
+
* `stores` are **not mutated**: if an application intentionally shares a Keyv
|
|
109
|
+
* instance with another subsystem, this package must not change that object's
|
|
110
|
+
* error semantics globally.
|
|
111
|
+
*
|
|
112
|
+
* @example Fully package-owned Redis cache
|
|
113
|
+
* ```ts
|
|
114
|
+
* const cache = buildCache({ store: { type: 'redis', url: redisUrl } });
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* @example Caller-owned shared store — configuration remains caller-controlled
|
|
118
|
+
* ```ts
|
|
119
|
+
* const shared = new Keyv({ store: sharedRedis, throwOnErrors: false });
|
|
120
|
+
* const cache = buildCache({ stores: [shared] });
|
|
121
|
+
* // buildCache() will not mutate shared.throwOnErrors.
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export function buildCache(options) {
|
|
125
|
+
if (options.cache) {
|
|
126
|
+
return options.cache;
|
|
127
|
+
}
|
|
128
|
+
let stores;
|
|
129
|
+
if (options.stores && options.stores.length > 0) {
|
|
130
|
+
stores = options.stores;
|
|
131
|
+
}
|
|
132
|
+
else if (options.store) {
|
|
133
|
+
const configs = Array.isArray(options.store)
|
|
134
|
+
? options.store
|
|
135
|
+
: [options.store];
|
|
136
|
+
stores = configs.map(buildKeyv);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
stores = [new Keyv({ throwOnErrors: true })];
|
|
140
|
+
}
|
|
141
|
+
return createCache({
|
|
142
|
+
stores,
|
|
143
|
+
ttl: options.ttl,
|
|
144
|
+
nonBlocking: options.nonBlocking,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=cache-factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-factory.js","sourceRoot":"","sources":["../../src/helpers/cache-factory.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAc,WAAW,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,IAAI,EAAyB,MAAM,MAAM,CAAC;AASnD,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;AAE5C,gFAAgF;AAChF,MAAM,gBAAgB,GAAsD;IAC1E,KAAK,EAAE,aAAa;IACpB,QAAQ,EAAE,gBAAgB;IAC1B,MAAM,EAAE,cAAc;IACtB,QAAQ,EAAE,gBAAgB;CAC3B,CAAC;AAEF;;;;;;;GAOG;AACH,SAAS,wBAAwB,CAAC,KAAc,EAAE,GAAW;IAC3D,IAAI,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAK,KAAoC,CAAC,IAAI,KAAK,kBAAkB,EAAE,CAAC;QACtE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,CACL,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,uBAAuB,GAAG,GAAG,CAAC;QACrD,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,uBAAuB,GAAG,GAAG,CAAC,CACtD,CAAC;AACJ,CAAC;AAED,mFAAmF;AACnF,SAAS,sBAAsB,CAAC,KAAc;IAC5C,IAAI,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5C,OAAO,CACL,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC;QACvC,KAAK,CAAC,OAAO,CAAC,QAAQ,CACpB,kDAAkD,CACnD;QACD,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAC7C,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,cAAc,CAAC,GAAW;IACjC,IAAI,GAA0D,CAAC;IAC/D,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,GAAG,CAA0D,CAAC;IAC3E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,wBAAwB,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC;YACzC,MAAM,IAAI,KAAK,CACb,kCAAkC,GAAG,wEAAwE,GAAG,EAAE,EAClH,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAC;QACJ,CAAC;QAED,IAAI,sBAAsB,CAAC,KAAK,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CACb,qBAAqB,GAAG,6DAA6D;gBACnF,kEAAkE,GAAG,kDAAkD,EACzH,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAC;QACJ,CAAC;QAED,oEAAoE;QACpE,2EAA2E;QAC3E,+BAA+B;QAC/B,MAAM,KAAK,CAAC;IACd,CAAC;IACD,OAAO,CACJ,GAAwC,CAAC,OAAO;QAChD,GAA0B,CAC5B,CAAC;AACJ,CAAC;AAED,2EAA2E;AAC3E,SAAS,kBAAkB,CAAC,MAAwB;IAClD,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;IACtC,MAAM,OAAO,GAAG,cAAc,CAC5B,gBAAgB,CAAC,IAAyC,CAAC,CAC5D,CAAC;IAEF,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QACxB,OAAO,IAAI,OAAO,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,OAAO,IAAI,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACnC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,MAAwB;IAChD,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAI,IAAI,CAAC;YACd,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,GAAG,EAAE,MAAM,CAAC,GAAG;YACf,aAAa,EAAE,IAAI;SACpB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,IAAI,IAAI,CAAC;QACd,KAAK,EAAE,kBAAkB,CAAC,MAAM,CAAC;QACjC,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,aAAa,EAAE,IAAI;KACpB,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,UAAU,CAAC,OAA0B;IACnD,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,OAAO,OAAO,CAAC,KAAK,CAAC;IACvB,CAAC;IAED,IAAI,MAAc,CAAC;IACnB,IAAI,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChD,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC1B,CAAC;SAAM,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC;YAC1C,CAAC,CAAC,OAAO,CAAC,KAAK;YACf,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACpB,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAClC,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAC/C,CAAC;IAED,OAAO,WAAW,CAAC;QACjB,MAAM;QACN,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,WAAW,EAAE,OAAO,CAAC,WAAW;KACjC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type IPipelineContext, type TenantPartitionOptions } from '@cqrs-ddd/pipeline';
|
|
2
|
+
import type { CacheKeyFactory } from '../interfaces/cache-options.interface.js';
|
|
3
|
+
/** Options for {@link createPartitionedCacheKeyFactory}. */
|
|
4
|
+
export interface PartitionedCacheKeyOptions extends TenantPartitionOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the principal the response is scoped to.
|
|
7
|
+
*
|
|
8
|
+
* Required whenever the handler performs entity-level authorization or field
|
|
9
|
+
* filtering, because a cache hit skips the handler entirely — and therefore
|
|
10
|
+
* skips those checks.
|
|
11
|
+
*/
|
|
12
|
+
principal: (context: IPipelineContext) => string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Resolves a fingerprint of the caller's permission scope — a role-set hash or
|
|
15
|
+
* a capability version.
|
|
16
|
+
*
|
|
17
|
+
* Without it, a principal whose roles change keeps reading responses computed
|
|
18
|
+
* under the old permissions until the entry expires.
|
|
19
|
+
*/
|
|
20
|
+
scope?: (context: IPipelineContext) => string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Whether a missing principal is an error.
|
|
23
|
+
*
|
|
24
|
+
* Set to `false` only for genuinely public, identical-for-everyone responses.
|
|
25
|
+
*
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
requirePrincipal?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Whether a missing authorization scope is an error rather than an omitted segment.
|
|
31
|
+
*
|
|
32
|
+
* Set to `false` only when responses do not depend on the caller's permissions;
|
|
33
|
+
* `scope` may then be omitted.
|
|
34
|
+
*
|
|
35
|
+
* @default true
|
|
36
|
+
*/
|
|
37
|
+
requireScope?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Builds a cache key that partitions every dimension capable of changing an
|
|
41
|
+
* authorized response.
|
|
42
|
+
*
|
|
43
|
+
* The key includes tenant, principal, optional permission scope, request type,
|
|
44
|
+
* and a SHA-256 digest of the request payload. This keeps raw request data out
|
|
45
|
+
* of cache key listings while preventing authorized responses from being shared
|
|
46
|
+
* across callers with different security context.
|
|
47
|
+
*
|
|
48
|
+
* Use this helper when a cache hit can bypass authorization or response
|
|
49
|
+
* filtering performed inside the handler.
|
|
50
|
+
*
|
|
51
|
+
* @param options - Resolvers and fail-closed requirements for key partitioning.
|
|
52
|
+
* @returns A `CacheKeyFactory` suitable for `CacheBehaviorOptions.key`.
|
|
53
|
+
* @throws {TypeError} When `requireScope` is not `false` and no `scope` resolver is given.
|
|
54
|
+
* @throws {MissingCachePartitionError} (from the returned factory) When a required
|
|
55
|
+
* tenant, principal or scope is absent.
|
|
56
|
+
*
|
|
57
|
+
* @example Per principal, tenant-aware, invalidated when roles change
|
|
58
|
+
* ```ts
|
|
59
|
+
* class GetUsersHandler {
|
|
60
|
+
* @pipeline.wrap({ kind: 'query' }, [CacheBehavior, {
|
|
61
|
+
* key: createPartitionedCacheKeyFactory({
|
|
62
|
+
* principal: (ctx) => ctx.items.get('currentUserId') as string | undefined,
|
|
63
|
+
* scope: (ctx) => ctx.items.get('capabilityVersion') as string | undefined,
|
|
64
|
+
* }),
|
|
65
|
+
* }])
|
|
66
|
+
* async handle(query: GetUsersQuery) {}
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @example Public reference data, identical for every caller
|
|
71
|
+
* ```ts
|
|
72
|
+
* createPartitionedCacheKeyFactory({
|
|
73
|
+
* principal: () => 'public',
|
|
74
|
+
* requirePrincipal: false,
|
|
75
|
+
* requireTenant: false,
|
|
76
|
+
* requireScope: false,
|
|
77
|
+
* });
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export declare function createPartitionedCacheKeyFactory(options: PartitionedCacheKeyOptions): CacheKeyFactory;
|
|
81
|
+
//# sourceMappingURL=cache-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-key.d.ts","sourceRoot":"","sources":["../../src/helpers/cache-key.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAE5B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0CAA0C,CAAC;AAKhF,4DAA4D;AAC5D,MAAM,WAAW,0BAA2B,SAAQ,sBAAsB;IACxE;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,MAAM,GAAG,SAAS,CAAC;IAE7D;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,MAAM,GAAG,SAAS,CAAC;IAE1D;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAOD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,0BAA0B,GAClC,eAAe,CAuDjB"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { tenantSegments, } from '@cqrs-ddd/pipeline';
|
|
4
|
+
import { joinKeySegments, stableStringify } from '@cqrs-ddd/safe-stringify';
|
|
5
|
+
import { MissingCachePartitionError } from '../errors/missing-partition.error.js';
|
|
6
|
+
/** Key format version. Bump to make a format change produce a cold cache. */
|
|
7
|
+
const KEY_VERSION = 'v3';
|
|
8
|
+
/** Deterministic digest of the request payload, so secrets stay out of key listings. */
|
|
9
|
+
function digestRequest(request) {
|
|
10
|
+
return createHash('sha256').update(stableStringify(request)).digest('hex');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Builds a cache key that partitions every dimension capable of changing an
|
|
14
|
+
* authorized response.
|
|
15
|
+
*
|
|
16
|
+
* The key includes tenant, principal, optional permission scope, request type,
|
|
17
|
+
* and a SHA-256 digest of the request payload. This keeps raw request data out
|
|
18
|
+
* of cache key listings while preventing authorized responses from being shared
|
|
19
|
+
* across callers with different security context.
|
|
20
|
+
*
|
|
21
|
+
* Use this helper when a cache hit can bypass authorization or response
|
|
22
|
+
* filtering performed inside the handler.
|
|
23
|
+
*
|
|
24
|
+
* @param options - Resolvers and fail-closed requirements for key partitioning.
|
|
25
|
+
* @returns A `CacheKeyFactory` suitable for `CacheBehaviorOptions.key`.
|
|
26
|
+
* @throws {TypeError} When `requireScope` is not `false` and no `scope` resolver is given.
|
|
27
|
+
* @throws {MissingCachePartitionError} (from the returned factory) When a required
|
|
28
|
+
* tenant, principal or scope is absent.
|
|
29
|
+
*
|
|
30
|
+
* @example Per principal, tenant-aware, invalidated when roles change
|
|
31
|
+
* ```ts
|
|
32
|
+
* class GetUsersHandler {
|
|
33
|
+
* @pipeline.wrap({ kind: 'query' }, [CacheBehavior, {
|
|
34
|
+
* key: createPartitionedCacheKeyFactory({
|
|
35
|
+
* principal: (ctx) => ctx.items.get('currentUserId') as string | undefined,
|
|
36
|
+
* scope: (ctx) => ctx.items.get('capabilityVersion') as string | undefined,
|
|
37
|
+
* }),
|
|
38
|
+
* }])
|
|
39
|
+
* async handle(query: GetUsersQuery) {}
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @example Public reference data, identical for every caller
|
|
44
|
+
* ```ts
|
|
45
|
+
* createPartitionedCacheKeyFactory({
|
|
46
|
+
* principal: () => 'public',
|
|
47
|
+
* requirePrincipal: false,
|
|
48
|
+
* requireTenant: false,
|
|
49
|
+
* requireScope: false,
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function createPartitionedCacheKeyFactory(options) {
|
|
54
|
+
const requirePrincipal = options.requirePrincipal ?? true;
|
|
55
|
+
const requireScope = options.requireScope ?? true;
|
|
56
|
+
if (requireScope && !options.scope) {
|
|
57
|
+
throw new TypeError('createPartitionedCacheKeyFactory requires a `scope` resolver: a cache hit ' +
|
|
58
|
+
'skips the handler, so a response computed under revoked permissions would ' +
|
|
59
|
+
'keep being served. Pass requireScope: false when responses do not depend ' +
|
|
60
|
+
"on the caller's permissions.");
|
|
61
|
+
}
|
|
62
|
+
return (context) => {
|
|
63
|
+
const tenant = tenantSegments(context, options, MissingCachePartitionError);
|
|
64
|
+
const resolvedPrincipal = options.principal(context);
|
|
65
|
+
const principal = typeof resolvedPrincipal === 'string' ? resolvedPrincipal.trim() : '';
|
|
66
|
+
if (requirePrincipal && !principal) {
|
|
67
|
+
throw new MissingCachePartitionError(context.requestName, 'principal', 'A cache hit skips the handler and therefore its entity-level ' +
|
|
68
|
+
'authorization. Return the authenticated principal, or pass ' +
|
|
69
|
+
'requirePrincipal: false for a genuinely public response.');
|
|
70
|
+
}
|
|
71
|
+
const resolvedScope = options.scope?.(context);
|
|
72
|
+
const scope = typeof resolvedScope === 'string' && resolvedScope.trim()
|
|
73
|
+
? resolvedScope.trim()
|
|
74
|
+
: undefined;
|
|
75
|
+
if (requireScope && !scope) {
|
|
76
|
+
throw new MissingCachePartitionError(context.requestName, 'scope', 'A cache hit skips the handler and therefore its entity-level ' +
|
|
77
|
+
'authorization. Return the authorization scope, or pass ' +
|
|
78
|
+
'requireScope: false when responses are scope-independent.');
|
|
79
|
+
}
|
|
80
|
+
return joinKeySegments([
|
|
81
|
+
'cache',
|
|
82
|
+
KEY_VERSION,
|
|
83
|
+
...tenant,
|
|
84
|
+
principal || undefined,
|
|
85
|
+
scope,
|
|
86
|
+
context.requestName,
|
|
87
|
+
digestRequest(context.request),
|
|
88
|
+
]);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=cache-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-key.js","sourceRoot":"","sources":["../../src/helpers/cache-key.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAGL,cAAc,GACf,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC5E,OAAO,EAAE,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AAGlF,6EAA6E;AAC7E,MAAM,WAAW,GAAG,IAAI,CAAC;AA0CzB,wFAAwF;AACxF,SAAS,aAAa,CAAC,OAAgB;IACrC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC7E,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,MAAM,UAAU,gCAAgC,CAC9C,OAAmC;IAEnC,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,IAAI,IAAI,CAAC;IAC1D,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,IAAI,CAAC;IAClD,IAAI,YAAY,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACnC,MAAM,IAAI,SAAS,CACjB,4EAA4E;YAC1E,4EAA4E;YAC5E,2EAA2E;YAC3E,8BAA8B,CACjC,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,OAAO,EAAE,EAAE;QACjB,MAAM,MAAM,GAAG,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,0BAA0B,CAAC,CAAC;QAE5E,MAAM,iBAAiB,GAAG,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QACrD,MAAM,SAAS,GACb,OAAO,iBAAiB,KAAK,QAAQ,CAAC,CAAC,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAExE,IAAI,gBAAgB,IAAI,CAAC,SAAS,EAAE,CAAC;YACnC,MAAM,IAAI,0BAA0B,CAClC,OAAO,CAAC,WAAW,EACnB,WAAW,EACX,+DAA+D;gBAC7D,6DAA6D;gBAC7D,0DAA0D,CAC7D,CAAC;QACJ,CAAC;QAED,MAAM,aAAa,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,CAAC;QAC/C,MAAM,KAAK,GACT,OAAO,aAAa,KAAK,QAAQ,IAAI,aAAa,CAAC,IAAI,EAAE;YACvD,CAAC,CAAC,aAAa,CAAC,IAAI,EAAE;YACtB,CAAC,CAAC,SAAS,CAAC;QAEhB,IAAI,YAAY,IAAI,CAAC,KAAK,EAAE,CAAC;YAC3B,MAAM,IAAI,0BAA0B,CAClC,OAAO,CAAC,WAAW,EACnB,OAAO,EACP,+DAA+D;gBAC7D,yDAAyD;gBACzD,2DAA2D,CAC9D,CAAC;QACJ,CAAC;QAED,OAAO,eAAe,CAAC;YACrB,OAAO;YACP,WAAW;YACX,GAAG,MAAM;YACT,SAAS,IAAI,SAAS;YACtB,KAAK;YACL,OAAO,CAAC,WAAW;YACnB,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC;SAC/B,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { PipelineBehaviorTuple } from '@cqrs-ddd/pipeline';
|
|
2
|
+
import { CacheBehavior } from '../cache.behavior.js';
|
|
3
|
+
import type { CacheBehaviorOptions } from '../interfaces/cache-options.interface.js';
|
|
4
|
+
export type CacheIntentOptions = Omit<CacheBehaviorOptions, 'key'> & ({
|
|
5
|
+
key: NonNullable<CacheBehaviorOptions['key']>;
|
|
6
|
+
inheritModuleKey?: never;
|
|
7
|
+
} | {
|
|
8
|
+
inheritModuleKey: true;
|
|
9
|
+
key?: never;
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Returns a cache behavior entry for `pipeline.wrap()` or `createPipeline({ globalBehaviors })`.
|
|
13
|
+
* @param options A key factory and cache options, or `inheritModuleKey: true`
|
|
14
|
+
* when the behavior's constructor defaults supply the key factory. The inheritance marker is not
|
|
15
|
+
* forwarded to the behavior and does not validate module configuration.
|
|
16
|
+
* @returns The behavior class and options tuple.
|
|
17
|
+
* Keys for authorized responses must separate tenant, principal and permission
|
|
18
|
+
* scope; prefer `createPartitionedCacheKeyFactory` for those responses.
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* export const getUsers = pipeline.wrap(
|
|
22
|
+
* { name: 'getUsers', kind: 'query' },
|
|
23
|
+
* cache({ key: userCacheKey, ttl: 30_000 }),
|
|
24
|
+
* )(loadUsers);
|
|
25
|
+
*
|
|
26
|
+
* // The key factory comes from the CacheBehavior constructor defaults.
|
|
27
|
+
* const entry = cache({ inheritModuleKey: true, ttl: 30_000 });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function cache(options: CacheIntentOptions): PipelineBehaviorTuple<CacheBehavior, CacheBehaviorOptions>;
|
|
31
|
+
//# sourceMappingURL=cache.intent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.intent.d.ts","sourceRoot":"","sources":["../../src/helpers/cache.intent.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,0CAA0C,CAAC;AAErF,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,oBAAoB,EAAE,KAAK,CAAC,GAChE,CACI;IACE,GAAG,EAAE,WAAW,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9C,gBAAgB,CAAC,EAAE,KAAK,CAAC;CAC1B,GACD;IAAE,gBAAgB,EAAE,IAAI,CAAC;IAAC,GAAG,CAAC,EAAE,KAAK,CAAA;CAAE,CAC1C,CAAC;AAEJ;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,KAAK,CACnB,OAAO,EAAE,kBAAkB,GAC1B,qBAAqB,CAAC,aAAa,EAAE,oBAAoB,CAAC,CAG5D"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
import { CacheBehavior } from '../cache.behavior.js';
|
|
3
|
+
/**
|
|
4
|
+
* Returns a cache behavior entry for `pipeline.wrap()` or `createPipeline({ globalBehaviors })`.
|
|
5
|
+
* @param options A key factory and cache options, or `inheritModuleKey: true`
|
|
6
|
+
* when the behavior's constructor defaults supply the key factory. The inheritance marker is not
|
|
7
|
+
* forwarded to the behavior and does not validate module configuration.
|
|
8
|
+
* @returns The behavior class and options tuple.
|
|
9
|
+
* Keys for authorized responses must separate tenant, principal and permission
|
|
10
|
+
* scope; prefer `createPartitionedCacheKeyFactory` for those responses.
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* export const getUsers = pipeline.wrap(
|
|
14
|
+
* { name: 'getUsers', kind: 'query' },
|
|
15
|
+
* cache({ key: userCacheKey, ttl: 30_000 }),
|
|
16
|
+
* )(loadUsers);
|
|
17
|
+
*
|
|
18
|
+
* // The key factory comes from the CacheBehavior constructor defaults.
|
|
19
|
+
* const entry = cache({ inheritModuleKey: true, ttl: 30_000 });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function cache(options) {
|
|
23
|
+
const { inheritModuleKey: _inheritModuleKey, ...behaviorOptions } = options;
|
|
24
|
+
return [CacheBehavior, behaviorOptions];
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=cache.intent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.intent.js","sourceRoot":"","sources":["../../src/helpers/cache.intent.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAGtE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAYrD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,KAAK,CACnB,OAA2B;IAE3B,MAAM,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,GAAG,eAAe,EAAE,GAAG,OAAO,CAAC;IAC5E,OAAO,CAAC,aAAa,EAAE,eAAe,CAAC,CAAC;AAC1C,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { CacheManagerAdapter, type IPipelineCache, } from './adapters/cache-manager.adapter.js';
|
|
2
|
+
export { CACHE_HIT_ITEM, CACHE_HIT_ITEM_TOKEN, CACHE_KEY_ITEM, CACHE_KEY_ITEM_TOKEN, CacheBehavior, } from './cache.behavior.js';
|
|
3
|
+
export { type CachePartitionDimension, MissingCachePartitionError, } from './errors/missing-partition.error.js';
|
|
4
|
+
export { buildCacheAttributes } from './helpers/build-attributes.js';
|
|
5
|
+
export { type CacheIntentOptions, cache, } from './helpers/cache.intent.js';
|
|
6
|
+
export { buildCache, buildKeyv } from './helpers/cache-factory.js';
|
|
7
|
+
export { createPartitionedCacheKeyFactory, type PartitionedCacheKeyOptions, } from './helpers/cache-key.js';
|
|
8
|
+
export type { BuildCacheOptions, CacheBehaviorOptions, CacheCondition, CacheKeyFactory, CacheStoreConfig, CacheStoreType, } from './interfaces/cache-options.interface.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,mBAAmB,EACnB,KAAK,cAAc,GACpB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,cAAc,EACd,oBAAoB,EACpB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,KAAK,uBAAuB,EAC5B,0BAA0B,GAC3B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,GACN,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EACL,gCAAgC,EAChC,KAAK,0BAA0B,GAChC,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACV,iBAAiB,EACjB,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,cAAc,GACf,MAAM,yCAAyC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
export { CacheManagerAdapter, } from './adapters/cache-manager.adapter.js';
|
|
3
|
+
export { CACHE_HIT_ITEM, CACHE_HIT_ITEM_TOKEN, CACHE_KEY_ITEM, CACHE_KEY_ITEM_TOKEN, CacheBehavior, } from './cache.behavior.js';
|
|
4
|
+
export { MissingCachePartitionError, } from './errors/missing-partition.error.js';
|
|
5
|
+
export { buildCacheAttributes } from './helpers/build-attributes.js';
|
|
6
|
+
export { cache, } from './helpers/cache.intent.js';
|
|
7
|
+
export { buildCache, buildKeyv } from './helpers/cache-factory.js';
|
|
8
|
+
export { createPartitionedCacheKeyFactory, } from './helpers/cache-key.js';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EACL,mBAAmB,GAEpB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,cAAc,EACd,oBAAoB,EACpB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAEL,0BAA0B,GAC3B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAEL,KAAK,GACN,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EACL,gCAAgC,GAEjC,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { IPipelineContext } from '@cqrs-ddd/pipeline';
|
|
2
|
+
import type { Cache } from 'cache-manager';
|
|
3
|
+
import type { Keyv } from 'keyv';
|
|
4
|
+
/**
|
|
5
|
+
* Built-in store backends supported through declarative configuration.
|
|
6
|
+
*
|
|
7
|
+
* - `memory` — in-process `Keyv` map (default, no extra dependency).
|
|
8
|
+
* - `redis` — requires the optional `@keyv/redis` package.
|
|
9
|
+
* - `memcache` — requires the optional `@keyv/memcache` package.
|
|
10
|
+
* - `sqlite` — requires the optional `@keyv/sqlite` package.
|
|
11
|
+
* - `postgres` — requires the optional `@keyv/postgres` package.
|
|
12
|
+
*/
|
|
13
|
+
export type CacheStoreType = 'memory' | 'redis' | 'memcache' | 'sqlite' | 'postgres';
|
|
14
|
+
/**
|
|
15
|
+
* Declarative description of a single cache store backend. Use this when you
|
|
16
|
+
* want the module to construct the `Keyv` adapter for you.
|
|
17
|
+
*/
|
|
18
|
+
export interface CacheStoreConfig {
|
|
19
|
+
/** Which backend to build. */
|
|
20
|
+
type: CacheStoreType;
|
|
21
|
+
/**
|
|
22
|
+
* Connection string / URI for the backend, e.g. `redis://localhost:6379`,
|
|
23
|
+
* `postgresql://user:pass@localhost:5432/db`, `sqlite://./cache.sqlite`, or
|
|
24
|
+
* `localhost:11211` for memcache. Ignored for `memory`.
|
|
25
|
+
*/
|
|
26
|
+
url?: string;
|
|
27
|
+
/** Key namespace/prefix applied to every entry in this store. */
|
|
28
|
+
namespace?: string;
|
|
29
|
+
/** Default time-to-live (milliseconds) for entries written to this store. */
|
|
30
|
+
ttl?: number;
|
|
31
|
+
/** Adapter-specific options passed through to the underlying `@keyv/*` package. */
|
|
32
|
+
options?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
/** Factory that derives the cache key for a given pipeline request. */
|
|
35
|
+
export type CacheKeyFactory = (context: IPipelineContext) => string;
|
|
36
|
+
/** Predicate deciding whether a given request should participate in caching. */
|
|
37
|
+
export type CacheCondition = (context: IPipelineContext) => boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Per-handler caching options, supplied through `pipeline.wrap(options, [CacheBehavior, options])`
|
|
40
|
+
* and/or as defaults, the second argument of the {@link CacheBehavior} constructor.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* class GetUserHandler {
|
|
45
|
+
* @pipeline.wrap({ kind: 'query' }, [CacheBehavior, {
|
|
46
|
+
* key: createPartitionedCacheKeyFactory({
|
|
47
|
+
* principal: (ctx) => ctx.items.get('userId') as string | undefined,
|
|
48
|
+
* scope: (ctx) => ctx.items.get('capabilityVersion') as string | undefined,
|
|
49
|
+
* }),
|
|
50
|
+
* ttl: 60_000,
|
|
51
|
+
* }])
|
|
52
|
+
* async handle(query: GetUserQuery) {}
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export interface CacheBehaviorOptions {
|
|
57
|
+
/**
|
|
58
|
+
* Request kinds eligible for caching. Defaults to `['query']` so that
|
|
59
|
+
* commands and events bypass the cache automatically.
|
|
60
|
+
*/
|
|
61
|
+
kinds?: Array<IPipelineContext['requestKind']>;
|
|
62
|
+
/** Time-to-live (milliseconds) for entries written by this handler. */
|
|
63
|
+
ttl?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Cache-key factory. **Required** when this behavior runs — there is no default.
|
|
66
|
+
*
|
|
67
|
+
* A cache hit returns without executing the handler, so the key must partition
|
|
68
|
+
* every dimension that can change the authorized response: tenant, principal,
|
|
69
|
+
* permission scope and request payload. Use `createPartitionedCacheKeyFactory`
|
|
70
|
+
* rather than composing one by hand.
|
|
71
|
+
*/
|
|
72
|
+
key?: CacheKeyFactory;
|
|
73
|
+
/** Optional predicate gating whether a given request is cached. */
|
|
74
|
+
condition?: CacheCondition;
|
|
75
|
+
/**
|
|
76
|
+
* When a cache read or write throws, bypass the cache and continue (`true`,
|
|
77
|
+
* default) or propagate the store error (`false`). A failed read bypasses the
|
|
78
|
+
* write for that execution. This option does not catch key, condition, or
|
|
79
|
+
* downstream handler errors.
|
|
80
|
+
*/
|
|
81
|
+
failOpen?: boolean;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Options of {@link buildCache}. The store can be provided in
|
|
85
|
+
* three ways: a pre-built `cache`, pre-built `stores`, or declarative `store`
|
|
86
|
+
* configuration. When several are given, `cache` takes precedence over
|
|
87
|
+
* `stores`, which takes precedence over `store`; the others are ignored. An
|
|
88
|
+
* empty `stores` array counts as absent. When none is supplied an in-memory
|
|
89
|
+
* store is used.
|
|
90
|
+
*
|
|
91
|
+
* @example Redis-backed cache
|
|
92
|
+
* ```ts
|
|
93
|
+
* buildCache({
|
|
94
|
+
* store: { type: 'redis', url: redisUrl },
|
|
95
|
+
* ttl: 30_000,
|
|
96
|
+
* });
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export interface BuildCacheOptions {
|
|
100
|
+
/** Escape hatch: a fully constructed `cache-manager` instance. */
|
|
101
|
+
cache?: Cache;
|
|
102
|
+
/** Escape hatch: pre-built `Keyv` stores (tiered, highest priority first). */
|
|
103
|
+
stores?: Keyv[];
|
|
104
|
+
/** Declarative store configuration — a single store or a tiered list. */
|
|
105
|
+
store?: CacheStoreConfig | CacheStoreConfig[];
|
|
106
|
+
/** Default time-to-live (milliseconds) applied across stores and handlers. */
|
|
107
|
+
ttl?: number;
|
|
108
|
+
/** Forwarded to `cache-manager`; optimizes behavior across multiple stores. */
|
|
109
|
+
nonBlocking?: boolean;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=cache-options.interface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-options.interface.d.ts","sourceRoot":"","sources":["../../src/interfaces/cache-options.interface.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAEjC;;;;;;;;GAQG;AACH,MAAM,MAAM,cAAc,GACtB,QAAQ,GACR,OAAO,GACP,UAAU,GACV,QAAQ,GACR,UAAU,CAAC;AAEf;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,8BAA8B;IAC9B,IAAI,EAAE,cAAc,CAAC;IACrB;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,mFAAmF;IACnF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,uEAAuE;AACvE,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,MAAM,CAAC;AAEpE,gFAAgF;AAChF,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC;AAEpE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,gBAAgB,CAAC,aAAa,CAAC,CAAC,CAAC;IAC/C,uEAAuE;IACvE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,GAAG,CAAC,EAAE,eAAe,CAAC;IACtB,mEAAmE;IACnE,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,iBAAiB;IAChC,kEAAkE;IAClE,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,8EAA8E;IAC9E,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC;IAChB,yEAAyE;IACzE,KAAK,CAAC,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,CAAC;IAC9C,8EAA8E;IAC9E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-options.interface.js","sourceRoot":"","sources":["../../src/interfaces/cache-options.interface.ts"],"names":[],"mappings":"AAAA,sEAAsE"}
|
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cqrs-ddd/pipeline-cache",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Result caching for @cqrs-ddd/pipeline on cache-manager and Keyv stores, with keys partitioned by tenant, principal and permission scope",
|
|
5
|
+
"author": "Aristotelis",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22.12.0"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"pipeline",
|
|
13
|
+
"cqrs",
|
|
14
|
+
"cache",
|
|
15
|
+
"caching",
|
|
16
|
+
"cache-manager",
|
|
17
|
+
"keyv",
|
|
18
|
+
"redis",
|
|
19
|
+
"behavior"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/aristoteliss/ddd-cqrs.git",
|
|
24
|
+
"directory": "packages/pipeline-cache"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://aristoteliss.github.io/ddd-cqrs/packages/pipeline-cache/",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/aristoteliss/ddd-cqrs/issues"
|
|
29
|
+
},
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"main": "dist/index.js",
|
|
38
|
+
"types": "dist/index.d.ts",
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"LICENSE",
|
|
42
|
+
"COMMERCIAL_LICENSE.txt"
|
|
43
|
+
],
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@keyv/memcache": "^2.0.0",
|
|
49
|
+
"@keyv/postgres": "^2.0.0",
|
|
50
|
+
"@keyv/redis": "^4.0.0 || ^5.0.0",
|
|
51
|
+
"@keyv/sqlite": "^4.0.0",
|
|
52
|
+
"cache-manager": "^7.0.0",
|
|
53
|
+
"keyv": "^5.0.0",
|
|
54
|
+
"@cqrs-ddd/pipeline": "^0.5.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"@keyv/memcache": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"@keyv/postgres": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"@keyv/redis": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"@keyv/sqlite": {
|
|
67
|
+
"optional": true
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
72
|
+
"cache-manager": "^7.2.9",
|
|
73
|
+
"keyv": "^5.6.0",
|
|
74
|
+
"vitest": "^5.0.3",
|
|
75
|
+
"@cqrs-ddd/pipeline": "0.5.0"
|
|
76
|
+
},
|
|
77
|
+
"dependencies": {
|
|
78
|
+
"@cqrs-ddd/safe-stringify": "^0.5.0"
|
|
79
|
+
},
|
|
80
|
+
"scripts": {
|
|
81
|
+
"build": "tsc -p tsconfig.build.json",
|
|
82
|
+
"build:watch": "tsc -p tsconfig.build.json --watch",
|
|
83
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
84
|
+
"rebuild": "pnpm run clean && pnpm run build",
|
|
85
|
+
"lint": "tsc --noEmit",
|
|
86
|
+
"test": "vitest run",
|
|
87
|
+
"test:watch": "vitest"
|
|
88
|
+
}
|
|
89
|
+
}
|