cds-caching 1.2.1 → 1.3.1

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.
Files changed (80) hide show
  1. package/README.md +195 -1114
  2. package/app/dashboard/Component-dbg.js +59 -0
  3. package/app/dashboard/Component-dbg.js.map +1 -0
  4. package/app/dashboard/Component.js +2 -0
  5. package/app/dashboard/Component.js.map +1 -0
  6. package/app/dashboard/Component.ts +59 -0
  7. package/app/dashboard/controller/App-dbg.controller.js +132 -0
  8. package/app/dashboard/controller/App-dbg.controller.js.map +1 -0
  9. package/app/dashboard/controller/App.controller.js +2 -0
  10. package/app/dashboard/controller/App.controller.js.map +1 -0
  11. package/app/dashboard/controller/App.controller.ts +147 -0
  12. package/app/dashboard/controller/BaseController-dbg.js +75 -0
  13. package/app/dashboard/controller/BaseController-dbg.js.map +1 -0
  14. package/app/dashboard/controller/BaseController.js +2 -0
  15. package/app/dashboard/controller/BaseController.js.map +1 -0
  16. package/app/dashboard/controller/BaseController.ts +88 -0
  17. package/app/dashboard/controller/Cache-dbg.controller.js +421 -0
  18. package/app/dashboard/controller/Cache-dbg.controller.js.map +1 -0
  19. package/app/dashboard/controller/Cache.controller.js +2 -0
  20. package/app/dashboard/controller/Cache.controller.js.map +1 -0
  21. package/app/dashboard/controller/Cache.controller.ts +490 -0
  22. package/app/dashboard/controller/Main-dbg.controller.js +32 -0
  23. package/app/dashboard/controller/Main-dbg.controller.js.map +1 -0
  24. package/app/dashboard/controller/Main.controller.js +2 -0
  25. package/app/dashboard/controller/Main.controller.js.map +1 -0
  26. package/app/dashboard/controller/Main.controller.ts +31 -0
  27. package/app/dashboard/controller/SingleMetric-dbg.controller.js +306 -0
  28. package/app/dashboard/controller/SingleMetric-dbg.controller.js.map +1 -0
  29. package/app/dashboard/controller/SingleMetric.controller.js +2 -0
  30. package/app/dashboard/controller/SingleMetric.controller.js.map +1 -0
  31. package/app/dashboard/controller/SingleMetric.controller.ts +335 -0
  32. package/app/dashboard/i18n/i18n.properties +61 -0
  33. package/app/dashboard/i18n/i18n_de.properties +3 -0
  34. package/app/dashboard/i18n/i18n_en.properties +3 -0
  35. package/app/dashboard/index-cdn.html +28 -0
  36. package/app/dashboard/index.html +19 -0
  37. package/app/dashboard/manifest.json +145 -0
  38. package/app/dashboard/model/formatter-dbg.js +253 -0
  39. package/app/dashboard/model/formatter-dbg.js.map +1 -0
  40. package/app/dashboard/model/formatter.js +2 -0
  41. package/app/dashboard/model/formatter.js.map +1 -0
  42. package/app/dashboard/model/formatter.ts +275 -0
  43. package/app/dashboard/model/models-dbg.js +13 -0
  44. package/app/dashboard/model/models-dbg.js.map +1 -0
  45. package/app/dashboard/model/models.js +2 -0
  46. package/app/dashboard/model/models.js.map +1 -0
  47. package/app/dashboard/model/models.ts +13 -0
  48. package/app/dashboard/resources/sap-ui-custom-dbg.js +4695 -0
  49. package/app/dashboard/resources/sap-ui-custom-dbg.js.map +1 -0
  50. package/app/dashboard/resources/sap-ui-custom.js +5794 -0
  51. package/app/dashboard/resources/sap-ui-custom.js.map +1 -0
  52. package/app/dashboard/service/CacheStatisticsService-dbg.js +267 -0
  53. package/app/dashboard/service/CacheStatisticsService-dbg.js.map +1 -0
  54. package/app/dashboard/service/CacheStatisticsService.js +2 -0
  55. package/app/dashboard/service/CacheStatisticsService.js.map +1 -0
  56. package/app/dashboard/service/CacheStatisticsService.ts +397 -0
  57. package/app/dashboard/view/App.view.xml +18 -0
  58. package/app/dashboard/view/Cache.view.xml +777 -0
  59. package/app/dashboard/view/Main.view.xml +57 -0
  60. package/app/dashboard/view/SingleMetric.view.xml +337 -0
  61. package/cds-plugin.js +52 -0
  62. package/db/cache-store.cds +12 -0
  63. package/db/statistics.cds +128 -0
  64. package/index.cds +2 -128
  65. package/lib/CachingService.js +24 -10
  66. package/lib/add.js +17 -0
  67. package/lib/operations/AsyncOperations.js +162 -111
  68. package/lib/operations/BasicOperations.js +42 -4
  69. package/lib/operations/CapOperations.js +92 -40
  70. package/lib/support/CacheStatisticsHandler.js +35 -10
  71. package/lib/support/CacheStoreManager.js +29 -0
  72. package/lib/support/KeyManager.js +40 -2
  73. package/lib/support/KeyvCDS.js +149 -0
  74. package/lib/support/MultitenancyDetector.js +25 -0
  75. package/lib/support/RuntimeConfigurationManager.js +69 -37
  76. package/lib/support/StatisticsPersistenceManager.js +18 -2
  77. package/lib/support/Telemetry.js +137 -0
  78. package/lib/util.js +29 -11
  79. package/package.json +43 -11
  80. package/srv/caching-api-service.js +47 -2
