cds-caching 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +182 -25
- package/lib/CachingService.d.ts +16 -10
- package/lib/CachingService.js +69 -13
- package/lib/operations/AsyncOperations.js +179 -16
- package/lib/operations/BasicOperations.js +166 -20
- package/lib/operations/CapOperations.js +251 -45
- package/lib/support/CacheStoreManager.js +44 -11
- package/lib/support/RuntimeConfigurationManager.js +4 -2
- package/lib/support/optionalRequire.js +45 -0
- package/lib/util.js +1 -1
- package/package.json +40 -12
|
@@ -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('RESULT', { result, operationName, context });
|
|
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
|
+
}
|
|
134
|
+
|
|
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
|
+
}
|
|
86
151
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
} else {
|
|
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
|
/**
|
|
@@ -292,8 +500,6 @@ class CapOperations {
|
|
|
292
500
|
*/
|
|
293
501
|
extractMetadataFromRequest(req) {
|
|
294
502
|
|
|
295
|
-
console.log(JSON.stringify(req.http?.req, null, 2));
|
|
296
|
-
|
|
297
503
|
const metadata = {
|
|
298
504
|
dataType: req.constructor.name,
|
|
299
505
|
serviceName: req.target?.name || '',
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
const { Keyv } = require('keyv');
|
|
2
|
-
const {
|
|
3
|
-
const { default: KeyvSqlite } = require('@keyv/sqlite');
|
|
4
|
-
const { default: KeyvLz4 } = require('@keyv/compress-lz4');
|
|
5
|
-
const { default: KeyvGzip } = require('@keyv/compress-gzip');
|
|
2
|
+
const { requireOptional, requireAnyOptional } = require('./optionalRequire');
|
|
6
3
|
|
|
7
4
|
/**
|
|
8
5
|
* Manages cache store initialization and configuration
|
|
@@ -10,6 +7,7 @@ const { default: KeyvGzip } = require('@keyv/compress-gzip');
|
|
|
10
7
|
class CacheStoreManager {
|
|
11
8
|
constructor(options = {}) {
|
|
12
9
|
this.options = options;
|
|
10
|
+
this.log = cds.log('cds-caching')
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
/**
|
|
@@ -20,12 +18,13 @@ class CacheStoreManager {
|
|
|
20
18
|
*/
|
|
21
19
|
createStore(options, cacheName) {
|
|
22
20
|
const store = this._createStoreInstance(options);
|
|
23
|
-
const cacheOptions = this._createCacheOptions(options, cacheName);
|
|
21
|
+
const cacheOptions = this._createCacheOptions(options, cacheName, store);
|
|
24
22
|
const cache = new Keyv(cacheOptions);
|
|
23
|
+
cache.throwOnErrors = options.throwOnErrors;
|
|
25
24
|
|
|
26
25
|
// Set up error handling
|
|
27
26
|
cache.on('error', err => {
|
|
28
|
-
|
|
27
|
+
this.log.error(`Cache error for ${cacheName}:`, err);
|
|
29
28
|
});
|
|
30
29
|
|
|
31
30
|
// Set up cleanup function
|
|
@@ -41,15 +40,46 @@ class CacheStoreManager {
|
|
|
41
40
|
_createStoreInstance(options) {
|
|
42
41
|
switch (options.store) {
|
|
43
42
|
case "sqlite":
|
|
43
|
+
// Support both adapters:
|
|
44
|
+
// - @keyv/sqlite (default export)
|
|
45
|
+
// - @resolid/keyv-sqlite (named export KeyvSqlite) https://github.com/huijiewei/keyv-sqlite
|
|
46
|
+
const sqliteMod = requireAnyOptional(['@keyv/sqlite', '@resolid/keyv-sqlite'], { feature: 'store', value: 'sqlite' });
|
|
47
|
+
const KeyvSqlite = sqliteMod?.KeyvSqlite ?? sqliteMod?.default ?? sqliteMod;
|
|
44
48
|
return new KeyvSqlite({
|
|
45
49
|
url: options.credentials?.url,
|
|
46
50
|
table: options.credentials?.table || 'cache',
|
|
47
51
|
busyTimeout: options.credentials?.busyTimeout || 10000
|
|
48
52
|
});
|
|
49
53
|
case "redis":
|
|
50
|
-
|
|
54
|
+
const KeyvRedis = requireOptional('@keyv/redis', { feature: 'store', value: 'redis' });
|
|
55
|
+
const store = new KeyvRedis({
|
|
51
56
|
...options.credentials,
|
|
52
57
|
...(options.credentials?.uri ? { url: options.credentials?.uri } : {}),
|
|
58
|
+
...{ throwOnConnectErrors: options.throwOnErrors, useKeyPrefix: false, throwOnErrors: options.throwOnErrors }
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// see https://keyv.org/docs/storage-adapters/redis/#gracefully-handling-errors-and-timeouts for more details
|
|
62
|
+
if (options.throwOnErrors) {
|
|
63
|
+
store.throwOnConnectErrors = false; // We want to handle the errors ourselves
|
|
64
|
+
store.throwOnErrors = true; // Redis will throw errors for connection issues, etc.
|
|
65
|
+
const redisClient = store.client;
|
|
66
|
+
if (redisClient.options) {
|
|
67
|
+
redisClient.options.disableOfflineQueue = true;
|
|
68
|
+
if (redisClient.options.socket) {
|
|
69
|
+
redisClient.options.socket.reconnectStrategy = false; // Disable automatic reconnection
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return store;
|
|
75
|
+
case "postgres":
|
|
76
|
+
const KeyvPostgres = requireOptional('@keyv/postgres', { feature: 'store', value: 'postgres' });
|
|
77
|
+
return new KeyvPostgres({
|
|
78
|
+
...options.credentials,
|
|
79
|
+
...(options.credentials?.url ? { uri: options.credentials?.url } : options.credentials?.uri ? { url: options.credentials?.uri } : {}),
|
|
80
|
+
...(options.credentials?.schema ? { schema: options.credentials?.schema } : {}),
|
|
81
|
+
...(options.credentials?.table ? { table: options.credentials?.table } : {}),
|
|
82
|
+
...{ throwOnConnectErrors: options.throwOnErrors, useKeyPrefix: false, throwOnErrors: options.throwOnErrors }
|
|
53
83
|
});
|
|
54
84
|
default:
|
|
55
85
|
return new Map();
|
|
@@ -60,11 +90,12 @@ class CacheStoreManager {
|
|
|
60
90
|
* Create cache options object
|
|
61
91
|
* @private
|
|
62
92
|
*/
|
|
63
|
-
_createCacheOptions(options, cacheName) {
|
|
93
|
+
_createCacheOptions(options, cacheName, store) {
|
|
64
94
|
return {
|
|
65
95
|
namespace: options.namespace || cacheName,
|
|
66
|
-
store
|
|
67
|
-
compression: this._createCompression(options.compression)
|
|
96
|
+
store,
|
|
97
|
+
compression: this._createCompression(options.compression),
|
|
98
|
+
useKeyPrefix: store.constructor.name === 'KeyvRedis' ? false : true // see https://github.com/mikezaschka/cds-caching/issues/11
|
|
68
99
|
};
|
|
69
100
|
}
|
|
70
101
|
|
|
@@ -75,8 +106,10 @@ class CacheStoreManager {
|
|
|
75
106
|
_createCompression(compressionType) {
|
|
76
107
|
switch (compressionType) {
|
|
77
108
|
case "lz4":
|
|
109
|
+
const KeyvLz4 = requireOptional('@keyv/compress-lz4', { feature: 'compression', value: 'lz4' });
|
|
78
110
|
return new KeyvLz4();
|
|
79
111
|
case "gzip":
|
|
112
|
+
const KeyvGzip = requireOptional('@keyv/compress-gzip', { feature: 'compression', value: 'gzip' });
|
|
80
113
|
return new KeyvGzip();
|
|
81
114
|
default:
|
|
82
115
|
return undefined;
|
|
@@ -93,7 +126,7 @@ class CacheStoreManager {
|
|
|
93
126
|
try {
|
|
94
127
|
await store.disconnect();
|
|
95
128
|
} catch (err) {
|
|
96
|
-
|
|
129
|
+
this.log.error(`Error disconnecting from store for ${cacheName}:`, err);
|
|
97
130
|
}
|
|
98
131
|
}
|
|
99
132
|
};
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
function isMissingModuleError(err, packageName) {
|
|
2
|
+
const msg = String(err?.message || '');
|
|
3
|
+
return (
|
|
4
|
+
(err?.code === 'MODULE_NOT_FOUND' || err?.code === 'ERR_MODULE_NOT_FOUND') &&
|
|
5
|
+
(msg.includes(`'${packageName}'`) || msg.includes(`"${packageName}"`) || msg.includes(packageName))
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function requireOptional(packageName, { feature, value } = {}) {
|
|
10
|
+
try {
|
|
11
|
+
const mod = require(packageName);
|
|
12
|
+
return mod?.default ?? mod;
|
|
13
|
+
} catch (err) {
|
|
14
|
+
if (!isMissingModuleError(err, packageName)) throw err;
|
|
15
|
+
|
|
16
|
+
const featureText = feature ? `${feature}="${value}" ` : '';
|
|
17
|
+
throw new Error(`cds-caching: ${featureText}requires installing "${packageName}" (npm i ${packageName})`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function requireAnyOptional(packageNames, { feature, value } = {}) {
|
|
22
|
+
const missing = [];
|
|
23
|
+
for (const packageName of packageNames) {
|
|
24
|
+
try {
|
|
25
|
+
return require(packageName);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (isMissingModuleError(err, packageName)) {
|
|
28
|
+
missing.push(packageName);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const featureText = feature ? `${feature}="${value}" ` : '';
|
|
36
|
+
const pkgsQuoted = missing.map((p) => `"${p}"`).join(' or ');
|
|
37
|
+
const pkgsInstall = missing.map((p) => `npm i ${p}`).join(' OR ');
|
|
38
|
+
throw new Error(`cds-caching: ${featureText}requires installing ${pkgsQuoted} (${pkgsInstall})`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
requireOptional,
|
|
43
|
+
requireAnyOptional
|
|
44
|
+
};
|
|
45
|
+
|
package/lib/util.js
CHANGED
|
@@ -93,7 +93,7 @@ const bindEntity = async (service, entity) => {
|
|
|
93
93
|
const createCacheEntry = async (cacheName, serviceConfig = {}) => {
|
|
94
94
|
try {
|
|
95
95
|
const db = await cds.connect.to('db');
|
|
96
|
-
const { Caches } =
|
|
96
|
+
const { Caches } = cds.entities('plugin.cds_caching');
|
|
97
97
|
|
|
98
98
|
// Check if cache entry already exists
|
|
99
99
|
const existingCache = await db.read(Caches).where({ name: cacheName });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cds-caching",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A caching plugin for SAP CAP applications",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,7 +34,33 @@
|
|
|
34
34
|
"release": "release-it"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@sap/cds": ">=8"
|
|
37
|
+
"@sap/cds": ">=8",
|
|
38
|
+
"@keyv/compress-gzip": "^2.0.3",
|
|
39
|
+
"@keyv/compress-lz4": "^1.0.1",
|
|
40
|
+
"@keyv/redis": "^5.0.0",
|
|
41
|
+
"@keyv/sqlite": "^4.0.5",
|
|
42
|
+
"@resolid/keyv-sqlite": "^4.0.5",
|
|
43
|
+
"@keyv/postgres": "^4.0.5"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"@keyv/redis": {
|
|
47
|
+
"optional": true
|
|
48
|
+
},
|
|
49
|
+
"@keyv/sqlite": {
|
|
50
|
+
"optional": true
|
|
51
|
+
},
|
|
52
|
+
"@keyv/compress-lz4": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"@keyv/compress-gzip": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"@resolid/keyv-sqlite": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"@keyv/postgres": {
|
|
62
|
+
"optional": true
|
|
63
|
+
}
|
|
38
64
|
},
|
|
39
65
|
"workspaces": [
|
|
40
66
|
".",
|
|
@@ -43,20 +69,22 @@
|
|
|
43
69
|
"examples/app"
|
|
44
70
|
],
|
|
45
71
|
"dependencies": {
|
|
46
|
-
"
|
|
47
|
-
"@keyv/compress-lz4": "^1.0.0",
|
|
48
|
-
"@keyv/redis": "^4.5.0",
|
|
49
|
-
"@keyv/sqlite": "^4.0.5",
|
|
50
|
-
"keyv": "^5.3.4"
|
|
72
|
+
"keyv": "^5.6.0"
|
|
51
73
|
},
|
|
52
74
|
"devDependencies": {
|
|
53
|
-
"
|
|
75
|
+
"@cap-js/cds-test": "^0.4.1",
|
|
76
|
+
"@release-it/conventional-changelog": "^10.0.5",
|
|
77
|
+
"eslint": "^10.0.0",
|
|
54
78
|
"husky": "^9.1.7",
|
|
55
|
-
"jest": "^30.0
|
|
56
|
-
"release-it": "^19.
|
|
57
|
-
"@
|
|
79
|
+
"jest": "^30.2.0",
|
|
80
|
+
"release-it": "^19.2.4",
|
|
81
|
+
"@resolid/keyv-sqlite": "~5.0.1",
|
|
82
|
+
"@keyv/sqlite": "~4.0.8",
|
|
83
|
+
"@keyv/compress-lz4": "~1.0.1",
|
|
84
|
+
"@keyv/compress-gzip": "~2.0.3",
|
|
85
|
+
"@keyv/postgres": "~2.2.3"
|
|
58
86
|
},
|
|
59
87
|
"engines": {
|
|
60
|
-
"node": ">=
|
|
88
|
+
"node": ">=24"
|
|
61
89
|
}
|
|
62
90
|
}
|