cds-caching 1.3.0 → 1.3.2

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 (35) hide show
  1. package/README.md +16 -0
  2. package/app/dashboard/controller/App-dbg.controller.js +14 -14
  3. package/app/dashboard/controller/App-dbg.controller.js.map +1 -1
  4. package/app/dashboard/controller/App.controller.js +1 -1
  5. package/app/dashboard/controller/App.controller.js.map +1 -1
  6. package/app/dashboard/controller/App.controller.ts +14 -14
  7. package/app/dashboard/controller/BaseController-dbg.js +7 -0
  8. package/app/dashboard/controller/BaseController-dbg.js.map +1 -1
  9. package/app/dashboard/controller/BaseController.js +1 -1
  10. package/app/dashboard/controller/BaseController.js.map +1 -1
  11. package/app/dashboard/controller/BaseController.ts +10 -0
  12. package/app/dashboard/controller/Cache-dbg.controller.js +34 -25
  13. package/app/dashboard/controller/Cache-dbg.controller.js.map +1 -1
  14. package/app/dashboard/controller/Cache.controller.js +1 -1
  15. package/app/dashboard/controller/Cache.controller.js.map +1 -1
  16. package/app/dashboard/controller/Cache.controller.ts +36 -25
  17. package/app/dashboard/controller/SingleMetric-dbg.controller.js +56 -50
  18. package/app/dashboard/controller/SingleMetric-dbg.controller.js.map +1 -1
  19. package/app/dashboard/controller/SingleMetric.controller.js +1 -1
  20. package/app/dashboard/controller/SingleMetric.controller.js.map +1 -1
  21. package/app/dashboard/controller/SingleMetric.controller.ts +29 -22
  22. package/app/dashboard/i18n/i18n.properties +235 -0
  23. package/app/dashboard/i18n/i18n_de.properties +271 -3
  24. package/app/dashboard/i18n/i18n_en.properties +295 -2
  25. package/app/dashboard/resources/sap-ui-custom.js +11 -11
  26. package/app/dashboard/resources/sap-ui-custom.js.map +1 -1
  27. package/app/dashboard/view/Cache.view.xml +149 -149
  28. package/app/dashboard/view/Main.view.xml +7 -7
  29. package/app/dashboard/view/SingleMetric.view.xml +30 -30
  30. package/cds-plugin.js +31 -8
  31. package/lib/CachingService.js +11 -10
  32. package/lib/operations/BasicOperations.js +7 -4
  33. package/lib/support/KeyManager.js +40 -2
  34. package/lib/util.js +28 -2
  35. package/package.json +1 -1
@@ -13,30 +13,30 @@
13
13
  formatter: 'cds/plugin/caching/dashboard/model/formatter'
14
14
  }">
15
15
 
16
- <f:DynamicPage title="cds-caching – Dashboard" headerExpanded="true" headerPinned="true">
16
+ <f:DynamicPage title="{i18n>homePageTitle}" headerExpanded="true" headerPinned="true">
17
17
  <f:title>
18
18
  <f:DynamicPageTitle>
19
19
  <f:heading>
20
- <Title text="cds-caching – Dashboard" />
20
+ <Title text="{i18n>homePageTitle}" />
21
21
  </f:heading>
22
22
  <f:actions>
23
- <Button type="Transparent" icon="sap-icon://refresh" text="Refresh" press="onRefresh" />
23
+ <Button type="Transparent" icon="sap-icon://refresh" text="{i18n>refreshButton}" press="onRefresh" />
24
24
  </f:actions>
25
25
  </f:DynamicPageTitle>
26
26
  </f:title>
27
27
 
28
28
  <f:content>
29
29
 
30
- <Table id="cachesList" headerText="Caching Services" mode="SingleSelectMaster" items="{path: '/Caches'}" itemPress="onCacheSelect" noDataText="No caches available">
30
+ <Table id="cachesList" headerText="{i18n>cachingServicesHeader}" mode="SingleSelectMaster" items="{path: '/Caches'}" itemPress="onCacheSelect" noDataText="{i18n>noCachesAvailable}">
31
31
  <columns>
