cds-caching 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -29
- package/lib/CachingService.js +7 -3
- package/lib/operations/AsyncOperations.js +179 -16
- package/lib/operations/BasicOperations.js +12 -2
- package/lib/operations/CapOperations.js +251 -43
- package/lib/support/CacheStoreManager.js +20 -3
- package/lib/support/RuntimeConfigurationManager.js +4 -2
- package/package.json +9 -8
package/README.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# Welcome to cds-caching
|
|
2
|
+
[](https://www.npmjs.com/package/cds-caching/common)
|
|
3
|
+
[](https://www.npmjs.com/package/cds-caching)
|
|
2
4
|
|
|
3
5
|
## Overview
|
|
4
6
|
|
|
@@ -136,45 +138,27 @@ await cache.set(data, value, {
|
|
|
136
138
|
|
|
137
139
|
For detailed API documentation, see [Programmatic API Reference](docs/programmatic-api.md).
|
|
138
140
|
|
|
139
|
-
###
|
|
140
|
-
|
|
141
|
-
Installing and using cds-caching is straightforward since it's a CAP plugin. Simply run:
|
|
142
|
-
|
|
143
|
-
```bash
|
|
144
|
-
npm install cds-caching
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### TypeScript Support
|
|
141
|
+
### Example Application
|
|
148
142
|
|
|
149
|
-
cds-caching includes comprehensive
|
|
150
|
-
|
|
151
|
-
#### Basic Usage with TypeScript
|
|
143
|
+
The cds-caching plugin includes a comprehensive example application demonstrating various caching use cases and a UI5-based dashboard for monitoring cache performance.
|
|
152
144
|
|
|
153
|
-
|
|
154
|
-
import { CachingService, CacheOptions, ReadThroughResult } from 'cds-caching';
|
|
145
|
+

|
|
155
146
|
|
|
156
|
-
|
|
147
|
+
The example consists of:
|
|
148
|
+
- **Backend Application** (`examples/app/`) - A CAP application showing annotation-based and programmatic caching patterns
|
|
149
|
+
- **Dashboard** (`examples/dashboard/`) - A UI5-based monitoring interface with real-time metrics, key-level analytics, and historical data
|
|
157
150
|
|
|
158
|
-
|
|
159
|
-
await cache.set('my-key', { data: 'value' }, { ttl: 3600 });
|
|
160
|
-
const value = await cache.get('my-key');
|
|
151
|
+
[See the full Example Application Guide →](docs/example-app.md)
|
|
161
152
|
|
|
162
|
-
// Read-through operations with full type safety
|
|
163
|
-
const { result, cacheKey, metadata } = await cache.rt.send(request, service, {
|
|
164
|
-
ttl: 1800,
|
|
165
|
-
tags: ['user-data']
|
|
166
|
-
});
|
|
167
153
|
|
|
168
|
-
|
|
169
|
-
const cachedFunction = cache.rt.wrap('expensive-operation', async (id: string) => {
|
|
170
|
-
return await this.performExpensiveOperation(id);
|
|
171
|
-
});
|
|
154
|
+
### Installation
|
|
172
155
|
|
|
173
|
-
|
|
156
|
+
Installing and using cds-caching is straightforward since it's a CAP plugin. Simply run:
|
|
174
157
|
|
|
158
|
+
```bash
|
|
159
|
+
npm install cds-caching
|
|
175
160
|
```
|
|
176
161
|
|
|
177
|
-
|
|
178
162
|
### Configuration
|
|
179
163
|
|
|
180
164
|
The cds-caching plugin supports comprehensive configuration through `package.json`. Here are all available configuration options:
|
|
@@ -212,6 +196,7 @@ The cds-caching plugin supports comprehensive configuration through `package.jso
|
|
|
212
196
|
"namespace": "caching",
|
|
213
197
|
"store": "in-memory", // "in-memory", "sqlite", or "redis"
|
|
214
198
|
"compression": "lz4", // "lz4" or "gzip"
|
|
199
|
+
"throwOnErrors": false, // Whether basic operations should throw errors (default: false)
|
|
215
200
|
"credentials": {
|
|
216
201
|
// Redis configuration
|
|
217
202
|
"host": "localhost",
|
|
@@ -253,6 +238,36 @@ Configure default key templates for read-through operations:
|
|
|
253
238
|
|
|
254
239
|
**Default behavior** (if not configured): All context elements are disabled by default.
|
|
255
240
|
|
|
241
|
+
#### Error Handling Configuration
|
|
242
|
+
|
|
243
|
+
Configure how the caching service handles errors:
|
|
244
|
+
|
|
245
|
+
```json
|
|
246
|
+
{
|
|
247
|
+
"cds": {
|
|
248
|
+
"requires": {
|
|
249
|
+
"caching": {
|
|
250
|
+
...
|
|
251
|
+
"throwOnErrors": true, // Basic operations (set, get, delete, has) throw errors
|
|
252
|
+
// Default: false - operations return undefined/null instead of throwing
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Error Handling Behavior:**
|
|
260
|
+
|
|
261
|
+
- **Basic Operations** (`set`, `get`, `delete`, `has`):
|
|
262
|
+
- When `throwOnErrors: false` (default): Operations return `undefined`/`null` on errors
|
|
263
|
+
- When `throwOnErrors: true`: Operations throw errors for connection issues, etc.
|
|
264
|
+
|
|
265
|
+
- **Read-Through Operations** (`rt.run`, `rt.send`, `rt.wrap`, `rt.exec`):
|
|
266
|
+
- Never throw errors, regardless of `throwOnErrors` setting
|
|
267
|
+
- Include `cacheErrors` array in response when errors occur
|
|
268
|
+
- Always fetch from remote service when cache operations fail
|
|
269
|
+
- Log errors for monitoring and debugging
|
|
270
|
+
|
|
256
271
|
#### Environment-Specific Configuration
|
|
257
272
|
|
|
258
273
|
You can override settings for different environments:
|
|
@@ -318,6 +333,7 @@ cds-caching provides 3 storage options:
|
|
|
318
333
|
- Works across multiple app instances, making it ideal for scalable applications
|
|
319
334
|
- Available on SAP BTP via hyperscaler options (e.g., AWS, Azure, Google Cloud)
|
|
320
335
|
- Even trial accounts provide Redis access
|
|
336
|
+
- Redis will be non-blocking
|
|
321
337
|
|
|
322
338
|
#### Redis Development Setup
|
|
323
339
|
|
|
@@ -431,6 +447,31 @@ await cache.delete("bp:1000001")
|
|
|
431
447
|
await cache.clear()
|
|
432
448
|
```
|
|
433
449
|
|
|
450
|
+
**Error Handling for Basic Operations:**
|
|
451
|
+
|
|
452
|
+
```javascript
|
|
453
|
+
// With throwOnErrors: false (default)
|
|
454
|
+
try {
|
|
455
|
+
const value = await cache.get("bp:1000001")
|
|
456
|
+
if (value === undefined) {
|
|
457
|
+
// Handle cache miss or error
|
|
458
|
+
console.log("Value not found or cache error occurred")
|
|
459
|
+
}
|
|
460
|
+
} catch (error) {
|
|
461
|
+
// Only thrown for non-cache related errors
|
|
462
|
+
console.error("Unexpected error:", error)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// With throwOnErrors: true
|
|
466
|
+
try {
|
|
467
|
+
const value = await cache.get("bp:1000001")
|
|
468
|
+
// Value will be undefined if not found, but errors will be thrown
|
|
469
|
+
} catch (error) {
|
|
470
|
+
// Errors thrown for connection issues, etc.
|
|
471
|
+
console.error("Cache error:", error)
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
434
475
|
#### 2. CQN Query Caching
|
|
435
476
|
|
|
436
477
|
For more advanced CAP integration, cache CAP's CQN queries directly. By passing in the query, a dynamic key is generated based on the CQN structure of the query. Note, that passing in queries with dynamic parameters (e.g. `SELECT.from(Foo).where({id: 1})`) will result in a different key for each query execution.
|
|
@@ -463,6 +504,23 @@ Because the cache key has been dynamically created at runtime, it will also be r
|
|
|
463
504
|
const { result, cacheKey } = await cache.rt.run(query, db)
|
|
464
505
|
```
|
|
465
506
|
|
|
507
|
+
**Error Handling for Read-Through Operations:**
|
|
508
|
+
|
|
509
|
+
Read-through operations never throw errors, even when cache operations fail. Instead, they include error information in the response:
|
|
510
|
+
|
|
511
|
+
```javascript
|
|
512
|
+
// Read-through operations always return a result, even on cache errors
|
|
513
|
+
const { result, cacheKey, metadata, cacheErrors } = await cache.rt.run(query, db)
|
|
514
|
+
|
|
515
|
+
if (cacheErrors && cacheErrors.length > 0) {
|
|
516
|
+
console.log("Cache errors occurred:", cacheErrors)
|
|
517
|
+
// Result will be fetched from remote service despite cache errors
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// The result is always available, regardless of cache errors
|
|
521
|
+
return result
|
|
522
|
+
```
|
|
523
|
+
|
|
466
524
|
#### 3. RemoteService Request-Level Caching
|
|
467
525
|
|
|
468
526
|
Cache entire CAP requests with context awareness (e.g. user, tenant, locale, etc.), which is useful for caching slow remote service calls or even application services. The caching service will automatically generate a key for the request based on the request object and the current user, tenant and locale (if not configured otherwise).
|
|
@@ -770,6 +828,36 @@ for await (const entry of iterator) {
|
|
|
770
828
|
|
|
771
829
|
This will return an iterator over all cache entries. You can use this to traverse all cache entries and invalidate them based on a specific condition. You should only use this for small caches (e.g. by using multiple caching services with different namespaces).
|
|
772
830
|
|
|
831
|
+
### TypeScript Support
|
|
832
|
+
|
|
833
|
+
cds-caching includes comprehensive TypeScript definitions. The library is written in JavaScript but provides full TypeScript support for better development experience.
|
|
834
|
+
|
|
835
|
+
#### Basic Usage with TypeScript
|
|
836
|
+
|
|
837
|
+
```typescript
|
|
838
|
+
import { CachingService, CacheOptions, ReadThroughResult } from 'cds-caching';
|
|
839
|
+
|
|
840
|
+
const cache = await cds.connect.to('caching') as CachingService;
|
|
841
|
+
|
|
842
|
+
// Basic cache operations
|
|
843
|
+
await cache.set('my-key', { data: 'value' }, { ttl: 3600 });
|
|
844
|
+
const value = await cache.get('my-key');
|
|
845
|
+
|
|
846
|
+
// Read-through operations with full type safety
|
|
847
|
+
const { result, cacheKey, metadata } = await cache.rt.send(request, service, {
|
|
848
|
+
ttl: 1800,
|
|
849
|
+
tags: ['user-data']
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// Function wrapping with type inference
|
|
853
|
+
const cachedFunction = cache.rt.wrap('expensive-operation', async (id: string) => {
|
|
854
|
+
return await this.performExpensiveOperation(id);
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
const { result: operationResult } = await cachedFunction('user-123');
|
|
858
|
+
|
|
859
|
+
```
|
|
860
|
+
|
|
773
861
|
### OData Service Caching Considerations
|
|
774
862
|
|
|
775
863
|
While caching individual requests can improve performance, **caching an entire OData service is generally not recommended**. Here's why:
|
package/lib/CachingService.js
CHANGED
|
@@ -16,7 +16,9 @@ class CachingService extends cds.Service {
|
|
|
16
16
|
this.options = this.options || {
|
|
17
17
|
store: null,
|
|
18
18
|
compression: null,
|
|
19
|
-
credentials: {}
|
|
19
|
+
credentials: {},
|
|
20
|
+
namespace: null,
|
|
21
|
+
throwOnErrors: false,
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
// Initialize managers
|
|
@@ -164,7 +166,8 @@ class CachingService extends cds.Service {
|
|
|
164
166
|
if (typeof args[0] !== "object" || !args[1].send || typeof args[1] !== "object" || args[0].method !== "GET") {
|
|
165
167
|
return super.send(...args);
|
|
166
168
|
} else {
|
|
167
|
-
|
|
169
|
+
const result = await this.rt.send(...args);
|
|
170
|
+
return result.result;
|
|
168
171
|
}
|
|
169
172
|
}
|
|
170
173
|
|
|
@@ -177,7 +180,8 @@ class CachingService extends cds.Service {
|
|
|
177
180
|
if (typeof args[0] !== "object" && !["Request", "NoaRequest", "ODataRequest", "cds.ql"].includes(args[0].constructor.name)) {
|
|
178
181
|
return super.run(...args);
|
|
179
182
|
} else {
|
|
180
|
-
|
|
183
|
+
const result = await this.rt.run(...args);
|
|
184
|
+
return result.result;
|
|
181
185
|
}
|
|
182
186
|
}
|
|
183
187
|
|
|
@@ -7,6 +7,36 @@ class AsyncOperations {
|
|
|
7
7
|
this.keyManager = keyManager;
|
|
8
8
|
this.statistics = statistics;
|
|
9
9
|
this.runtimeConfigManager = runtimeConfigManager;
|
|
10
|
+
this.log = console; // Default logger
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Safely execute cache operations with error handling
|
|
15
|
+
* @param {Function} operation - The cache operation to execute
|
|
16
|
+
* @param {string} operationName - Name of the operation for logging
|
|
17
|
+
* @param {object} context - Context information for logging
|
|
18
|
+
* @returns {Promise<object>} - The result with error information
|
|
19
|
+
*/
|
|
20
|
+
async safeCacheOperation(operation, operationName, context = {}) {
|
|
21
|
+
try {
|
|
22
|
+
const result = await operation();
|
|
23
|
+
return { success: true, result, error: null };
|
|
24
|
+
} catch (error) {
|
|
25
|
+
this.log.warn(`Cache ${operationName} failed:`, {
|
|
26
|
+
error: error.message,
|
|
27
|
+
stack: error.stack,
|
|
28
|
+
context: context
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
success: false,
|
|
32
|
+
result: null,
|
|
33
|
+
error: {
|
|
34
|
+
message: error.message,
|
|
35
|
+
operation: operationName,
|
|
36
|
+
context: context
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
10
40
|
}
|
|
11
41
|
|
|
12
42
|
/**
|
|
@@ -85,25 +115,91 @@ class AsyncOperations {
|
|
|
85
115
|
cacheOptions: JSON.stringify(options)
|
|
86
116
|
};
|
|
87
117
|
|
|
88
|
-
if
|
|
118
|
+
// Safely check if key exists in cache
|
|
119
|
+
const hasKeyResult = await this.safeCacheOperation(
|
|
120
|
+
() => this.cache.has(cacheKey),
|
|
121
|
+
'has',
|
|
122
|
+
{ key: cacheKey, functionName: asyncFunction.name || 'anonymous' }
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const hasKey = hasKeyResult.success && hasKeyResult.result;
|
|
126
|
+
const cacheErrors = [];
|
|
127
|
+
|
|
128
|
+
if (hasKey) {
|
|
89
129
|
const latency = this.getElapsedMs(startTime);
|
|
90
|
-
|
|
130
|
+
|
|
131
|
+
// Safely record hit statistics
|
|
132
|
+
const hitStatsResult = await this.safeCacheOperation(
|
|
133
|
+
() => this.statistics.recordHit(latency, cacheKey, metadata),
|
|
134
|
+
'recordHit',
|
|
135
|
+
{ key: cacheKey, latency }
|
|
136
|
+
);
|
|
137
|
+
if (!hitStatsResult.success) {
|
|
138
|
+
cacheErrors.push(hitStatsResult.error);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Safely get value from cache
|
|
142
|
+
const getResult = await this.safeCacheOperation(
|
|
143
|
+
() => this.cache.send("GET", { key: cacheKey }),
|
|
144
|
+
'get',
|
|
145
|
+
{ key: cacheKey }
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
if (getResult.success && getResult.result?.value !== undefined) {
|
|
149
|
+
return {
|
|
150
|
+
result: getResult.result.value,
|
|
151
|
+
cacheKey,
|
|
152
|
+
metadata: { hit: true, latency },
|
|
153
|
+
cacheErrors: cacheErrors
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
91
157
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
} else {
|
|
158
|
+
// Cache miss or cache error - delegate to underlying function
|
|
159
|
+
try {
|
|
95
160
|
const response = await asyncFunction(...args);
|
|
96
161
|
const latency = this.getElapsedMs(startTime);
|
|
97
|
-
|
|
162
|
+
|
|
163
|
+
// Safely record miss statistics
|
|
164
|
+
const missStatsResult = await this.safeCacheOperation(
|
|
165
|
+
() => this.statistics.recordMiss(latency, cacheKey, metadata),
|
|
166
|
+
'recordMiss',
|
|
167
|
+
{ key: cacheKey, latency }
|
|
168
|
+
);
|
|
169
|
+
if (!missStatsResult.success) {
|
|
170
|
+
cacheErrors.push(missStatsResult.error);
|
|
171
|
+
}
|
|
98
172
|
|
|
173
|
+
// Safely store in cache
|
|
99
174
|
const wrappedValue = {
|
|
100
175
|
value: response,
|
|
101
176
|
tags: options.tags || [],
|
|
102
177
|
timestamp: Date.now()
|
|
103
178
|
};
|
|
104
|
-
|
|
179
|
+
|
|
180
|
+
const setResult = await this.safeCacheOperation(
|
|
181
|
+
() => this.cache.send("SET", { key: cacheKey, value: wrappedValue, ttl: options.ttl || 0 }),
|
|
182
|
+
'set',
|
|
183
|
+
{ key: cacheKey, ttl: options.ttl }
|
|
184
|
+
);
|
|
185
|
+
if (!setResult.success) {
|
|
186
|
+
cacheErrors.push(setResult.error);
|
|
187
|
+
}
|
|
105
188
|
|
|
106
|
-
return {
|
|
189
|
+
return {
|
|
190
|
+
result: response,
|
|
191
|
+
cacheKey,
|
|
192
|
+
metadata: { hit: false, latency },
|
|
193
|
+
cacheErrors: cacheErrors
|
|
194
|
+
};
|
|
195
|
+
} catch (functionError) {
|
|
196
|
+
// If the underlying function fails, throw the error
|
|
197
|
+
this.log.error('Function execution failed:', {
|
|
198
|
+
error: functionError.message,
|
|
199
|
+
functionName: asyncFunction.name || 'anonymous',
|
|
200
|
+
key: cacheKey
|
|
201
|
+
});
|
|
202
|
+
throw functionError;
|
|
107
203
|
}
|
|
108
204
|
}
|
|
109
205
|
}
|
|
@@ -137,24 +233,91 @@ class AsyncOperations {
|
|
|
137
233
|
cacheOptions: JSON.stringify(options)
|
|
138
234
|
};
|
|
139
235
|
|
|
140
|
-
if
|
|
236
|
+
// Safely check if key exists in cache
|
|
237
|
+
const hasKeyResult = await this.safeCacheOperation(
|
|
238
|
+
() => this.cache.has(cacheKey),
|
|
239
|
+
'has',
|
|
240
|
+
{ key: cacheKey, functionName: asyncFunction.name || 'anonymous' }
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const hasKey = hasKeyResult.success && hasKeyResult.result;
|
|
244
|
+
const cacheErrors = [];
|
|
245
|
+
|
|
246
|
+
if (hasKey) {
|
|
141
247
|
const latency = this.getElapsedMs(startTime);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
248
|
+
|
|
249
|
+
// Safely record hit statistics
|
|
250
|
+
const hitStatsResult = await this.safeCacheOperation(
|
|
251
|
+
() => this.statistics.recordHit(latency, cacheKey, metadata),
|
|
252
|
+
'recordHit',
|
|
253
|
+
{ key: cacheKey, latency }
|
|
254
|
+
);
|
|
255
|
+
if (!hitStatsResult.success) {
|
|
256
|
+
cacheErrors.push(hitStatsResult.error);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Safely get value from cache
|
|
260
|
+
const getResult = await this.safeCacheOperation(
|
|
261
|
+
() => this.cache.send("GET", { key: cacheKey }),
|
|
262
|
+
'get',
|
|
263
|
+
{ key: cacheKey }
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (getResult.success && getResult.result?.value !== undefined) {
|
|
267
|
+
return {
|
|
268
|
+
result: getResult.result.value,
|
|
269
|
+
cacheKey,
|
|
270
|
+
metadata: { hit: true, latency },
|
|
271
|
+
cacheErrors: cacheErrors
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Cache miss or cache error - delegate to underlying function
|
|
277
|
+
try {
|
|
146
278
|
const response = await asyncFunction(...args);
|
|
147
279
|
const latency = this.getElapsedMs(startTime);
|
|
148
|
-
|
|
280
|
+
|
|
281
|
+
// Safely record miss statistics
|
|
282
|
+
const missStatsResult = await this.safeCacheOperation(
|
|
283
|
+
() => this.statistics.recordMiss(latency, cacheKey, metadata),
|
|
284
|
+
'recordMiss',
|
|
285
|
+
{ key: cacheKey, latency }
|
|
286
|
+
);
|
|
287
|
+
if (!missStatsResult.success) {
|
|
288
|
+
cacheErrors.push(missStatsResult.error);
|
|
289
|
+
}
|
|
149
290
|
|
|
291
|
+
// Safely store in cache
|
|
150
292
|
const wrappedValue = {
|
|
151
293
|
value: response,
|
|
152
294
|
tags: options.tags || [],
|
|
153
295
|
timestamp: Date.now()
|
|
154
296
|
};
|
|
155
|
-
|
|
297
|
+
|
|
298
|
+
const setResult = await this.safeCacheOperation(
|
|
299
|
+
() => this.cache.send("SET", { key: cacheKey, value: wrappedValue, ttl: options.ttl || 0 }),
|
|
300
|
+
'set',
|
|
301
|
+
{ key: cacheKey, ttl: options.ttl }
|
|
302
|
+
);
|
|
303
|
+
if (!setResult.success) {
|
|
304
|
+
cacheErrors.push(setResult.error);
|
|
305
|
+
}
|
|
156
306
|
|
|
157
|
-
return {
|
|
307
|
+
return {
|
|
308
|
+
result: response,
|
|
309
|
+
cacheKey,
|
|
310
|
+
metadata: { hit: false, latency },
|
|
311
|
+
cacheErrors: cacheErrors
|
|
312
|
+
};
|
|
313
|
+
} catch (functionError) {
|
|
314
|
+
// If the underlying function fails, throw the error
|
|
315
|
+
this.log.error('Function execution failed:', {
|
|
316
|
+
error: functionError.message,
|
|
317
|
+
functionName: asyncFunction.name || 'anonymous',
|
|
318
|
+
key: cacheKey
|
|
319
|
+
});
|
|
320
|
+
throw functionError;
|
|
158
321
|
}
|
|
159
322
|
}
|
|
160
323
|
|
|
@@ -70,7 +70,15 @@ class BasicOperations {
|
|
|
70
70
|
*/
|
|
71
71
|
async has(key) {
|
|
72
72
|
const createdKey = this.keyManager.createKey(key);
|
|
73
|
-
|
|
73
|
+
try {
|
|
74
|
+
return await this.cache.cache.has(createdKey);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (this.cache.options.throwOnErrors) {
|
|
77
|
+
throw error;
|
|
78
|
+
} else {
|
|
79
|
+
return false; // We don't want to throw errors here
|
|
80
|
+
}
|
|
81
|
+
}
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
/**
|
|
@@ -101,7 +109,9 @@ class BasicOperations {
|
|
|
101
109
|
* @returns {Promise<void>}
|
|
102
110
|
*/
|
|
103
111
|
async clear() {
|
|
104
|
-
await this.cache.send('CLEAR'
|
|
112
|
+
await this.cache.send('CLEAR', {
|
|
113
|
+
deleteAll: true
|
|
114
|
+
});
|
|
105
115
|
|
|
106
116
|
// Record the native clear operation
|
|
107
117
|
const metadata = {
|
|
@@ -15,9 +15,39 @@ class CapOperations {
|
|
|
15
15
|
this.statistics = statistics;
|
|
16
16
|
this.tagResolver = new TagResolver();
|
|
17
17
|
this.runtimeConfigManager = runtimeConfigManager;
|
|
18
|
+
this.log = log || console;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Safely execute cache operations with error handling
|
|
23
|
+
* @param {Function} operation - The cache operation to execute
|
|
24
|
+
* @param {string} operationName - Name of the operation for logging
|
|
25
|
+
* @param {object} context - Context information for logging
|
|
26
|
+
* @returns {Promise<object>} - The result with error information
|
|
27
|
+
*/
|
|
28
|
+
async safeCacheOperation(operation, operationName, context = {}) {
|
|
29
|
+
try {
|
|
30
|
+
const result = await operation();
|
|
31
|
+
this.log.info('REEEESULT', { result });
|
|
32
|
+
return { success: true, result, error: null };
|
|
33
|
+
} catch (error) {
|
|
34
|
+
this.log.warn(`Cache ${operationName} failed:`, {
|
|
35
|
+
error: error.message,
|
|
36
|
+
stack: error.stack,
|
|
37
|
+
context: context
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
success: false,
|
|
42
|
+
result: null,
|
|
43
|
+
error: {
|
|
44
|
+
message: error.message,
|
|
45
|
+
operation: operationName,
|
|
46
|
+
context: context
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
21
51
|
|
|
22
52
|
/**
|
|
23
53
|
* Send a request with caching with read-through capabilities.
|
|
@@ -34,7 +64,7 @@ class CapOperations {
|
|
|
34
64
|
}
|
|
35
65
|
|
|
36
66
|
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(request.method?.toUpperCase()) || !service.send) {
|
|
37
|
-
return { result: null, cacheKey: null, metadata: { hit: false, latency: 0 } };
|
|
67
|
+
return { result: null, cacheKey: null, metadata: { hit: false, latency: 0 }, cacheErrors: [] };
|
|
38
68
|
}
|
|
39
69
|
|
|
40
70
|
const keyParts = {
|
|
@@ -79,31 +109,94 @@ class CapOperations {
|
|
|
79
109
|
cacheOptions: JSON.stringify(requestOptions)
|
|
80
110
|
};
|
|
81
111
|
|
|
82
|
-
if
|
|
112
|
+
// Safely check if key exists in cache
|
|
113
|
+
const hasKeyResult = await this.safeCacheOperation(
|
|
114
|
+
() => this.cache.has(key),
|
|
115
|
+
'has',
|
|
116
|
+
{ key, serviceName: service.name }
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const hasKey = hasKeyResult.success && hasKeyResult.result;
|
|
120
|
+
const cacheErrors = [];
|
|
121
|
+
|
|
122
|
+
if (hasKey) {
|
|
83
123
|
const latency = this.getElapsedMs(startTime);
|
|
84
124
|
|
|
85
|
-
|
|
125
|
+
// Safely record hit statistics
|
|
126
|
+
const hitStatsResult = await this.safeCacheOperation(
|
|
127
|
+
() => this.statistics.recordHit(latency, key, metadata),
|
|
128
|
+
'recordHit',
|
|
129
|
+
{ key, latency }
|
|
130
|
+
);
|
|
131
|
+
if (!hitStatsResult.success) {
|
|
132
|
+
cacheErrors.push(hitStatsResult.error);
|
|
133
|
+
}
|
|
86
134
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
135
|
+
// Safely get value from cache
|
|
136
|
+
const getResult = await this.safeCacheOperation(
|
|
137
|
+
() => this.cache.send("GET", { key }),
|
|
138
|
+
'get',
|
|
139
|
+
{ key }
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
if (getResult.success && getResult.result?.value !== undefined) {
|
|
143
|
+
return {
|
|
144
|
+
result: getResult.result.value,
|
|
145
|
+
cacheKey: key,
|
|
146
|
+
metadata: { hit: true, latency: latency },
|
|
147
|
+
cacheErrors: cacheErrors
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Cache miss or cache error - delegate to underlying service
|
|
153
|
+
try {
|
|
90
154
|
const response = await service.send(request);
|
|
91
155
|
const totalLatency = this.getElapsedMs(startTime);
|
|
92
|
-
|
|
156
|
+
|
|
157
|
+
// Safely record miss statistics
|
|
158
|
+
const missStatsResult = await this.safeCacheOperation(
|
|
159
|
+
() => this.statistics.recordMiss(totalLatency, key, metadata),
|
|
160
|
+
'recordMiss',
|
|
161
|
+
{ key, latency: totalLatency }
|
|
162
|
+
);
|
|
163
|
+
if (!missStatsResult.success) {
|
|
164
|
+
cacheErrors.push(missStatsResult.error);
|
|
165
|
+
}
|
|
93
166
|
|
|
167
|
+
// Safely store in cache
|
|
94
168
|
const wrappedValue = {
|
|
95
169
|
value: response,
|
|
96
170
|
tags: this.tagResolver.resolveTags(requestOptions.tags, response, { ...request.params, user: request.user?.id, tenant: request.tenant, locale: request.locale, hash: this.keyManager.createContentHash(request) }),
|
|
97
171
|
timestamp: Date.now()
|
|
98
172
|
};
|
|
99
|
-
|
|
173
|
+
|
|
174
|
+
const setResult = await this.safeCacheOperation(
|
|
175
|
+
() => this.cache.send("SET", { key, value: wrappedValue, ttl: requestOptions.ttl || 0 }),
|
|
176
|
+
'set',
|
|
177
|
+
{ key, ttl: requestOptions.ttl }
|
|
178
|
+
);
|
|
179
|
+
if (!setResult.success) {
|
|
180
|
+
cacheErrors.push(setResult.error);
|
|
181
|
+
}
|
|
100
182
|
|
|
101
|
-
return {
|
|
183
|
+
return {
|
|
184
|
+
result: response,
|
|
185
|
+
cacheKey: key,
|
|
186
|
+
metadata: { hit: false, latency: totalLatency },
|
|
187
|
+
cacheErrors: cacheErrors
|
|
188
|
+
};
|
|
189
|
+
} catch (serviceError) {
|
|
190
|
+
// If the underlying service fails, throw the error
|
|
191
|
+
this.log.error('Service operation failed:', {
|
|
192
|
+
error: serviceError.message,
|
|
193
|
+
serviceName: service.name,
|
|
194
|
+
key: key
|
|
195
|
+
});
|
|
196
|
+
throw serviceError;
|
|
102
197
|
}
|
|
103
198
|
}
|
|
104
199
|
|
|
105
|
-
|
|
106
|
-
|
|
107
200
|
/**
|
|
108
201
|
* Run a cached operation with automatic key generation
|
|
109
202
|
* @param {object} req - the request object
|
|
@@ -133,34 +226,87 @@ class CapOperations {
|
|
|
133
226
|
|
|
134
227
|
// Track cache operation timing
|
|
135
228
|
const startTime = process.hrtime();
|
|
136
|
-
|
|
137
|
-
|
|
229
|
+
|
|
230
|
+
// Safely get value from cache
|
|
231
|
+
const getResult = await this.safeCacheOperation(
|
|
232
|
+
() => this.cache.send("GET", { key: req.cacheKey }),
|
|
233
|
+
'get',
|
|
234
|
+
{ key: req.cacheKey, serviceName: req.target?.name }
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const cacheHit = getResult.success && getResult.result?.value !== undefined;
|
|
138
238
|
const cacheLatency = this.getElapsedMs(startTime);
|
|
139
239
|
const metadata = this.extractMetadataFromRequest(req);
|
|
240
|
+
const cacheErrors = [];
|
|
140
241
|
|
|
141
242
|
if (cacheHit) {
|
|
142
243
|
// Cache hit
|
|
143
|
-
this.
|
|
244
|
+
const hitStatsResult = await this.safeCacheOperation(
|
|
245
|
+
() => this.statistics.recordHit(cacheLatency, req.cacheKey, metadata),
|
|
246
|
+
'recordHit',
|
|
247
|
+
{ key: req.cacheKey, latency: cacheLatency }
|
|
248
|
+
);
|
|
249
|
+
if (!hitStatsResult.success) {
|
|
250
|
+
cacheErrors.push(hitStatsResult.error);
|
|
251
|
+
}
|
|
252
|
+
|
|
144
253
|
req.res?.setHeader('x-sap-cap-cache', "hit");
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
254
|
+
return {
|
|
255
|
+
result: getResult.result.value,
|
|
256
|
+
cacheKey: req.cacheKey,
|
|
257
|
+
metadata: { hit: true, latency: cacheLatency },
|
|
258
|
+
cacheErrors: cacheErrors
|
|
259
|
+
};
|
|
148
260
|
} else {
|
|
149
261
|
// Cache miss - track the backend operation
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// Track the miss with total latency (cache lookup + backend operation)
|
|
154
|
-
this.statistics.recordMiss(totalLatency, req.cacheKey, metadata);
|
|
155
|
-
req.res?.setHeader('x-sap-cap-cache', "miss");
|
|
262
|
+
try {
|
|
263
|
+
const response = await next();
|
|
264
|
+
const totalLatency = this.getElapsedMs(startTime);
|
|
156
265
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
266
|
+
// Safely record miss statistics
|
|
267
|
+
const missStatsResult = await this.safeCacheOperation(
|
|
268
|
+
() => this.statistics.recordMiss(totalLatency, req.cacheKey, metadata),
|
|
269
|
+
'recordMiss',
|
|
270
|
+
{ key: req.cacheKey, latency: totalLatency }
|
|
271
|
+
);
|
|
272
|
+
if (!missStatsResult.success) {
|
|
273
|
+
cacheErrors.push(missStatsResult.error);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
req.res?.setHeader('x-sap-cap-cache', "miss");
|
|
277
|
+
|
|
278
|
+
// Safely store in cache
|
|
279
|
+
const wrappedValue = {
|
|
280
|
+
value: response,
|
|
281
|
+
tags: this.tagResolver.resolveTags(req.cacheOptions.tags, response, { ...req.params, hash: this.keyManager.createContentHash(req) }),
|
|
282
|
+
timestamp: Date.now()
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const setResult = await this.safeCacheOperation(
|
|
286
|
+
() => this.cache.send("SET", { key: req.cacheKey, value: wrappedValue, ttl: req.cacheOptions.ttl || 0 }),
|
|
287
|
+
'set',
|
|
288
|
+
{ key: req.cacheKey, ttl: req.cacheOptions.ttl }
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
if (!setResult.success) {
|
|
292
|
+
cacheErrors.push(setResult.error);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
result: response,
|
|
297
|
+
cacheKey: req.cacheKey,
|
|
298
|
+
metadata: { hit: false, latency: totalLatency },
|
|
299
|
+
cacheErrors: cacheErrors
|
|
300
|
+
};
|
|
301
|
+
} catch (serviceError) {
|
|
302
|
+
// If the underlying service fails, throw the error
|
|
303
|
+
this.log.error('Service operation failed:', {
|
|
304
|
+
error: serviceError.message,
|
|
305
|
+
serviceName: req.target?.name,
|
|
306
|
+
key: req.cacheKey
|
|
307
|
+
});
|
|
308
|
+
throw serviceError;
|
|
309
|
+
}
|
|
164
310
|
}
|
|
165
311
|
case "cds.ql":
|
|
166
312
|
const query = arg1;
|
|
@@ -177,7 +323,15 @@ class CapOperations {
|
|
|
177
323
|
|
|
178
324
|
// Track cache operation timing
|
|
179
325
|
const startTime = process.hrtime();
|
|
180
|
-
|
|
326
|
+
|
|
327
|
+
// Safely check if key exists in cache
|
|
328
|
+
const hasCachedValueResult = await this.safeCacheOperation(
|
|
329
|
+
() => this.cache.has(query.cacheKey),
|
|
330
|
+
'has',
|
|
331
|
+
{ key: query.cacheKey, serviceName: srv?.name }
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const hasCachedValue = hasCachedValueResult.success && hasCachedValueResult.result;
|
|
181
335
|
const cacheLatency = this.getElapsedMs(startTime);
|
|
182
336
|
const metadata = {
|
|
183
337
|
dataType: 'Query',
|
|
@@ -197,35 +351,89 @@ class CapOperations {
|
|
|
197
351
|
}),
|
|
198
352
|
cacheOptions: JSON.stringify(options)
|
|
199
353
|
};
|
|
354
|
+
const cacheErrors = [];
|
|
200
355
|
|
|
201
356
|
if (hasCachedValue) {
|
|
202
357
|
// Cache hit
|
|
203
|
-
this.
|
|
358
|
+
const hitStatsResult = await this.safeCacheOperation(
|
|
359
|
+
() => this.statistics.recordHit(cacheLatency, query.cacheKey, metadata),
|
|
360
|
+
'recordHit',
|
|
361
|
+
{ key: query.cacheKey, latency: cacheLatency }
|
|
362
|
+
);
|
|
363
|
+
if (!hitStatsResult.success) {
|
|
364
|
+
cacheErrors.push(hitStatsResult.error);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const getResult = await this.safeCacheOperation(
|
|
368
|
+
() => this.cache.send("GET", { key: query.cacheKey }),
|
|
369
|
+
'get',
|
|
370
|
+
{ key: query.cacheKey }
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
if (getResult.success && getResult.result?.value !== undefined) {
|
|
374
|
+
return {
|
|
375
|
+
result: getResult.result.value,
|
|
376
|
+
cacheKey: query.cacheKey,
|
|
377
|
+
metadata: { hit: true, latency: cacheLatency },
|
|
378
|
+
cacheErrors: cacheErrors
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
204
382
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
} else {
|
|
208
|
-
// Cache miss
|
|
383
|
+
// Cache miss or cache error
|
|
384
|
+
try {
|
|
209
385
|
const data = await srv.run(query);
|
|
210
386
|
const totalLatency = this.getElapsedMs(startTime);
|
|
211
387
|
|
|
212
|
-
//
|
|
213
|
-
this.
|
|
214
|
-
|
|
388
|
+
// Safely record miss statistics
|
|
389
|
+
const missStatsResult = await this.safeCacheOperation(
|
|
390
|
+
() => this.statistics.recordMiss(totalLatency, query.cacheKey, metadata),
|
|
391
|
+
'recordMiss',
|
|
392
|
+
{ key: query.cacheKey, latency: totalLatency }
|
|
393
|
+
);
|
|
394
|
+
if (!missStatsResult.success) {
|
|
395
|
+
cacheErrors.push(missStatsResult.error);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Safely store in cache
|
|
215
399
|
const wrappedValue = {
|
|
216
400
|
value: data,
|
|
217
401
|
tags: this.tagResolver.resolveTags(options.tags, data, { ...query.params, hash: this.keyManager.createKey(query, { serviceName: srv?.name, template: '{hash}' }) }),
|
|
218
402
|
timestamp: Date.now()
|
|
219
403
|
};
|
|
220
|
-
|
|
221
|
-
|
|
404
|
+
|
|
405
|
+
const setResult = await this.safeCacheOperation(
|
|
406
|
+
() => this.cache.send("SET", { key: query.cacheKey, value: wrappedValue, ttl: options.ttl || 0 }),
|
|
407
|
+
'set',
|
|
408
|
+
{ key: query.cacheKey, ttl: options.ttl }
|
|
409
|
+
);
|
|
410
|
+
if (!setResult.success) {
|
|
411
|
+
cacheErrors.push(setResult.error);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
this.log.info('REEEESULT', { setResult, cacheErrors, wrappedValue });
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
result: data,
|
|
418
|
+
cacheKey: query.cacheKey,
|
|
419
|
+
metadata: { hit: false, latency: totalLatency },
|
|
420
|
+
cacheErrors: cacheErrors
|
|
421
|
+
};
|
|
422
|
+
} catch (serviceError) {
|
|
423
|
+
// If the underlying service fails, throw the error
|
|
424
|
+
this.log.error('Service operation failed:', {
|
|
425
|
+
error: serviceError.message,
|
|
426
|
+
serviceName: srv?.name,
|
|
427
|
+
key: query.cacheKey
|
|
428
|
+
});
|
|
429
|
+
throw serviceError;
|
|
222
430
|
}
|
|
223
431
|
} else {
|
|
224
432
|
return srv.run(query);
|
|
225
433
|
}
|
|
226
434
|
}
|
|
227
435
|
}
|
|
228
|
-
return null;
|
|
436
|
+
return { result: null, cacheKey: null, metadata: { hit: false, latency: 0 }, cacheErrors: [] };
|
|
229
437
|
}
|
|
230
438
|
|
|
231
439
|
/**
|
|
@@ -10,6 +10,7 @@ const { default: KeyvGzip } = require('@keyv/compress-gzip');
|
|
|
10
10
|
class CacheStoreManager {
|
|
11
11
|
constructor(options = {}) {
|
|
12
12
|
this.options = options;
|
|
13
|
+
this.log = cds.log('cds-caching')
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -22,10 +23,11 @@ class CacheStoreManager {
|
|
|
22
23
|
const store = this._createStoreInstance(options);
|
|
23
24
|
const cacheOptions = this._createCacheOptions(options, cacheName);
|
|
24
25
|
const cache = new Keyv(cacheOptions);
|
|
26
|
+
cache.throwOnErrors = options.throwOnErrors;
|
|
25
27
|
|
|
26
28
|
// Set up error handling
|
|
27
29
|
cache.on('error', err => {
|
|
28
|
-
|
|
30
|
+
this.log.error(`Cache error for ${cacheName}:`, err);
|
|
29
31
|
});
|
|
30
32
|
|
|
31
33
|
// Set up cleanup function
|
|
@@ -47,10 +49,25 @@ class CacheStoreManager {
|
|
|
47
49
|
busyTimeout: options.credentials?.busyTimeout || 10000
|
|
48
50
|
});
|
|
49
51
|
case "redis":
|
|
50
|
-
|
|
52
|
+
const store = new KeyvRedis({
|
|
51
53
|
...options.credentials,
|
|
52
54
|
...(options.credentials?.uri ? { url: options.credentials?.uri } : {}),
|
|
55
|
+
...{ throwOnConnectErrors: options.throwOnErrors, useKeyPrefix: false, throwOnErrors: options.throwOnErrors }
|
|
53
56
|
});
|
|
57
|
+
|
|
58
|
+
if (options.throwOnErrors) {
|
|
59
|
+
store.throwOnConnectErrors = false; // We want to handle the errors ourselves
|
|
60
|
+
store.throwOnErrors = true;
|
|
61
|
+
const redisClient = store.client;
|
|
62
|
+
if (redisClient.options) {
|
|
63
|
+
redisClient.options.disableOfflineQueue = true;
|
|
64
|
+
if (redisClient.options.socket) {
|
|
65
|
+
redisClient.options.socket.reconnectStrategy = false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return store;
|
|
54
71
|
default:
|
|
55
72
|
return new Map();
|
|
56
73
|
}
|
|
@@ -93,7 +110,7 @@ class CacheStoreManager {
|
|
|
93
110
|
try {
|
|
94
111
|
await store.disconnect();
|
|
95
112
|
} catch (err) {
|
|
96
|
-
|
|
113
|
+
this.log.error(`Error disconnecting from store for ${cacheName}:`, err);
|
|
97
114
|
}
|
|
98
115
|
}
|
|
99
116
|
};
|
|
@@ -103,7 +103,8 @@ class RuntimeConfigurationManager {
|
|
|
103
103
|
return {
|
|
104
104
|
metricsEnabled: cacheConfig?.metricsEnabled === true || cacheConfig?.metricsEnabled === 1 || false,
|
|
105
105
|
keyMetricsEnabled: cacheConfig?.keyMetricsEnabled === true || cacheConfig?.keyMetricsEnabled === 1 || false,
|
|
106
|
-
keyManagement
|
|
106
|
+
keyManagement,
|
|
107
|
+
throwOnErrors: this.options.throwOnErrors
|
|
107
108
|
};
|
|
108
109
|
} catch (error) {
|
|
109
110
|
this.log.warn(`Failed to get runtime configuration for cache ${this.cacheName}:`, error);
|
|
@@ -114,7 +115,8 @@ class RuntimeConfigurationManager {
|
|
|
114
115
|
isUserAware: false,
|
|
115
116
|
isTenantAware: false,
|
|
116
117
|
isLocaleAware: false
|
|
117
|
-
}
|
|
118
|
+
},
|
|
119
|
+
throwOnErrors: this.options.throwOnErrors
|
|
118
120
|
};
|
|
119
121
|
}
|
|
120
122
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cds-caching",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A caching plugin for SAP CAP applications",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -44,17 +44,18 @@
|
|
|
44
44
|
],
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@keyv/compress-gzip": "^2.0.3",
|
|
47
|
-
"@keyv/compress-lz4": "^1.0.
|
|
48
|
-
"@keyv/redis": "^
|
|
47
|
+
"@keyv/compress-lz4": "^1.0.1",
|
|
48
|
+
"@keyv/redis": "^5.0.0",
|
|
49
49
|
"@keyv/sqlite": "^4.0.5",
|
|
50
|
-
"keyv": "^5.
|
|
50
|
+
"keyv": "^5.4.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"
|
|
53
|
+
"@cap-js/cds-test": "^0.4.0",
|
|
54
|
+
"@release-it/conventional-changelog": "^10.0.1",
|
|
55
|
+
"eslint": "^9.32.0",
|
|
54
56
|
"husky": "^9.1.7",
|
|
55
|
-
"jest": "^30.0.
|
|
56
|
-
"release-it": "^19.0.
|
|
57
|
-
"@cap-js/cds-test": "^0.4.0"
|
|
57
|
+
"jest": "^30.0.5",
|
|
58
|
+
"release-it": "^19.0.4"
|
|
58
59
|
},
|
|
59
60
|
"engines": {
|
|
60
61
|
"node": ">=20"
|