node-red-contrib-redis-variable 1.4.1 → 1.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-redis-variable",
3
- "version": "1.4.1",
3
+ "version": "1.5.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",
@@ -43,11 +43,11 @@ module.exports = function (RED) {
43
43
  // Helper function to get nested values like "redis_config.host"
44
44
  function getNestedValue(context, path) {
45
45
  if (!context) return undefined;
46
-
46
+
47
47
  if (path.includes('.')) {
48
48
  const parts = path.split('.');
49
49
  let result = context.get(parts[0]);
50
-
50
+
51
51
  // If the first part returns an object, traverse it
52
52
  if (result && typeof result === 'object') {
53
53
  for (let i = 1; i < parts.length; i++) {
@@ -71,7 +71,7 @@ module.exports = function (RED) {
71
71
  RED.nodes.createNode(this, config);
72
72
  this.name = config.name || "Redis Config";
73
73
  this.cluster = config.cluster || false;
74
-
74
+
75
75
  // Connection configuration
76
76
  this.hostType = config.hostType || 'str';
77
77
  this.host = config.host || 'localhost';
@@ -79,7 +79,7 @@ module.exports = function (RED) {
79
79
  this.port = config.port || 6379;
80
80
  this.portType = config.portType || 'str';
81
81
  this.portContext = config.portContext;
82
-
82
+
83
83
  // Authentication
84
84
  this.passwordType = config.passwordType || 'str';
85
85
  this.password = config.password;
@@ -87,7 +87,7 @@ module.exports = function (RED) {
87
87
  this.username = config.username;
88
88
  this.usernameType = config.usernameType || 'str';
89
89
  this.usernameContext = config.usernameContext;
90
-
90
+
91
91
  // SSL/TLS Configuration
92
92
  this.enableTLS = config.enableTLS || false;
93
93
  this.tlsRejectUnauthorized = config.tlsRejectUnauthorized !== false; // Default to true
@@ -97,12 +97,12 @@ module.exports = function (RED) {
97
97
  this.tlsKeyContext = config.tlsKeyContext;
98
98
  this.tlsCaType = config.tlsCaType || 'str';
99
99
  this.tlsCaContext = config.tlsCaContext;
100
-
100
+
101
101
  // Database and other options
102
102
  this.database = config.database || 0;
103
103
  this.databaseType = config.databaseType || 'str';
104
104
  this.databaseContext = config.databaseContext;
105
-
105
+
106
106
  // Advanced options
107
107
  this.optionsType = config.optionsType || 'json';
108
108
  this.options = config.options || '{}';
@@ -116,7 +116,7 @@ module.exports = function (RED) {
116
116
  if (!value && value !== 0) {
117
117
  return null;
118
118
  }
119
-
119
+
120
120
  try {
121
121
  let result;
122
122
  switch (type) {
@@ -164,11 +164,11 @@ module.exports = function (RED) {
164
164
  if (this.hostType === 'str' && this.portType === 'str') {
165
165
  return true;
166
166
  }
167
-
167
+
168
168
  // For context-based configuration, check if the required values exist
169
169
  let hasHost = false;
170
170
  let hasPort = false;
171
-
171
+
172
172
  // Check host configuration
173
173
  if (this.hostType === 'str') {
174
174
  hasHost = !!(this.host);
@@ -176,7 +176,7 @@ module.exports = function (RED) {
176
176
  const contextHost = this.parseCredentialValue(this.hostContext, this.hostType, msg, executingNode);
177
177
  hasHost = !!(contextHost);
178
178
  }
179
-
179
+
180
180
  // Check port configuration
181
181
  if (this.portType === 'str') {
182
182
  hasPort = !!(this.port);
@@ -184,16 +184,16 @@ module.exports = function (RED) {
184
184
  const contextPort = this.parseCredentialValue(this.portContext, this.portType, msg, executingNode);
185
185
  hasPort = !!(contextPort);
186
186
  }
187
-
187
+
188
188
  // We need at least host and port to have a valid configuration
189
189
  const result = hasHost && hasPort;
190
-
190
+
191
191
  if (executingNode && process.env.NODE_RED_DEBUG) {
192
192
  executingNode.log(`Configuration check - hasHost: ${hasHost}, hasPort: ${hasPort}, result: ${result}`);
193
193
  }
194
-
194
+
195
195
  return result;
196
-
196
+
197
197
  } catch (error) {
198
198
  if (executingNode) {
199
199
  executingNode.error(`Error checking configuration: ${error.message}`);
@@ -339,7 +339,7 @@ module.exports = function (RED) {
339
339
  this.getClient = function(msg, executingNode, nodeId) {
340
340
  try {
341
341
  const id = nodeId || this.id;
342
-
342
+
343
343
  // Return existing connection if available
344
344
  if (connections[id]) {
345
345
  usedConn[id]++;
@@ -353,7 +353,7 @@ module.exports = function (RED) {
353
353
  if (!executingNode._configWarningShown) {
354
354
  executingNode.warn("Redis configuration not available in context. Skipping connection attempt.");
355
355
  executingNode._configWarningShown = true;
356
-
356
+
357
357
  // Reset warning flag after 30 seconds
358
358
  setTimeout(() => {
359
359
  if (executingNode) {
@@ -382,58 +382,14 @@ module.exports = function (RED) {
382
382
  maxRetriesPerRequest: 0
383
383
  };
384
384
 
385
- // Create Redis client
386
385
  // Create Redis client
387
386
  let client;
388
387
  if (this.cluster) {
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);
388
+ // For cluster mode, options should be an array of nodes
389
+ const clusterNodes = Array.isArray(connectionOptions) ? connectionOptions : [connectionOptions];
390
+ client = new Redis.Cluster(clusterNodes);
420
391
  } else {
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
- });
392
+ client = new Redis(connectionOptions);
437
393
  }
438
394
 
439
395
  // Track error state to prevent spam
@@ -444,11 +400,11 @@ module.exports = function (RED) {
444
400
  // Handle connection errors
445
401
  client.on("error", (e) => {
446
402
  const now = Date.now();
447
-
403
+
448
404
  // Only report errors once per interval to prevent spam
449
405
  if (!errorReported || (now - lastErrorTime) > ERROR_REPORT_INTERVAL) {
450
406
  let errorMsg = `Redis connection error: ${e.message}`;
451
-
407
+
452
408
  // Add specific diagnostics for common SSL issues
453
409
  if (e.message.includes("Protocol error") || e.message.includes("\\u0015")) {
454
410
  errorMsg += "\nThis usually indicates an SSL/TLS configuration issue. Try:";
@@ -457,13 +413,13 @@ module.exports = function (RED) {
457
413
  errorMsg += "\n3. Check if your Redis server is configured for SSL";
458
414
  errorMsg += "\n4. Enable 'Debug: Force No SSL' option for testing";
459
415
  }
460
-
416
+
461
417
  if (executingNode) {
462
418
  executingNode.error(errorMsg, {});
463
419
  } else {
464
420
  this.error(errorMsg, {});
465
421
  }
466
-
422
+
467
423
  errorReported = true;
468
424
  lastErrorTime = now;
469
425
  }
package/redis-variable.js CHANGED
@@ -4,7 +4,7 @@ module.exports = function (RED) {
4
4
  function RedisVariableNode(config) {
5
5
  RED.nodes.createNode(this, config);
6
6
  const node = this;
7
-
7
+
8
8
  // Node configuration
9
9
  this.operation = config.operation || "get";
10
10
  this.timeout = config.timeout || 0;
@@ -52,6 +52,19 @@ module.exports = function (RED) {
52
52
  return str;
53
53
  }
54
54
 
55
+ function stableStringify(value) {
56
+ if (value === null || value === undefined) return String(value);
57
+ if (Array.isArray(value)) {
58
+ return `[${value.map(stableStringify).join(',')}]`;
59
+ }
60
+ if (typeof value === 'object') {
61
+ const keys = Object.keys(value).sort();
62
+ const entries = keys.map((key) => `"${key}":${stableStringify(value[key])}`);
63
+ return `{${entries.join(',')}}`;
64
+ }
65
+ return JSON.stringify(value);
66
+ }
67
+
55
68
  // Handle different operations
56
69
  switch (this.operation) {
57
70
  case "subscribe":
@@ -309,6 +322,26 @@ module.exports = function (RED) {
309
322
  return;
310
323
  }
311
324
 
325
+ // Recreate client when connection options change (e.g., context-based config)
326
+ try {
327
+ const options = redisConfig.getConnectionOptions(msg, node);
328
+ const configKey = stableStringify({
329
+ cluster: redisConfig.cluster === true,
330
+ options
331
+ });
332
+ if (node._lastConnKey && node._lastConnKey !== configKey) {
333
+ redisConfig.forceDisconnect(node.id);
334
+ client = null;
335
+ }
336
+ node._lastConnKey = configKey;
337
+ } catch (configError) {
338
+ node.error(configError.message, msg);
339
+ msg.payload = { error: configError.message };
340
+ send(msg);
341
+ done();
342
+ return;
343
+ }
344
+
312
345
  // Only create client if not already created
313
346
  try {
314
347
  if (!client) {
@@ -322,11 +355,13 @@ module.exports = function (RED) {
322
355
  return;
323
356
  }
324
357
  }
325
-
358
+
326
359
  // Check if client is connected before proceeding
327
360
  if (client.status !== 'ready' && client.status !== 'connect') {
328
361
  // Try to connect if not connected
329
- if (client.status === 'disconnect' || client.status === 'end') {
362
+ if (client.status === 'wait') {
363
+ await client.connect();
364
+ } else if (client.status === 'disconnect' || client.status === 'end') {
330
365
  await client.connect();
331
366
  } else {
332
367
  // Force disconnect and recreate client for other error states
@@ -380,12 +415,25 @@ module.exports = function (RED) {
380
415
  if (!payload.key) {
381
416
  throw new Error("Missing key for SET operation. Use payload.key");
382
417
  }
383
- let setValue = payload.value !== undefined ? payload.value : payload.data;
418
+ const payloadIsObject = payload && typeof payload === 'object' && !Array.isArray(payload);
419
+ const payloadHasKey = payloadIsObject && Object.prototype.hasOwnProperty.call(payload, 'key');
420
+ const payloadHasValue = payloadIsObject && (
421
+ Object.prototype.hasOwnProperty.call(payload, 'value') ||
422
+ Object.prototype.hasOwnProperty.call(payload, 'data')
423
+ );
424
+ let setValue;
425
+ if (payloadHasValue) {
426
+ setValue = payload.value !== undefined ? payload.value : payload.data;
427
+ } else if (payloadHasKey) {
428
+ setValue = undefined;
429
+ } else {
430
+ setValue = payload;
431
+ }
384
432
  if (setValue === undefined) {
385
- throw new Error("Missing value for SET operation. Use payload.value or payload.data");
433
+ throw new Error("Missing value for SET operation. Use payload.value, payload.data, or payload as value");
386
434
  }
387
435
  setValue = smartSerialize(setValue);
388
-
436
+
389
437
  // Support TTL
390
438
  if (payload.ttl && payload.ttl > 0) {
391
439
  response = await client.setex(payload.key, payload.ttl, setValue);
@@ -420,14 +468,14 @@ module.exports = function (RED) {
420
468
  if (!pattern || typeof pattern !== 'string') {
421
469
  throw new Error("Missing pattern for MATCH operation. Use payload.pattern or payload as string");
422
470
  }
423
-
471
+
424
472
  let count = payload.count || 100; // Default scan count
425
473
  let startCursor = payload.cursor || payload.skip || 0; // Support cursor/skip for pagination
426
474
  let allKeys = [];
427
475
  let cursor = startCursor;
428
476
  let maxIterations = 1000; // Prevent infinite loops
429
477
  let iterations = 0;
430
-
478
+
431
479
  // If skip is specified, we need to scan through keys without collecting them
432
480
  if (payload.skip && payload.skip > 0) {
433
481
  let skipped = 0;
@@ -438,7 +486,7 @@ module.exports = function (RED) {
438
486
  const scanResult = await client.scan(cursor, 'MATCH', pattern, 'COUNT', count);
439
487
  cursor = scanResult[0];
440
488
  skipped += scanResult[1].length;
441
-
489
+
442
490
  if (skipped >= payload.skip) {
443
491
  // We've skipped enough, start collecting from this point
444
492
  const excessSkipped = skipped - payload.skip;
@@ -450,7 +498,7 @@ module.exports = function (RED) {
450
498
  }
451
499
  } while (cursor !== 0 && cursor !== '0');
452
500
  }
453
-
501
+
454
502
  // Continue scanning to collect keys up to the limit
455
503
  do {
456
504
  if (iterations++ > maxIterations) {
@@ -459,15 +507,15 @@ module.exports = function (RED) {
459
507
  const scanResult = await client.scan(cursor, 'MATCH', pattern, 'COUNT', count);
460
508
  cursor = scanResult[0];
461
509
  allKeys = allKeys.concat(scanResult[1]);
462
-
510
+
463
511
  // Break early if we've found enough keys
464
512
  if (allKeys.length >= count) {
465
513
  allKeys = allKeys.slice(0, count); // Trim to exact count
466
514
  break;
467
515
  }
468
516
  } while (cursor !== 0 && cursor !== '0');
469
-
470
- msg.payload = {
517
+
518
+ msg.payload = {
471
519
  pattern: pattern,
472
520
  keys: allKeys,
473
521
  count: allKeys.length,
@@ -487,7 +535,7 @@ module.exports = function (RED) {
487
535
  throw new Error("Missing key for TTL operation. Use payload.key or payload as string");
488
536
  }
489
537
  response = await client.ttl(ttlKey);
490
- msg.payload = {
538
+ msg.payload = {
491
539
  key: ttlKey,
492
540
  ttl: response,
493
541
  status: response === -1 ? "no expiration" : response === -2 ? "key not found" : "expires in " + response + " seconds"
@@ -500,7 +548,7 @@ module.exports = function (RED) {
500
548
  }
501
549
  let expireSeconds = payload.ttl || payload.seconds || payload.value || 3600;
502
550
  response = await client.expire(payload.key, expireSeconds);
503
- msg.payload = {
551
+ msg.payload = {
504
552
  success: response === 1,
505
553
  key: payload.key,
506
554
  ttl: expireSeconds,
@@ -514,7 +562,7 @@ module.exports = function (RED) {
514
562
  throw new Error("Missing key for PERSIST operation. Use payload.key or payload as string");
515
563
  }
516
564
  response = await client.persist(persistKey);
517
- msg.payload = {
565
+ msg.payload = {
518
566
  success: response === 1,
519
567
  key: persistKey,
520
568
  message: response === 1 ? "Expiration removed" : "Key not found or no expiration"
@@ -695,7 +743,7 @@ module.exports = function (RED) {
695
743
  }
696
744
  } catch (redisError) {
697
745
  // Handle Redis-specific errors (connection, command errors, etc.)
698
- if (redisError.message.includes('ECONNREFUSED') ||
746
+ if (redisError.message.includes('ECONNREFUSED') ||
699
747
  redisError.message.includes('ENOTFOUND') ||
700
748
  redisError.message.includes('ETIMEDOUT')) {
701
749
  // Connection-related errors - force disconnect to prevent dead connections
@@ -707,8 +755,8 @@ module.exports = function (RED) {
707
755
  // Ignore disconnect errors
708
756
  }
709
757
  }
710
-
711
- msg.payload = {
758
+
759
+ msg.payload = {
712
760
  error: `Redis connection failed: ${redisError.message}`,
713
761
  operation: node.operation,
714
762
  retryable: true
@@ -716,7 +764,7 @@ module.exports = function (RED) {
716
764
  node.status({ fill: "red", shape: "ring", text: "connection failed" });
717
765
  } else {
718
766
  // Other Redis errors (command errors, etc.)
719
- msg.payload = {
767
+ msg.payload = {
720
768
  error: `Redis operation failed: ${redisError.message}`,
721
769
  operation: node.operation,
722
770
  retryable: false
@@ -763,7 +811,7 @@ module.exports = function (RED) {
763
811
  node.on("close", async (undeploy, done) => {
764
812
  node.status({});
765
813
  running = false;
766
-
814
+
767
815
  if (node.operation === "instance" && node.location && node.topic) {
768
816
  try {
769
817
  node.context()[node.location].set(node.topic, null);
@@ -771,7 +819,7 @@ module.exports = function (RED) {
771
819
  // Ignore errors when cleaning up context
772
820
  }
773
821
  }
774
-
822
+
775
823
  if (client) {
776
824
  try {
777
825
  if (node.operation === 'subscribe' && node.topic) {
@@ -788,7 +836,7 @@ module.exports = function (RED) {
788
836
  const nodeId = node.block ? node.id : redisConfig.id;
789
837
  redisConfig.disconnect(nodeId);
790
838
  }
791
-
839
+
792
840
  client = null;
793
841
  done();
794
842
  });