32
32
  <Column>
33
- <Text text="Name" />
33
+ <Text text="{i18n>colName}" />
34
34
  </Column>
35
35
  <Column>
36
- <Text text="Statistic Tracking Enabled" />
36
+ <Text text="{i18n>colStatisticTrackingEnabled}" />
37
37
  </Column>
38
38
  <Column>
39
- <Text text="Key Tracking Enabled" />
39
+ <Text text="{i18n>colKeyTrackingEnabled}" />
40
40
  </Column>
41
41
  </columns>
42
42
  <items>
@@ -14,19 +14,19 @@
14
14
  formatter: 'cds/plugin/caching/dashboard/model/formatter'
15
15
  }">
16
16
 
17
- <f:DynamicPage title="Metric Details" showFooter="false">
17
+ <f:DynamicPage title="{i18n>metricDetailsPageTitle}" showFooter="false">
18
18
  <f:title>
19
19
  <f:DynamicPageTitle>
20
20
  <f:heading>
21
- <Title text="Metric: {ui>/metricId}" />
21
+ <Title text="{= ${i18n>metricPagePrefix} + ' ' + ${ui>/metricId} }" />
22
22
  </f:heading>
23
23
  <f:actions>
24
- <Button icon="sap-icon://refresh" text="Refresh" type="Transparent" press="onRefresh" />
24
+ <Button icon="sap-icon://refresh" text="{i18n>refreshButton}" type="Transparent" press="onRefresh" />
25
25
  </f:actions>
26
26
  <f:navigationActions>
27
- <OverflowToolbarButton type="Transparent" icon="sap-icon://full-screen" press="handleFullScreen" tooltip="Enter Full Screen Mode" visible="{= ${app>/layout} !== 'EndColumnFullScreen' }"/>
28
- <OverflowToolbarButton type="Transparent" icon="sap-icon://exit-full-screen" press="handleExitFullScreen" tooltip="Exit Full Screen Mode" visible="{= ${app>/layout} === 'EndColumnFullScreen' }"/>
29
- <OverflowToolbarButton type="Transparent" icon="sap-icon://decline" press="handleClose" tooltip="Close end column"/>
27
+ <OverflowToolbarButton type="Transparent" icon="sap-icon://full-screen" press="handleFullScreen" tooltip="{i18n>ttFullScreenEnter}" visible="{= ${app>/layout} !== 'EndColumnFullScreen' }"/>
28
+ <OverflowToolbarButton type="Transparent" icon="sap-icon://exit-full-screen" press="handleExitFullScreen" tooltip="{i18n>ttFullScreenExit}" visible="{= ${app>/layout} === 'EndColumnFullScreen' }"/>
29
+ <OverflowToolbarButton type="Transparent" icon="sap-icon://decline" press="handleClose" tooltip="{i18n>ttCloseEndColumn}"/>
30
30
  </f:navigationActions>
31
31
  </f:DynamicPageTitle>
32
32
  </f:title>
@@ -43,10 +43,10 @@
43
43
  <cssgrid:ResponsiveColumnItemLayoutData columns="24"/>
44
44
  </layoutData>
45
45
 
46
- <Title text="Read-Through Performance" titleStyle="H5" class="sapUiMediumMarginTop sapUiTinyMarginBottom" />
46
+ <Title text="{i18n>sectionReadThroughPerformance}" titleStyle="H5" class="sapUiMediumMarginTop sapUiTinyMarginBottom" />
47
47
 
48
48
  <f:GridContainer id="metricCacheThroughGrid" columnsChange=".onColumnsChange">
49
- <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Hit Ratio" subheader="Cache Hit Rate">
49
+ <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="{i18n>hitRatio}" subheader="{i18n>tileSubCacheHitRate}">
50
50
  <TileContent unit="%">
