cds-caching 0.3.3 → 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 +691 -278
- package/cds-plugin.js +2 -2
- package/index.cds +165 -20
- package/lib/CachingService.d.ts +328 -0
- package/lib/CachingService.js +373 -0
- package/lib/operations/AsyncOperations.js +335 -0
- package/lib/operations/BasicOperations.js +211 -0
- package/lib/operations/CapOperations.js +540 -0
- package/lib/support/CacheStatisticsHandler.js +1124 -0
- package/lib/support/CacheStoreManager.js +120 -0
- package/lib/support/KeyManager.js +132 -0
- package/lib/support/RuntimeConfigurationManager.js +148 -0
- package/lib/support/StatisticsPersistenceManager.js +375 -0
- package/lib/support/TagResolver.js +125 -0
- package/lib/util.js +201 -0
- package/package.json +18 -13
- package/srv/caching-api-service.js +124 -0
- package/srv/CacheStatisticsHandler.js +0 -213
- package/srv/CachingService.js +0 -587
- package/srv/statistics-service.cds +0 -37
- package/srv/statistics-service.js +0 -72
- package/srv/util.js +0 -101
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const { Keyv } = require('keyv');
|
|
2
|
+
const { default: KeyvRedis } = require('@keyv/redis');
|
|
3
|
+
const { default: KeyvSqlite } = require('@keyv/sqlite');
|
|
4
|
+
const { default: KeyvLz4 } = require('@keyv/compress-lz4');
|
|
5
|
+
const { default: KeyvGzip } = require('@keyv/compress-gzip');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Manages cache store initialization and configuration
|
|
9
|
+
*/
|
|
10
|
+
class CacheStoreManager {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.log = cds.log('cds-caching')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create and configure the cache store based on options
|
|
18
|
+
* @param {object} options - Cache configuration options
|
|
19
|
+
* @param {string} cacheName - Name of the cache for logging
|
|
20
|
+
* @returns {object} - Configured Keyv instance and cleanup function
|
|
21
|
+
*/
|
|
22
|
+
createStore(options, cacheName) {
|
|
23
|
+
const store = this._createStoreInstance(options);
|
|
24
|
+
const cacheOptions = this._createCacheOptions(options, cacheName);
|
|
25
|
+
const cache = new Keyv(cacheOptions);
|
|
26
|
+
cache.throwOnErrors = options.throwOnErrors;
|
|
27
|
+
|
|
28
|
+
// Set up error handling
|
|
29
|
+
cache.on('error', err => {
|
|
30
|
+
this.log.error(`Cache error for ${cacheName}:`, err);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Set up cleanup function
|
|
34
|
+
const cleanup = this._createCleanupFunction(store, cacheName);
|
|
35
|
+
|
|
36
|
+
return { cache, cleanup };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create the appropriate store instance based on configuration
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
43
|
+
_createStoreInstance(options) {
|
|
44
|
+
switch (options.store) {
|
|
45
|
+
case "sqlite":
|
|
46
|
+
return new KeyvSqlite({
|
|
47
|
+
url: options.credentials?.url,
|
|
48
|
+
table: options.credentials?.table || 'cache',
|
|
49
|
+
busyTimeout: options.credentials?.busyTimeout || 10000
|
|
50
|
+
});
|
|
51
|
+
case "redis":
|
|
52
|
+
const store = new KeyvRedis({
|
|
53
|
+
...options.credentials,
|
|
54
|
+
...(options.credentials?.uri ? { url: options.credentials?.uri } : {}),
|
|
55
|
+
...{ throwOnConnectErrors: options.throwOnErrors, useKeyPrefix: false, throwOnErrors: options.throwOnErrors }
|
|
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;
|
|
71
|
+
default:
|
|
72
|
+
return new Map();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create cache options object
|
|
78
|
+
* @private
|
|
79
|
+
*/
|
|
80
|
+
_createCacheOptions(options, cacheName) {
|
|
81
|
+
return {
|
|
82
|
+
namespace: options.namespace || cacheName,
|
|
83
|
+
store: this._createStoreInstance(options),
|
|
84
|
+
compression: this._createCompression(options.compression)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create compression instance if specified
|
|
90
|
+
* @private
|
|
91
|
+
*/
|
|
92
|
+
_createCompression(compressionType) {
|
|
93
|
+
switch (compressionType) {
|
|
94
|
+
case "lz4":
|
|
95
|
+
return new KeyvLz4();
|
|
96
|
+
case "gzip":
|
|
97
|
+
return new KeyvGzip();
|
|
98
|
+
default:
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Create cleanup function for the store
|
|
105
|
+
* @private
|
|
106
|
+
*/
|
|
107
|
+
_createCleanupFunction(store, cacheName) {
|
|
108
|
+
return async () => {
|
|
109
|
+
if (store?.disconnect) {
|
|
110
|
+
try {
|
|
111
|
+
await store.disconnect();
|
|
112
|
+
} catch (err) {
|
|
113
|
+
this.log.error(`Error disconnecting from store for ${cacheName}:`, err);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = CacheStoreManager;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manages cache key generation and creation
|
|
5
|
+
*/
|
|
6
|
+
class KeyManager {
|
|
7
|
+
constructor(runtimeConfigManager) {
|
|
8
|
+
this.createHash = (data) => crypto.createHash('md5').update(JSON.stringify(data)).digest('hex');
|
|
9
|
+
this.runtimeConfigManager = runtimeConfigManager;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a cache key from various input types
|
|
14
|
+
* @param {string|object} keyOrObject - Key string or object to create key from
|
|
15
|
+
* @param {object} additionalContext - Additional context to include in the key
|
|
16
|
+
* @param {string} key - Optional key string to override default
|
|
17
|
+
* @returns {string} - Generated cache key
|
|
18
|
+
*/
|
|
19
|
+
createKey(keyOrObject, additionalContext = {}, key = null) {
|
|
20
|
+
// Use provided key or get default from configuration
|
|
21
|
+
const keyTemplate = key || this.runtimeConfigManager.getDefaultKeyTemplate();
|
|
22
|
+
|
|
23
|
+
// Create content hash from the object being cached
|
|
24
|
+
let contentHash = '';
|
|
25
|
+
if (typeof keyOrObject === "string") {
|
|
26
|
+
// For strings, use the string itself as the hash
|
|
27
|
+
contentHash = keyOrObject;
|
|
28
|
+
} else if (typeof keyOrObject === "object" && keyOrObject !== null) {
|
|
29
|
+
contentHash = this.createContentHash(keyOrObject);
|
|
30
|
+
// If contentHash is undefined, the object should not be cached
|
|
31
|
+
if (contentHash === undefined) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
contentHash = this.createHash({ content: keyOrObject });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Get context values
|
|
39
|
+
const contextVars = {
|
|
40
|
+
tenant: additionalContext.tenant || cds.context?.tenant || 'global',
|
|
41
|
+
user: additionalContext.user || (cds.context?.user?.id || cds.context?.user) || 'anonymous',
|
|
42
|
+
locale: additionalContext.locale || cds.context?.locale || 'en',
|
|
43
|
+
hash: contentHash,
|
|
44
|
+
baseKey: additionalContext.baseKey || ''
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Handle argument placeholders for async functions
|
|
48
|
+
if (additionalContext.args && Array.isArray(additionalContext.args)) {
|
|
49
|
+
additionalContext.args.forEach((arg, index) => {
|
|
50
|
+
contextVars[`args[${index}]`] = this.serializeArgument(arg);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Replace placeholders in template
|
|
55
|
+
return keyTemplate.replace(
|
|
56
|
+
/\{(tenant|user|locale|hash|baseKey|args\[\d+\])\}/g,
|
|
57
|
+
(match, variable) => {
|
|
58
|
+
if (variable.startsWith('args[')) {
|
|
59
|
+
return contextVars[variable] || '';
|
|
60
|
+
}
|
|
61
|
+
return contextVars[variable] || '';
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create a hash from the content being cached (without context)
|
|
68
|
+
* @param {object} keyOrObject - Object to create hash from
|
|
69
|
+
* @returns {string} - Generated hash
|
|
70
|
+
*/
|
|
71
|
+
createContentHash(keyOrObject) {
|
|
72
|
+
if (typeof keyOrObject === "string") {
|
|
73
|
+
return this.createHash({ content: keyOrObject });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (typeof keyOrObject === "object" && keyOrObject !== null) {
|
|
77
|
+
switch (keyOrObject.constructor.name) {
|
|
78
|
+
case "Request":
|
|
79
|
+
case "NoaRequest":
|
|
80
|
+
case "ODataRequest":
|
|
81
|
+
// Only hash the request information, not user/tenant/locale
|
|
82
|
+
return this.createHash({
|
|
83
|
+
method: keyOrObject.method,
|
|
84
|
+
path: keyOrObject.path || keyOrObject.http?.req?.path,
|
|
85
|
+
data: keyOrObject.data,
|
|
86
|
+
params: keyOrObject.params,
|
|
87
|
+
query: keyOrObject.query,
|
|
88
|
+
event: keyOrObject.event,
|
|
89
|
+
target: keyOrObject.target?.name
|
|
90
|
+
});
|
|
91
|
+
case "cds.ql":
|
|
92
|
+
if (keyOrObject.SELECT) {
|
|
93
|
+
// Only hash the query structure, exclude cacheKey property
|
|
94
|
+
const { cacheKey, ...queryWithoutCacheKey } = keyOrObject;
|
|
95
|
+
return this.createHash({ query: queryWithoutCacheKey });
|
|
96
|
+
} else {
|
|
97
|
+
// Non-SELECT queries should not be cached
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
default:
|
|
101
|
+
return this.createHash({ content: keyOrObject });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return this.createHash({ content: keyOrObject });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Serialize function arguments for cache key generation
|
|
110
|
+
* @param {any} arg - argument to serialize
|
|
111
|
+
* @returns {string} serialized argument
|
|
112
|
+
*/
|
|
113
|
+
serializeArgument(arg) {
|
|
114
|
+
if (arg === null) return 'null';
|
|
115
|
+
if (arg === undefined) return 'undefined';
|
|
116
|
+
if (typeof arg === 'string') return arg;
|
|
117
|
+
if (typeof arg === 'number') return arg.toString();
|
|
118
|
+
if (typeof arg === 'boolean') return arg.toString();
|
|
119
|
+
if (Array.isArray(arg)) return `[${arg.map(a => this.serializeArgument(a)).join(',')}]`;
|
|
120
|
+
if (typeof arg === 'object') {
|
|
121
|
+
// For objects, create a stable string representation
|
|
122
|
+
try {
|
|
123
|
+
return this.createHash(arg);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return 'object';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return String(arg);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = KeyManager;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const cds = require('@sap/cds');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manages runtime configuration for cache services
|
|
5
|
+
*/
|
|
6
|
+
class RuntimeConfigurationManager {
|
|
7
|
+
constructor(cacheName, log, options = {}) {
|
|
8
|
+
this.cacheName = cacheName;
|
|
9
|
+
this.log = log || cds.log('cds-caching');
|
|
10
|
+
this.options = options;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Load runtime configuration from database and package.json
|
|
15
|
+
* @returns {Promise<object>} Configuration object
|
|
16
|
+
*/
|
|
17
|
+
async loadRuntimeConfiguration() {
|
|
18
|
+
try {
|
|
19
|
+
this.log.info(`Loading runtime configuration for cache ${this.cacheName}...`);
|
|
20
|
+
|
|
21
|
+
const cacheConfig = await SELECT.one.from("plugin_cds_caching_Caches")
|
|
22
|
+
.where({ name: this.cacheName });
|
|
23
|
+
|
|
24
|
+
// Get key management configuration from package.json options (defaults to false)
|
|
25
|
+
const keyManagementConfig = this.options?.keyManagement || {};
|
|
26
|
+
const keyManagement = {
|
|
27
|
+
isUserAware: keyManagementConfig.isUserAware === true, // Default to false
|
|
28
|
+
isTenantAware: keyManagementConfig.isTenantAware === true, // Default to false
|
|
29
|
+
isLocaleAware: keyManagementConfig.isLocaleAware === true // Default to false
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
if (cacheConfig) {
|
|
33
|
+
return {
|
|
34
|
+
enableKeyTracking: cacheConfig.enableKeyTracking || false,
|
|
35
|
+
enableStatistics: cacheConfig.enableStatistics || false,
|
|
36
|
+
keyManagement
|
|
37
|
+
};
|
|
38
|
+
} else {
|
|
39
|
+
this.log.warn(`No cache configuration found for cache ${this.cacheName}`);
|
|
40
|
+
return {
|
|
41
|
+
enableKeyTracking: false,
|
|
42
|
+
enableStatistics: false,
|
|
43
|
+
keyManagement
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
this.log.warn(`Failed to load runtime configuration for cache ${this.cacheName}:`, error);
|
|
48
|
+
return {
|
|
49
|
+
enableKeyTracking: false,
|
|
50
|
+
enableStatistics: false,
|
|
51
|
+
keyManagement: {
|
|
52
|
+
isUserAware: false,
|
|
53
|
+
isTenantAware: false,
|
|
54
|
+
isLocaleAware: false
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Update metrics enabled status in database
|
|
62
|
+
* @param {boolean} enabled - Whether metrics should be enabled
|
|
63
|
+
*/
|
|
64
|
+
async setMetricsEnabled(enabled) {
|
|
65
|
+
await UPDATE('plugin_cds_caching_Caches')
|
|
66
|
+
.set({ metricsEnabled: enabled })
|
|
67
|
+
.where({ name: this.cacheName });
|
|
68
|
+
|
|
69
|
+
this.log.debug(`Statistics ${enabled ? 'enabled' : 'disabled'} for cache ${this.cacheName}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Update key metrics enabled status in database
|
|
74
|
+
* @param {boolean} enabled - Whether key metrics should be enabled
|
|
75
|
+
*/
|
|
76
|
+
async setKeyMetricsEnabled(enabled) {
|
|
77
|
+
await UPDATE('plugin_cds_caching_Caches')
|
|
78
|
+
.set({ keyMetricsEnabled: enabled })
|
|
79
|
+
.where({ name: this.cacheName });
|
|
80
|
+
|
|
81
|
+
this.log.debug(`Key tracking ${enabled ? 'enabled' : 'disabled'} for cache ${this.cacheName}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get current runtime configuration
|
|
88
|
+
* @returns {Promise<object>} Current configuration
|
|
89
|
+
*/
|
|
90
|
+
async getRuntimeConfiguration() {
|
|
91
|
+
try {
|
|
92
|
+
const cacheConfig = await SELECT.one.from("plugin_cds_caching_Caches")
|
|
93
|
+
.where({ name: this.cacheName });
|
|
94
|
+
|
|
95
|
+
// Get key management configuration from package.json options (defaults to false)
|
|
96
|
+
const keyManagementConfig = this.options?.keyManagement || {};
|
|
97
|
+
const keyManagement = {
|
|
98
|
+
isUserAware: keyManagementConfig.isUserAware === true,
|
|
99
|
+
isTenantAware: keyManagementConfig.isTenantAware === true,
|
|
100
|
+
isLocaleAware: keyManagementConfig.isLocaleAware === true
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
metricsEnabled: cacheConfig?.metricsEnabled === true || cacheConfig?.metricsEnabled === 1 || false,
|
|
105
|
+
keyMetricsEnabled: cacheConfig?.keyMetricsEnabled === true || cacheConfig?.keyMetricsEnabled === 1 || false,
|
|
106
|
+
keyManagement,
|
|
107
|
+
throwOnErrors: this.options.throwOnErrors
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
this.log.warn(`Failed to get runtime configuration for cache ${this.cacheName}:`, error);
|
|
111
|
+
return {
|
|
112
|
+
metricsEnabled: false,
|
|
113
|
+
keyMetricsEnabled: false,
|
|
114
|
+
keyManagement: {
|
|
115
|
+
isUserAware: false,
|
|
116
|
+
isTenantAware: false,
|
|
117
|
+
isLocaleAware: false
|
|
118
|
+
},
|
|
119
|
+
throwOnErrors: this.options.throwOnErrors
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get default key template based on runtime configuration
|
|
126
|
+
* @returns {string} Default template string
|
|
127
|
+
*/
|
|
128
|
+
getDefaultKeyTemplate() {
|
|
129
|
+
// Get key management configuration from package.json options (defaults to false)
|
|
130
|
+
const keyManagementConfig = this.options?.keyManagement || {};
|
|
131
|
+
const keyManagement = {
|
|
132
|
+
isUserAware: keyManagementConfig.isUserAware === true, // Default to false
|
|
133
|
+
isTenantAware: keyManagementConfig.isTenantAware === true, // Default to false
|
|
134
|
+
isLocaleAware: keyManagementConfig.isLocaleAware === true // Default to false
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const parts = [];
|
|
138
|
+
|
|
139
|
+
if (keyManagement.isTenantAware) parts.push('{tenant}');
|
|
140
|
+
if (keyManagement.isUserAware) parts.push('{user}');
|
|
141
|
+
if (keyManagement.isLocaleAware) parts.push('{locale}');
|
|
142
|
+
parts.push('{hash}');
|
|
143
|
+
|
|
144
|
+
return parts.join(':');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = RuntimeConfigurationManager;
|