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.
@@ -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;