51
51
  <NumericContent valueColor="{formatter: 'formatter.formatHitRatioWithColor', path: 'ui>/hitRatio'}" value="{ path: 'ui>/hitRatio', type: 'sap.ui.model.type.Float', formatOptions: {style: 'short', decimals: 2}}" />
52
52
  </TileContent>
@@ -55,7 +55,7 @@
55
55
  </layoutData>
56
56
  </GenericTile>
57
57
 
58
- <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Cache Efficiency" subheader="Performance Factor">
58
+ <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="{i18n>headerCacheEfficiency}" subheader="{i18n>tileSubPerformanceFactor}">
59
59
  <TileContent unit="x">
60
60
  <NumericContent valueColor="{formatter: 'formatter.formatCacheEfficiencyWithColor', path: 'ui>/cacheEfficiency'}" value="{path: 'ui>/cacheEfficiency', type: 'sap.ui.model.type.Float', formatOptions: {style: 'short', decimals: 2}}" />
61
61
  </TileContent>
@@ -64,7 +64,7 @@
64
64
  </layoutData>
65
65
  </GenericTile>
66
66
 
67
- <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Throughput" subheader="Requests per Second">
67
+ <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="{i18n>throughput}" subheader="{i18n>tileSubReqPerSec}">
68
68
  <TileContent unit="req/s">
69
69
  <NumericContent value="{ path: 'ui>/throughput', type: 'sap.ui.model.type.Float', formatOptions: {style: 'short', decimals: 2}}" withMargin="false" />
70
70
  </TileContent>
@@ -73,7 +73,7 @@
73
73
  </layoutData>
74
74
  </GenericTile>
75
75
 
76
- <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Error Rate" subheader="Cache Errors">
76
+ <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="{i18n>headerErrorRate}" subheader="{i18n>tileSubCacheErrors}">
77
77
  <TileContent unit="%">
78
78
  <NumericContent valueColor="{formatter: 'formatter.formatErrorRateWithColor', path: 'ui>/errorRate'}" value="{ path: 'ui>/errorRate', formatter: 'formatter.formatPercentageValue' }" />
79
79
  </TileContent>
@@ -82,7 +82,7 @@
82
82
  </layoutData>
83
83
  </GenericTile>
84
84
 
85
- <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Total Requests" subheader="Cache Operations">
85
+ <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="{i18n>totalRequests}" subheader="{i18n>tileSubCacheOperations}">
86
86
  <TileContent unit="req">
87
87
  <NumericContent value="{= ${ui>/hits} + ${ui>/misses}}" withMargin="false" />
88
88
  </TileContent>
@@ -91,7 +91,7 @@
91
91
  </layoutData>
92
92
  </GenericTile>
93
93
 
94
- <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Cache Hits" subheader="Successful Retrievals">
94
+ <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="{i18n>headerCacheHits}" subheader="{i18n>tileSubSuccessfulRetrievals}">
95
95
  <TileContent unit="hits">
96
96
  <NumericContent value="{ui>/hits}" withMargin="false" />
97
97
  </TileContent>
@@ -100,7 +100,7 @@
100
100
  </layoutData>
101
101
  </GenericTile>
102
102
 
103
- <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Cache Misses" subheader="Failed Retrievals">
103
+ <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="{i18n>headerCacheMisses}" subheader="{i18n>tileSubFailedRetrievals}">
104
104
  <TileContent unit="misses">
105
105
  <NumericContent value="{ui>/misses}" withMargin="false" />
106
106
  </TileContent>
@@ -110,7 +110,7 @@
110
110
  </GenericTile>
111
111
 
112
112
 
113
- <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Cache Errors" subheader="Error Count">
113
+ <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="{i18n>headerCacheErrors}" subheader="{i18n>tileSubErrorCount}">
114
114
  <TileContent unit="errors">
115
115
  <NumericContent value="{ui>/errors}" withMargin="false" />
116
116
  </TileContent>
