@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 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` and can be included into HTML5 Repository automatically
436
- when copied at `resources/cdm.json` during build time.
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.version(packageJSON.version, "-v, --version").usage("[options]").option("-f, --force", "Force generation");
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.5",
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.1.2",
63
- "@sap/cds": "^9.7.0",
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.0",
66
- "eslint": "^9.39.2",
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.12.1",
69
- "eslint-plugin-n": "^17.23.2",
70
- "jest": "^30.2.0",
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.home`,
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 (!client) {
55
- return;
56
- }
57
- if (!isConnectionCheck) {
58
- client.on("error", (err) => {
59
- const dateNow = Date.now();
60
- if (dateNow - this.lastErrorLog > LOG_AFTER_SEC * 1000) {
61
- this.log.error("Error from redis client", err);
62
- this.lastErrorLog = dateNow;
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
- client.on("reconnecting", () => {
67
- const dateNow = Date.now();
68
- if (dateNow - this.lastErrorLog > LOG_AFTER_SEC * 1000) {
69
- this.log.info("Redis client trying reconnect...");
70
- this.lastErrorLog = dateNow;
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
- const socket = {
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" + err);
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?.quit) {
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) {