cds-caching 0.3.2 → 1.0.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.
@@ -0,0 +1,375 @@
1
+ const cds = require('@sap/cds');
2
+
3
+ // Helper to merge min values, treating 0 as unset
4
+ function mergeMin(a, b) {
5
+ if ((a === 0 || a === undefined) && (b === 0 || b === undefined)) return 0;
6
+ if (a === 0 || a === undefined) return b;
7
+ if (b === 0 || b === undefined) return a;
8
+ return Math.min(a, b);
9
+ }
10
+
11
+ /**
12
+ * Manages persistence of cache statistics to the database
13
+ */
14
+ class StatisticsPersistenceManager {
15
+ constructor(cacheName, log) {
16
+ this.cacheName = cacheName;
17
+ this.log = log || cds.log('cds-caching');
18
+ }
19
+
20
+ /**
21
+ * Persist hourly statistics
22
+ * @param {object} stats - Statistics to persist
23
+ * @param {string} hourlyId - Hourly ID
24
+ * @param {string} hourlyTimestamp - Hourly timestamp
25
+ * @param {boolean} enabled - Whether main metrics are enabled
26
+ */
27
+ async persistHourlyStats(stats, hourlyId, hourlyTimestamp, enabled) {
28
+ if (!enabled) return;
29
+
30
+ try {
31
+ const existingHourly = await SELECT.one.from("plugin_cds_caching_Metrics")
32
+ .where({ ID: hourlyId, cache: this.cacheName });
33
+
34
+ if (!existingHourly) {
35
+ await this._createHourlyStats(stats, hourlyId, hourlyTimestamp);
36
+ } else {
37
+ await this._updateHourlyStats(stats, hourlyId, existingHourly);
38
+ }
39
+ } catch (error) {
40
+ this.log.error(`Failed to persist hourly stats for cache ${this.cacheName}:`, error);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Persist key metrics
46
+ * @param {Map} keyAccess - Key access data
47
+ * @param {boolean} keyMetricsEnabled - Whether key metrics are enabled
48
+ */
49
+ async persistKeyMetrics(keyAccess, keyMetricsEnabled) {
50
+ if (!keyMetricsEnabled || keyAccess.size === 0) return;
51
+
52
+ this.log.debug(`Persisting ${keyAccess.size} key access records for cache ${this.cacheName}`);
53
+
54
+ for (const [key, keyStats] of keyAccess) {
55
+ try {
56
+ await this._persistKeyMetric(key, keyStats);
57
+ } catch (error) {
58
+ this.log.error(`Failed to persist key metric for key ${key}:`, error);
59
+ }
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Delete metrics for this cache
65
+ */
66
+ async deleteMetrics() {
67
+ await DELETE('plugin_cds_caching_Metrics')
68
+ .where({ cache: this.cacheName });
69
+ this.log.debug(`Deleted metrics for cache ${this.cacheName}`);
70
+ }
71
+
72
+ /**
73
+ * Delete key metrics for this cache
74
+ */
75
+ async deleteKeyMetrics() {
76
+ await DELETE('plugin_cds_caching_KeyMetrics')
77
+ .where({ cache: this.cacheName });
78
+ this.log.debug(`Deleted key metrics for cache ${this.cacheName}`);
79
+ }
80
+
81
+ /**
82
+ * Create new hourly stats record
83
+ * @private
84
+ */
85
+ async _createHourlyStats(stats, hourlyId, hourlyTimestamp) {
86
+ await INSERT.into('plugin_cds_caching_Metrics').entries([{
87
+ ID: hourlyId,
88
+ cache: this.cacheName,
89
+ timestamp: hourlyTimestamp,
90
+ period: 'hourly',
91
+ // Read-through metrics
92
+ hits: stats.hits,
93
+ misses: stats.misses,
94
+ errors: stats.errors,
95
+ totalRequests: stats.totalRequests,
96
+
97
+ // Read-through latency metrics
98
+ avgHitLatency: stats.avgHitLatency,
99
+ minHitLatency: stats.minHitLatency,
100
+ maxHitLatency: stats.maxHitLatency,
101
+ avgMissLatency: stats.avgMissLatency,
102
+ minMissLatency: stats.minMissLatency,
103
+ maxMissLatency: stats.maxMissLatency,
104
+ avgReadThroughLatency: stats.avgReadThroughLatency,
105
+
106
+ // Read-through performance metrics
107
+ hitRatio: stats.hitRatio,
108
+ throughput: stats.throughput,
109
+ errorRate: stats.errorRate,
110
+ cacheEfficiency: stats.cacheEfficiency,
111
+
112
+ // Native function metrics
113
+ nativeSets: stats.nativeSets,
114
+ nativeGets: stats.nativeGets,
115
+ nativeDeletes: stats.nativeDeletes,
116
+ nativeClears: stats.nativeClears,
117
+ nativeDeleteByTags: stats.nativeDeleteByTags,
118
+ nativeErrors: stats.nativeErrors,
119
+ totalNativeOperations: stats.totalNativeOperations,
120
+
121
+ // Native function performance metrics
122
+ nativeThroughput: stats.nativeThroughput,
123
+ nativeErrorRate: stats.nativeErrorRate,
124
+
125
+ // Common metrics
126
+ memoryUsage: stats.memoryUsage,
127
+ itemCount: stats.itemCount,
128
+ uptimeMs: stats.uptimeMs
129
+ }]);
130
+ this.log.debug(`Created new hourly stats for cache ${this.cacheName}`);
131
+ }
132
+
133
+ /**
134
+ * Update existing hourly stats record
135
+ * @private
136
+ */
137
+ async _updateHourlyStats(stats, hourlyId, existingHourly) {
138
+ const updatedStats = this._calculateUpdatedStats(stats, existingHourly);
139
+
140
+ await UPDATE('plugin_cds_caching_Metrics')
141
+ .set(updatedStats)
142
+ .where({ ID: hourlyId, cache: this.cacheName });
143
+ this.log.debug(`Updated existing hourly stats for cache ${this.cacheName}`);
144
+ }
145
+
146
+ /**
147
+ * Calculate updated stats with weighted averages
148
+ * @private
149
+ */
150
+ _calculateUpdatedStats(stats, existingHourly) {
151
+ const updatedStats = {
152
+ // Read-through metrics
153
+ hits: existingHourly.hits + stats.hits,
154
+ misses: existingHourly.misses + stats.misses,
155
+ errors: existingHourly.errors + stats.errors,
156
+ totalRequests: existingHourly.totalRequests + stats.totalRequests,
157
+
158
+ // Native function metrics
159
+ nativeSets: existingHourly.nativeSets + stats.nativeSets,
160
+ nativeGets: existingHourly.nativeGets + stats.nativeGets,
161
+ nativeDeletes: existingHourly.nativeDeletes + stats.nativeDeletes,
162
+ nativeClears: existingHourly.nativeClears + stats.nativeClears,
163
+ nativeDeleteByTags: existingHourly.nativeDeleteByTags + stats.nativeDeleteByTags,
164
+ nativeErrors: existingHourly.nativeErrors + stats.nativeErrors,
165
+ totalNativeOperations: existingHourly.totalNativeOperations + stats.totalNativeOperations,
166
+
167
+ // Common metrics
168
+ memoryUsage: stats.memoryUsage,
169
+ itemCount: stats.itemCount,
170
+ uptimeMs: stats.uptimeMs
171
+ };
172
+
173
+ // Calculate weighted averages for latencies
174
+ const totalRequests = updatedStats.hits + updatedStats.misses;
175
+
176
+ // Weighted average for hit latency
177
+ if (updatedStats.hits > 0) {
178
+ const existingHitLatencySum = existingHourly.avgHitLatency * existingHourly.hits;
179
+ const newHitLatencySum = stats.avgHitLatency * stats.hits;
180
+ updatedStats.avgHitLatency = (existingHitLatencySum + newHitLatencySum) / updatedStats.hits;
181
+ } else {
182
+ updatedStats.avgHitLatency = 0;
183
+ }
184
+
185
+ // Weighted average for miss latency
186
+ if (updatedStats.misses > 0) {
187
+ const existingMissLatencySum = existingHourly.avgMissLatency * existingHourly.misses;
188
+ const newMissLatencySum = stats.avgMissLatency * stats.misses;
189
+ updatedStats.avgMissLatency = (existingMissLatencySum + newMissLatencySum) / updatedStats.misses;
190
+ } else {
191
+ updatedStats.avgMissLatency = 0;
192
+ }
193
+
194
+ // Weighted average for read-through latency (combined hits and misses)
195
+ const existingCount = (existingHourly.hits || 0) + (existingHourly.misses || 0);
196
+ const newCount = (stats.hits || 0) + (stats.misses || 0);
197
+ const totalCount = existingCount + newCount;
198
+ if (totalCount > 0) {
199
+ updatedStats.avgReadThroughLatency =
200
+ ((existingHourly.avgReadThroughLatency || 0) * existingCount +
201
+ (stats.avgReadThroughLatency || 0) * newCount) / totalCount;
202
+ } else {
203
+ updatedStats.avgReadThroughLatency = 0;
204
+ }
205
+
206
+ // Calculate read-through performance metrics
207
+ if (totalRequests > 0) {
208
+ updatedStats.hitRatio = (updatedStats.hits / totalRequests) * 100;
209
+ updatedStats.throughput = totalRequests / (stats.uptimeMs / 1000);
210
+ updatedStats.errorRate = (updatedStats.errors / totalRequests) * 100;
211
+ }
212
+
213
+ // Calculate cache efficiency from weighted averages
214
+ if (updatedStats.avgHitLatency > 0 && updatedStats.avgMissLatency > 0) {
215
+ updatedStats.cacheEfficiency = updatedStats.avgMissLatency / updatedStats.avgHitLatency;
216
+ }
217
+
218
+ // Calculate native function performance metrics
219
+ const totalNativeOps = updatedStats.totalNativeOperations;
220
+ if (totalNativeOps > 0) {
221
+ updatedStats.nativeThroughput = totalNativeOps / (stats.uptimeMs / 1000);
222
+ updatedStats.nativeErrorRate = (updatedStats.nativeErrors / totalNativeOps) * 100;
223
+ }
224
+
225
+ // Update percentiles (use max/min of existing and current, with min using mergeMin)
226
+ updatedStats.maxHitLatency = Math.max(existingHourly.maxHitLatency || 0, stats.maxHitLatency || 0);
227
+ updatedStats.maxMissLatency = Math.max(existingHourly.maxMissLatency || 0, stats.maxMissLatency || 0);
228
+ updatedStats.minHitLatency = mergeMin(existingHourly.minHitLatency, stats.minHitLatency);
229
+ updatedStats.minMissLatency = mergeMin(existingHourly.minMissLatency, stats.minMissLatency);
230
+
231
+ return updatedStats;
232
+ }
233
+
234
+ /**
235
+ * Persist a single key metric
236
+ * @private
237
+ */
238
+ async _persistKeyMetric(key, keyStats) {
239
+ const keyId = `key:${this.cacheName}:${key}`;
240
+
241
+ const existingKey = await SELECT.one.from("plugin_cds_caching_KeyMetrics")
242
+ .where({ ID: keyId, cache: this.cacheName, keyName: key });
243
+
244
+ if (!existingKey) {
245
+ await this._createKeyMetric(key, keyStats, keyId);
246
+ } else {
247
+ await this._updateKeyMetric(key, keyStats, keyId, existingKey);
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Create new key metric record
253
+ * @private
254
+ */
255
+ async _createKeyMetric(key, keyStats, keyId) {
256
+
257
+ await INSERT.into('plugin_cds_caching_KeyMetrics').entries([{
258
+ ID: keyId,
259
+ cache: this.cacheName,
260
+ keyName: key,
261
+ lastAccess: new Date(keyStats.lastAccess).toISOString(),
262
+ period: 'current',
263
+ // Operation type tracking
264
+ operationType: keyStats.operationType,
265
+
266
+ // Read-through metrics
267
+ hits: keyStats.hits,
268
+ misses: keyStats.misses,
269
+ errors: keyStats.errors,
270
+ totalRequests: keyStats.totalRequests,
271
+ hitRatio: keyStats.hitRatio,
272
+ cacheEfficiency: keyStats.cacheEfficiency,
273
+
274
+ // Read-through latency metrics
275
+ avgHitLatency: keyStats.avgHitLatency || 0,
276
+ minHitLatency: keyStats.minHitLatency === Infinity ? 0 : keyStats.minHitLatency,
277
+ maxHitLatency: keyStats.maxHitLatency || 0,
278
+ avgMissLatency: keyStats.avgMissLatency || 0,
279
+ minMissLatency: keyStats.minMissLatency === Infinity ? 0 : keyStats.minMissLatency,
280
+ maxMissLatency: keyStats.maxMissLatency || 0,
281
+ avgReadThroughLatency: keyStats.avgReadThroughLatency || 0,
282
+
283
+ // Read-through performance metrics
284
+ throughput: keyStats.throughput || 0,
285
+ errorRate: keyStats.errorRate || 0,
286
+
287
+ // Native function metrics
288
+ nativeHits: keyStats.nativeHits || 0,
289
+ nativeMisses: keyStats.nativeMisses || 0,
290
+ nativeSets: keyStats.nativeSets || 0,
291
+ nativeDeletes: keyStats.nativeDeletes || 0,
292
+ nativeClears: keyStats.nativeClears || 0,
293
+ nativeDeleteByTags: keyStats.nativeDeleteByTags || 0,
294
+ nativeErrors: keyStats.nativeErrors || 0,
295
+ totalNativeOperations: keyStats.totalNativeOperations || 0,
296
+
297
+ // Native function performance metrics
298
+ nativeThroughput: keyStats.nativeThroughput || 0,
299
+ nativeErrorRate: keyStats.nativeErrorRate || 0,
300
+
301
+ // Enhanced metadata
302
+ dataType: keyStats.dataType,
303
+ operation: keyStats.operation,
304
+ metadata: keyStats.metadata,
305
+
306
+ // Enhanced context information
307
+ context: keyStats.context,
308
+ query: keyStats.query,
309
+ subject: keyStats.subject,
310
+ target: keyStats.target,
311
+ tenant: keyStats.tenant,
312
+ user: keyStats.user,
313
+ locale: keyStats.locale,
314
+ timestamp: new Date(keyStats.timestamp).toISOString(),
315
+ cacheOptions: keyStats.cacheOptions
316
+ }]);
317
+ }
318
+
319
+ /**
320
+ * Update existing key metric record
321
+ * @private
322
+ */
323
+ async _updateKeyMetric(key, keyStats, keyId, existingKey) {
324
+ const totalHits = existingKey.hits + keyStats.hits;
325
+ const totalMisses = existingKey.misses + keyStats.misses;
326
+
327
+ const updatedKeyStats = {
328
+ hits: totalHits,
329
+ misses: totalMisses,
330
+ errors: (existingKey.errors || 0) + (keyStats.errors || 0),
331
+ totalRequests: totalHits + totalMisses,
332
+ lastAccess: new Date(keyStats.lastAccess).toISOString(),
333
+
334
+ // Native function metrics
335
+ nativeHits: (existingKey.nativeHits || 0) + (keyStats.nativeHits || 0),
336
+ nativeMisses: (existingKey.nativeMisses || 0) + (keyStats.nativeMisses || 0),
337
+ nativeSets: (existingKey.nativeSets || 0) + (keyStats.nativeSets || 0),
338
+ nativeDeletes: (existingKey.nativeDeletes || 0) + (keyStats.nativeDeletes || 0),
339
+ nativeClears: (existingKey.nativeClears || 0) + (keyStats.nativeClears || 0),
340
+ nativeDeleteByTags: (existingKey.nativeDeleteByTags || 0) + (keyStats.nativeDeleteByTags || 0),
341
+ nativeErrors: (existingKey.nativeErrors || 0) + (keyStats.nativeErrors || 0),
342
+ totalNativeOperations: (existingKey.totalNativeOperations || 0) + (keyStats.totalNativeOperations || 0),
343
+
344
+ // Weighted average for hit latency
345
+ avgHitLatency: totalHits > 0
346
+ ? ((existingKey.avgHitLatency * existingKey.hits) + (keyStats.avgHitLatency * keyStats.hits)) / totalHits
347
+ : 0,
348
+ // Weighted average for miss latency
349
+ avgMissLatency: totalMisses > 0
350
+ ? ((existingKey.avgMissLatency * existingKey.misses) + (keyStats.avgMissLatency * keyStats.misses)) / totalMisses
351
+ : 0,
352
+
353
+ // Update percentiles (use max/min of existing and current, with min using mergeMin)
354
+ minHitLatency: mergeMin(existingKey.minHitLatency, keyStats.minHitLatency),
355
+ maxHitLatency: Math.max(existingKey.maxHitLatency || 0, keyStats.maxHitLatency || 0),
356
+ minMissLatency: mergeMin(existingKey.minMissLatency, keyStats.minMissLatency),
357
+ maxMissLatency: Math.max(existingKey.maxMissLatency || 0, keyStats.maxMissLatency || 0)
358
+ };
359
+
360
+ // Update context information if new data is available
361
+ if (keyStats.context) updatedKeyStats.context = keyStats.context;
362
+ if (keyStats.query) updatedKeyStats.query = keyStats.query;
363
+ if (keyStats.subject) updatedKeyStats.subject = keyStats.subject;
364
+ if (keyStats.target) updatedKeyStats.target = keyStats.target;
365
+ if (keyStats.tenant) updatedKeyStats.tenant = keyStats.tenant;
366
+ if (keyStats.user) updatedKeyStats.user = keyStats.user;
367
+ if (keyStats.locale) updatedKeyStats.locale = keyStats.locale;
368
+ if (keyStats.cacheOptions) updatedKeyStats.cacheOptions = keyStats.cacheOptions;
369
+ await UPDATE('plugin_cds_caching_KeyMetrics')
370
+ .set(updatedKeyStats)
371
+ .where({ ID: keyId, cache: this.cacheName, keyName: key });
372
+ }
373
+ }
374
+
375
+ module.exports = StatisticsPersistenceManager;
@@ -0,0 +1,125 @@
1
+ const crypto = require('crypto');
2
+
3
+ /**
4
+ * Manages cache tag resolution and generation
5
+ */
6
+ class TagResolver {
7
+ constructor() {
8
+ this.createHash = (data) => crypto.createHash('md5').update(JSON.stringify(data)).digest('hex');
9
+ }
10
+
11
+ /**
12
+ * Resolves tags from tag configurations and data
13
+ * @param {Array} tagConfigs - Array of tag configuration objects
14
+ * @param {Object|Array} data - Data object(s) to extract data values from
15
+ * @param {Object} params - Parameters object to extract values from
16
+ * @returns {string[]} Array of resolved tags
17
+ */
18
+ resolveTags(tagConfigs = [], data, params = {}) {
19
+ // Handle empty/invalid configs
20
+ if (!tagConfigs?.length) return [];
21
+
22
+ // Convert data to array if single object or string
23
+ const dataArray = !data ? [] :
24
+ Array.isArray(data) ? data :
25
+ typeof data === 'string' ? [data] : [data];
26
+
27
+ // Process each tag configuration
28
+ const resolvedTags = tagConfigs.flatMap(config => {
29
+ // Handle string tags
30
+ if (typeof config === 'string') {
31
+ return [config];
32
+ }
33
+
34
+ // Handle invalid/empty config objects
35
+ if (!config || typeof config !== 'object') {
36
+ return [];
37
+ }
38
+
39
+ // Handle static value tags
40
+ if (config.value) {
41
+ const tag = [
42
+ config.prefix,
43
+ config.value,
44
+ config.suffix
45
+ ].filter(Boolean).join('');
46
+ return [tag];
47
+ }
48
+
49
+ // Handle template-based tags
50
+ if (config.template) {
51
+ const hashParts = [
52
+ ...(data ? [data] : []),
53
+ ...(params ? [params] : [])
54
+ ];
55
+
56
+ const contextVars = {
57
+ tenant: params.tenant || 'global',
58
+ user: params.user || 'anonymous',
59
+ locale: params.locale || 'en',
60
+ hash: this.createHash(hashParts)
61
+ };
62
+
63
+ const value = config.template.replace(
64
+ /\{(tenant|user|locale|hash)\}/g,
65
+ (match, variable) => contextVars[variable]
66
+ );
67
+
68
+ const tag = [
69
+ config.prefix,
70
+ value,
71
+ config.suffix
72
+ ].filter(Boolean).join('');
73
+
74
+ return [tag];
75
+ }
76
+
77
+ // Handle data-based tags
78
+ if (config.data && dataArray.length) {
79
+ return dataArray.flatMap(item => {
80
+ if (typeof item !== 'object') return [];
81
+
82
+ const dataFields = Array.isArray(config.data) ? config.data : [config.data];
83
+ const values = dataFields
84
+ .map(field => item[field])
85
+ .filter(Boolean);
86
+
87
+ if (!values.length) return [];
88
+
89
+ const value = values.join(config.separator || ':');
90
+ const tag = [
91
+ config.prefix,
92
+ value,
93
+ config.suffix
94
+ ].filter(Boolean).join('');
95
+ return [tag];
96
+ });
97
+ }
98
+
99
+ // Handle param-based tags
100
+ if (config.param) {
101
+ const paramFields = Array.isArray(config.param) ? config.param : [config.param];
102
+ const values = paramFields
103
+ .map(field => params[field])
104
+ .filter(Boolean);
105
+
106
+ if (!values.length) return [];
107
+
108
+ const value = values.join(config.separator || ':');
109
+ const tag = [
110
+ config.prefix,
111
+ value,
112
+ config.suffix
113
+ ].filter(Boolean).join('');
114
+ return [tag];
115
+ }
116
+
117
+ return [];
118
+ });
119
+
120
+ // Remove duplicates
121
+ return [...new Set(resolvedTags)];
122
+ }
123
+ }
124
+
125
+ module.exports = TagResolver;
package/lib/util.js ADDED
@@ -0,0 +1,201 @@
1
+ const cds = require("@sap/cds")
2
+
3
+ const extractCacheProperties = (entity, prefix) => {
4
+ const result = {};
5
+ for (const key of Object.keys(entity)) {
6
+ if (key.startsWith(`@cache.${prefix}.`)) {
7
+ const subKey = key.substring(`@cache.${prefix}.`.length);
8
+ result[subKey] = entity[key];
9
+ }
10
+ }
11
+ // If there are no subproperties but the main property exists, use it directly
12
+ if (Object.keys(result).length === 0 && entity[`@cache.${prefix}`]) {
13
+ return entity[`@cache.${prefix}`];
14
+ }
15
+ return Object.keys(result).length > 0 ? result : undefined;
16
+ };
17
+
18
+
19
+ const getCachingServicesFromConfig = (srvs) => {
20
+ const cachingServices = [];
21
+
22
+ for (const [serviceName, serviceConfig] of Object.entries(srvs)) {
23
+ if (serviceConfig && serviceConfig.constructor && serviceConfig.constructor.name === "CachingService") {
24
+
25
+ const statistics = { ...serviceConfig.statistics } || {};
26
+ delete statistics.stats;
27
+
28
+ cachingServices.push({
29
+ name: serviceName,
30
+ impl: serviceConfig.impl || 'cds-caching',
31
+ store: serviceConfig.store || 'memory',
32
+ namespace: serviceConfig.namespace || serviceName,
33
+ //statistics: statistics,
34
+ //credentials: serviceConfig.credentials || {},
35
+ //...serviceConfig
36
+ })
37
+ }
38
+ };
39
+
40
+ for (const [name, config] of Object.entries(cds.env.requires)) {
41
+ if (config.impl === 'cds-caching') {
42
+ if (cachingServices.find(service => service.name === name)) {
43
+ continue;
44
+ }
45
+
46
+ const statistics = { ...config.statistics } || {};
47
+ delete statistics.stats;
48
+
49
+ cachingServices.push({
50
+ name: name,
51
+ impl: config.impl || 'cds-caching',
52
+ store: config.store || 'memory',
53
+ namespace: config.namespace || name,
54
+ statistics: statistics,
55
+ })
56
+ }
57
+ }
58
+
59
+ return cachingServices;
60
+ }
61
+
62
+ const bindFunction = async (service, action, isBound = false) => {
63
+ const cache = await cds.connect.to(action['@cache.service'] || "caching");
64
+ cache.addCachableFunction(action.name.split('.').pop(), action, isBound);
65
+
66
+ service.prepend(function () {
67
+ service.on(action.name.split('.').pop(), async (req, next) => {
68
+ const cache = await cds.connect.to(action['@cache.service'] || "caching");
69
+ const { result, cacheKey } = await cache.rt.run(req, next, {
70
+ ttl: action['@cache.ttl'],
71
+ tags: action['@cache.tags'],
72
+ key: extractCacheProperties(action, 'key')
73
+ });
74
+ return result;
75
+ })
76
+ })
77
+ }
78
+
79
+ const bindEntity = async (service, entity) => {
80
+ service.prepend(function () {
81
+ service.on('READ', entity.name, async (req, next) => {
82
+ const cache = await cds.connect.to(entity['@cache.service'] || "caching");
83
+ const { result, cacheKey } = await cache.rt.run(req, next, {
84
+ ttl: entity['@cache.ttl'],
85
+ tags: entity['@cache.tags'],
86
+ key: extractCacheProperties(entity, 'key')
87
+ });
88
+ return result;
89
+ })
90
+ })
91
+ }
92
+
93
+ const createCacheEntry = async (cacheName, serviceConfig = {}) => {
94
+ try {
95
+ const db = await cds.connect.to('db');
96
+ const { Caches } = db.entities('plugin.cds_caching');
97
+
98
+ // Check if cache entry already exists
99
+ const existingCache = await db.read(Caches).where({ name: cacheName });
100
+
101
+ if (existingCache.length === 0) {
102
+ // Create new cache entry
103
+ const cacheEntry = {
104
+ name: cacheName,
105
+ config: JSON.stringify(serviceConfig)
106
+ };
107
+
108
+ await db.create(Caches).entries(cacheEntry);
109
+ cds.log('cds-caching').info(`Created cache entry for: ${cacheName}`);
110
+ } else {
111
+ // Update existing cache entry
112
+ await db.update(Caches)
113
+ .set({ config: JSON.stringify(serviceConfig) })
114
+ .where({ name: cacheName });
115
+ cds.log('cds-caching').info(`Updated cache entry for: ${cacheName}`);
116
+ }
117
+ } catch (error) {
118
+ cds.log('cds-caching').error(`Failed to create/update cache entry for ${cacheName}:`, error);
119
+ }
120
+ };
121
+
122
+ const scanCachingAnnotations = async (srvs) => {
123
+ LOG = cds.log('cds-caching')
124
+
125
+ // Grep all app services
126
+ const services = [];
127
+ for (const [name, config] of Object.entries(srvs)) {
128
+
129
+ const service = await cds.connect.to(name);
130
+ if (service.definition?.kind === 'service') {
131
+ services.push(service);
132
+ }
133
+ }
134
+
135
+ // Grep all external services
136
+ for (const [name, config] of Object.entries(cds.env.requires)) {
137
+ if (config.kind === 'odata-v2' || config.kind === 'odata' || config.kind === 'rest') {
138
+ const service = await cds.connect.to(name);
139
+ services.push(service);
140
+ }
141
+ }
142
+
143
+ for (const service of services) {
144
+ let cacheConfig = {};
145
+
146
+ // functions
147
+ for (const [name, action] of Object.entries(service.actions)) {
148
+ if (Object.keys(action).some(key => key.startsWith('@cache')) && action.kind === 'function') {
149
+ LOG._debug && LOG.debug(`Caching enabled for function ${action.name}`);
150
+ await bindFunction(service, action);
151
+
152
+ // Collect cache configuration
153
+ cacheConfig.functions = cacheConfig.functions || {};
154
+ cacheConfig.functions[action.name] = {
155
+ ttl: action['@cache.ttl'],
156
+ tags: action['@cache.tags'],
157
+ key: extractCacheProperties(action, 'key')
158
+ };
159
+ }
160
+ }
161
+
162
+ // entities
163
+ for (const entity of service.entities) {
164
+ if (Object.keys(entity).some(key => key.startsWith('@cache'))) {
165
+ await bindEntity(service, entity);
166
+ LOG._debug && LOG.debug(`Caching enabled for entity ${entity.name}`);
167
+
168
+ // Collect cache configuration
169
+ cacheConfig.entities = cacheConfig.entities || {};
170
+ cacheConfig.entities[entity.name] = {
171
+ ttl: entity['@cache.ttl'],
172
+ tags: entity['@cache.tags'],
173
+ key: extractCacheProperties(entity, 'key')
174
+ };
175
+ }
176
+
177
+ // bound functions
178
+ for (const [name, action] of Object.entries(entity.actions || {})) {
179
+ if (Object.keys(action).some(key => key.startsWith('@cache')) && action.kind === 'function') {
180
+ bindFunction(service, action, true);
181
+ LOG._debug && LOG.debug(`Caching enabled for bound function ${action.name}`);
182
+
183
+ // Collect cache configuration
184
+ cacheConfig.boundFunctions = cacheConfig.boundFunctions || {};
185
+ cacheConfig.boundFunctions[`${entity.name}.${action.name}`] = {
186
+ ttl: action['@cache.ttl'],
187
+ tags: action['@cache.tags'],
188
+ key: extractCacheProperties(action, 'key')
189
+ };
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ const cachingServices = getCachingServicesFromConfig(srvs);
196
+ for (const service of cachingServices) {
197
+ await createCacheEntry(service.name, service);
198
+ }
199
+ }
200
+
201
+ module.exports = { scanCachingAnnotations }