@@ -120,7 +120,7 @@
120
120
  </GenericTile>
121
121
 
122
122
 
123
- <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Hit Latency" subheader="Cache Hit Response">
123
+ <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="{i18n>headerHitLatency}" subheader="{i18n>tileSubCacheHitResponse}">
124
124
  <TileContent unit="ms">
125
125
  <NumericContent valueColor="{formatter: 'formatter.formatLatencyWithColor', path: 'ui>/avgHitLatency'}" value="{ path: 'ui>/avgHitLatency', type: 'sap.ui.model.type.Float', formatOptions: {style: 'short', decimals: 2}}" />
126
126
  </TileContent>
@@ -129,7 +129,7 @@
129
129
  </layoutData>
130
130
  </GenericTile>
131
131
 
132
- <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Miss Latency" subheader="Cache Miss Response">
132
+ <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="{i18n>headerMissLatency}" subheader="{i18n>tileSubCacheMissResponse}">
133
133
  <TileContent unit="ms">
134
134
  <NumericContent valueColor="{formatter: 'formatter.formatLatencyWithColor', path: 'ui>/avgMissLatency'}" value="{ path: 'ui>/avgMissLatency', type: 'sap.ui.model.type.Float', formatOptions: {style: 'short', decimals: 2}}" />
135
135
  </TileContent>
@@ -141,7 +141,7 @@
141
141
  <!-- Read-Through Distribution Chart -->
142
142
  <f:Card class="sapUiTinyMargin">
143
143
  <f:header>
144
- <card:Header title="Read-Through Operations" subtitle="Hit/Miss/Error Distribution" />
144
+ <card:Header title="{i18n>cardReadThroughOps}" subtitle="{i18n>cardReadThroughOpsSub}" />
145
145
  </f:header>
146
146
  <f:content>
147
147
  <l:VerticalLayout width="100%">
@@ -175,7 +175,7 @@
175
175
  <!-- Latency Chart -->
176
176
  <f:Card class="sapUiTinyMargin">
177
177
  <f:header>
178
- <card:Header title="Latency Comparison" subtitle="Read-Through Latency Metrics" />
178
+ <card:Header title="{i18n>cardLatencyCompare}" subtitle="{i18n>cardLatencyCompareSub}" />
179
179
  </f:header>
180
180
  <f:content>
181
181
  <l:VerticalLayout width="100%">
@@ -214,10 +214,10 @@
214
214
  <cssgrid:ResponsiveColumnItemLayoutData columns="24"/>
215
215
  </layoutData>
216
216
 
217
- <Title text="Native Function Performance" titleStyle="H5" class="sapUiMediumMarginTop sapUiTinyMarginBottom" />
217
+ <Title text="{i18n>sectionNativeFunctionPerformance}" titleStyle="H5" class="sapUiMediumMarginTop sapUiTinyMarginBottom" />
218
218
 
219
219
  <f:GridContainer id="metricNativeGrid" columnsChange=".onColumnsChange">
220
- <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Native Throughput" subheader="Operations per Second">
220
+ <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="{i18n>headerNativeThroughput}" subheader="{i18n>tileSubNativeOpsPerSec}">
221
221
  <TileContent unit="ops/s">
222
222
  <NumericContent value="{ path: 'ui>/nativeThroughput', formatter: 'formatter.formatNumberValue' }" withMargin="false" />
223
223
  </TileContent>
@@ -226,7 +226,7 @@
226
226
  </layoutData>
227
227
  </GenericTile>
228
228
 
229
- <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Native Error Rate" subheader="Native Function Errors">
229
+ <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="{i18n>headerNativeErrorRate}" subheader="{i18n>tileSubCacheErrors}">
230
230
  <TileContent unit="%">
231
231
  <NumericContent valueColor="{formatter: 'formatter.formatErrorRateWithColor', path: 'ui>/nativeErrorRate'}" value="{ path: 'ui>/nativeErrorRate', formatter: 'formatter.formatPercentageValue' }" />
