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 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 robust caching service for CAP applications, addressing common performance challenges in distributed systems.
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
- By reducing database requests and accelerating response times, caching is ideal for handling expensive operations like complex queries/calculations or external API calls. However, it introduces another layer of complexity and should be used with caution.
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-based implementation with an intuitive API for seamless integration.
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 (WIP)** – Integrated statistics on cache hits, etc.
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 its a CAP plugin. Simply run:
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
- ```json
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. Some of those will be explained later:
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
+ ![Redis on SAP BTP](./docs/caching-btp.png)
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
- The **cds-caching** plugin provides direct **key-value storage**, allowing fine-grained caching control. Its API follows a familiar pattern, making it easy to use if you have ever worked with other caching solutions and frameworks.
76
- Here’s how you can interact with the cache:
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
- This **low-level API** is useful when you need direct access to cached data, such as storing configuration values, or precomputed results. While the core API is simple, **cds-caching** also provides **higher-level caching strategies** that are more integrated with CAP. We will look at those in a minute. Before, let's focus on cache events.
99
- #### Cache Events
214
+ #### 2. CQN Query Caching
100
215
 
101
- The caching service follows the same event-driven principles as cds.Service instances. This means you can hook into events **before** and **after** storing or retrieving data. This is useful for logging, debugging, or performing additional actions when cache operations occur.
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
- Each cache event (event.data.value) contains the following structure:
104
- * **value** The cached data.
105
- * **tags** Tags assigned to the cached entry.
106
- * **timestamp** The timestamp when the cache entry was created.
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
- Here’s how you can listen for and react to cache events:
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
- // Log before the cache is cleared
113
- cache.before("CLEAR", () => {
114
- console.log("Cache is about to be cleared")
115
- })
234
+ // Runs the query internally and caches the result
235
+ const result = await cache.run(query, db)
236
+ ```
116
237
 
