cds-caching 0.1.0 → 0.2.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.
- package/README.md +592 -158
- package/package.json +5 -3
- package/srv/CachingService.js +19 -1
package/README.md
CHANGED
|
@@ -2,33 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
This plugin for the [SAP Cloud Application Programming Model (CAP)](https://cap.cloud.sap/docs/) provides a
|
|
5
|
+
This plugin for the [SAP Cloud Application Programming Model (CAP)](https://cap.cloud.sap/docs/) provides a caching service to improve performance in CAP applications.
|
|
6
|
+
While CAP in general performs well for most use cases, caching can help with:
|
|
7
|
+
- Slow remote service calls
|
|
8
|
+
- Complex calculations
|
|
9
|
+
- Heavy queries
|
|
10
|
+
- External API integration
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
While caching can help with these, it also adds complexity and should be used judiciously.
|
|
13
|
+
|
|
14
|
+
Please also read the introduction blog post in the SAP Community: [Boosting performance in SAP Cloud Application Programming Model (CAP) applications with cds-caching](https://community.sap.com/t5/technology-blogs-by-members/boosting-performance-in-sap-cloud-application-programming-model-cap/ba-p/14002015).
|
|
15
|
+
|
|
16
|
+
### Caching vs. Replication
|
|
17
|
+
|
|
18
|
+
It's important to understand the difference between **caching** and **data replication**:
|
|
19
|
+
|
|
20
|
+
* **Caching** temporarily stores data to reduce latency and improve response times. It's ideal for read-heavy workloads but does not maintain data integrity or understand data semantics.
|
|
21
|
+
|
|
22
|
+
* **Replication** creates full, persistent copies of remote data within your application to ensure availability and enable seamless data sharing across systems. It focuses on resilience rather than performance optimization.
|
|
23
|
+
|
|
24
|
+
cds-caching is specifically designed for efficient caching, not data replication.
|
|
8
25
|
|
|
9
26
|
### Key Features
|
|
10
27
|
|
|
11
28
|
* **Flexible Key-Value Store** – Store and retrieve data using simple key-based access.
|
|
12
|
-
* **CachingService** – A cds.Service
|
|
29
|
+
* **CachingService** – A CALESI-compliant cds.Service implementation with an intuitive API for seamless integration.
|
|
13
30
|
* **Event Handling** – Monitor and react to cache events, such as before/after storage and retrieval.
|
|
14
31
|
* **CAP-specific Caching** – Effortlessly cache CQN queries or CAP cds.Requests using code or the @cache annotation.
|
|
15
32
|
* **TTL Support** – Automatically manage data expiration with configurable time-to-live (TTL) settings.
|
|
16
33
|
* **Tag Support** – Use dynamic tags for flexible cache invalidation options.
|
|
17
34
|
* **Pluggable Storage Options** – Choose between in-memory caching or Redis.
|
|
18
|
-
* **Compression** – Compress cached data to save memory.
|
|
19
|
-
* **Integrated Statistics
|
|
35
|
+
* **Compression** – Compress cached data to save memory using LZ4 or GZIP.
|
|
36
|
+
* **Integrated Statistics** – Monitor cache performance with hit rates, latencies, and more.
|
|
37
|
+
|
|
20
38
|
### Installation
|
|
21
39
|
|
|
22
|
-
Installing and using cds-caching is straightforward since it
|
|
40
|
+
Installing and using cds-caching is straightforward since it's a CAP plugin. Simply run:
|
|
23
41
|
|
|
24
|
-
```
|
|
42
|
+
```bash
|
|
25
43
|
npm install cds-caching
|
|
26
44
|
```
|
|
27
45
|
|
|
28
46
|
Next, add a caching service configuration to your package.json. You can even define **multiple caching services**, which is recommended if you need to cache different types of data within your application.
|
|
29
47
|
|
|
30
|
-
```
|
|
31
|
-
|
|
48
|
+
```javascript
|
|
32
49
|
{
|
|
33
50
|
"cds": {
|
|
34
51
|
"requires": {
|
|
@@ -36,6 +53,7 @@ Next, add a caching service configuration to your package.json. You can even def
|
|
|
36
53
|
"impl": "cds-caching",
|
|
37
54
|
"namespace": "my::app::caching"
|
|
38
55
|
},
|
|
56
|
+
// Optional: Define a specific caching service for Business Partner API
|
|
39
57
|
"bp-caching": {
|
|
40
58
|
"impl": "cds-caching",
|
|
41
59
|
"namespace": "my::app::bp-caching"
|
|
@@ -43,14 +61,13 @@ Next, add a caching service configuration to your package.json. You can even def
|
|
|
43
61
|
}
|
|
44
62
|
}
|
|
45
63
|
}
|
|
46
|
-
|
|
47
64
|
```
|
|
65
|
+
|
|
48
66
|
### Advanced Configuration
|
|
49
67
|
|
|
50
|
-
For more control, you can specify additional options
|
|
68
|
+
For more control, you can specify additional options:
|
|
51
69
|
|
|
52
70
|
```javascript
|
|
53
|
-
|
|
54
71
|
{
|
|
55
72
|
"cds": {
|
|
56
73
|
"requires": {
|
|
@@ -63,23 +80,122 @@ For more control, you can specify additional options. Some of those will be expl
|
|
|
63
80
|
"host": "localhost",
|
|
64
81
|
"port": 6379,
|
|
65
82
|
"password": "optional",
|
|
83
|
+
"url": "redis://..." // Alternative: Redis connection URI
|
|
84
|
+
},
|
|
85
|
+
"statistics": {
|
|
86
|
+
"enabled": true,
|
|
87
|
+
"persistenceInterval": 60000, // Optional: Interval for statistics persistence
|
|
88
|
+
"maxLatencies": 1000 // Optional: Maximum number of latencies to track
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Real-World Usage and Deployment
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
#### Storage Options
|
|
100
|
+
|
|
101
|
+
cds-caching provides two storage options:
|
|
102
|
+
|
|
103
|
+
##### In-Memory Cache (for small-scale use)
|
|
104
|
+
- Simple and fast, but not persistent
|
|
105
|
+
- Not suitable for production since Node.js runtime memory is limited
|
|
106
|
+
- Data is lost when the application restarts
|
|
107
|
+
|
|
108
|
+
##### Redis Cache (recommended for production)
|
|
109
|
+
- Persistent and supports distributed caching
|
|
110
|
+
- Works across multiple app instances, making it ideal for scalable applications
|
|
111
|
+
- Available on SAP BTP via hyperscaler options (e.g., AWS, Azure, Google Cloud)
|
|
112
|
+
- Even trial accounts provide Redis access
|
|
113
|
+
|
|
114
|
+
#### Development Setup
|
|
115
|
+
|
|
116
|
+
##### Running Redis Locally via Docker
|
|
117
|
+
For local development, Redis can be quickly set up using Docker. A simple docker-compose configuration provides a lightweight caching environment:
|
|
118
|
+
|
|
119
|
+
1. Create a `docker-compose.yml` file:
|
|
120
|
+
```yaml
|
|
121
|
+
services:
|
|
122
|
+
redis:
|
|
123
|
+
image: redis:latest
|
|
124
|
+
container_name: local-redis
|
|
125
|
+
ports:
|
|
126
|
+
- "6379:6379"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
2. Run Redis with:
|
|
130
|
+
```bash
|
|
131
|
+
docker compose up -d
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
3. Modify the `package.json` configuration to connect to the local Redis instance:
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"cds": {
|
|
138
|
+
"requires": {
|
|
139
|
+
"caching": {
|
|
140
|
+
"impl": "cds-caching",
|
|
141
|
+
"namespace": "myCache",
|
|
142
|
+
"store": "redis",
|
|
143
|
+
"[development]": {
|
|
144
|
+
"credentials": {
|
|
145
|
+
"host": "localhost",
|
|
146
|
+
"port": 6379
|
|
147
|
+
}
|
|
66
148
|
}
|
|
67
149
|
}
|
|
68
150
|
}
|
|
69
151
|
}
|
|
70
152
|
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Now, caching will be handled by Redis instead of in-memory storage during development.
|
|
156
|
+
|
|
157
|
+
#### Production Deployment on SAP BTP
|
|
158
|
+
|
|
159
|
+

|
|
160
|
+
|
|
161
|
+
For production deployments on SAP BTP, Redis can be provisioned as a managed service through the Redis on SAP BTP hyperscaler option. An instance can be provisioned via trial or even as a Free Tier to explore the service. However, for production scenarios the size of the Redis instance should match your caching requirements.
|
|
162
|
+
|
|
163
|
+
To bind Redis to your CAP application on SAP BTP, add the following configuration in `mta.yaml`. This will automatically create the service instance and bind your application to it. Since the credentials will automatically be fetched by CAP, make sure to maintain the service-tags to match the kind property of your cds-caching service(s) in the package.json:
|
|
71
164
|
|
|
165
|
+
```yaml
|
|
166
|
+
modules:
|
|
167
|
+
- name: cap-app-srv
|
|
168
|
+
# ... other module configuration ...
|
|
169
|
+
requires:
|
|
170
|
+
- name: redis-cache
|
|
171
|
+
|
|
172
|
+
resources:
|
|
173
|
+
- name: redis-cache
|
|
174
|
+
type: org.cloudfoundry.managed-service
|
|
175
|
+
parameters:
|
|
176
|
+
service: redis-cache
|
|
177
|
+
service-plan: trial
|
|
178
|
+
service-tags:
|
|
179
|
+
# Must match the kind property in the package.json
|
|
180
|
+
- cds-caching
|
|
72
181
|
```
|
|
73
|
-
### Low level usage
|
|
74
182
|
|
|
75
|
-
|
|
76
|
-
|
|
183
|
+
> 👉 **Tip**: There is a detailed [blog series on Redis in SAP BTP](https://community.sap.com/t5/technology-blogs-by-sap/redis-on-sap-btp-understanding-service-entitlements-and-metrics/ba-p/13738371) explaining how to set up Redis and connect via SSH for local/hybrid testing, as this is by default not possible.
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
### Usage Patterns
|
|
188
|
+
|
|
189
|
+
The caching service provides a flexible API for caching data in CAP applications. Here are the key usage patterns:
|
|
190
|
+
#### 1. Low-Level Key-Value API
|
|
191
|
+
|
|
192
|
+
The most basic way to use cds-caching is through its key-value API:
|
|
77
193
|
|
|
78
194
|
```javascript
|
|
79
195
|
// Connect to the caching service
|
|
80
196
|
const cache = await cds.connect.to("caching")
|
|
81
197
|
|
|
82
|
-
// Store a value
|
|
198
|
+
// Store a value (can be any object)
|
|
83
199
|
await cache.set("key", "value")
|
|
84
200
|
|
|
85
201
|
// Retrieve the value
|
|
@@ -95,228 +211,546 @@ await cache.delete("key")
|
|
|
95
211
|
await cache.clear()
|
|
96
212
|
```
|
|
97
213
|
|
|
98
|
-
|
|
99
|
-
#### Cache Events
|
|
214
|
+
#### 2. CQN Query Caching
|
|
100
215
|
|
|
101
|
-
|
|
216
|
+
For more advanced CAP integration, cache CAP's CQN queries directly. By passing in the query, a dynamic key is generated based on the CQN structure of the query. Note, that passing in queries with dynamic parameters (e.g. `SELECT.from(Foo).where({id: 1})`) will result in a different key for each query execution.
|
|
102
217
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
218
|
+
```javascript
|
|
219
|
+
// Create and execute a CQN query
|
|
220
|
+
const query = SELECT.from(Foo)
|
|
221
|
+
const result = await db.run(query)
|
|
107
222
|
|
|
108
|
-
|
|
223
|
+
// Cache the result
|
|
224
|
+
await cache.set(query, result)
|
|
225
|
+
|
|
226
|
+
// Retrieve from cache using the same query
|
|
227
|
+
const cachedResult = await cache.get(query)
|
|
228
|
+
```
|
|
229
|
+
Handling the cache manually via read-aside pattern is possible, but the caching service provides a more convenient way to cache and retrieve CQN queries. By using the `run` method, the caching service will transparently cache the result of the query and return the cached result if available for all further requests.
|
|
109
230
|
|
|
110
231
|
```javascript
|
|
232
|
+
const query = SELECT.from(Foo)
|
|
111
233
|
|
|
112
|
-
//
|
|
113
|
-
cache.
|
|
114
|
-
|
|
115
|
-
})
|
|
234
|
+
// Runs the query internally and caches the result
|
|
235
|
+
const result = await cache.run(query, db)
|
|
236
|
+
```
|
|
116
237
|
|
|
117
|
-
|
|
118
|
-
cache.before("SET", (event) => {
|
|
119
|
-
console.log(`Storing key: ${event.data.key} with value: ${event.data.value}`)
|
|
120
|
-
})
|
|
238
|
+
This will transparently cache the result of the query and return the cached result if available for all further requests.
|
|
121
239
|
|
|
122
|
-
|
|
123
|
-
cache.after("GET", (event) => {
|
|
124
|
-
console.log(`Retrieved key: ${event.data.key} with value: ${event.data.value}`)
|
|
125
|
-
})
|
|
240
|
+
#### 3. Request-Level Caching
|
|
126
241
|
|
|
242
|
+
Cache entire CAP requests with context awareness (e.g. user, tenant, locale, etc.), which is useful for caching slow remote service calls. The caching service will automatically generate a key for the request based on the request object and the current user, tenant and locale.
|
|
127
243
|
|
|
244
|
+
```javascript
|
|
245
|
+
this.on('READ', BusinessPartners, async (req, next) => {
|
|
246
|
+
const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
|
|
247
|
+
let value = await cache.get(req)
|
|
248
|
+
if(!value) {
|
|
249
|
+
value = await bupa.run(req)
|
|
250
|
+
await cache.set(req, value, { ttl: 3600 })
|
|
251
|
+
}
|
|
252
|
+
return value
|
|
253
|
+
})
|
|
128
254
|
```
|
|
129
255
|
|
|
130
|
-
|
|
131
|
-
#### Invalidation via Time-To-Live (TTL)
|
|
132
|
-
|
|
133
|
-
**cds-caching** supports automatic cache invalidation via **TTL (Time-To-Live)**. Cached values will expire after the specified TTL, preventing stale data from lingering in memory.
|
|
256
|
+
Alternatively use read-through caching via the `run` method to let the caching service handle the caching transparently:
|
|
134
257
|
|
|
135
258
|
```javascript
|
|
136
|
-
|
|
137
|
-
await cache.
|
|
259
|
+
const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
|
|
260
|
+
const result = await cache.run(req, bupa)
|
|
261
|
+
```
|
|
138
262
|
|
|
139
|
-
|
|
140
|
-
await cache.get("key") // => value
|
|
263
|
+
This will transparently cache the result of the request and return the cached result if available for all further requests.
|
|
141
264
|
|
|
142
|
-
|
|
265
|
+
#### 4. Declarative Caching with Annotations
|
|
143
266
|
|
|
144
|
-
|
|
145
|
-
await cache.get("key") // => undefined
|
|
146
|
-
```
|
|
267
|
+
Use annotations to enable caching on service entities or OData functions. The caching service will automatically generate a key for the request based on the request object and the current user, tenant and locale.
|
|
147
268
|
|
|
148
|
-
|
|
149
|
-
#### Compression
|
|
269
|
+
**Caching an entire entity should be used with caution, as it will cache all permutations of requests ($filter, $expand, $orderby, etc.) on the entity, which will lead to a huge number of cache entries. Use this only for entities where you can guarantee a low number of different queries.**
|
|
150
270
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
271
|
+
```
|
|
272
|
+
service MyService {
|
|
273
|
+
@cache: {
|
|
274
|
+
ttl: 3600
|
|
275
|
+
}
|
|
276
|
+
entity BusinessPartners as projection on BusinessPartner {
|
|
277
|
+
// ... entity definition
|
|
278
|
+
}
|
|
156
279
|
|
|
157
|
-
|
|
158
|
-
{
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
"compression": "lz4"
|
|
164
|
-
}
|
|
165
|
-
}
|
|
280
|
+
|
|
281
|
+
@cache: {
|
|
282
|
+
ttl: 1800,
|
|
283
|
+
tags: [{
|
|
284
|
+
template: 'user-{user}'
|
|
285
|
+
}]
|
|
166
286
|
}
|
|
287
|
+
function getUserPreferences() returns array of Preferences;
|
|
167
288
|
}
|
|
168
289
|
```
|
|
169
290
|
|
|
170
|
-
|
|
291
|
+
#### 5. Function Caching
|
|
171
292
|
|
|
172
|
-
|
|
173
|
-
#### Caching CQN queries
|
|
293
|
+
While not directly related to CAP functionality, the caching service provides two methods for read-through caching of JavaScript functions:
|
|
174
294
|
|
|
175
|
-
|
|
295
|
+
```javascript
|
|
296
|
+
// Using wrap() to create a cached version of a function
|
|
297
|
+
const expensiveOperation = async (value) => {
|
|
298
|
+
// ... some expensive computation
|
|
299
|
+
return result
|
|
300
|
+
}
|
|
176
301
|
|
|
177
|
-
|
|
302
|
+
// Creates a cached version of the function
|
|
303
|
+
const cachedOperation = cache.wrap("key", expensiveOperation, {
|
|
304
|
+
ttl: 3600,
|
|
305
|
+
tags: ['computation']
|
|
306
|
+
})
|
|
178
307
|
|
|
179
|
-
|
|
308
|
+
// Each call checks cache first, only executes if cache miss
|
|
309
|
+
const result = await cachedOperation("input")
|
|
180
310
|
|
|
181
|
-
|
|
311
|
+
// Using exec() for immediate execution with caching
|
|
312
|
+
const result = await cache.exec("key", async () => {
|
|
313
|
+
// ... some expensive computation
|
|
314
|
+
return result
|
|
315
|
+
}, {
|
|
316
|
+
ttl: 3600,
|
|
317
|
+
tags: ['computation']
|
|
318
|
+
})
|
|
319
|
+
```
|
|
182
320
|
|
|
183
|
-
|
|
184
|
-
|
|
321
|
+
The key differences between `wrap()` and `exec()`:
|
|
322
|
+
- `wrap()` returns a new function that includes caching logic
|
|
323
|
+
- `exec()` immediately executes the function and caches the result
|
|
324
|
+
- Use `wrap()` when you need to reuse the cached function multiple times
|
|
325
|
+
- Use `exec()` for one-off executions with caching
|
|
185
326
|
|
|
186
|
-
|
|
187
|
-
const result = await cds.run(query) // => [{...}, {...}]
|
|
327
|
+
### Cache Invalidation Strategies
|
|
188
328
|
|
|
189
|
-
|
|
190
|
-
await cache.set(query, result)
|
|
329
|
+
The caching service provides different strategies to invalidate cached values.
|
|
191
330
|
|
|
192
|
-
|
|
193
|
-
const cachedResult = await cache.get(query) // => [{...}, {...}]
|
|
331
|
+
**IMPORTANT: You should not use cds-caching without a proper invalidation strategy.**
|
|
194
332
|
|
|
195
|
-
|
|
196
|
-
const key = cache.createKey(query)
|
|
333
|
+
#### 1. Time-Based (TTL)
|
|
197
334
|
|
|
198
|
-
|
|
199
|
-
|
|
335
|
+
The most basic strategy is to use a time-to-live (TTL) for the cache. The caching service will automatically delete the value from the cache after the specified TTL has expired.
|
|
336
|
+
The TTL can be specified for individually through all cache methods (e.g. `set`, `run`, `send`, `wrap`, `exec`).
|
|
200
337
|
|
|
201
|
-
```
|
|
338
|
+
```javascript
|
|
339
|
+
// Store with 60 seconds TTL
|
|
340
|
+
await cache.set("key", "value", { ttl: 60000 })
|
|
202
341
|
|
|
203
|
-
|
|
204
|
-
|
|
342
|
+
// Run with 30 seconds TTL
|
|
343
|
+
const result = await cache.run(query, db, { ttl: 30000 })
|
|
205
344
|
|
|
206
|
-
|
|
345
|
+
// Send with 10 seconds TTL
|
|
346
|
+
const result = await cache.send(request, service, { ttl: 10000 })
|
|
207
347
|
|
|
208
|
-
|
|
348
|
+
// Wrap with 10 seconds TTL
|
|
349
|
+
const cachedOperation = cache.wrap("key", expensiveOperation, { ttl: 10000 })
|
|
209
350
|
|
|
210
|
-
|
|
351
|
+
// Exec with 10 seconds TTL
|
|
352
|
+
const result = await cache.exec("key", async () => {
|
|
353
|
+
// ... some expensive computation
|
|
354
|
+
return result
|
|
355
|
+
}, {
|
|
356
|
+
ttl: 10000
|
|
357
|
+
})
|
|
358
|
+
```
|
|
211
359
|
|
|
212
|
-
|
|
213
|
-
* req.user – Allows user-specific caching when necessary.
|
|
214
|
-
* req.locale – Supports localized responses when caching multilingual content.
|
|
360
|
+
#### 2. Key-Based
|
|
215
361
|
|
|
216
|
-
|
|
362
|
+
Key-based invalidation is a way to invalidate cache entries based on a specific key.
|
|
217
363
|
|
|
218
364
|
```javascript
|
|
219
|
-
|
|
220
|
-
const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
|
|
221
|
-
let value = await cache.get(req)
|
|
222
|
-
if(!value) {
|
|
223
|
-
value = await bupa.run(req)
|
|
224
|
-
await cache.set(req, next, { ttl: 3600 })
|
|
225
|
-
}
|
|
226
|
-
return value;
|
|
227
|
-
})
|
|
365
|
+
await cache.delete("key")
|
|
228
366
|
```
|
|
229
367
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
368
|
+
Keys are critical for cache invalidation. To allow custom key management, you can override the auto-generated key. This option is available for all essential methods (e.g cache.set, cache.run, cache.send, cache.createKey) and for the annotations.
|
|
369
|
+
|
|
370
|
+
```javascript
|
|
371
|
+
// No key override given, string will just be used as keys
|
|
372
|
+
await cache.set('key', 'value') // key: key
|
|
233
373
|
|
|
234
|
-
|
|
374
|
+
// No key override given, objects will be smartly hashed
|
|
375
|
+
await cache.set(SELECT.from(Foo)) // key: bd3f3690d3e96a569bd89d9e207a89af
|
|
235
376
|
|
|
236
|
-
|
|
377
|
+
// Automatically build the key for retrieval/deletion
|
|
378
|
+
cache.createKey(SELECT.from(Foo)) // key: bd3f3690d3e96a569bd89d9e207a89af
|
|
237
379
|
|
|
238
|
-
|
|
380
|
+
// Override and use your own key based on a fixed value
|
|
381
|
+
await cache.set(SELECT.from(Foo, 1), { key: { value: "foo:1" } })
|
|
239
382
|
|
|
240
|
-
|
|
383
|
+
// Override and only for requests, use request context information
|
|
384
|
+
await cache.run(req, remoteService, { key: { template: "mykey:{tenant}:{user}:{locale}:{hash}" } })
|
|
241
385
|
|
|
242
|
-
|
|
386
|
+
// This requests will be cached for all users and for each locale
|
|
387
|
+
await cache.set(req, remoteService, { key: { template: "mykey:{user}:{locale}:{hash}" } })
|
|
388
|
+
```
|
|
243
389
|
|
|
244
|
-
|
|
390
|
+
Overriding keys support the following configuration options:
|
|
391
|
+
- `value` – generates a static value
|
|
392
|
+
- `prefix` – will add this piece at the beginning
|
|
393
|
+
- `suffix` - will ad this piece at the end
|
|
394
|
+
- `template` - will set a value filled with placeholders, available placeholders are (only relevant for cds.Requests):
|
|
395
|
+
- `{user}`: The current user
|
|
396
|
+
- `{tenant}`: The current tenant
|
|
397
|
+
- `{locale}`: The current locale
|
|
398
|
+
- `{hash}`: The hash of the request query/params/data/path/etc.
|
|
245
399
|
|
|
246
|
-
|
|
400
|
+
With well-structured keys, invalidating cache entries becomes a lot easier. However, for more complex scenarios tags provide an even more effective solution, as tags can automatically be created based on the cached data.
|
|
247
401
|
|
|
248
|
-
|
|
402
|
+
#### 3. Tag-Based
|
|
249
403
|
|
|
250
|
-
|
|
251
|
-
|
|
404
|
+
Tags are a way to invalidate cache entries based on a specific tag. Tags need to be provided explicitly when storing a value in the cache and are supported for all cache methods (e.g. `set`, `run`, `send`, `wrap`, `exec`).
|
|
405
|
+
Tags can be provided as an array of strings or as an array of objects with the following properties:
|
|
406
|
+
- `value`: The value to use for the tag.
|
|
407
|
+
- `data`: A field from the value to use for the tag. This is working for objects and arrays of objects.
|
|
408
|
+
- `prefix`: A prefix that will be added to the tag.
|
|
409
|
+
- `suffix`: A suffix that will be added to the tag.
|
|
410
|
+
- `template`: A template string that will be used to generate the tag (e.g. `{tenant}-{locale}-{user}-{hash}`). This is useful for dynamic tags based on cds.Requests.
|
|
411
|
+
Templates support the following properties:
|
|
412
|
+
- `{user}`: The current user
|
|
413
|
+
- `{tenant}`: The current tenant
|
|
414
|
+
- `{locale}`: The current locale
|
|
415
|
+
- `{hash}`: The hash of the current query
|
|
252
416
|
|
|
253
|
-
* send – Sends **custom synchronous requests** (e.g., to REST APIs) with configurable paths and headers.
|
|
254
|
-
[CAP Documentation: srv.send(request)](https://cap.cloud.sap/docs/node.js/core-services#srv-send-request)
|
|
255
417
|
|
|
256
|
-
Using **read-through caching**, the previous read-aside pattern can be reduced to a **single line of code**:
|
|
257
418
|
|
|
258
419
|
```javascript
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
420
|
+
|
|
421
|
+
// Store with tags
|
|
422
|
+
await cache.set("key", "value", {
|
|
423
|
+
tags: [{ value: "user-123" }]
|
|
262
424
|
})
|
|
425
|
+
|
|
426
|
+
// Invalidate by tag
|
|
427
|
+
await cache.deleteByTag('user-123')
|
|
263
428
|
```
|
|
429
|
+
This is really useful for invalidating cache entries based on a specific attribute or context.
|
|
264
430
|
|
|
265
|
-
|
|
431
|
+
#### 3. Dynamic Tags
|
|
266
432
|
|
|
267
|
-
|
|
433
|
+
Dynamic tags using data `data` property are a way to invalidate cache entries based on the data itself. The caching service will automatically generate a tag for the value and invalidate the cache entry when the value changes.
|
|
268
434
|
|
|
269
435
|
```javascript
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
436
|
+
|
|
437
|
+
const businessPartners = [
|
|
438
|
+
{
|
|
439
|
+
businessPartner: 1,
|
|
440
|
+
name: 'John Doe'
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
businessPartner: 2,
|
|
444
|
+
name: 'Jane Doe'
|
|
278
445
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
446
|
+
]
|
|
447
|
+
|
|
448
|
+
// Store with dynamic tags
|
|
449
|
+
await cache.set("key", businessPartners, {
|
|
450
|
+
tags: [
|
|
451
|
+
{ data: 'businessPartner', prefix: 'bp-' },
|
|
452
|
+
{ value: "businessPartner" }
|
|
453
|
+
]
|
|
454
|
+
})
|
|
282
455
|
|
|
283
|
-
|
|
284
|
-
|
|
456
|
+
// Introspect the tags
|
|
457
|
+
const tags = await cache.tags("key") // => ["bp-1", "bp-2", "businessPartner"]
|
|
458
|
+
|
|
459
|
+
// Invalidate by tag
|
|
460
|
+
await cache.deleteByTag('bp-1')
|
|
461
|
+
await cache.deleteByTag('bp-2')
|
|
462
|
+
```
|
|
285
463
|
|
|
286
|
-
|
|
464
|
+
This is really usefull for caching results with multiple rows where you can't predict the tags beforehand or when you want to invalidate cache entries based on the data itself. This is also possible for the `run` method.
|
|
287
465
|
|
|
288
466
|
```javascript
|
|
289
|
-
const
|
|
467
|
+
const result = await cache.run(query, db, {
|
|
468
|
+
tags: [{ data: 'businessPartner', prefix: 'bp-' }]
|
|
469
|
+
})
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
This will transparently cache the result of the query and create a tag for each business partner in the result. If you use the same technique in other places and you want to invalidate the cache entries for a specific business partner, you can do this by simply invalidating the tag `bp-1`.
|
|
290
473
|
|
|
291
|
-
|
|
292
|
-
const cachedExpensiveFunction = await cache.wrap("key", expensiveFunction, { ttl: 3600 })
|
|
474
|
+
### Cache Iteration
|
|
293
475
|
|
|
294
|
-
|
|
295
|
-
result = await cachedExpensiveFunction("someParam"); // No cache hit
|
|
476
|
+
The caching service provides an iterator interface to traverse all cache entries:
|
|
296
477
|
|
|
297
|
-
|
|
298
|
-
|
|
478
|
+
```javascript
|
|
479
|
+
const iterator = await cache.iterator()
|
|
480
|
+
|
|
481
|
+
for await (const entry of iterator) {
|
|
482
|
+
console.log(entry)
|
|
483
|
+
}
|
|
299
484
|
```
|
|
300
485
|
|
|
301
|
-
This
|
|
486
|
+
This will return an iterator over all cache entries. You can use this to traverse all cache entries and invalidate them based on a specific condition. You should only use this for small caches (e.g. by using multiple caching services with different namespaces).
|
|
487
|
+
|
|
488
|
+
### OData Service Caching Considerations
|
|
489
|
+
|
|
490
|
+
While caching individual requests can improve performance, **caching an entire OData service is generally not recommended**. Here's why:
|
|
491
|
+
|
|
492
|
+
1. **Data Consistency**: OData services expose live business data that frequently changes. Caching responses without an appropriate invalidation strategy can lead to outdated or incorrect data being served.
|
|
493
|
+
|
|
494
|
+
2. **Query Complexity**: OData allows dynamic query parameters ($filter, $expand, $orderby, etc.), making it difficult to cache efficiently without storing excessive variations.
|
|
495
|
+
|
|
496
|
+
3. **Payload Size**: Full OData responses can be significantly large, consuming cache memory inefficiently compared to caching targeted CQN queries or specific request results.
|
|
497
|
+
|
|
498
|
+
Instead of caching entire OData service responses, focus on:
|
|
499
|
+
- Specific queries or request results
|
|
500
|
+
- Static master data
|
|
501
|
+
- Computed results
|
|
502
|
+
- Remote service calls with stable data
|
|
503
|
+
|
|
504
|
+
### Best Practices
|
|
505
|
+
|
|
506
|
+
1. **Cache Selectively**: Not all data benefits from caching. Focus on:
|
|
507
|
+
- Frequently accessed, rarely changed data
|
|
508
|
+
- Computationally expensive operations
|
|
509
|
+
- Remote service calls with stable data
|
|
510
|
+
|
|
511
|
+
2. **Use Appropriate TTLs**: Set TTLs based on data volatility:
|
|
512
|
+
- Short TTLs (seconds/minutes) for frequently changing data
|
|
513
|
+
- Longer TTLs (hours/days) for stable reference data
|
|
514
|
+
|
|
515
|
+
3. **Implement Cache Tags**: Use tags for granular cache invalidation:
|
|
516
|
+
- Group related cache entries
|
|
517
|
+
- Enable targeted invalidation
|
|
518
|
+
- Use dynamic tags for user/tenant-specific caching
|
|
519
|
+
|
|
520
|
+
4. **Monitor Cache Performance**: Regularly check cache statistics:
|
|
521
|
+
- Hit rates
|
|
522
|
+
- Memory usage
|
|
523
|
+
- Response times
|
|
524
|
+
- Error rates
|
|
525
|
+
|
|
526
|
+
### Limitations and Considerations
|
|
527
|
+
|
|
528
|
+
1. **Memory Usage**: Monitor cache size, especially with in-memory storage
|
|
529
|
+
2. **Consistency**: Consider data freshness requirements when setting TTLs
|
|
530
|
+
3. **Multi-Tenant**: Use appropriate namespacing and key strategies
|
|
531
|
+
4. **Redis Setup**: Ensure proper configuration for production use
|
|
532
|
+
|
|
533
|
+
## Full API
|
|
534
|
+
|
|
535
|
+
### `cache.createKey(key: any)` : `string`
|
|
536
|
+
|
|
537
|
+
Creates a key from a string or an object. This method is used internally when passing keys to the cache methods, so you don't need to call it directly other then to retrieve the dynamic generated key for a given object.
|
|
538
|
+
|
|
539
|
+
#### `key: any`
|
|
540
|
+
|
|
541
|
+
The key to create the key from. The key can be a string or an object. If an object is used, it will be hashed to a string key using MD5. cds.Requests are handled explicitly as the dynamic generated key includes the user, tenant and locale and query hash.
|
|
542
|
+
|
|
543
|
+
#### Returns
|
|
544
|
+
|
|
545
|
+
A string key.
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
### `await cache.set(key: any, value: any[, options: object])`
|
|
550
|
+
|
|
551
|
+
Sets a value in the cache.
|
|
552
|
+
|
|
553
|
+
#### `key: any`
|
|
554
|
+
|
|
555
|
+
The key to store the value under. The key handling is the same as for the `≈` method.
|
|
556
|
+
|
|
557
|
+
#### `value: any`
|
|
558
|
+
|
|
559
|
+
The value to store in the cache. The value will be serialized to a string using `JSON.stringify` (unless the value is already a string).
|
|
560
|
+
|
|
561
|
+
#### `options: object`
|
|
562
|
+
|
|
563
|
+
Object literal containing cache options.
|
|
564
|
+
|
|
565
|
+
The following properties are accepted:
|
|
566
|
+
|
|
567
|
+
| Property | Description | Example |
|
|
568
|
+
| ------------- | ------------- | ----------
|
|
569
|
+
| ttl | Time-to-live in milliseconds. | `1000`
|
|
570
|
+
| key | Key override for the cache for full control over the key management (see chapter Cache Invalidation Strategies) | `{template: 'user-{user}', value: '123'}`
|
|
571
|
+
| tags | Array of tags to associate with the value. Tags can be dynamic based on the stored cache data (see chapter Cache Invalidation Strategies) | `[{template: 'user-{user}', value: '123'}]`
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
### `await cache.get(key: any)`
|
|
576
|
+
|
|
577
|
+
Gets a value from the cache.
|
|
578
|
+
|
|
579
|
+
#### `key: any`
|
|
580
|
+
|
|
581
|
+
The key to retrieve the value from. The key handling is the same as for the `createKey` method.
|
|
582
|
+
|
|
583
|
+
#### Returns
|
|
584
|
+
|
|
585
|
+
The deserialized value from the cache or `undefined` if the value does not exist.
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
### `await cache.has(key: any)`
|
|
590
|
+
|
|
591
|
+
Checks if a value exists in the cache.
|
|
592
|
+
|
|
593
|
+
#### `key: any`
|
|
594
|
+
|
|
595
|
+
The key to check for existence. The key handling is the same as for the `createKey` method.
|
|
596
|
+
|
|
597
|
+
#### Returns
|
|
598
|
+
|
|
599
|
+
`true` if the value exists in the cache, `false` otherwise.
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
### `await cache.delete(key: any)`
|
|
604
|
+
|
|
605
|
+
Deletes a value from the cache.
|
|
606
|
+
|
|
607
|
+
#### `key: any`
|
|
608
|
+
|
|
609
|
+
The key to delete the value from. The key handling is the same as for the `createKey` method.
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
613
|
+
### `await cache.clear()`
|
|
614
|
+
|
|
615
|
+
Clears the whole cache.
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
### `await cache.deleteByTag(tag: string)`
|
|
620
|
+
|
|
621
|
+
Deletes all values from the cache that are associated with the given tag.
|
|
622
|
+
|
|
623
|
+
#### `tag: string`
|
|
624
|
+
|
|
625
|
+
The tag to delete the values from.
|
|
626
|
+
|
|
627
|
+
---
|
|
628
|
+
|
|
629
|
+
### `await cache.run(query: cds.CQN , service: cds.Service)`
|
|
630
|
+
|
|
631
|
+
Runs a query against the provided service and caches the result for all further requests. This method is useful for read-through caching. (see Usage Patterns and [CAP docs](https://cap.cloud.sap/docs/node.js/core-services#srv-run-query) for more information)
|
|
632
|
+
|
|
633
|
+
#### `object: cds.CQN`
|
|
634
|
+
|
|
635
|
+
The CQN query to run.
|
|
636
|
+
|
|
637
|
+
#### `service: cds.Service`
|
|
638
|
+
|
|
639
|
+
The service to run the query on.
|
|
640
|
+
|
|
641
|
+
#### Returns
|
|
642
|
+
|
|
643
|
+
The result of the query, either from the cache or the service.
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
### `await cache.send(request: cds.Request, service: cds.Service)`
|
|
648
|
+
|
|
649
|
+
Sends a request to a cds.Service and caches the result. In contrast to the `run` method, this method is useful for caching full cds.Requests.
|
|
650
|
+
|
|
651
|
+
#### `request: cds.Request`
|
|
652
|
+
|
|
653
|
+
The request to send.
|
|
654
|
+
|
|
655
|
+
#### `service: cds.Service `
|
|
656
|
+
|
|
657
|
+
The service to send the request to.
|
|
658
|
+
|
|
659
|
+
#### Returns
|
|
660
|
+
|
|
661
|
+
The result of the request, either from the cache or the service.
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
### `await cache.wrap(key: any, fn: async function, options: object)`
|
|
666
|
+
|
|
667
|
+
Wraps a function in a cache.
|
|
668
|
+
|
|
669
|
+
#### `key: any`
|
|
670
|
+
|
|
671
|
+
The key to store the cached function under. The key handling is the same as for the `createKey` method.
|
|
672
|
+
|
|
673
|
+
#### `fn: async function`
|
|
674
|
+
|
|
675
|
+
The async function to wrap in a cache.
|
|
676
|
+
|
|
677
|
+
#### `options: object`
|
|
678
|
+
|
|
679
|
+
The options to use for the cache.
|
|
680
|
+
|
|
681
|
+
#### Returns
|
|
682
|
+
|
|
683
|
+
A cached version of the function. The cached function will check the cache first and only execute the function if the cache miss.
|
|
684
|
+
|
|
685
|
+
---
|
|
686
|
+
|
|
687
|
+
### `await cache.exec(key: any, fn: async function, options: object)`
|
|
688
|
+
|
|
689
|
+
Executes a function and caches the result. This method is useful for one-off executions with caching.
|
|
690
|
+
|
|
691
|
+
#### `key: any`
|
|
692
|
+
|
|
693
|
+
The key to store the cached function under. The key handling is the same as for the `createKey` method.
|
|
694
|
+
|
|
695
|
+
#### `fn: async function`
|
|
696
|
+
|
|
697
|
+
The async function to execute.
|
|
698
|
+
|
|
699
|
+
#### `options: object`
|
|
700
|
+
|
|
701
|
+
The options to use for the cache.
|
|
702
|
+
|
|
703
|
+
#### Returns
|
|
704
|
+
|
|
705
|
+
The result of the function.
|
|
706
|
+
|
|
707
|
+
---
|
|
708
|
+
|
|
709
|
+
### `await cache.iterator() : AsyncIterator<{ key: string, value: { value: any, tags: string[], timestamp: number } }>`
|
|
710
|
+
|
|
711
|
+
Returns an iterator over all cache entries.
|
|
712
|
+
|
|
713
|
+
#### Returns
|
|
714
|
+
|
|
715
|
+
An iterator over all cache entries.
|
|
716
|
+
|
|
717
|
+
---
|
|
718
|
+
|
|
719
|
+
### `await cache.tags(key: any) : string[]`
|
|
720
|
+
|
|
721
|
+
Returns the tags for a given key.
|
|
722
|
+
|
|
723
|
+
#### `key: any`
|
|
724
|
+
|
|
725
|
+
The key to get the tags for. The key handling is the same as for the `createKey` method.
|
|
726
|
+
|
|
727
|
+
#### Returns
|
|
728
|
+
|
|
729
|
+
An array of tags. If the key does not exist, an empty array is returned.
|
|
730
|
+
|
|
731
|
+
---
|
|
732
|
+
|
|
733
|
+
### `await cache.metadata(key: any) : { tags: string[], timestamp: number } | undefined`
|
|
734
|
+
|
|
735
|
+
Returns the metadata for a given key.
|
|
736
|
+
|
|
737
|
+
#### `key: any`
|
|
738
|
+
|
|
739
|
+
The key to get the metadata for. The key handling is the same as for the `createKey` method.
|
|
302
740
|
|
|
741
|
+
#### Returns
|
|
303
742
|
|
|
304
|
-
|
|
743
|
+
An object containing the metadata for the given key or `undefined` if the key does not exist. The metadata object contains the following properties:
|
|
305
744
|
|
|
306
|
-
-
|
|
307
|
-
-
|
|
308
|
-
- [ ] Add documentation for cache annotations
|
|
309
|
-
- [ ] Add documentation for cache key generation
|
|
310
|
-
- [ ] Add documentation for cache tags
|
|
311
|
-
- [ ] Add documentation for cache statistics
|
|
312
|
-
- [ ] Add documentation for running locally with redis on docker
|
|
313
|
-
- [ ] Add documentation for running with redis on BTP
|
|
745
|
+
- `tags`: An array of tags.
|
|
746
|
+
- `timestamp`: The timestamp of the cache entry.
|
|
314
747
|
|
|
748
|
+
---
|
|
315
749
|
|
|
316
|
-
|
|
750
|
+
### Contributing
|
|
317
751
|
|
|
318
|
-
Contributions are welcome! Please
|
|
752
|
+
Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.
|
|
319
753
|
|
|
320
|
-
|
|
754
|
+
### License
|
|
321
755
|
|
|
322
|
-
This project is licensed under the
|
|
756
|
+
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cds-caching",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A caching plugin for SAP CAP applications supporting Redis",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"scripts": {
|
|
29
29
|
"test": "jest --runInBand --silent",
|
|
30
30
|
"test:watch": "jest --watch",
|
|
31
|
-
"lint": "eslint ."
|
|
31
|
+
"lint": "eslint .",
|
|
32
|
+
"release": "release-it"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
35
|
"@sap/cds": ">=8"
|
|
@@ -49,7 +50,8 @@
|
|
|
49
50
|
"chai-subset": "^1.6.0",
|
|
50
51
|
"eslint": "^8.57.0",
|
|
51
52
|
"husky": "^9.0.11",
|
|
52
|
-
"jest": "^29.7.0"
|
|
53
|
+
"jest": "^29.7.0",
|
|
54
|
+
"release-it": "^18.1.2"
|
|
53
55
|
},
|
|
54
56
|
"engines": {
|
|
55
57
|
"node": ">=16"
|
package/srv/CachingService.js
CHANGED
|
@@ -29,7 +29,7 @@ class CachingService extends cds.Service {
|
|
|
29
29
|
...(this.options.store === "redis" ? { store: new KeyvRedis({
|
|
30
30
|
...this.options.credentials,
|
|
31
31
|
// Redis, Hyperscaler Option on BTP provides a URI
|
|
32
|
-
...(this.options.credentials
|
|
32
|
+
...(this.options.credentials?.uri ? { url: this.options.credentials?.uri } : {}),
|
|
33
33
|
}) } : {}),
|
|
34
34
|
compression: this.options.compression === "lz4" ? new KeyvLz4() : this.options.compression === "gzip" ? new KeyvGzip() : undefined
|
|
35
35
|
}
|
|
@@ -523,6 +523,24 @@ class CachingService extends cds.Service {
|
|
|
523
523
|
].filter(Boolean).join('');
|
|
524
524
|
}
|
|
525
525
|
|
|
526
|
+
/**
|
|
527
|
+
* Executes an async function and caches its result
|
|
528
|
+
*
|
|
529
|
+
* @param {string} key - the key to cache
|
|
530
|
+
* @param {function} asyncFunction - the async function to execute
|
|
531
|
+
* @param {object} options - additional options
|
|
532
|
+
* @returns {Promise<any>} - the result
|
|
533
|
+
*/
|
|
534
|
+
async exec(key, asyncFunction, options = {}) {
|
|
535
|
+
const cacheKey = this.createKey(key, options.key);
|
|
536
|
+
if (await this.has(cacheKey)) {
|
|
537
|
+
return this.get(cacheKey);
|
|
538
|
+
}
|
|
539
|
+
const response = await asyncFunction();
|
|
540
|
+
await this.set(cacheKey, response, options);
|
|
541
|
+
return response;
|
|
542
|
+
}
|
|
543
|
+
|
|
526
544
|
}
|
|
527
545
|
|
|
528
546
|
module.exports = CachingService;
|