232
232
  </TileContent>
@@ -235,7 +235,7 @@
235
235
  </layoutData>
236
236
  </GenericTile>
237
237
 
238
- <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Native Sets" subheader="Write Operations">
238
+ <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="{i18n>headerNativeSets}" subheader="{i18n>tileSubWriteOps}">
239
239
  <TileContent unit="sets">
240
240
  <NumericContent value="{ui>/nativeSets}" withMargin="false" />
241
241
  </TileContent>
@@ -244,7 +244,7 @@
244
244
  </layoutData>
245
245
  </GenericTile>
246
246
 
247
- <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Native Gets" subheader="Read Operations">
247
+ <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="{i18n>headerNativeGets}" subheader="{i18n>tileSubReadOps}">
248
248
  <TileContent unit="gets">
249
249
  <NumericContent value="{ui>/nativeGets}" withMargin="false" />
250
250
  </TileContent>
@@ -253,7 +253,7 @@
253
253
  </layoutData>
254
254
  </GenericTile>
255
255
 
256
- <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Native Deletes" subheader="Delete Operations">
256
+ <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="{i18n>headerNativeDeletes}" subheader="{i18n>tileSubDeleteOps}">
257
257
  <TileContent unit="deletes">
258
258
  <NumericContent value="{ui>/nativeDeletes}" withMargin="false" />
259
259
  </TileContent>
@@ -262,7 +262,7 @@
262
262
  </layoutData>
263
263
  </GenericTile>
264
264
 
265
- <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Native Clears" subheader="Clear Operations">
265
+ <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="{i18n>headerNativeClears}" subheader="{i18n>tileSubClearOps}">
266
266
  <TileContent unit="clears">
267
267
  <NumericContent value="{ui>/nativeClears}" withMargin="false" />
268
268
  </TileContent>
@@ -271,7 +271,7 @@
271
271
  </layoutData>
272
272
  </GenericTile>
273
273
 
274
- <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Delete By Tags" subheader="Tag-based Deletes">
274
+ <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="{i18n>headerDeleteByTags}" subheader="{i18n>tileSubTagDeletes}">
275
275
  <TileContent unit="deletes">
276
276
  <NumericContent value="{ui>/nativeDeleteByTags}" withMargin="false" />
277
277
  </TileContent>
@@ -280,7 +280,7 @@
280
280
  </layoutData>
281
281
  </GenericTile>
282
282
 
283
- <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Native Errors" subheader="Error Count">
283
+ <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="{i18n>headerNativeErrors}" subheader="{i18n>tileSubErrorCount}">
284
284
  <TileContent unit="errors">
285
285
  <NumericContent value="{ui>/nativeErrors}" withMargin="false" />
286
286
  </TileContent>
@@ -289,7 +289,7 @@
289
289
  </layoutData>
290
290
  </GenericTile>
291
291
 
292
- <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Total Native Ops" subheader="All Operations">
292
+ <GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="{i18n>headerTotalNativeOps}" subheader="{i18n>tileSubAllOperations}">
293
293
  <TileContent unit="ops">
294
294
  <NumericContent value="{ui>/totalNativeOperations}" withMargin="false" />
295
295
  </TileContent>
@@ -300,7 +300,7 @@
300
300
  <!-- Native Function Distribution Chart -->
301
301
  <f:Card class="sapUiTinyMargin">
302
302
  <f:header>
303
- <card:Header title="Native Function Operations" subtitle="Hit/Miss/Error Distribution" />
303
+ <card:Header title="{i18n>cardNativeFunctionOps}" subtitle="{i18n>cardReadThroughOpsSub}" />
304
304
  </f:header>
305
305
  <f:content>
306
306
  <l:VerticalLayout width="100%">
package/cds-plugin.js CHANGED
@@ -14,8 +14,6 @@ if (cachingConfigs.some(c => c.statistics)) {
14
14
  cds.env.roots.push(path.join(__dirname, 'db', 'statistics'));
15
15
  }
