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 +0 -0
- package/examples/example.json +0 -0
- package/package.json +1 -1
- package/redis-variable-config.html +0 -0
- package/redis-variable-config.js +112 -4
- package/redis-variable.html +8 -1
- package/redis-variable.js +103 -47
package/README.md
CHANGED
|
File without changes
|
package/examples/example.json
CHANGED
|
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
|
+
"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
|
package/redis-variable-config.js
CHANGED
|
@@ -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
|
-
//
|
|
325
|
-
const
|
|
326
|
-
|
|
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
|
-
|
|
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
|
package/redis-variable.html
CHANGED
|
@@ -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 (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|