117
- // Log before storing data
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
- // Log after retrieving data
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
- This approach allows you to monitor cache activity and, if needed, manipulate data before storage or retrieval.
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
- // Store a value with a ttl
137
- await cache.set("key", "value", { ttl: 6000 }) // 60 seconds
259
+ const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
260
+ const result = await cache.run(req, bupa)
261
+ ```
138
262
 
139
- // Retrieve the value in time
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
- await new Promise((resolve) => setTimeout(resolve, 6100)) // wait 61 seonds
265
+ #### 4. Declarative Caching with Annotations
143
266
 
144
- // Now the value is not available anymore
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
- TTL-based invalidation is useful for **temporary data, rate-limiting mechanisms, and frequently updated information**. However, **cds-caching** also supports other invalidation strategies, which will be covered in the advanced section.
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
- To optimize storage, **cds-caching** supports **data compression**. This reduces cache size and can improve performance, especially when caching large datasets. Compression is **applied only when storing data**, meaning applications interact with uncompressed values. Available compression methods include:
152
- * **lz4** – Faster compression and decompression, ideal for performance-critical applications.
153
- * **gzip** – Higher compression ratio, reducing storage footprint at the cost of slightly increased CPU usage.
154
-
155
- Compression can be configured via package.json:
271
+ ```
272
+ service MyService {
273
+ @cache: {
274
+ ttl: 3600
275
+ }
276
+ entity BusinessPartners as projection on BusinessPartner {
277
+ // ... entity definition
278
+ }
156
279
 
157
- ```json
158
- {
159
- "cds": {
160
- "requires": {
161
- "caching": {
162
- "impl": "cds-caching",
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
- ### Medium level usage
291
+ #### 5. Function Caching
171
292
 
172
- One of the core principles of **cds-caching** is **seamless integration with CAP**, aligning with CAP’s design and query execution model. As a result, **cds-caching** provides **native support** for **CQN (Core Query Notation) queries** and **cds.Requests**.
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
- CQN queries are widely used in CAP for **database operations** and **remote service calls (e.g., OData requests)**. Since these queries often involve **repeated data retrieval**, caching them can **significantly reduce response times** and **offload system resources**.
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
- **cds-caching** treats **CQN queries as first-class objects**, allowing them to be passed directly into the caching API. Internally, it generates a unique key based on the CQN query structure, ensuring **consistent retrieval** and **cache integrity**.
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
- **Example: Caching a CQN Query**
308
+ // Each call checks cache first, only executes if cache miss
309
+ const result = await cachedOperation("input")
180
310
 
181
- ```javascript
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
- // Create the CQN object
184
- const query = SELECT.from(Foo)
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
- // Execute to fetch the result
187
- const result = await cds.run(query) // => [{...}, {...}]
327
+ ### Cache Invalidation Strategies
188
328
 
189
- // Store value in the cache
190
- await cache.set(query, result)
329
+ The caching service provides different strategies to invalidate cached values.
191
330
 
192
- // Retrieve the value from the cache using the same CQN object
193
- const cachedResult = await cache.get(query) // => [{...}, {...}]
331
+ **IMPORTANT: You should not use cds-caching without a proper invalidation strategy.**
194
332
 
195
- // Create the key that is used internally
196
- const key = cache.createKey(query)
333
+ #### 1. Time-Based (TTL)
197
334
 
198
- // Delete the value from the cache
199
- await cache.delete(query)
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
- While **CQN queries** can be cached directly, **cds-caching** also supports **caching full** cds.Requests, including their **request context**.
204
- #### Caching cds.Requests
342
+ // Run with 30 seconds TTL
343
+ const result = await cache.run(query, db, { ttl: 30000 })
205
344
 
206
- Caching requests is particularly useful when exposing **remote services** through **local CAP services**. For example, if your CAP application proxies an **external API**, caching can significantly reduce redundant requests and improve response times.
345
+ // Send with 10 seconds TTL
346
+ const result = await cache.send(request, service, { ttl: 10000 })
207
347
 
208
- 👉 **Use Case:** [Exposing Remote Services](https://cap.cloud.sap/docs/guides/using-services#expose-remote-services)
348
+ // Wrap with 10 seconds TTL
349
+ const cachedOperation = cache.wrap("key", expensiveOperation, { ttl: 10000 })
209
350
 
210
- **cds-caching** automatically includes contextual information in the cache key, making request caching **tenant- and user-aware**. The cache key incorporates:
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
- * req.tenant – Ensures data is scoped per tenant in multi-tenant environments.
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
- **Example: Caching a cds.Request**
362
+ Key-based invalidation is a way to invalidate cache entries based on a specific key.
217
363
 
218
364
  ```javascript
219
- this.on('READ', BusinessPartners, async (req, next) => {
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
- **When Not to Cache Full OData Services**
231
-
232
- While caching individual **requests** can improve performance, **caching an entire OData service is generally not recommended**. Here’s why:
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
- 1. **Data Inconsistency** OData services expose **live business data**, which frequently changes. Caching responses without an **appropriate invalidation strategy** can lead to outdated or incorrect data being served.
374
+ // No key override given, objects will be smartly hashed
375
+ await cache.set(SELECT.from(Foo)) // key: bd3f3690d3e96a569bd89d9e207a89af
235
376
 
236
- 2. **Complex Query Variations** OData allows **dynamic query parameters** ($filter, $expand, $orderby, etc.), making it difficult to cache efficiently without storing excessive variations.
377
+ // Automatically build the key for retrieval/deletion
378
+ cache.createKey(SELECT.from(Foo)) // key: bd3f3690d3e96a569bd89d9e207a89af
237
379
 
238
- 3. **Large Payloads** Full OData responses can be **significantly large**, consuming cache memory inefficiently compared to caching targeted **CQN queries** or specific **request results**.
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
- 👉 **Best Practice:** Instead of caching entire OData service responses, cache **specific queries or request results** where the data is frequently accessed and doesn’t change often.
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
- For example, focussing on remote services, **static master data**, or **computed results** is much safer and more efficient than blindly caching full OData responses.
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
- ### Read-through CQN queries and cds.Requests
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
- While this API is useful, it follows the **read-aside cache pattern**, meaning **manual cache checks** are required before fetching and storing data. In scenarios where caching logic becomes repetitive, **higher-level caching strategies** can help streamline this process and thanks to the already available CAP API, those are also available in cds-caching.
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
- In **read-through caching**, queries and requests are executed **through the caching service itself**, reducing the need for manual cache handling. **cds-caching** extends CAP’s built-in service methods with two key functions:
402
+ #### 3. Tag-Based
249
403
 
250
- * run Executes **CQN queries** or **requests** against a database or remote OData service.
251
- [CAP Documentation: srv.run(query)](https://cap.cloud.sap/docs/node.js/core-services#srv-run-query)
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
- this.on('READ', BusinessPartners, async (req, next) => {
260
- const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
261
- return cache.run(req, bupa, { ttl: 3600 })
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
- With this approach, **all requests to the external service will be automatically executed and cached**, eliminating the need for manual cache handling.
431
+ #### 3. Dynamic Tags
266
432
 
267
- Some other examples :
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
- // Read-through for a CQN query
271
- const queryResult = await cache.run(SELECT.from("Foo"), db, { ttl: 3600 })
272
-
273
- // Read-through for a custom REST request
274
- const restService = await cds.connect.to({
275
- "kind": "rest",
276
- "credentials": {
277
- "url": "https://services.odata.org/V3/Northwind/Northwind.svc/"
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
- const restResult = await cache.send({ method: "GET", path: "Products" }, restService, { ttl: 3600 });
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
- The **read-through strategy** makes caching **cleaner and more maintainable**, as it abstracts cache management entirely. However, **the first request is still slow** (since there’s no cached value yet), but all subsequent requests will be **served instantly from the cache**.
284
- #### Wrapping async complex code
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
- The **read-through approach** can also be applied to **non-CAP-specific** operations. **cds-caching** provides a wrap function that caches the result of **any asynchronous function**.
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 expensiveFunction = async (param) => { /* Do something complex */ }
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
- // Wrap the function with caching
292
- const cachedExpensiveFunction = await cache.wrap("key", expensiveFunction, { ttl: 3600 })
474
+ ### Cache Iteration
293
475
 
294
- // First call executes the function
295
- result = await cachedExpensiveFunction("someParam"); // No cache hit
476
+ The caching service provides an iterator interface to traverse all cache entries:
296
477
 
297
- // Subsequent calls retrieve the result from cache
298
- result = await cachedExpensiveFunction("someParam"); // Cache hit
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 is particularly useful for **heavy computations**, ensuring they only need to be executed **once per TTL period**.
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
- ## TODO:
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
- - [ ] Add documentation for cache iteration
307
- - [ ] Add documentation for cache invalidation
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
- ## Contributing
750
+ ### Contributing
317
751
 
318
- Contributions are welcome! Please feel free to submit a Pull Request.
752
+ Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.
319
753
 
320
- ## License
754
+ ### License
321
755
 
322
- This project is licensed under the MIT License - see the LICENSE file for detail
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.0",
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"
@@ -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.uri ? { url: this.options.credentials.uri } : {}),
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;