16
16
 
17
- cds.add?.register?.('caching-dashboard', require('./lib/add'))
18
-
19
17
  cds.on('served', scanCachingAnnotations)
20
18
  const LOG = cds.log("cds-caching");
21
19
 
@@ -27,14 +25,20 @@ cds.build?.register?.('cds-caching', class CachingBuildPlugin extends cds.build.
27
25
  clean() { }
28
26
 
29
27
  static hasTask() {
28
+ cds.log('cds-caching').info('hasTask', cds.env.requires);
30
29
  const requires = cds.env.requires || {};
30
+ const dbKind = requires.db?.kind || '';
31
+ const isHanaDB = dbKind === 'hana' || dbKind === 'sql';
31
32
  return Object.values(requires).some(
32
- r => r.impl === 'cds-caching' && r.store === 'hana'
33
+ r => r.impl === 'cds-caching' && (r.store === 'hana' || (r.store === 'cds' && isHanaDB))
33
34
  );
34
35
  }
35
36
 
36
37
  async build() {
37
38
  const requires = cds.env.requires || {};
39
+ const compileOpts = { ...this.options(), sql_mapping: cds.env.sql.names };
40
+ let wroteCacheStore = false;
41
+
38
42
  for (const [, config] of Object.entries(requires)) {
39
43
  if (config.impl === 'cds-caching' && config.store === 'hana') {
40
44
  const table = config.credentials?.table || 'KEYV';
@@ -48,12 +52,31 @@ cds.build?.register?.('cds-caching', class CachingBuildPlugin extends cds.build.
48
52
  await this.write(content).to(`src/gen/${table}.hdbtable`);
49
53
  LOG.info('Building cds-caching hana table', { table, keySize });
50
54
  }
51
- // For store: 'cds' with HANA, the CacheStore entity from db/cache-store.cds
52
- // is loaded via cds.env.roots and deployed automatically by the HDI deployer —
53
- // no manual .hdbtable needed. For SQLite/Postgres, CAP's cds deploy handles
54
- // table creation from the CDS model.
55
+
56
+ if (config.store === 'cds' && !wroteCacheStore) {
57
+ await this._buildCacheStoreHdbtables(compileOpts);
58
+ wroteCacheStore = true;
59
+ }
60
+ }
61
+ }
62
+
63
+ /**
64
+ * The HANA build task compiles only the app `db/` folder, so `plugin.cds_caching.CacheStore`
65
+ * from env.roots is not part of that CSN. Emit matching .hdbtable files here.
66
+ * Non-HANA DBs still get the table via normal CDS deploy / DDL.
67
+ */
68
+ async _buildCacheStoreHdbtables(compileOpts) {
69
+ const modelPath = path.join(__dirname, 'db', 'cache-store');
70
+ const model = await cds.load(modelPath, { ...compileOpts, cwd: cds.root });
71
+ const artifacts = cds.compile.to.hdbtable(model, compileOpts);
72
+ for (const [content, key] of artifacts) {
73
+ const file = key.file || `${key.name}${key.suffix || ''}`;
74
+ await this.write(content).to(path.join('src/gen', file));
55
75
  }
76
+ LOG.info('Built cds-caching CacheStore HANA artifacts from cache-store model');
56
77
  }
57
78
  });
58
79
 