@@ -1,4 +1,6 @@
1
1
  const cds = require('@sap/cds');
2
+ const { isMultitenantMode, hasTenantContext } = require('./MultitenancyDetector');
3
+ const { isPluginModelAvailable } = require('../util');
2
4
 
3
5
  /**
4
6
  * Manages runtime configuration for cache services
@@ -10,25 +12,50 @@ class RuntimeConfigurationManager {
10
12
  this.options = options;
11
13
  }
12
14
 
15
+ /**
16
+ * Resolve the effective keyManagement settings.
17
+ * In MTX mode, isTenantAware defaults to true unless explicitly set to false.
18
+ * @returns {object} keyManagement object
19
+ * @private
20
+ */
21
+ _resolveKeyManagement() {
22
+ const keyManagementConfig = this.options?.keyManagement || {};
23
+ return {
24
+ isUserAware: keyManagementConfig.isUserAware === true,
25
+ isTenantAware: keyManagementConfig.isTenantAware !== undefined
26
+ ? keyManagementConfig.isTenantAware === true
27
+ : isMultitenantMode(),
28
+ isLocaleAware: keyManagementConfig.isLocaleAware === true
29
+ };
30
+ }
31
+
13
32
  /**
14
33
  * Load runtime configuration from database and package.json
15
34
  * @returns {Promise<object>} Configuration object
16
35
  */
