node-red-contrib-redis-variable 1.3.0 → 1.4.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
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-redis-variable",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "A comprehensive Node-RED node for Redis operations with universal payload-based configuration, automatic JSON handling, SSL/TLS support, and advanced pattern matching with pagination",
5
5
  "keywords": [
6
6
  "node-red",
File without changes
@@ -157,6 +157,51 @@ module.exports = function (RED) {
157
157
  }
158
158
  };
159
159
 
160
+ // Check if configuration exists in context
161
+ this.hasValidConfiguration = function(msg, executingNode) {
162
+ try {
163
+ // If using string configuration for host and port, it's always valid
164
+ if (this.hostType === 'str' && this.portType === 'str') {
165
+ return true;
166
+ }
167
+
168
+ // For context-based configuration, check if the required values exist
169
+ let hasHost = false;
170
+ let hasPort = false;
171
+
172
+ // Check host configuration
173
+ if (this.hostType === 'str') {
174
+ hasHost = !!(this.host);
175
+ } else {
176
+ const contextHost = this.parseCredentialValue(this.hostContext, this.hostType, msg, executingNode);
177
+ hasHost = !!(contextHost);
178
+ }
179
+
180
+ // Check port configuration
181
+ if (this.portType === 'str') {
182
+ hasPort = !!(this.port);
183
+ } else {
184
+ const contextPort = this.parseCredentialValue(this.portContext, this.portType, msg, executingNode);
185
+ hasPort = !!(contextPort);
186
+ }
187
+
188
+ // We need at least host and port to have a valid configuration
189
+ const result = hasHost && hasPort;
190
+
191
+ if (executingNode && process.env.NODE_RED_DEBUG) {
192
+ executingNode.log(`Configuration check - hasHost: ${hasHost}, hasPort: ${hasPort}, result: ${result}`);
193
+ }
194
+
195
+ return result;
196
+
197
+ } catch (error) {
198
+ if (executingNode) {
199
+ executingNode.error(`Error checking configuration: ${error.message}`);
200
+ }
201
+ return false;
202
+ }
203
+ };
204
+
160
205
  // Get Redis connection options