59
- module.exports = cds.service.impl(CachingService)
80
+ // CAP resolves `impl: 'cds-caching'` to this file; the main export must be the service class
81
+ // so `cds.connect.to('<cache>')` returns CachingService (e.g. addCachableFunction).
82
+ module.exports = CachingService
@@ -87,15 +87,16 @@ class CachingService extends cds.Service {
87
87
  this.log.debug(`CLEAR`);
88
88
  await this.cache.clear();
89
89
 
90
- // Also clear statistics
91
90
  if (this.statistics) {
92
91
  await this.statistics.resetCurrentStats();
93
92
 
94
- try {
95
- await this.statistics.deleteMetrics();
96
- await this.statistics.deleteKeyMetrics();
97
- } catch (error) {
98
- this.log.warn(`Failed to clear statistics for cache ${this.name}:`, error);
93
+ if (event.data.clearStatistics) {
94
+ try {
95
+ await this.statistics.deleteMetrics();
96
+ await this.statistics.deleteKeyMetrics();
97
+ } catch (error) {
98
+ this.log.warn(`Failed to clear statistics for cache ${this.name}:`, error);
99
+ }
99
100
  }
100
101
  }
101
102
  }
@@ -173,10 +174,10 @@ class CachingService extends cds.Service {
173
174
  return this.basicOperations.delete(key);
174
175
  }
175
176
 
176
- async clear(tx = null) {
177
- if (tx) return this.basicOperations.clear(tx);
178
- if (this.options.transactionalOperations) return this.basicOperations.clearInTx();
179
- return this.basicOperations.clear();
177
+ async clear(options = {}, tx = null) {
178
+ if (tx) return this.basicOperations.clear(options, tx);
179
+ if (this.options.transactionalOperations) return this.basicOperations.clearInTx(options);
180
+ return this.basicOperations.clear(options);
180
181
  }
181
182
 
182
183
  async deleteByTag(tag, tx = null) {
@@ -156,12 +156,15 @@ class BasicOperations {
156
156
 
157
157
  /**
158
158
  * Clear all cache entries
159
+ * @param {object} options - clear options
160
+ * @param {boolean} [options.clearStatistics=false] - also delete persisted metrics and key metrics
159
161
  * @returns {Promise<void>}
160
162
  */
161
- async clear(tx = null) {
163
+ async clear(options = {}, tx = null) {
162
164
  const srv = tx || this.cache;
163
165
  await srv.send('CLEAR', {
164
- deleteAll: true
166
+ deleteAll: true,
167
+ clearStatistics: options.clearStatistics || false
165
168
  });
166
169
 
167
170
  // Record the native clear operation
@@ -318,10 +321,10 @@ class BasicOperations {
318
321
  }
319
322
  }
320
323
 
321
- async clearInTx() {
324
+ async clearInTx(options = {}) {
322
325
  const tx = await this.cache.tx();
323
326
  try {
324
- await this.clear(tx);
327
+ await this.clear(options, tx);
325
328
  await tx.commit();
326
329
  return;
327
330
  } catch (error) {
@@ -78,13 +78,34 @@ class KeyManager {
78
78
  case "Request":
79
79
  case "NoaRequest":
80
80
  case "ODataRequest":
81
- // Only hash the request information, not user/tenant/locale
81
+ // Hash request information including the HTTP URL which
82
+ // contains $filter/$select/$orderby query parameters.
83
+ //
84
+ // Why req.query (CQN) is unreliable for hashing:
85
+ // NoaRequest ("New OData Adapter Request", defined in
86
+ // @sap/cds/libx/odata/ODataAdapter.js) carries cds.ql
87
+ // query objects where SELECT.where lives on the prototype
88
+ // chain, not as an own property. JSON.stringify only
89
+ // serializes own enumerable properties, so the where clause
90
+ // from $filter is silently dropped — two requests that
91
+ // differ only by $filter produce the same hash.
92
+ // This does NOT affect programmatic CQL (e.g.
93
+ // SELECT.from(Entity).where(...)) where `where` is an own
94
+ // property. It only affects CQN built from OData URL
95
+ // parsing in the adapter pipeline.
96
+ //
97
+ // The HTTP URL (req._.req.url / req.http.req.url) always
98
+ // contains the full query string as a plain string and is
99
+ // the reliable source for all OData query options. When a
100
+ // URL is available it supersedes req.query.
101
+ const url = this.normalizeUrl(keyOrObject._?.req?.url || keyOrObject.http?.req?.url);
82
102
  return this.createHash({
83
103
  method: keyOrObject.method,
84
104
  path: keyOrObject.path || keyOrObject.http?.req?.path,
105
+ url,
85
106
  data: keyOrObject.data,
86
107
  params: keyOrObject.params,
87
- query: keyOrObject.query,
108
+ query: url ? undefined : keyOrObject.query,
88
109
  event: keyOrObject.event,
89
110
  target: keyOrObject.target?.name
90
111
  });
@@ -105,6 +126,23 @@ class KeyManager {
105
126
  return this.createHash({ content: keyOrObject });
106
127
  }
107
128
 
129
+ /**
130
+ * Normalize a URL by sorting its query parameters alphabetically by key.
131
+ * Ensures the same parameters in different order produce the same cache key.
132
+ * @param {string} url - URL string to normalize
133
+ * @returns {string|undefined} - Normalized URL or undefined if input is falsy
134
+ */
135
+ normalizeUrl(url) {
136
+ if (!url) return undefined;
137
+ try {
138
+ const urlObj = new URL(url, 'http://localhost');
139
+ urlObj.searchParams.sort();
140
+ return urlObj.pathname + (urlObj.searchParams.size ? '?' + urlObj.searchParams.toString() : '');
141
+ } catch {
142
+ return url;
143
+ }
144
+ }
145
+
108
146
  /**
109
147
  * Serialize function arguments for cache key generation
110
148
  * @param {any} arg - argument to serialize
package/lib/util.js CHANGED
@@ -85,17 +85,42 @@ const bindFunction = async (service, action, isBound = false) => {
85
85
  })
86
86
  }
87
87
 
88
+ const ENTITY_TAG_PREFIX = 'entity:';
89
+
90
+ const getEntityTag = (entity) => `${ENTITY_TAG_PREFIX}${entity.name}`;
91
+
88
92
  const bindEntity = async (service, entity) => {
93
+ const invalidateOnWrite = entity['@cache.invalidateOnWrite'];
94
+ const entityTag = invalidateOnWrite ? getEntityTag(entity) : null;
95
+
89
96
  service.prepend(function () {
90
97
  service.on('READ', entity.name, async (req, next) => {
91
98
  const cache = await cds.connect.to(entity['@cache.service'] || "caching");
99
+
100
+ const tags = [...(entity['@cache.tags'] || [])];
101
+ if (entityTag) tags.push({ value: entityTag });
102
+
92
103
  const { result, cacheKey } = await cache.rt.run(req, next, {
93
104
  ttl: entity['@cache.ttl'],
94
- tags: entity['@cache.tags'],
105
+ tags,
95
106
  key: extractCacheProperties(entity, 'key')
96
107
  });
97
108
  return result;
98
109
  })
110
+
111
+ if (invalidateOnWrite) {
112
+ const cudHandler = async (data, req) => {
113
+ try {
114
+ const cache = await cds.connect.to(entity['@cache.service'] || "caching");
115
+ await cache.deleteByTag(entityTag);
116
+ } catch (error) {
117
+ cds.log('cds-caching').warn(`Cache invalidation failed for ${entity.name}:`, error);
118
+ }
119
+ };
120
+ service.after('CREATE', entity.name, cudHandler);
121
+ service.after('UPDATE', entity.name, cudHandler);
122
+ service.after('DELETE', entity.name, cudHandler);
123
+ }
99
124
  })
100
125
  }
101
126
 
@@ -183,7 +208,8 @@ const scanCachingAnnotations = async (srvs) => {
183
208
  cacheConfig.entities[entity.name] = {
184
209
  ttl: entity['@cache.ttl'],
185
210
  tags: entity['@cache.tags'],
186
- key: extractCacheProperties(entity, 'key')
211
+ key: extractCacheProperties(entity, 'key'),
212
+ invalidateOnWrite: entity['@cache.invalidateOnWrite'] || false
187
213
  };
188
214
  }
189
215
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cds-caching",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "A caching plugin for SAP CAP applications",
5
5
  "repository": {
6
6
  "type": "git",