17
36
  async loadRuntimeConfiguration() {
37
+ const keyManagement = this._resolveKeyManagement();
38
+
39
+ if (!isPluginModelAvailable()) {
40
+ return { enableKeyTracking: false, enableStatistics: false, keyManagement };
41
+ }
42
+
43
+ // In MTX mode without tenant context, skip DB access — return defaults
44
+ if (isMultitenantMode() && !hasTenantContext()) {
45
+ this.log.info(`Loading runtime configuration for cache ${this.cacheName} (MTX mode, no tenant context — using defaults)`);
46
+ return {
47
+ enableKeyTracking: false,
48
+ enableStatistics: false,
49
+ keyManagement
50
+ };
51
+ }
52
+
18
53
  try {
19
54
  this.log.info(`Loading runtime configuration for cache ${this.cacheName}...`);
20
55
 
21
56
  const cacheConfig = await SELECT.one.from("plugin_cds_caching_Caches")
22
57
  .where({ name: this.cacheName });
23
58
 
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
59
  if (cacheConfig) {
33
60
  return {
34
61
  enableKeyTracking: cacheConfig.enableKeyTracking || false,
@@ -48,11 +75,7 @@ class RuntimeConfigurationManager {
48
75
  return {
49
76
  enableKeyTracking: false,
50
77
  enableStatistics: false,
51
- keyManagement: {
52
- isUserAware: false,
53
- isTenantAware: false,
54
- isLocaleAware: false
55
- }
78
+ keyManagement
56
79
  };
57
80
  }
58
81
  }
@@ -62,6 +85,11 @@ class RuntimeConfigurationManager {
62
85
  * @param {boolean} enabled - Whether metrics should be enabled
63
86
  */
64
87
  async setMetricsEnabled(enabled) {
88
+ if (!isPluginModelAvailable()) return;
89
+ if (isMultitenantMode() && !hasTenantContext()) {
90
+ this.log.debug(`Skipping setMetricsEnabled in MTX mode (no tenant context)`);
91
+ return;
92
+ }
65
93
  await UPDATE('plugin_cds_caching_Caches')
66
94
  .set({ metricsEnabled: enabled })
67
95
  .where({ name: this.cacheName });
@@ -74,6 +102,11 @@ class RuntimeConfigurationManager {
74
102
  * @param {boolean} enabled - Whether key metrics should be enabled
75
103
  */
76
104
  async setKeyMetricsEnabled(enabled) {
105
+ if (!isPluginModelAvailable()) return;
106
+ if (isMultitenantMode() && !hasTenantContext()) {
107
+ this.log.debug(`Skipping setKeyMetricsEnabled in MTX mode (no tenant context)`);
108
+ return;
109
+ }
77
110
  await UPDATE('plugin_cds_caching_Caches')
78
111
  .set({ keyMetricsEnabled: enabled })
79
112
  .where({ name: this.cacheName });
@@ -88,18 +121,26 @@ class RuntimeConfigurationManager {
88
121
  * @returns {Promise<object>} Current configuration
89
122
  */
90
123
  async getRuntimeConfiguration() {
124
+ const keyManagement = this._resolveKeyManagement();
125
+
126
+ if (!isPluginModelAvailable()) {
127
+ return { metricsEnabled: false, keyMetricsEnabled: false, keyManagement, throwOnErrors: this.options.throwOnErrors };
128
+ }
129
+
130
+ // In MTX mode without tenant context, skip DB access — return defaults
131
+ if (isMultitenantMode() && !hasTenantContext()) {
132
+ return {
133
+ metricsEnabled: false,
134
+ keyMetricsEnabled: false,
135
+ keyManagement,
136
+ throwOnErrors: this.options.throwOnErrors
137
+ };
138
+ }
139
+
91
140
  try {
92
141
  const cacheConfig = await SELECT.one.from("plugin_cds_caching_Caches")
93
142
  .where({ name: this.cacheName });
94
143
 
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
144
  return {
104
145
  metricsEnabled: cacheConfig?.metricsEnabled === true || cacheConfig?.metricsEnabled === 1 || false,
105
146
  keyMetricsEnabled: cacheConfig?.keyMetricsEnabled === true || cacheConfig?.keyMetricsEnabled === 1 || false,
@@ -111,36 +152,27 @@ class RuntimeConfigurationManager {
111
152
  return {
112
153
  metricsEnabled: false,
113
154
  keyMetricsEnabled: false,
114
- keyManagement: {
115
- isUserAware: false,
116
- isTenantAware: false,
117
- isLocaleAware: false
118
- },
155
+ keyManagement,
119
156
  throwOnErrors: this.options.throwOnErrors
120
157
  };
121
158
  }
122
159
  }
123
160
 
124
161
  /**
125
- * Get default key template based on runtime configuration
162
+ * Get default key template based on runtime configuration.
163
+ * In MTX mode, isTenantAware is automatically enabled unless explicitly disabled.
126
164
  * @returns {string} Default template string
127
165
  */
128
166
  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
-
167
+ const keyManagement = this._resolveKeyManagement();
168
+
137
169
  const parts = [];
138
-
170
+
139
171
  if (keyManagement.isTenantAware) parts.push('{tenant}');
140
172
  if (keyManagement.isUserAware) parts.push('{user}');
141
173
  if (keyManagement.isLocaleAware) parts.push('{locale}');
142
174
  parts.push('{hash}');
143
-
175
+
144
176
  return parts.join(':');
145
177
  }
146
178
  }
@@ -1,4 +1,6 @@
1
1
  const cds = require('@sap/cds');
2
+ const { isMultitenantMode, hasTenantContext } = require('./MultitenancyDetector');
3
+ const { isPluginModelAvailable } = require('../util');
2
4
 
3
5
  // Helper to merge min values, treating 0 as unset
4
6
  function mergeMin(a, b) {
@@ -25,7 +27,13 @@ class StatisticsPersistenceManager {
25
27
  * @param {boolean} enabled - Whether main metrics are enabled
26
28
  */
27
29
  async persistHourlyStats(stats, hourlyId, hourlyTimestamp, enabled) {
28
- if (!enabled) return;
30
+ if (!enabled || !isPluginModelAvailable()) return;
31
+
32
+ // In MTX mode, only persist when we have a tenant context
33
+ if (isMultitenantMode() && !hasTenantContext()) {
34
+ this.log.debug(`Skipping hourly stats persistence in MTX mode (no tenant context)`);
35
+ return;
36
+ }
29
37
 
30
38
  try {
31
39
  const existingHourly = await SELECT.one.from("plugin_cds_caching_Metrics")
@@ -47,7 +55,13 @@ class StatisticsPersistenceManager {
47
55
  * @param {boolean} keyMetricsEnabled - Whether key metrics are enabled
48
56
  */
49
57
  async persistKeyMetrics(keyAccess, keyMetricsEnabled) {
50
- if (!keyMetricsEnabled || keyAccess.size === 0) return;
58
+ if (!keyMetricsEnabled || keyAccess.size === 0 || !isPluginModelAvailable()) return;
59
+
60
+ // In MTX mode, only persist when we have a tenant context
61
+ if (isMultitenantMode() && !hasTenantContext()) {
62
+ this.log.debug(`Skipping key metrics persistence in MTX mode (no tenant context)`);
63
+ return;
64
+ }
51
65
 
52
66
  this.log.debug(`Persisting ${keyAccess.size} key access records for cache ${this.cacheName}`);
53
67
 
@@ -64,6 +78,7 @@ class StatisticsPersistenceManager {
64
78
  * Delete metrics for this cache
65
79
  */
66
80
  async deleteMetrics() {
81
+ if (!isPluginModelAvailable()) return;
67
82
  await DELETE('plugin_cds_caching_Metrics')
68
83
  .where({ cache: this.cacheName });
69
84
  this.log.debug(`Deleted metrics for cache ${this.cacheName}`);
@@ -73,6 +88,7 @@ class StatisticsPersistenceManager {
73
88
  * Delete key metrics for this cache
74
89
  */
75
90
  async deleteKeyMetrics() {
91
+ if (!isPluginModelAvailable()) return;
76
92
  await DELETE('plugin_cds_caching_KeyMetrics')
77
93
  .where({ cache: this.cacheName });
78
94
  this.log.debug(`Deleted key metrics for cache ${this.cacheName}`);
@@ -0,0 +1,137 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Optional OpenTelemetry integration for cds-caching.
5
+ *
6
+ * If `@opentelemetry/api` is installed (e.g. via `@cap-js/telemetry`), this
7
+ * module exposes helpers to enrich auto-created spans and emit cache-specific
8
+ * OTel metrics. When the dependency is absent everything is a safe no-op —
9
+ * no runtime errors, no performance impact.
10
+ */
11
+
12
+ let otel;
13
+ try {
14
+ otel = require('@opentelemetry/api');
15
+ } catch (err) {
16
+ if (err && (err.code === 'MODULE_NOT_FOUND' || err.code === 'ERR_MODULE_NOT_FOUND')) {
17
+ otel = null;
18
+ } else {
19
+ throw err;
20
+ }
21
+ }
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Default attributes (set once during CachingService.init)
25
+ // ---------------------------------------------------------------------------
26
+ let _defaults = {};
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Lazy metric instances — created on first use so the MeterProvider set up
30
+ // by @cap-js/telemetry is already in place.
31
+ // ---------------------------------------------------------------------------
32
+ let _meter;
33
+ let _hitCounter;
34
+ let _missCounter;
35
+ let _setCounter;
36
+ let _deleteCounter;
37
+ let _errorCounter;
38
+ let _latencyHistogram;
39
+
40
+ function ensureMetrics() {
41
+ if (_meter) return;
42
+ if (!otel) return;
43
+ _meter = otel.metrics.getMeter('cds-caching');
44
+ _hitCounter = _meter.createCounter('cds_caching.hits', {
45
+ description: 'Number of cache hits'
46
+ });
47
+ _missCounter = _meter.createCounter('cds_caching.misses', {
48
+ description: 'Number of cache misses'
49
+ });
50
+ _setCounter = _meter.createCounter('cds_caching.sets', {
51
+ description: 'Number of cache set operations'
52
+ });
53
+ _deleteCounter = _meter.createCounter('cds_caching.deletes', {
54
+ description: 'Number of cache delete operations'
55
+ });
56
+ _errorCounter = _meter.createCounter('cds_caching.errors', {
57
+ description: 'Number of cache operation errors'
58
+ });
59
+ _latencyHistogram = _meter.createHistogram('cds_caching.latency', {
60
+ description: 'Cache operation latency in milliseconds',
61
+ unit: 'ms'
62
+ });
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Helpers
67
+ // ---------------------------------------------------------------------------
68
+ function attrs(extra) {
69
+ return { ..._defaults, ...extra };
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Public API
74
+ // ---------------------------------------------------------------------------
75
+ module.exports = {
76
+
77
+ /** Whether `@opentelemetry/api` could be loaded. */
78
+ isAvailable() { return !!otel; },
79
+
80
+ /** Return the currently active span, or `undefined`. */
81
+ getActiveSpan() { return otel?.trace?.getActiveSpan(); },
82
+
83
+ /** Return a Tracer scoped to the given name. */
84
+ getTracer(name) { return otel?.trace?.getTracer(name); },
85
+
86
+ /** Return a Meter scoped to the given name. */
87
+ getMeter(name) { return otel?.metrics?.getMeter(name); },
88
+
89
+ /** Convenience re-export so callers don't need to require `@opentelemetry/api`. */
90
+ SpanStatusCode: otel?.SpanStatusCode ?? { ERROR: 2, OK: 1 },
91
+
92
+ // -- configuration ------------------------------------------------------
93
+
94
+ /**
95
+ * Set attributes that will be merged into every metric recording.
96
+ * Typically called once during `CachingService.init()`.
97
+ */
98
+ setDefaultAttributes(attributes) {
99
+ _defaults = { ..._defaults, ...attributes };
100
+ },
101
+
102
+ // -- metric convenience methods -----------------------------------------
103
+
104
+ recordHit(latency, extra = {}) {
105
+ if (!otel) return;
106
+ ensureMetrics();
107
+ const a = attrs({ 'cache.operation': 'hit', ...extra });
108
+ _hitCounter.add(1, a);
109
+ if (latency != null) _latencyHistogram.record(latency, a);
110
+ },
111
+
112
+ recordMiss(latency, extra = {}) {
113
+ if (!otel) return;
114
+ ensureMetrics();
115
+ const a = attrs({ 'cache.operation': 'miss', ...extra });
116
+ _missCounter.add(1, a);
117
+ if (latency != null) _latencyHistogram.record(latency, a);
118
+ },
119
+
120
+ recordSet(extra = {}) {
121
+ if (!otel) return;
122
+ ensureMetrics();
123
+ _setCounter.add(1, attrs({ 'cache.operation': 'set', ...extra }));
124
+ },
125
+
126
+ recordDelete(extra = {}) {
127
+ if (!otel) return;
128
+ ensureMetrics();
129
+ _deleteCounter.add(1, attrs({ 'cache.operation': 'delete', ...extra }));
130
+ },
131
+
132
+ recordError(extra = {}) {
133
+ if (!otel) return;
134
+ ensureMetrics();
135
+ _errorCounter.add(1, attrs({ 'cache.operation': 'error', ...extra }));
136
+ }
137
+ };
package/lib/util.js CHANGED
@@ -1,4 +1,13 @@
1
1
  const cds = require("@sap/cds")
2
+ const { isMultitenantMode } = require("./support/MultitenancyDetector")
3
+
4
+ let _modelAvailable = null;
5
+
6
+ const isPluginModelAvailable = () => {
7
+ if (_modelAvailable !== null) return _modelAvailable;
8
+ _modelAvailable = !!cds.model?.definitions?.['plugin.cds_caching.Caches'];
9
+ return _modelAvailable;
10
+ };
2
11
 
3
12
  const extractCacheProperties = (entity, prefix) => {
4
13
  const result = {};
@@ -91,24 +100,20 @@ const bindEntity = async (service, entity) => {
91
100
  }
92
101
 
93
102
  const createCacheEntry = async (cacheName, serviceConfig = {}) => {
103
+ if (!isPluginModelAvailable()) return;
94
104
  try {
95
105
  const db = await cds.connect.to('db');
96
106
  const { Caches } = cds.entities('plugin.cds_caching');
97
107
 
98
- // Check if cache entry already exists
99
108
  const existingCache = await db.read(Caches).where({ name: cacheName });
100
109
 
101
110
  if (existingCache.length === 0) {
102
- // Create new cache entry
103
- const cacheEntry = {
111
+ await db.create(Caches).entries({
104
112
  name: cacheName,
105
113
  config: JSON.stringify(serviceConfig)
106
- };
107
-
108
- await db.create(Caches).entries(cacheEntry);
114
+ });
109
115
  cds.log('cds-caching').info(`Created cache entry for: ${cacheName}`);
110
116
  } else {
111
- // Update existing cache entry
112
117
  await db.update(Caches)
113
118
  .set({ config: JSON.stringify(serviceConfig) })
114
119
  .where({ name: cacheName });
@@ -122,6 +127,14 @@ const createCacheEntry = async (cacheName, serviceConfig = {}) => {
122
127
  const scanCachingAnnotations = async (srvs) => {
123
128
  LOG = cds.log('cds-caching')
124
129
 
130
+ if (!isPluginModelAvailable()) {
131
+ LOG.warn(
132
+ `Plugin data model not compiled into cds.model. Database features (dashboard, metrics persistence, CDS store) are disabled. ` +
133
+ `The model auto-loads when 'statistics' is configured or 'store: cds' is used. ` +
134
+ `For the dashboard, add: using {plugin.cds_caching.CachingApiService} from 'cds-caching/index.cds';`
135
+ );
136
+ }
137
+
125
138
  // Grep all app services
126
139
  const services = [];
127
140
  for (const [name, config] of Object.entries(srvs)) {
@@ -192,10 +205,15 @@ const scanCachingAnnotations = async (srvs) => {
192
205
  }
193
206
  }
194
207
 
195
- const cachingServices = getCachingServicesFromConfig(srvs);
196
- for (const service of cachingServices) {
197
- await createCacheEntry(service.name, service);
208
+ // In MTX mode, skip DB persistence at startup — no tenant context available.
209
+ // Cache entries are informational (mirrors package.json config) and will be
210
+ // created lazily via CachingApiService when a tenant accesses the dashboard.
211
+ if (!isMultitenantMode()) {
212
+ const cachingServices = getCachingServicesFromConfig(srvs);
213
+ for (const service of cachingServices) {
214
+ await createCacheEntry(service.name, service);
215
+ }
198
216
  }
199
217
  }
200
218
 
201
- module.exports = { scanCachingAnnotations }
219
+ module.exports = { scanCachingAnnotations, isPluginModelAvailable }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cds-caching",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "A caching plugin for SAP CAP applications",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,6 +12,7 @@
12
12
  "cds",
13
13
  "caching",
14
14
  "redis",
15
+ "hana",
15
16
  "keyv"
16
17
  ],
17
18
  "author": "Mike Zaschka <opensource@mikezaschka.com>",
@@ -23,26 +24,38 @@
23
24
  "main": "cds-plugin.js",
24
25
  "types": "lib/CachingService.d.ts",
25
26
  "files": [
27
+ "db/**",
26
28
  "srv/**",
27
29
  "index.cds",
28
- "lib/**"
30
+ "lib/**",
31
+ "app/dashboard/**"
29
32
  ],
30
33
  "scripts": {
31
34
  "test": "jest --runInBand --silent",
32
35
  "test:watch": "jest --watch",
36
+ "test:docker:start": "docker compose -f test/docker/docker-compose.yaml up -d --wait",
37
+ "test:docker:stop": "docker compose -f test/docker/docker-compose.yaml down",
38
+ "test:all": "npm run test:docker:start && npm test; status=$?; npm run test:docker:stop; exit $status",
33
39
  "lint": "eslint .",
34
- "release": "release-it"
40
+ "release": "release-it",
41
+ "prepublishOnly": "cd examples/dashboard && npx ui5 build self-contained --clean-dest --dest ../../app/dashboard"
35
42
  },
36
43
  "peerDependencies": {
37
- "@sap/cds": ">=8",
38
44
  "@keyv/compress-gzip": "^2.0.3",
39
45
  "@keyv/compress-lz4": "^1.0.1",
46
+ "@keyv/postgres": "^4.0.5",
40
47
  "@keyv/redis": "^5.0.0",
41
48
  "@keyv/sqlite": "^4.0.5",
49
+ "@opentelemetry/api": "^1.0.0",
42
50
  "@resolid/keyv-sqlite": "^4.0.5",
43
- "@keyv/postgres": "^4.0.5"
51
+ "@sap/cds": ">=8",
52
+ "@sap/cds-dk": ">=9",
53
+ "keyv-hana": ">=1.0.0"
44
54
  },
45
55
  "peerDependenciesMeta": {
56
+ "@opentelemetry/api": {
57
+ "optional": true
58
+ },
46
59
  "@keyv/redis": {
47
60
  "optional": true
48
61
  },
@@ -60,6 +73,9 @@
60
73
  },
61
74
  "@keyv/postgres": {
62
75
  "optional": true
76
+ },
77
+ "keyv-hana": {
78
+ "optional": true
63
79
  }
64
80
  },
65
81
  "workspaces": [
@@ -73,18 +89,34 @@
73
89
  },
74
90
  "devDependencies": {
75
91
  "@cap-js/cds-test": "^0.4.1",
92
+ "@keyv/compress-gzip": "~2.0.3",
93
+ "@keyv/compress-lz4": "~1.0.1",
94
+ "@keyv/postgres": "~2.2.3",
95
+ "@keyv/sqlite": "~4.0.8",
96
+ "@opentelemetry/api": "^1.9.1",
97
+ "@opentelemetry/sdk-metrics": "^1.30.1",
98
+ "@opentelemetry/sdk-trace-base": "^1.30.1",
99
+ "@opentelemetry/sdk-trace-node": "^1.30.1",
76
100
  "@release-it/conventional-changelog": "^10.0.5",
101
+ "@resolid/keyv-sqlite": "~5.0.1",
77
102
  "eslint": "^10.0.0",
78
103
  "husky": "^9.1.7",
79
104
  "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"
105
+ "release-it": "^19.2.4"
86
106
  },
87
107
  "engines": {
88
108
  "node": ">=20"
109
+ },
110
+ "cds": {
111
+ "requires": {
112
+ "kinds": {
113
+ "cds-caching": {
114
+ "impl": "cds-caching"
115
+ },
116
+ "caching": {
117
+ "impl": "cds-caching"
118
+ }
119
+ }
120
+ }
89
121
  }
90
122
  }
@@ -1,10 +1,22 @@
1
1
  const cds = require('@sap/cds')
2
+ const { isMultitenantMode } = require('../lib/support/MultitenancyDetector')
3
+ const { isPluginModelAvailable } = require('../lib/util')
4
+
5
+ const DEFAULT_TENANT = '_default';
2
6
 
3
7
  class CachingApiService extends cds.ApplicationService {
4
8
  log = cds.log('cds-caching');
5
9
 
6
10
  async init() {
7
11
 
12
+ // In MTX mode, lazily create cache entries on first dashboard access
13
+ // (skipped at startup because there's no tenant context)
14
+ if (isMultitenantMode()) {
15
+ this.before('READ', 'Caches', async () => {
16
+ await this._ensureCacheEntries();
17
+ });
18
+ }
19
+
8
20
  // Handle setMetricsEnabled action
9
21
  this.on('setMetricsEnabled', async (req) => {
10
22
  const { enabled } = req.data
@@ -117,8 +129,41 @@ class CachingApiService extends cds.ApplicationService {
117
129
  await super.init()
118
130
  }
119
131
 
120
-
132
+ /**
133
+ * Ensure cache entries exist in the current tenant's DB.
134
+ * Called lazily in MTX mode on first dashboard access.
135
+ */
136
+ async _ensureCacheEntries() {
137
+ const tenant = cds.context?.tenant ?? DEFAULT_TENANT;
138
+ if (!this._cacheEntriesInitialized) this._cacheEntriesInitialized = new Set();
139
+ if (this._cacheEntriesInitialized.has(tenant) || !isPluginModelAvailable()) return;
121
140
 
141
+ try {
142
+ const { Caches } = cds.entities('plugin.cds_caching');
143
+ const requires = cds.env.requires || {};
144
+
145
+ for (const [name, config] of Object.entries(requires)) {
146
+ if (config.impl === 'cds-caching') {
147
+ const existing = await SELECT.one.from(Caches).where({ name });
148
+ if (!existing) {
149
+ await INSERT.into(Caches).entries({
150
+ name,
151
+ config: JSON.stringify({
152
+ impl: config.impl,
153
+ store: config.store || 'memory',
154
+ namespace: config.namespace || name
155
+ })
156
+ });
157
+ this.log.info(`Created cache entry for: ${name} (lazy init)`);
158
+ }
159
+ }
160
+ }
161
+
162
+ this._cacheEntriesInitialized.add(tenant);
163
+ } catch (error) {
164
+ this.log.warn('Failed to lazily initialize cache entries:', error);
165
+ }
166
+ }
122
167
  }
123
168
 
124
- module.exports = CachingApiService
169
+ module.exports = CachingApiService