161
206
  this.getConnectionOptions = function(msg, executingNode) {
162
207
  try {
@@ -301,6 +346,25 @@ module.exports = function (RED) {
301
346
  return connections[id];
302
347
  }
303
348
 
349
+ // Check if configuration is available before attempting connection
350
+ if (!this.hasValidConfiguration(msg, executingNode)) {
351
+ if (executingNode) {
352
+ // Only warn once per node to avoid spam
353
+ if (!executingNode._configWarningShown) {
354
+ executingNode.warn("Redis configuration not available in context. Skipping connection attempt.");
355
+ executingNode._configWarningShown = true;
356
+
357
+ // Reset warning flag after 30 seconds
358
+ setTimeout(() => {
359
+ if (executingNode) {
360
+ executingNode._configWarningShown = false;
361
+ }
362
+ }, 30000);
363
+ }
364
+ }
365
+ return null;
366
+ }
367
+
304
368
  const options = this.getConnectionOptions(msg, executingNode);
305
369
 
306
370
  // Add connection limits to prevent infinite retry loops
@@ -318,14 +382,58 @@ module.exports = function (RED) {
318
382
  maxRetriesPerRequest: 0
319
383
  };
320
384
 
385
+ // Create Redis client
321
386
  // Create Redis client
322
387
  let client;
323
388
  if (this.cluster) {
324
- // For cluster mode, options should be an array of nodes
325
- const clusterNodes = Array.isArray(connectionOptions) ? connectionOptions : [connectionOptions];
326
- client = new Redis.Cluster(clusterNodes);
389
+ // Prepare cluster node(s)
390
+ const host = options.host;
391
+ const port = parseInt(options.port);
392
+
393
+ // AWS ElastiCache cluster connection (TLS-safe)
394
+ const clusterNodes = [{ host, port }];
395
+
396
+ const clusterOptions = {
397
+ scaleReads: "slave",
398
+ enableReadyCheck: false,
399
+ slotsRefreshTimeout: 5000,
400
+ redisOptions: {
401
+ username: options.username || undefined,
402
+ password: options.password || undefined,
403
+ db: options.db || 0,
404
+ connectTimeout: 5000,
405
+ maxRetriesPerRequest: 3,
406
+ retryStrategy: (times) => Math.min(times * 200, 2000),
407
+ tls: this.enableTLS
408
+ ? {
409
+ servername: host,
410
+ rejectUnauthorized: this.tlsRejectUnauthorized,
411
+ ca: this.credentials?.tlsCa || undefined,
412
+ cert: this.credentials?.tlsCert || undefined,
413
+ key: this.credentials?.tlsKey || undefined,
414
+ }
415
+ : undefined,
416
+ },
417
+ };
418
+
419
+ client = new Redis.Cluster(clusterNodes, clusterOptions);
327
420
  } else {
328
- client = new Redis(connectionOptions);
421
+ // Non-cluster (single node)
422
+ client = new Redis({
423
+ ...options,
424
+ enableReadyCheck: false,
425
+ connectTimeout: 5000,
426
+ maxRetriesPerRequest: 3,
427
+ tls: this.enableTLS
428
+ ? {
429
+ servername: options.host,
430
+ rejectUnauthorized: this.tlsRejectUnauthorized,
431
+ ca: this.credentials?.tlsCa || undefined,
432
+ cert: this.credentials?.tlsCert || undefined,
433
+ key: this.credentials?.tlsKey || undefined,
434
+ }
435
+ : undefined,
436
+ });
329
437
  }
330
438
 
331
439
  // Track error state to prevent spam
@@ -5,7 +5,8 @@
5
5
  defaults: {
6
6
  name: { value: "" },
7
7
  redisConfig: { value: "", type: "redis-variable-config" },
8
- operation: { value: "get" }
8
+ operation: { value: "get" },
9
+ topic: { value: "" }
9
10
  },
10
11
  inputs: 1,
11
12
  outputs: 1,
@@ -62,9 +63,15 @@
62
63
  </optgroup>
63
64
  <optgroup label="Pub/Sub Operations">
64
65
  <option value="publish">PUBLISH - Publish message</option>
66
+ <option value="subscribe">SUBSCRIBE - Subscribe to channel</option>
67
+ <option value="psubscribe">PSUBSCRIBE - Pattern subscribe</option>
65
68
  </optgroup>
66
69
  </select>
67
70
  </div>
71
+ <div class="form-row">
72
+ <label for="node-input-topic"><i class="fa fa-hashtag"></i> Channel/Pattern</label>
73
+ <input type="text" id="node-input-topic" placeholder="notifications or news.*">
74
+ </div>
68
75
  </script>
69
76
 
70
77
  <script type="text/x-red" data-help-name="redis-variable">
package/redis-variable.js CHANGED
@@ -14,6 +14,7 @@ module.exports = function (RED) {
14
14
  this.stored = config.stored || false;
15
15
  this.params = config.params;
16
16
  this.location = config.location || 'flow';
17
+ this.topic = config.topic || "";
17
18
  this.sha1 = "";
18
19
 
19
20
  // Save redisConfig once in the constructor
@@ -75,56 +76,89 @@ module.exports = function (RED) {
75
76
  // Subscription operations (subscribe, psubscribe)
76
77
  function handleSubscription() {
77
78
  try {
78
- if (node.operation === "psubscribe") {
79
- client.on("pmessage", function (pattern, channel, message) {
80
- var payload = smartParse(message);
81
- node.send({
82
- pattern: pattern,
83
- topic: channel,
84
- payload: payload,
85
- });
86
- });
87
- client[node.operation](node.topic, (err, count) => {
88
- if (err) {
89
- node.error(err.message);
90
- node.status({
91
- fill: "red",
92
- shape: "dot",
93
- text: "error",
79
+ if (!redisConfig) {
80
+ node.status({ fill: "yellow", shape: "ring", text: "no config" });
81
+ node.warn("Redis configuration not found for subscription.");
82
+ return;
83
+ }
84
+
85
+ if (!node.topic) {
86
+ node.status({ fill: "red", shape: "dot", text: "missing channel" });
87
+ node.error("Missing channel/pattern for subscription (topic)");
88
+ return;
89
+ }
90
+
91
+ client = redisConfig.getClient(null, node, node.id);
92
+ if (!client) {
93
+ node.status({ fill: "yellow", shape: "ring", text: "no config" });
94
+ node.warn("Redis configuration not available. Subscription skipped.");
95
+ return;
96
+ }
97
+
98
+ const setup = async () => {
99
+ try {
100
+ if (client.status !== 'ready' && client.status !== 'connect') {
101
+ await client.connect();
102
+ }
103
+
104
+ const topics = (node.topic || "").split(/[\s,]+/).filter(Boolean);
105
+
106
+ if (node.operation === "psubscribe") {
107
+ client.on("pmessage", function (pattern, channel, message) {
108
+ var payload = smartParse(message);
109
+ node.send({
110
+ pattern: pattern,
111
+ topic: channel,
112
+ payload: payload,
113
+ });
94
114
  });
95
- } else {
96
- node.status({
97
- fill: "green",
98
- shape: "dot",
99
- text: "connected",
115
+ client.psubscribe(...topics, (err) => {
116
+ if (err) {
117
+ // Fallback: if server doesn't support PSUBSCRIBE and no wildcard is used, try SUBSCRIBE
118
+ if (err.message && err.message.toLowerCase().includes('unknown command')) {
119
+ const hasWildcard = topics.some(t => t.includes('*'));
120
+ if (!hasWildcard) {
121
+ client.subscribe(...topics, (subErr) => {
122
+ if (subErr) {
123
+ node.error(subErr.message);
124
+ node.status({ fill: "red", shape: "dot", text: "error" });
125
+ } else {
126
+ node.status({ fill: "green", shape: "dot", text: "connected" });
127
+ }
128
+ });
129
+ return;
130
+ }
131
+ }
132
+ node.error(err.message);
133
+ node.status({ fill: "red", shape: "dot", text: "error" });
134
+ } else {
135
+ node.status({ fill: "green", shape: "dot", text: "connected" });
136
+ }
100
137
  });
101
- }
102
- });
103
- } else if (node.operation === "subscribe") {
104
- client.on("message", function (channel, message) {
105
- var payload = smartParse(message);
106
- node.send({
107
- topic: channel,
108
- payload: payload,
109
- });
110
- });
111
- client[node.operation](node.topic, (err, count) => {
112
- if (err) {
113
- node.error(err.message);
114
- node.status({
115
- fill: "red",
116
- shape: "dot",
117
- text: "error",
138
+ } else if (node.operation === "subscribe") {
139
+ client.on("message", function (channel, message) {
140
+ var payload = smartParse(message);
141
+ node.send({
142
+ topic: channel,
143
+ payload: payload,
144
+ });
118
145
  });
119
- } else {
120
- node.status({
121
- fill: "green",
122
- shape: "dot",
123
- text: "connected",
146
+ client.subscribe(...topics, (err) => {
147
+ if (err) {
148
+ node.error(err.message);
149
+ node.status({ fill: "red", shape: "dot", text: "error" });
150
+ } else {
151
+ node.status({ fill: "green", shape: "dot", text: "connected" });
152
+ }
124
153
  });
125
154
  }
126
- });
127
- }
155
+ } catch (e) {
156
+ node.error(`Subscription setup failed: ${e.message}`);
157
+ node.status({ fill: "red", shape: "dot", text: "error" });
158
+ }
159
+ };
160
+
161
+ setup();
128
162
  } catch (error) {
129
163
  node.error(`Subscription error: ${error.message}`);
130
164
  node.status({
@@ -280,7 +314,12 @@ module.exports = function (RED) {
280
314
  if (!client) {
281
315
  client = redisConfig.getClient(msg, node, node.id);
282
316
  if (!client) {
283
- throw new Error("Failed to initialize Redis client");
317
+ node.warn("Redis configuration not available. Operation skipped.");
318
+ node.status({ fill: "yellow", shape: "ring", text: "no config" });
319
+ msg.payload = { error: "Redis configuration not available" };
320
+ send(msg);
321
+ done();
322
+ return;
284
323
  }
285
324
  }
286
325
 
@@ -296,7 +335,12 @@ module.exports = function (RED) {
296
335
  client = null;
297
336
  client = redisConfig.getClient(msg, node, node.id);
298
337
  if (!client) {
299
- throw new Error("Failed to recreate Redis client");
338
+ node.warn("Redis configuration not available during reconnection. Operation skipped.");
339
+ node.status({ fill: "yellow", shape: "ring", text: "no config" });
340
+ msg.payload = { error: "Redis configuration not available" };
341
+ send(msg);
342
+ done();
343
+ return;
300
344
  }
301
345
  } catch (reconnectError) {
302
346
  throw new Error(`Failed to reconnect: ${reconnectError.message}`);
@@ -728,6 +772,18 @@ module.exports = function (RED) {
728
772
  }
729
773
  }
730
774
 
775
+ if (client) {
776
+ try {
777
+ if (node.operation === 'subscribe' && node.topic) {
778
+ await client.unsubscribe(node.topic);
779
+ } else if (node.operation === 'psubscribe' && node.topic) {
780
+ await client.punsubscribe(node.topic);
781
+ }
782
+ } catch (e) {
783
+ // ignore
784
+ }
785
+ }
786
+
731
787
  if (redisConfig) {
732
788
  const nodeId = node.block ? node.id : redisConfig.id;
733
789
  redisConfig.disconnect(nodeId);