@cap-js-community/common 0.3.5 → 0.4.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/CHANGELOG.md +7 -0
- package/README.md +31 -2
- package/bin/cdm-build.js +5 -1
- package/package.json +9 -9
- package/src/cdm-build/CDMBuilder.js +4 -3
- package/src/redis-client/RedisClient.js +100 -34
- package/src/replication-cache/ReplicationCache.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## Version 0.4.0 - 2026-03-10
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Redis Sentinel Support
|
|
13
|
+
- Open dependencies to allow Redis 5
|
|
14
|
+
|
|
8
15
|
## Version 0.3.5 - 2026-02-03
|
|
9
16
|
|
|
10
17
|
### Fixed
|
package/README.md
CHANGED
|
@@ -399,6 +399,31 @@ const mainClient = await RedisClient.create().createMainClientAndConnect(options
|
|
|
399
399
|
|
|
400
400
|
For details on Redis `createClient` configuration options see [Redis Client Configuration](https://github.com/redis/node-redis/blob/master/docs/client-configuration.md).
|
|
401
401
|
|
|
402
|
+
### Redis Sentinel Support
|
|
403
|
+
|
|
404
|
+
Redis Sentinel mode is activated when `credentials.sentinel_nodes` is present and takes priority over cluster mode.
|
|
405
|
+
|
|
406
|
+
```json
|
|
407
|
+
{
|
|
408
|
+
"cds": {
|
|
409
|
+
"requires": {
|
|
410
|
+
"redis": {
|
|
411
|
+
"credentials": {
|
|
412
|
+
"sentinel_nodes": [
|
|
413
|
+
{ "host": "sentinel1.example.com", "port": 26379 },
|
|
414
|
+
{ "host": "sentinel2.example.com", "port": 26379 }
|
|
415
|
+
],
|
|
416
|
+
"master_name": "myprimary"
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
The `master_name` identifies which Redis master the Sentinels monitor. Alternatively, the master name can be provided as a URI fragment in `credentials.uri` (e.g., `redis://host#myprimary`).
|
|
425
|
+
If `sentinel_nodes[].port` is omitted, it defaults to `26379`. Both `host` and `hostname` are accepted for node addresses.
|
|
426
|
+
|
|
402
427
|
## Local HTML5 Repository
|
|
403
428
|
|
|
404
429
|
Developing HTML5 apps against hybrid environments including Approuter component requires a local HTML5 repository to directly test the changes to UI5 applications without deployment to a remote HTML5 repository.
|
|
@@ -432,8 +457,12 @@ The CDM Builder allows to build a CDM file `cdm.json` from apps, roles and porta
|
|
|
432
457
|
|
|
433
458
|
- Build CDM: `cdm-build`
|
|
434
459
|
|
|
435
|
-
The generated CDM is generated at `app/cdm.json
|
|
436
|
-
|
|
460
|
+
The generated CDM is (per default) generated at `app/cdm.json`.
|
|
461
|
+
|
|
462
|
+
### Options:
|
|
463
|
+
|
|
464
|
+
- `-f, --force`: Overwrite existing CDM file. Default is `false`
|
|
465
|
+
- `-t, --target`: Specify target path for generated CDM file. Default is `app/cdm.json`
|
|
437
466
|
|
|
438
467
|
## Support, Feedback, Contributing
|
|
439
468
|
|
package/bin/cdm-build.js
CHANGED
|
@@ -15,7 +15,11 @@ process.argv = process.argv.map((arg) => {
|
|
|
15
15
|
return arg.toLowerCase();
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
program
|
|
18
|
+
program
|
|
19
|
+
.version(packageJSON.version, "-v, --version")
|
|
20
|
+
.usage("[options]")
|
|
21
|
+
.option("-f, --force", "Force generation")
|
|
22
|
+
.option("-t, --target <target>", "Target path");
|
|
19
23
|
|
|
20
24
|
program.unknownOption = function () {};
|
|
21
25
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/common",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "CAP Node.js Community Common",
|
|
5
5
|
"homepage": "https://cap.cloud.sap/",
|
|
6
6
|
"engines": {
|
|
@@ -53,21 +53,21 @@
|
|
|
53
53
|
"commander": "^14.0.3",
|
|
54
54
|
"express": "^4.22.1 || ^5.2.1",
|
|
55
55
|
"http-proxy-middleware": "^3.0.5",
|
|
56
|
-
"redis": "^4.7.1",
|
|
56
|
+
"redis": "^4.7.1 || ^5.11.0",
|
|
57
57
|
"verror": "^1.10.1"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@cap-js-community/common": "./",
|
|
61
61
|
"@cap-js/cds-test": "^0.4.1",
|
|
62
|
-
"@cap-js/sqlite": "^2.
|
|
63
|
-
"@sap/cds": "^9.
|
|
62
|
+
"@cap-js/sqlite": "^2.2.0",
|
|
63
|
+
"@sap/cds": "^9.8.1",
|
|
64
64
|
"@sap/cds-common-content": "^3.1.0",
|
|
65
|
-
"@sap/cds-dk": "^9.7.
|
|
66
|
-
"eslint": "^
|
|
65
|
+
"@sap/cds-dk": "^9.7.2",
|
|
66
|
+
"eslint": "^10.0.3",
|
|
67
67
|
"eslint-config-prettier": "^10.1.8",
|
|
68
|
-
"eslint-plugin-jest": "^29.
|
|
69
|
-
"eslint-plugin-n": "^17.
|
|
70
|
-
"jest": "^30.
|
|
68
|
+
"eslint-plugin-jest": "^29.15.0",
|
|
69
|
+
"eslint-plugin-n": "^17.24.0",
|
|
70
|
+
"jest": "^30.3.0",
|
|
71
71
|
"jest-html-reporters": "^3.1.7",
|
|
72
72
|
"jest-junit": "^16.0.0",
|
|
73
73
|
"prettier": "^3.8.1",
|
|
@@ -64,7 +64,7 @@ class CDMBuilder {
|
|
|
64
64
|
this.addRoles(cdm, roles);
|
|
65
65
|
if (!this.options.skipWrite) {
|
|
66
66
|
fs.mkdirSync(path.dirname(this.targetPath), { recursive: true });
|
|
67
|
-
fs.writeFileSync(this.targetPath, JSON.stringify(cdm, null, 2));
|
|
67
|
+
fs.writeFileSync(this.targetPath, JSON.stringify(Object.values(cdm.payload).flat(), null, 2));
|
|
68
68
|
}
|
|
69
69
|
return cdm;
|
|
70
70
|
}
|
|
@@ -171,7 +171,7 @@ class CDMBuilder {
|
|
|
171
171
|
cdm.payload.spaces.push(space);
|
|
172
172
|
let index = 1;
|
|
173
173
|
for (const group of cdm.payload.groups || []) {
|
|
174
|
-
const pageId = `${this.namespace}-page${cdm.payload.groups.length > 1 ? `-${index++}` : ""}
|
|
174
|
+
const pageId = `${this.namespace}-page${cdm.payload.groups.length > 1 ? `-${index++}` : ""}`;
|
|
175
175
|
cdm.payload.pages.push({
|
|
176
176
|
_version: this.version,
|
|
177
177
|
identification: {
|
|
@@ -221,11 +221,12 @@ class CDMBuilder {
|
|
|
221
221
|
},
|
|
222
222
|
},
|
|
223
223
|
tags: {
|
|
224
|
+
keywords: [],
|
|
224
225
|
technicalAttributes: ["APPTYPE_HOMEPAGE"],
|
|
225
226
|
},
|
|
226
227
|
},
|
|
227
228
|
"sap.integration": {
|
|
228
|
-
urlTemplateId: `${this.namespace}-urltemplate
|
|
229
|
+
urlTemplateId: `${this.namespace}-urltemplate-home`,
|
|
229
230
|
urlTemplateParams: { path: "" },
|
|
230
231
|
},
|
|
231
232
|
"sap.ui": {
|
|
@@ -9,6 +9,7 @@ const TIMEOUT_SHUTDOWN = 2500;
|
|
|
9
9
|
|
|
10
10
|
class RedisClient {
|
|
11
11
|
#clusterClient = false;
|
|
12
|
+
#sentinelClient = false;
|
|
12
13
|
#beforeCloseHandler;
|
|
13
14
|
constructor(name, env) {
|
|
14
15
|
this.name = name;
|
|
@@ -51,27 +52,26 @@ class RedisClient {
|
|
|
51
52
|
async createClientAndConnect(options, errorHandlerCreateClient, isConnectionCheck) {
|
|
52
53
|
try {
|
|
53
54
|
const client = this.createClientBase(options);
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
});
|
|
55
|
+
if (client) {
|
|
56
|
+
if (!isConnectionCheck) {
|
|
57
|
+
client.on("error", (err) => {
|
|
58
|
+
const dateNow = Date.now();
|
|
59
|
+
if (dateNow - this.lastErrorLog > LOG_AFTER_SEC * 1000) {
|
|
60
|
+
this.log.error("Error from redis client", err);
|
|
61
|
+
this.lastErrorLog = dateNow;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
65
|
+
client.on("reconnecting", () => {
|
|
66
|
+
const dateNow = Date.now();
|
|
67
|
+
if (dateNow - this.lastErrorLog > LOG_AFTER_SEC * 1000) {
|
|
68
|
+
this.log.info("Redis client trying reconnect...");
|
|
69
|
+
this.lastErrorLog = dateNow;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
await client.connect();
|
|
73
74
|
}
|
|
74
|
-
await client.connect();
|
|
75
75
|
return client;
|
|
76
76
|
} catch (err) {
|
|
77
77
|
errorHandlerCreateClient(err);
|
|
@@ -105,20 +105,24 @@ class RedisClient {
|
|
|
105
105
|
createClientBase(redisOptions = {}) {
|
|
106
106
|
const { credentials, options } =
|
|
107
107
|
(this.env ? cds.env.requires[`redis-${this.env}`] : undefined) || cds.env.requires["redis"] || {};
|
|
108
|
-
|
|
109
|
-
host: credentials?.hostname ?? "127.0.0.1",
|
|
110
|
-
tls: !!credentials?.tls,
|
|
111
|
-
port: credentials?.port ?? 6379,
|
|
112
|
-
...options?.socket,
|
|
113
|
-
...redisOptions.socket,
|
|
114
|
-
};
|
|
115
|
-
const socketOptions = {
|
|
116
|
-
...options,
|
|
117
|
-
...redisOptions,
|
|
118
|
-
password: redisOptions?.password ?? options?.password ?? credentials?.password,
|
|
119
|
-
socket,
|
|
120
|
-
};
|
|
108
|
+
|
|
121
109
|
try {
|
|
110
|
+
if (credentials?.sentinel_nodes?.length > 0) {
|
|
111
|
+
return this.createSentinelClient(credentials, options, redisOptions);
|
|
112
|
+
}
|
|
113
|
+
const socket = {
|
|
114
|
+
host: credentials?.hostname ?? "127.0.0.1",
|
|
115
|
+
tls: !!credentials?.tls,
|
|
116
|
+
port: credentials?.port ?? 6379,
|
|
117
|
+
...options?.socket,
|
|
118
|
+
...redisOptions.socket,
|
|
119
|
+
};
|
|
120
|
+
const socketOptions = {
|
|
121
|
+
...options,
|
|
122
|
+
...redisOptions,
|
|
123
|
+
password: redisOptions?.password ?? options?.password ?? credentials?.password,
|
|
124
|
+
socket,
|
|
125
|
+
};
|
|
122
126
|
if (credentials?.cluster_mode) {
|
|
123
127
|
this.#clusterClient = true;
|
|
124
128
|
return redis.createCluster({
|
|
@@ -128,8 +132,61 @@ class RedisClient {
|
|
|
128
132
|
}
|
|
129
133
|
return redis.createClient(socketOptions);
|
|
130
134
|
} catch (err) {
|
|
131
|
-
throw new Error("Error during create client with redis-cache service"
|
|
135
|
+
throw new Error("Error during create client with redis-cache service", err);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
createSentinelClient(credentials, options, redisOptions) {
|
|
140
|
+
const masterName = this.extractMasterName(credentials);
|
|
141
|
+
const sentinelNodes = credentials.sentinel_nodes.map((node) => ({
|
|
142
|
+
host: node.host ?? node.hostname,
|
|
143
|
+
port: node.port ?? 26379,
|
|
144
|
+
}));
|
|
145
|
+
const clientOptions = {
|
|
146
|
+
...options,
|
|
147
|
+
...redisOptions,
|
|
148
|
+
password: redisOptions?.password ?? options?.password ?? credentials?.password,
|
|
149
|
+
socket: {
|
|
150
|
+
tls: !!credentials?.tls,
|
|
151
|
+
...options?.socket,
|
|
152
|
+
...redisOptions?.socket,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
this.#sentinelClient = true;
|
|
156
|
+
this.log.info("Creating Redis Sentinel client", { masterName, nodeCount: sentinelNodes.length });
|
|
157
|
+
return redis.createSentinel({
|
|
158
|
+
name: masterName,
|
|
159
|
+
sentinelRootNodes: sentinelNodes,
|
|
160
|
+
nodeClientOptions: clientOptions,
|
|
161
|
+
sentinelClientOptions: clientOptions,
|
|
162
|
+
passthroughClientErrorEvents: true,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Extracts the Sentinel master name from credentials.
|
|
168
|
+
* Priority: master_name field > URI fragment
|
|
169
|
+
* @param {Object} credentials - Redis credentials
|
|
170
|
+
* @returns {string} Master name
|
|
171
|
+
* @throws {Error} If master name cannot be determined
|
|
172
|
+
*/
|
|
173
|
+
extractMasterName(credentials) {
|
|
174
|
+
if (credentials?.master_name) {
|
|
175
|
+
return credentials.master_name;
|
|
176
|
+
}
|
|
177
|
+
if (credentials?.uri) {
|
|
178
|
+
try {
|
|
179
|
+
const url = new URL(credentials.uri);
|
|
180
|
+
if (url.hash && url.hash.length > 1) {
|
|
181
|
+
return url.hash.slice(1);
|
|
182
|
+
}
|
|
183
|
+
} catch (e) {
|
|
184
|
+
this.log.warn("Failed to parse master name from URI", e.message);
|
|
185
|
+
}
|
|
132
186
|
}
|
|
187
|
+
throw new Error(
|
|
188
|
+
"Redis Sentinel master name not found. Provide credentials.master_name or include #mastername in credentials.uri",
|
|
189
|
+
);
|
|
133
190
|
}
|
|
134
191
|
|
|
135
192
|
subscribeChannel(options, channel, subscribeHandler) {
|
|
@@ -149,6 +206,9 @@ class RedisClient {
|
|
|
149
206
|
subscribeChannels(options, errorHandlerCreateClient) {
|
|
150
207
|
this.subscriberClientPromise = this.createClientAndConnect(options, errorHandlerCreateClient)
|
|
151
208
|
.then((client) => {
|
|
209
|
+
if (!client) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
152
212
|
for (const channel in this.subscribedChannels) {
|
|
153
213
|
const fn = this.subscribedChannels[channel];
|
|
154
214
|
client._subscribedChannels ??= {};
|
|
@@ -214,7 +274,9 @@ class RedisClient {
|
|
|
214
274
|
|
|
215
275
|
async resilientClientClose(client) {
|
|
216
276
|
try {
|
|
217
|
-
if (client?.
|
|
277
|
+
if (client?.close) {
|
|
278
|
+
await client.close();
|
|
279
|
+
} else if (client?.quit) {
|
|
218
280
|
await client.quit();
|
|
219
281
|
}
|
|
220
282
|
} catch (err) {
|
|
@@ -248,6 +310,10 @@ class RedisClient {
|
|
|
248
310
|
return this.#clusterClient;
|
|
249
311
|
}
|
|
250
312
|
|
|
313
|
+
get isSentinel() {
|
|
314
|
+
return this.#sentinelClient;
|
|
315
|
+
}
|
|
316
|
+
|
|
251
317
|
static create(name = "default", env) {
|
|
252
318
|
env ??= name;
|
|
253
319
|
RedisClient._create ??= {};
|
|
@@ -741,7 +741,7 @@ class ReplicationCacheEntry {
|
|
|
741
741
|
selectQuery.replication = true;
|
|
742
742
|
const chunkSize = this.cache.options.chunks;
|
|
743
743
|
let offset = 0;
|
|
744
|
-
let entries
|
|
744
|
+
let entries;
|
|
745
745
|
do {
|
|
746
746
|
entries = await srcTx.run(selectQuery.limit(chunkSize, offset));
|
|
747
747
|
if (entries.length > 0) {
|