cds-caching 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -14
- package/package.json +8 -9
- package/srv/CachingService.js +110 -101
package/README.md
CHANGED
|
@@ -26,12 +26,12 @@ cds-caching is specifically designed for efficient caching, not data replication
|
|
|
26
26
|
### Key Features
|
|
27
27
|
|
|
28
28
|
* **Flexible Key-Value Store** – Store and retrieve data using simple key-based access.
|
|
29
|
-
* **CachingService** – A
|
|
29
|
+
* **CachingService** – A cds.Service implementation with an intuitive API for seamless integration into CAP.
|
|
30
30
|
* **Event Handling** – Monitor and react to cache events, such as before/after storage and retrieval.
|
|
31
31
|
* **CAP-specific Caching** – Effortlessly cache CQN queries or CAP cds.Requests using code or the @cache annotation.
|
|
32
32
|
* **TTL Support** – Automatically manage data expiration with configurable time-to-live (TTL) settings.
|
|
33
33
|
* **Tag Support** – Use dynamic tags for flexible cache invalidation options.
|
|
34
|
-
* **Pluggable Storage Options** – Choose between in-memory caching or Redis.
|
|
34
|
+
* **Pluggable Storage Options** – Choose between in-memory caching, SQLite or Redis.
|
|
35
35
|
* **Compression** – Compress cached data to save memory using LZ4 or GZIP.
|
|
36
36
|
* **Integrated Statistics** – Monitor cache performance with hit rates, latencies, and more.
|
|
37
37
|
|
|
@@ -74,13 +74,20 @@ For more control, you can specify additional options:
|
|
|
74
74
|
"caching": {
|
|
75
75
|
"impl": "cds-caching",
|
|
76
76
|
"namespace": "my::app::caching",
|
|
77
|
-
"store": "in-memory", // "in-memory" or "redis"
|
|
77
|
+
"store": "in-memory", // "in-memory" or "sqlite" or "redis"
|
|
78
78
|
"compression": "lz4", // "lz4" or "gzip"
|
|
79
|
-
"credentials": { // if store is redis
|
|
79
|
+
"credentials": { // if store is redis or sqlite
|
|
80
|
+
|
|
81
|
+
// Redis specific
|
|
80
82
|
"host": "localhost",
|
|
81
83
|
"port": 6379,
|
|
82
84
|
"password": "optional",
|
|
83
85
|
"url": "redis://..." // Alternative: Redis connection URI
|
|
86
|
+
|
|
87
|
+
// SQLite specific
|
|
88
|
+
"url": "sqlite://./cache.sqlite"
|
|
89
|
+
"table": "cache",
|
|
90
|
+
"busyTimeout": 10000
|
|
84
91
|
},
|
|
85
92
|
"statistics": {
|
|
86
93
|
"enabled": true,
|
|
@@ -93,6 +100,104 @@ For more control, you can specify additional options:
|
|
|
93
100
|
}
|
|
94
101
|
```
|
|
95
102
|
|
|
103
|
+
### Real-World Usage and Deployment
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
#### Storage Options
|
|
107
|
+
|
|
108
|
+
cds-caching provides 3 storage options:
|
|
109
|
+
|
|
110
|
+
##### In-Memory Cache (for development / small-scale uses)
|
|
111
|
+
- Simple and fast, but not persistent
|
|
112
|
+
- Not suitable for production since Node.js runtime memory is limited
|
|
113
|
+
- Data is lost when the application restarts
|
|
114
|
+
- Memory on SAP BTP Cloud Foundry is limited (up to 16 GB) and produces costs
|
|
115
|
+
|
|
116
|
+
##### SQLite (for medium-size use uses)
|
|
117
|
+
- Data is stored in local SQLite database
|
|
118
|
+
- Data is persited next to SAP BTP application with disk-quota up to 10 GB
|
|
119
|
+
- Cache will be removed after each deployment to SAP BTP
|
|
120
|
+
- No distributed cache between application instances (horizontal scaling)
|
|
121
|
+
|
|
122
|
+
##### Redis Cache (recommended for production)
|
|
123
|
+
- Persistent and supports distributed caching
|
|
124
|
+
- Works across multiple app instances, making it ideal for scalable applications
|
|
125
|
+
- Available on SAP BTP via hyperscaler options (e.g., AWS, Azure, Google Cloud)
|
|
126
|
+
- Even trial accounts provide Redis access
|
|
127
|
+
|
|
128
|
+
#### Redis Development Setup
|
|
129
|
+
|
|
130
|
+
##### Running Redis Locally via Docker
|
|
131
|
+
For local development, Redis can be quickly set up using Docker. A simple docker-compose configuration provides a lightweight caching environment:
|
|
132
|
+
|
|
133
|
+
1. Create a `docker-compose.yml` file:
|
|
134
|
+
```yaml
|
|
135
|
+
services:
|
|
136
|
+
redis:
|
|
137
|
+
image: redis:latest
|
|
138
|
+
container_name: local-redis
|
|
139
|
+
ports:
|
|
140
|
+
- "6379:6379"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
2. Run Redis with:
|
|
144
|
+
```bash
|
|
145
|
+
docker compose up -d
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
3. Modify the `package.json` configuration to connect to the local Redis instance:
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"cds": {
|
|
152
|
+
"requires": {
|
|
153
|
+
"caching": {
|
|
154
|
+
"impl": "cds-caching",
|
|
155
|
+
"namespace": "myCache",
|
|
156
|
+
"store": "redis",
|
|
157
|
+
"[development]": {
|
|
158
|
+
"credentials": {
|
|
159
|
+
"host": "localhost",
|
|
160
|
+
"port": 6379
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Now, caching will be handled by Redis instead of in-memory storage during development.
|
|
170
|
+
|
|
171
|
+
#### Production Deployment on SAP BTP
|
|
172
|
+
|
|
173
|
+

|
|
174
|
+
|
|
175
|
+
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.
|
|
176
|
+
|
|
177
|
+
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:
|
|
178
|
+
|
|
179
|
+
```yaml
|
|
180
|
+
modules:
|
|
181
|
+
- name: cap-app-srv
|
|
182
|
+
# ... other module configuration ...
|
|
183
|
+
requires:
|
|
184
|
+
- name: redis-cache
|
|
185
|
+
|
|
186
|
+
resources:
|
|
187
|
+
- name: redis-cache
|
|
188
|
+
type: org.cloudfoundry.managed-service
|
|
189
|
+
parameters:
|
|
190
|
+
service: redis-cache
|
|
191
|
+
service-plan: trial
|
|
192
|
+
service-tags:
|
|
193
|
+
# Must match the kind property in the package.json
|
|
194
|
+
- cds-caching
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
> 👉 **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.
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
|
96
201
|
### Usage Patterns
|
|
97
202
|
|
|
98
203
|
The caching service provides a flexible API for caching data in CAP applications. Here are the key usage patterns:
|
|
@@ -146,11 +251,12 @@ const result = await cache.run(query, db)
|
|
|
146
251
|
|
|
147
252
|
This will transparently cache the result of the query and return the cached result if available for all further requests.
|
|
148
253
|
|
|
149
|
-
#### 3. Request-Level Caching
|
|
254
|
+
#### 3. RemoteService Request-Level Caching
|
|
150
255
|
|
|
151
|
-
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.
|
|
256
|
+
Cache entire CAP requests with context awareness (e.g. user, tenant, locale, etc.), which is useful for caching slow remote service calls or even application services. The caching service will automatically generate a key for the request based on the request object and the current user, tenant and locale.
|
|
152
257
|
|
|
153
258
|
```javascript
|
|
259
|
+
// Cache the requests to an exposed external entity
|
|
154
260
|
this.on('READ', BusinessPartners, async (req, next) => {
|
|
155
261
|
const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
|
|
156
262
|
let value = await cache.get(req)
|
|
@@ -165,17 +271,45 @@ this.on('READ', BusinessPartners, async (req, next) => {
|
|
|
165
271
|
Alternatively use read-through caching via the `run` method to let the caching service handle the caching transparently:
|
|
166
272
|
|
|
167
273
|
```javascript
|
|
168
|
-
|
|
169
|
-
const
|
|
274
|
+
this.on('READ', BusinessPartners, async (req, next) => {
|
|
275
|
+
const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
|
|
276
|
+
return await cache.run(req, bupa)
|
|
277
|
+
})
|
|
278
|
+
|
|
170
279
|
```
|
|
171
280
|
|
|
172
281
|
This will transparently cache the result of the request and return the cached result if available for all further requests.
|
|
173
282
|
|
|
174
|
-
|
|
283
|
+
### 4. ApplicationService Request-Level Caching
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
> 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.
|
|
287
|
+
|
|
175
288
|
|
|
176
|
-
|
|
289
|
+
But not only external services can be cached, it's also possible to cache requests against an ApplicationService.
|
|
290
|
+
Here, you should make use of the [`prepend`](https://cap.cloud.sap/docs/node.js/core-services#srv-prepend) function, to register the `on` handler before the default handler. Thus, it is possible to first check for the cache entries and only execute the default behavior if necessary.
|
|
177
291
|
|
|
178
|
-
|
|
292
|
+
|
|
293
|
+
```javascript
|
|
294
|
+
class MyService extends cds.ApplicationService {
|
|
295
|
+
async init() {
|
|
296
|
+
|
|
297
|
+
// Read-through caching for the full entity
|
|
298
|
+
this.prepend(() => {
|
|
299
|
+
const { MyEntity } = this.entities;
|
|
300
|
+
this.on('READ', MyEntity, async (req, next) => {
|
|
301
|
+
const cache = cds.connect.to("caching");
|
|
302
|
+
return cache.run(req, next);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
return super.init()
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
#### 5. ApplicationService Request-Level Caching with Annotations
|
|
311
|
+
|
|
312
|
+
Alternatively to doing this via code, you can 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.
|
|
179
313
|
|
|
180
314
|
```
|
|
181
315
|
service MyService {
|
|
@@ -266,7 +400,49 @@ const result = await cache.exec("key", async () => {
|
|
|
266
400
|
})
|
|
267
401
|
```
|
|
268
402
|
|
|
269
|
-
#### 2.
|
|
403
|
+
#### 2. Key-Based
|
|
404
|
+
|
|
405
|
+
Key-based invalidation is a way to invalidate cache entries based on a specific key.
|
|
406
|
+
|
|
407
|
+
```javascript
|
|
408
|
+
await cache.delete("key")
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
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.
|
|
412
|
+
|
|
413
|
+
```javascript
|
|
414
|
+
// No key override given, string will just be used as keys
|
|
415
|
+
await cache.set('key', 'value') // key: key
|
|
416
|
+
|
|
417
|
+
// No key override given, objects will be smartly hashed
|
|
418
|
+
await cache.set(SELECT.from(Foo)) // key: bd3f3690d3e96a569bd89d9e207a89af
|
|
419
|
+
|
|
420
|
+
// Automatically build the key for retrieval/deletion
|
|
421
|
+
cache.createKey(SELECT.from(Foo)) // key: bd3f3690d3e96a569bd89d9e207a89af
|
|
422
|
+
|
|
423
|
+
// Override and use your own key based on a fixed value
|
|
424
|
+
await cache.set(SELECT.from(Foo, 1), { key: { value: "foo:1" } })
|
|
425
|
+
|
|
426
|
+
// Override and only for requests, use request context information
|
|
427
|
+
await cache.run(req, remoteService, { key: { template: "mykey:{tenant}:{user}:{locale}:{hash}" } })
|
|
428
|
+
|
|
429
|
+
// This requests will be cached for all users and for each locale
|
|
430
|
+
await cache.set(req, remoteService, { key: { template: "mykey:{user}:{locale}:{hash}" } })
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Overriding keys support the following configuration options:
|
|
434
|
+
- `value` – generates a static value
|
|
435
|
+
- `prefix` – will add this piece at the beginning
|
|
436
|
+
- `suffix` - will ad this piece at the end
|
|
437
|
+
- `template` - will set a value filled with placeholders, available placeholders are (only relevant for cds.Requests):
|
|
438
|
+
- `{user}`: The current user
|
|
439
|
+
- `{tenant}`: The current tenant
|
|
440
|
+
- `{locale}`: The current locale
|
|
441
|
+
- `{hash}`: The hash of the request query/params/data/path/etc.
|
|
442
|
+
|
|
443
|
+
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.
|
|
444
|
+
|
|
445
|
+
#### 3. Tag-Based
|
|
270
446
|
|
|
271
447
|
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`).
|
|
272
448
|
Tags can be provided as an array of strings or as an array of objects with the following properties:
|
|
@@ -434,7 +610,8 @@ The following properties are accepted:
|
|
|
434
610
|
| Property | Description | Example |
|
|
435
611
|
| ------------- | ------------- | ----------
|
|
436
612
|
| ttl | Time-to-live in milliseconds. | `1000`
|
|
437
|
-
|
|
|
613
|
+
| key | Key override for the cache for full control over the key management (see chapter Cache Invalidation Strategies) | `{template: 'user-{user}', value: '123'}`
|
|
614
|
+
| 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'}]`
|
|
438
615
|
|
|
439
616
|
---
|
|
440
617
|
|
|
@@ -572,7 +749,7 @@ The result of the function.
|
|
|
572
749
|
|
|
573
750
|
---
|
|
574
751
|
|
|
575
|
-
### `await cache.iterator()
|
|
752
|
+
### `await cache.iterator() : AsyncIterator<{ key: string, value: { value: any, tags: string[], timestamp: number } }>`
|
|
576
753
|
|
|
577
754
|
Returns an iterator over all cache entries.
|
|
578
755
|
|
|
@@ -582,6 +759,36 @@ An iterator over all cache entries.
|
|
|
582
759
|
|
|
583
760
|
---
|
|
584
761
|
|
|
762
|
+
### `await cache.tags(key: any) : string[]`
|
|
763
|
+
|
|
764
|
+
Returns the tags for a given key.
|
|
765
|
+
|
|
766
|
+
#### `key: any`
|
|
767
|
+
|
|
768
|
+
The key to get the tags for. The key handling is the same as for the `createKey` method.
|
|
769
|
+
|
|
770
|
+
#### Returns
|
|
771
|
+
|
|
772
|
+
An array of tags. If the key does not exist, an empty array is returned.
|
|
773
|
+
|
|
774
|
+
---
|
|
775
|
+
|
|
776
|
+
### `await cache.metadata(key: any) : { tags: string[], timestamp: number } | undefined`
|
|
777
|
+
|
|
778
|
+
Returns the metadata for a given key.
|
|
779
|
+
|
|
780
|
+
#### `key: any`
|
|
781
|
+
|
|
782
|
+
The key to get the metadata for. The key handling is the same as for the `createKey` method.
|
|
783
|
+
|
|
784
|
+
#### Returns
|
|
785
|
+
|
|
786
|
+
An object containing the metadata for the given key or `undefined` if the key does not exist. The metadata object contains the following properties:
|
|
787
|
+
|
|
788
|
+
- `tags`: An array of tags.
|
|
789
|
+
- `timestamp`: The timestamp of the cache entry.
|
|
790
|
+
|
|
791
|
+
---
|
|
585
792
|
|
|
586
793
|
### Contributing
|
|
587
794
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cds-caching",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A caching plugin for SAP CAP applications supporting Redis",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -41,17 +41,16 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@keyv/compress-gzip": "^2.0.2",
|
|
43
43
|
"@keyv/compress-lz4": "^1.0.0",
|
|
44
|
-
"@keyv/redis": "^4.
|
|
45
|
-
"keyv": "^
|
|
44
|
+
"@keyv/redis": "^4.3.1",
|
|
45
|
+
"@keyv/sqlite": "^4.0.1",
|
|
46
|
+
"keyv": "^5.3.1"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"chai-subset": "^1.6.0",
|
|
51
|
-
"eslint": "^8.57.0",
|
|
52
|
-
"husky": "^9.0.11",
|
|
49
|
+
"eslint": "^9.21.0",
|
|
50
|
+
"husky": "^9.1.7",
|
|
53
51
|
"jest": "^29.7.0",
|
|
54
|
-
"release-it": "^18.1.2"
|
|
52
|
+
"release-it": "^18.1.2",
|
|
53
|
+
"@cap-js/cds-test": "^0.2.0"
|
|
55
54
|
},
|
|
56
55
|
"engines": {
|
|
57
56
|
"node": ">=16"
|
package/srv/CachingService.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const cds = require('@sap/cds');
|
|
2
2
|
const { Keyv } = require('keyv');
|
|
3
3
|
const { default: KeyvRedis } = require('@keyv/redis');
|
|
4
|
+
const { default: KeyvSqlite } = require('@keyv/sqlite');
|
|
4
5
|
const { default: KeyvLz4 } = require('@keyv/compress-lz4');
|
|
5
6
|
const { default: KeyvGzip } = require('@keyv/compress-gzip');
|
|
6
7
|
const crypto = require('crypto');
|
|
@@ -20,30 +21,45 @@ class CachingService extends cds.Service {
|
|
|
20
21
|
this.options = this.options || {
|
|
21
22
|
store: null,
|
|
22
23
|
compression: null,
|
|
23
|
-
credentials: {
|
|
24
|
-
}
|
|
24
|
+
credentials: { }
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
let store;
|
|
28
|
+
|
|
29
|
+
switch (this.options.store) {
|
|
30
|
+
case "sqlite":
|
|
31
|
+
store = new KeyvSqlite({
|
|
32
|
+
url: this.options.credentials?.url,
|
|
33
|
+
table: this.options.credentials?.table || 'cache',
|
|
34
|
+
busyTimeout: this.options.credentials?.busyTimeout || 10000
|
|
35
|
+
});
|
|
36
|
+
break;
|
|
37
|
+
case "redis":
|
|
38
|
+
store = new KeyvRedis({
|
|
39
|
+
...this.options.credentials,
|
|
40
|
+
// Redis, Hyperscaler Option on BTP provides a URI
|
|
41
|
+
...(this.options.credentials?.uri ? { url: this.options.credentials?.uri } : {}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
cds.once("shutdown", async () => {
|
|
45
|
+
if (this.cache.store?.disconnect) {
|
|
46
|
+
await this.cache.store.disconnect().catch((err) => {
|
|
47
|
+
this.LOG._error && this.LOG.error('Error disconnecting from Redis', err);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
break;
|
|
52
|
+
default:
|
|
53
|
+
store = new Map();
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
|
|
27
57
|
let cacheOptions = {
|
|
28
58
|
namespace: this.options.namespace || this.name,
|
|
29
|
-
|
|
30
|
-
...this.options.credentials,
|
|
31
|
-
// Redis, Hyperscaler Option on BTP provides a URI
|
|
32
|
-
...(this.options.credentials?.uri ? { url: this.options.credentials?.uri } : {}),
|
|
33
|
-
}) } : {}),
|
|
59
|
+
store: store,
|
|
34
60
|
compression: this.options.compression === "lz4" ? new KeyvLz4() : this.options.compression === "gzip" ? new KeyvGzip() : undefined
|
|
35
61
|
}
|
|
36
62
|
|
|
37
|
-
if (this.options.store === "redis") {
|
|
38
|
-
|
|
39
|
-
cds.once("shutdown", async () => {
|
|
40
|
-
if (this.cache.store?.disconnect) {
|
|
41
|
-
await this.cache.store.disconnect().catch((err) => {
|
|
42
|
-
this.LOG._error && this.LOG.error('Error disconnecting from Redis', err);
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
63
|
|
|
48
64
|
this.cache = new Keyv(cacheOptions);
|
|
49
65
|
this.LOG._info && this.LOG.info(`Caching service initialized with namespace ${cacheOptions.namespace}`);
|
|
@@ -218,8 +234,9 @@ class CachingService extends cds.Service {
|
|
|
218
234
|
req.cacheOptions = req.event ? this.extractFunctionCacheOptions(req, arguments[2]) : this.extractEntityCacheOptions(req, arguments[2]);
|
|
219
235
|
req.cacheKey = this.createKey(req, req.cacheOptions.key);
|
|
220
236
|
req.res?.setHeader('x-sap-cap-cache-key', req.cacheKey);
|
|
221
|
-
|
|
222
|
-
|
|
237
|
+
const cachedValue = await this.get(req.cacheKey);
|
|
238
|
+
if (cachedValue) {
|
|
239
|
+
return cachedValue;
|
|
223
240
|
}
|
|
224
241
|
const response = await next();
|
|
225
242
|
req.cacheOptions.tags = this.resolveTags(req.cacheOptions.tags, response, req.params);
|
|
@@ -230,7 +247,7 @@ class CachingService extends cds.Service {
|
|
|
230
247
|
const srv = arguments[1];
|
|
231
248
|
|
|
232
249
|
if (query.SELECT) {
|
|
233
|
-
|
|
250
|
+
|
|
234
251
|
let options = {
|
|
235
252
|
ttl: 0,
|
|
236
253
|
tags: [],
|
|
@@ -277,7 +294,7 @@ class CachingService extends cds.Service {
|
|
|
277
294
|
async set(key, value, options = {}) {
|
|
278
295
|
const wrappedValue = {
|
|
279
296
|
value,
|
|
280
|
-
tags: options.tags || [],
|
|
297
|
+
tags: this.resolveTags(options.tags, value, options.params) || [],
|
|
281
298
|
timestamp: Date.now()
|
|
282
299
|
};
|
|
283
300
|
await this.send('SET', {
|
|
@@ -330,7 +347,7 @@ class CachingService extends cds.Service {
|
|
|
330
347
|
// Iterators
|
|
331
348
|
async *iterator() {
|
|
332
349
|
for await (const [key, value] of this.cache.iterator()) {
|
|
333
|
-
if (typeof value === "string") {
|
|
350
|
+
if (typeof value === "string") {
|
|
334
351
|
yield [key, JSON.parse(value)];
|
|
335
352
|
} else {
|
|
336
353
|
yield [key, value];
|
|
@@ -346,85 +363,77 @@ class CachingService extends cds.Service {
|
|
|
346
363
|
* @returns {string[]} Array of resolved tags
|
|
347
364
|
*/
|
|
348
365
|
resolveTags(tagConfigs = [], data, params = {}) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
// Handle no data/params case
|
|
352
|
-
if (!data && !params) {
|
|
353
|
-
// Only process static tags when no sources are present
|
|
354
|
-
return tagConfigs
|
|
355
|
-
.filter(config => config.value)
|
|
356
|
-
.map(config => config.value);
|
|
357
|
-
}
|
|
366
|
+
// Handle empty/invalid configs
|
|
367
|
+
if (!tagConfigs?.length) return [];
|
|
358
368
|
|
|
359
|
-
// Convert data to array if single object
|
|
360
|
-
const dataArray = data ?
|
|
369
|
+
// Convert data to array if single object or string
|
|
370
|
+
const dataArray = !data ? [] :
|
|
371
|
+
Array.isArray(data) ? data :
|
|
372
|
+
typeof data === 'string' ? [data] : [data];
|
|
361
373
|
|
|
362
374
|
// Process each tag configuration
|
|
363
|
-
tagConfigs.
|
|
375
|
+
const resolvedTags = tagConfigs.flatMap(config => {
|
|
376
|
+
// Handle string tags
|
|
377
|
+
if (typeof config === 'string') {
|
|
378
|
+
return [config];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Handle invalid/empty config objects
|
|
382
|
+
if (!config || typeof config !== 'object') {
|
|
383
|
+
return [];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Handle static value tags
|
|
364
387
|
if (config.value) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
} else if (Array.isArray(config.data)) {
|
|
382
|
-
// Multiple data configuration
|
|
383
|
-
const dataValues = config.data
|
|
384
|
-
.map(data => item[data])
|
|
385
|
-
.filter(Boolean);
|
|
386
|
-
|
|
387
|
-
if (dataValues.length > 0) {
|
|
388
|
-
const combinedValue = dataValues.join(config.separator || ':');
|
|
389
|
-
const tag = [
|
|
390
|
-
config.prefix,
|
|
391
|
-
combinedValue,
|
|
392
|
-
config.suffix
|
|
393
|
-
].filter(Boolean).join('');
|
|
394
|
-
resolvedTags.push(tag);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
} else if (config.param) {
|
|
399
|
-
// Dynamic tags from params
|
|
400
|
-
if (typeof config.param === "string") {
|
|
401
|
-
// Single param configuration
|
|
402
|
-
const paramValue = params[config.param];
|
|
403
|
-
if (paramValue) {
|
|
404
|
-
const tag = [
|
|
405
|
-
config.prefix,
|
|
406
|
-
paramValue,
|
|
407
|
-
config.suffix
|
|
408
|
-
].filter(Boolean).join('');
|
|
409
|
-
resolvedTags.push(tag);
|
|
410
|
-
}
|
|
411
|
-
} else if (Array.isArray(config.param)) {
|
|
412
|
-
// Multiple params configuration
|
|
413
|
-
const paramValues = config.param
|
|
414
|
-
.map(param => params[param])
|
|
388
|
+
const tag = [
|
|
389
|
+
config.prefix,
|
|
390
|
+
config.value,
|
|
391
|
+
config.suffix
|
|
392
|
+
].filter(Boolean).join('');
|
|
393
|
+
return [tag];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Handle data-based tags
|
|
397
|
+
if (config.data && dataArray.length) {
|
|
398
|
+
return dataArray.flatMap(item => {
|
|
399
|
+
if (typeof item !== 'object') return [];
|
|
400
|
+
|
|
401
|
+
const dataFields = Array.isArray(config.data) ? config.data : [config.data];
|
|
402
|
+
const values = dataFields
|
|
403
|
+
.map(field => item[field])
|
|
415
404
|
.filter(Boolean);
|
|
416
405
|
|
|
417
|
-
if (
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
406
|
+
if (!values.length) return [];
|
|
407
|
+
|
|
408
|
+
const value = values.join(config.separator || ':');
|
|
409
|
+
const tag = [
|
|
410
|
+
config.prefix,
|
|
411
|
+
value,
|
|
412
|
+
config.suffix
|
|
413
|
+
].filter(Boolean).join('');
|
|
414
|
+
return [tag];
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Handle param-based tags
|
|
419
|
+
if (config.param) {
|
|
420
|
+
const paramFields = Array.isArray(config.param) ? config.param : [config.param];
|
|
421
|
+
const values = paramFields
|
|
422
|
+
.map(field => params[field])
|
|
423
|
+
.filter(Boolean);
|
|
424
|
+
|
|
425
|
+
if (!values.length) return [];
|
|
426
|
+
|
|
427
|
+
const value = values.join(config.separator || ':');
|
|
428
|
+
const tag = [
|
|
429
|
+
config.prefix,
|
|
430
|
+
value,
|
|
431
|
+
config.suffix
|
|
432
|
+
].filter(Boolean).join('');
|
|
433
|
+
return [tag];
|
|
427
434
|
}
|
|
435
|
+
|
|
436
|
+
return [];
|
|
428
437
|
});
|
|
429
438
|
|
|
430
439
|
// Remove duplicates
|
|
@@ -451,13 +460,13 @@ class CachingService extends cds.Service {
|
|
|
451
460
|
case "Request":
|
|
452
461
|
case "NoaRequest":
|
|
453
462
|
|
|
454
|
-
return this.createCacheKey((!options.value && !options.template) ? { template: '{tenant}:{user}:{locale}:{hash}' } : options, {
|
|
455
|
-
req: keyOrObject,
|
|
456
|
-
params: keyOrObject.params,
|
|
457
|
-
data: keyOrObject.data,
|
|
458
|
-
locale: keyOrObject.locale,
|
|
459
|
-
user: keyOrObject.user.id,
|
|
460
|
-
tenant: keyOrObject.tenant
|
|
463
|
+
return this.createCacheKey((!options.value && !options.template) ? { template: '{tenant}:{user}:{locale}:{hash}' } : options, {
|
|
464
|
+
req: keyOrObject,
|
|
465
|
+
params: keyOrObject.params,
|
|
466
|
+
data: keyOrObject.data,
|
|
467
|
+
locale: keyOrObject.locale,
|
|
468
|
+
user: keyOrObject.user.id,
|
|
469
|
+
tenant: keyOrObject.tenant
|
|
461
470
|
});
|
|
462
471
|
case "cds.ql":
|
|
463
472
|
if (keyOrObject.SELECT) {
|