node-red-contrib-redis-variable 1.0.0 → 1.3.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/README.md CHANGED
@@ -26,6 +26,7 @@ A comprehensive Node-RED node for Redis operations with flexible connection mana
26
26
  - **SET** - Store value with optional TTL
27
27
  - **DEL** - Delete single or multiple keys
28
28
  - **EXISTS** - Check if single or multiple keys exist
29
+ - **MATCH** - Find keys by pattern using SCAN
29
30
 
30
31
  #### **TTL Operations**
31
32
  - **TTL** - Get remaining time to live in seconds
@@ -159,6 +160,61 @@ Port: Global Context → redis_config.port
159
160
  Password: Flow Context → redis_password
160
161
  ```
161
162
 
163
+ **Setting up Global Context Variables:**
164
+
165
+ 1. **In Node-RED Admin Panel:**
166
+ - Go to **Admin** → **Context** → **Global**
167
+ - Add variables:
168
+ - `redis_config.host` = `your-redis-host`
169
+ - `redis_config.port` = `6379`
170
+ - `redis_config.password` = `your-redis-password`
171
+
172
+ 2. **Via Function Node:**
173
+ ```javascript
174
+ // Set global context variables
175
+ flow.set("redis_config", {
176
+ host: "your-redis-host",
177
+ port: 6379,
178
+ password: "your-redis-password"
179
+ });
180
+ ```
181
+
182
+ 3. **Via HTTP API:**
183
+ ```bash
184
+ curl -X POST http://localhost:1880/context/global/redis_config \
185
+ -H "Content-Type: application/json" \
186
+ -d '{"host":"your-redis-host","port":6379,"password":"your-redis-password"}'
187
+ ```
188
+
189
+ **Troubleshooting Global Context:**
190
+ - Ensure variable names match exactly (case-sensitive)
191
+ - Check Node-RED logs for context lookup messages
192
+ - Verify global context variables are set before Redis operations
193
+
194
+ **Testing Global Context Setup:**
195
+
196
+ 1. **Set up test variables:**
197
+ ```javascript
198
+ // In a Function node
199
+ flow.set("redis_config", {
200
+ host: "localhost",
201
+ port: 6379,
202
+ password: "your-password"
203
+ });
204
+ ```
205
+
206
+ 2. **Test connection:**
207
+ ```javascript
208
+ // In another Function node
209
+ msg.payload = "test_key";
210
+ return msg;
211
+ ```
212
+
213
+ 3. **Check logs:**
214
+ - Enable debug mode: `NODE_RED_DEBUG=1 node-red`
215
+ - Look for context lookup messages in Node-RED logs
216
+ - Verify connection parameters are correct
217
+
162
218
  ## Operations
163
219
 
164
220
  **Universal Payload Interface**: All Redis operations use a unified `msg.payload` interface. Parameters can be passed as simple strings (for single keys) or as objects with specific properties. This provides flexibility while maintaining simplicity.
@@ -230,6 +286,82 @@ msg.payload = {
230
286
  // Returns: { payload: { exists: true, count: 2, keys: ["key1", "key2", "key3"] } }
231
287
  ```
232
288
 
289
+ #### MATCH - Find Keys by Pattern
290
+ ```javascript
291
+ // Simple pattern
292
+ msg.payload = "user:*";
293
+ // Returns: { payload: { pattern: "user:*", keys: ["user:123", "user:456"], count: 2, scanned: true } }
294
+
295
+ // Pattern with custom count
296
+ msg.payload = {
297
+ pattern: "session:*",
298
+ count: 50
299
+ };
300
+ // Returns: { payload: { pattern: "session:*", keys: ["session:abc123", "session:def456"], count: 2, limit: 50, scanned: true } }
301
+
302
+ // Pattern with pagination (cursor)
303
+ msg.payload = {
304
+ pattern: "user:*",
305
+ count: 30,
306
+ cursor: "12345"
307
+ };
308
+ // Returns: { payload: { pattern: "user:*", keys: ["user:31", "user:32"], count: 2, limit: 30, cursor: "67890", startCursor: "12345", scanned: true, truncated: false } }
309
+
310
+ // Pattern with skip (skip first N keys)
311
+ msg.payload = {
312
+ pattern: "session:*",
313
+ count: 30,
314
+ skip: 100
315
+ };
316
+ // Returns: { payload: { pattern: "session:*", keys: ["session:101", "session:102"], count: 2, limit: 30, cursor: "67890", startCursor: 0, scanned: true, truncated: false } }
317
+
318
+ // Complex patterns
319
+ msg.payload = "cache:page:*"; // All cache pages
320
+ msg.payload = "temp:*:data"; // Temporary data keys
321
+ msg.payload = "user:*:profile"; // User profiles
322
+ ```
323
+
324
+ **Advanced MATCH Features:**
325
+
326
+ - **Pattern Matching**: Uses Redis SCAN with pattern matching for efficient key discovery
327
+ - **Count Limit**: Limit the number of keys returned (default: 100)
328
+ - **Cursor Pagination**: Use `cursor` parameter for efficient pagination through large datasets
329
+ - **Skip Keys**: Use `skip` parameter to skip the first N matching keys
330
+ - **Performance Optimized**: Uses Redis SCAN for non-blocking operation on large datasets
331
+
332
+ **Response Format:**
333
+ ```javascript
334
+ {
335
+ "pattern": "user:*",
336
+ "keys": ["user:1", "user:2", "user:3"],
337
+ "count": 3, // Number of keys returned
338
+ "limit": 50, // Requested limit
339
+ "cursor": "67890", // Next cursor for pagination
340
+ "startCursor": "0", // Starting cursor
341
+ "scanned": true, // Operation completed
342
+ "truncated": false // true if results were limited by count
343
+ }
344
+ ```
345
+
346
+ **Pagination Example:**
347
+ ```javascript
348
+ // First request
349
+ msg.payload = {
350
+ pattern: "session:*",
351
+ count: 30
352
+ };
353
+
354
+ // Response contains cursor for next page
355
+ // Use that cursor in next request
356
+ msg.payload = {
357
+ pattern: "session:*",
358
+ count: 30,
359
+ cursor: "67890" // from previous response
360
+ };
361
+
362
+ // Continue until cursor becomes "0" (end of results)
363
+ ```
364
+
233
365
  #### TTL - Get Time To Live
234
366
  ```javascript
235
367
  msg.payload = "mykey";
@@ -566,6 +698,24 @@ msg.payload = {
566
698
  // Subscriber automatically receives notifications
567
699
  ```
568
700
 
701
+ ### Key Discovery and Cleanup
702
+ ```javascript
703
+ // Find all temporary keys
704
+ msg.payload = "temp:*";
705
+ // Returns: { pattern: "temp:*", keys: ["temp:cache1", "temp:cache2", "temp:session123"], count: 3, scanned: true }
706
+
707
+ // Find expired session keys
708
+ msg.payload = {
709
+ pattern: "session:*:expired",
710
+ count: 50
711
+ };
712
+ // Returns: { pattern: "session:*:expired", keys: ["session:abc:expired", "session:def:expired"], count: 2, scanned: true }
713
+
714
+ // Clean up old cache entries
715
+ msg.payload = "cache:old:*";
716
+ // Use returned keys with DEL operation for cleanup
717
+ ```
718
+
569
719
  ## 📖 Usage Examples
570
720
 
571
721
  ### Basic Operations
@@ -613,6 +763,24 @@ msg.payload = {
613
763
  // Returns: { success: true, deleted: 3, keys: [...] }
614
764
  ```
615
765
 
766
+ #### MATCH Operations
767
+ ```javascript
768
+ // Find all user keys
769
+ msg.payload = "user:*";
770
+ // Returns: { pattern: "user:*", keys: ["user:123", "user:456", "user:789"], count: 3, scanned: true }
771
+
772
+ // Find session keys with custom scan count
773
+ msg.payload = {
774
+ pattern: "session:*",
775
+ count: 25
776
+ };
777
+ // Returns: { pattern: "session:*", keys: ["session:abc123", "session:def456"], count: 2, scanned: true }
778
+
779
+ // Find cache keys
780
+ msg.payload = "cache:*";
781
+ // Returns: { pattern: "cache:*", keys: ["cache:page1", "cache:page2", "cache:api"], count: 3, scanned: true }
782
+ ```
783
+
616
784
  ### TTL Operations
617
785
 
618
786
  #### Check TTL
@@ -738,19 +906,55 @@ msg.payload = {
738
906
 
739
907
  ## Troubleshooting
740
908
 
909
+ ### SSL/TLS Connection Issues
910
+
911
+ If you encounter the error `"Protocol error, got "\u0015" as reply type byte"`, this indicates an SSL/TLS configuration problem:
912
+
913
+ **Solution**: Disable certificate verification in the SSL/TLS configuration:
914
+ 1. Enable SSL/TLS in the configuration node
915
+ 2. **Uncheck** "Verify Certificate" (Reject unauthorized certificates)
916
+ 3. This allows connections to servers with self-signed or invalid certificates
917
+
918
+ **Common scenarios where this is needed:**
919
+ - Self-signed certificates in development environments
920
+ - Local Redis servers with SSL enabled
921
+ - Cloud Redis services with custom certificates
922
+ - Test environments with temporary certificates
923
+
924
+ **Security Note**: Disabling certificate verification reduces security. Only use this in trusted environments or when you're certain about the server's identity.
925
+
741
926
  ### Common Issues
742
927
 
743
928
  1. **Connection Refused**: Check Redis server is running and accessible
744
- 2. **Authentication Failed**: Verify username/password configuration
929
+ 2. **Authentication Failed**: Verify username/password configuration
745
930
  3. **Timeout Errors**: Increase connection timeout in advanced options
746
931
  4. **Memory Issues**: Monitor Redis memory usage and configure appropriate limits
747
932
 
748
933
  ### Debug Mode
934
+
749
935
  Enable Node-RED debug mode to see detailed connection and operation logs:
750
936
  ```bash
751
937
  DEBUG=redis* node-red
752
938
  ```
753
939
 
940
+ **Context Debugging:**
941
+ Enable context debugging by setting the environment variable:
942
+ ```bash
943
+ NODE_RED_DEBUG=1 node-red
944
+ ```
945
+
946
+ Check Node-RED logs for messages like:
947
+ ```
948
+ Context lookup - Type: global, Path: redis_config.host, Result: your-redis-host
949
+ Redis connection config - Host: your-redis-host, Port: 6379, Database: 0, Username: not set, Password: set
950
+ ```
951
+
952
+ **Common Context Issues:**
953
+ 1. **Variable not found**: Check exact variable name spelling
954
+ 2. **Nested objects**: Use dot notation (e.g., `redis_config.host`)
955
+ 3. **Context type mismatch**: Ensure correct context type is selected
956
+ 4. **Timing issues**: Set context variables before Redis operations
957
+
754
958
  ## Contributing
755
959
 
756
960
  Contributions are welcome! Please read the contributing guidelines and submit pull requests to the GitHub repository.
@@ -761,9 +965,62 @@ MIT License - see LICENSE file for details.
761
965
 
762
966
  ## Changelog
763
967
 
968
+ ### v1.1.0
969
+ - **Enhanced MATCH Operation**: Added advanced pattern matching with pagination support
970
+ - **Cursor Pagination**: Efficient pagination through large datasets using Redis SCAN cursors
971
+ - **Skip Functionality**: Skip first N matching keys for offset-based pagination
972
+ - **Count Limiting**: Improved count parameter handling for precise result limiting
973
+ - **Performance Optimization**: Better SCAN integration for non-blocking operations
974
+ - **Improved Response Format**: Enhanced MATCH response with pagination metadata
975
+ - Added `cursor`, `startCursor`, `limit`, and `truncated` fields
976
+ - Better error handling and validation
977
+ - **Production Ready**: Removed debug logging and optimized for production use
978
+ - **Updated Documentation**: Comprehensive examples for all MATCH features
979
+ - **Enhanced Error Handling**: Better validation and error messages
980
+
764
981
  ### v1.0.0
765
982
  - Initial release
766
983
  - Complete Redis operations support
767
984
  - Flexible connection management
768
985
  - Modern ioredis integration
769
- - Comprehensive documentation
986
+ - Comprehensive documentation
987
+
988
+ ### Pattern Matching (MATCH)
989
+
990
+ Find keys by pattern using Redis SCAN:
991
+
992
+ ```javascript
993
+ // Find all keys starting with "user:"
994
+ msg.payload = {
995
+ operation: "match",
996
+ pattern: "user:*"
997
+ };
998
+
999
+ // Find keys with specific pattern and custom scan count
1000
+ msg.payload = {
1001
+ operation: "match",
1002
+ pattern: "session:*:active",
1003
+ count: 50 // Number of keys to scan per iteration
1004
+ };
1005
+
1006
+ // Simple pattern matching
1007
+ msg.payload = "temp:*"; // Find all keys starting with "temp:"
1008
+ ```
1009
+
1010
+ **Response format:**
1011
+ ```javascript
1012
+ {
1013
+ pattern: "user:*",
1014
+ keys: ["user:1", "user:2", "user:admin"],
1015
+ count: 3,
1016
+ scanned: true
1017
+ }
1018
+ ```
1019
+
1020
+ **Pattern examples:**
1021
+ - `user:*` - All keys starting with "user:"
1022
+ - `*:active` - All keys ending with ":active"
1023
+ - `session:*:data` - Keys with "session:" prefix and ":data" suffix
1024
+ - `temp_*` - Keys starting with "temp_"
1025
+
1026
+ ### Hash Operations
package/package.json CHANGED
@@ -1,14 +1,17 @@
1
1
  {
2
2
  "name": "node-red-contrib-redis-variable",
3
- "version": "1.0.0",
4
- "description": "A comprehensive Node-RED node for Redis operations with universal payload-based configuration, automatic JSON handling, and SSL/TLS support",
3
+ "version": "1.3.0",
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",
7
7
  "redis",
8
8
  "database",
9
9
  "cache",
10
10
  "variable",
11
- "storage"
11
+ "storage",
12
+ "pattern-matching",
13
+ "pagination",
14
+ "json-handling"
12
15
  ],
13
16
  "homepage": "https://github.com/lotockii/node-red-contrib-redis-variable#readme",
14
17
  "bugs": {
@@ -28,7 +28,8 @@
28
28
  tlsCaContext: { value: "" },
29
29
  options: { value: "{}" },
30
30
  optionsType: { value: "json" },
31
- optionsContext: { value: "" }
31
+ optionsContext: { value: "" },
32
+ debugNoSSL: { value: false }
32
33
  },
33
34
  credentials: {
34
35
  password: { type: "password" },
@@ -290,6 +291,18 @@
290
291
  $("#node-config-input-tlsCa-visible").on("input", function() {
291
292
  $("#node-config-input-tlsCa").val($(this).val());
292
293
  });
294
+
295
+ // Debug option
296
+ $("#node-config-input-debugNoSSL").prop('checked', this.debugNoSSL === true);
297
+
298
+ // Show/hide TLS options based on debug setting
299
+ $("#node-config-input-debugNoSSL").change(function() {
300
+ if ($(this).is(':checked')) {
301
+ $("#ssl-config-section").hide();
302
+ } else {
303
+ $("#ssl-config-section").show();
304
+ }
305
+ });
293
306
  },
294
307
  oneditsave: function() {
295
308
  // Save basic configuration
@@ -431,6 +444,9 @@
431
444
  this.tlsCaContext = tlsCaValue || '';
432
445
  $("#node-config-input-tlsCa").val('');
433
446
  }
447
+
448
+ // Debug option
449
+ this.debugNoSSL = $("#node-config-input-debugNoSSL").is(':checked');
434
450
  }
435
451
  });
436
452
  </script>
@@ -4,14 +4,6 @@ module.exports = function (RED) {
4
4
  let connections = {};
5
5
  let usedConn = {};
6
6
 
7
- /**
8
- * Helper function to get value from different contexts
9
- * @param {Object} node - Node instance
10
- * @param {string} value - Value to get
11
- * @param {string} type - Type of value (str, flow, global, env)
12
- * @param {Object} msg - Message object
13
- * @returns {string} Retrieved value
14
- */
15
7
  function getValueFromContext(node, value, type, msg) {
16
8
  if (value === null || value === undefined) return null;
17
9
 
@@ -55,14 +47,21 @@ module.exports = function (RED) {
55
47
  if (path.includes('.')) {
56
48
  const parts = path.split('.');
57
49
  let result = context.get(parts[0]);
58
- for (let i = 1; i < parts.length; i++) {
59
- if (result && typeof result === 'object') {
60
- result = result[parts[i]];
61
- } else {
62
- return undefined;
50
+
51
+ // If the first part returns an object, traverse it
52
+ if (result && typeof result === 'object') {
53
+ for (let i = 1; i < parts.length; i++) {
54
+ if (result && typeof result === 'object' && result[parts[i]] !== undefined) {
55
+ result = result[parts[i]];
56
+ } else {
57
+ return undefined;
58
+ }
63
59
  }
60
+ return result;
61
+ } else {
62
+ // If first part is not an object, try to get the full path as a single value
63
+ return context.get(path);
64
64
  }
65
- return result;
66
65
  } else {
67
66
  return context.get(path);
68
67
  }
@@ -129,6 +128,9 @@ module.exports = function (RED) {
129
128
  break;
130
129
  case 'global':
131
130
  result = getValueFromContext(executingNode || this, value, 'global', msg);
131
+ if (executingNode && process.env.NODE_RED_DEBUG) {
132
+ executingNode.log(`Context lookup - Type: global, Path: ${value}, Result: ${result}`);
133
+ }
132
134
  break;
133
135
  case 'env':
134
136
  result = process.env[value] || null;
@@ -158,29 +160,29 @@ module.exports = function (RED) {
158
160
  // Get Redis connection options
159
161
  this.getConnectionOptions = function(msg, executingNode) {
160
162
  try {
161
- // Parse host
162
- let host = this.parseCredentialValue(
163
- this.hostType === 'str' ? this.host : this.hostContext,
164
- this.hostType,
165
- msg,
166
- executingNode
167
- ) || 'localhost';
168
-
169
- // Parse port
170
- let port = this.parseCredentialValue(
171
- this.portType === 'str' ? this.port : this.portContext,
172
- this.portType,
173
- msg,
174
- executingNode
175
- ) || 6379;
176
-
177
- // Parse database
178
- let database = this.parseCredentialValue(
179
- this.databaseType === 'str' ? this.database : this.databaseContext,
180
- this.databaseType,
181
- msg,
182
- executingNode
183
- ) || 0;
163
+ // Parse host - handle both typedInput and direct context
164
+ let host;
165
+ if (this.hostType === 'str') {
166
+ host = this.host || 'localhost';
167
+ } else {
168
+ host = this.parseCredentialValue(this.hostContext, this.hostType, msg, executingNode) || 'localhost';
169
+ }
170
+
171
+ // Parse port - handle both typedInput and direct context
172
+ let port;
173
+ if (this.portType === 'str') {
174
+ port = this.port || 6379;
175
+ } else {
176
+ port = this.parseCredentialValue(this.portContext, this.portType, msg, executingNode) || 6379;
177
+ }
178
+
179
+ // Parse database - handle both typedInput and direct context
180
+ let database;
181
+ if (this.databaseType === 'str') {
182
+ database = this.database || 0;
183
+ } else {
184
+ database = this.parseCredentialValue(this.databaseContext, this.databaseType, msg, executingNode) || 0;
185
+ }
184
186
 
185
187
  // Parse password
186
188
  let password = null;
@@ -273,6 +275,9 @@ module.exports = function (RED) {
273
275
  if (!this.tlsRejectUnauthorized) {
274
276
  connectionOptions.tls.rejectUnauthorized = false;
275
277
  }
278
+ } else {
279
+ // Explicitly disable TLS for non-SSL connections
280
+ connectionOptions.tls = false;
276
281
  }
277
282
 
278
283
  return connectionOptions;
@@ -298,22 +303,76 @@ module.exports = function (RED) {
298
303
 
299
304
  const options = this.getConnectionOptions(msg, executingNode);
300
305
 
306
+ // Add connection limits to prevent infinite retry loops
307
+ const connectionOptions = {
308
+ ...options,
309
+ maxRetriesPerRequest: 1, // Limit retries per request
310
+ retryDelayOnFailover: 100,
311
+ enableReadyCheck: false,
312
+ lazyConnect: true, // Don't connect immediately
313
+ connectTimeout: 5000, // 5 second timeout
314
+ commandTimeout: 3000, // 3 second command timeout
315
+ // Disable automatic reconnection to prevent error loops
316
+ retryDelayOnClusterDown: 0,
317
+ retryDelayOnFailover: 0,
318
+ maxRetriesPerRequest: 0
319
+ };
320
+
301
321
  // Create Redis client
302
322
  let client;
303
323
  if (this.cluster) {
304
324
  // For cluster mode, options should be an array of nodes
305
- const clusterNodes = Array.isArray(options) ? options : [options];
325
+ const clusterNodes = Array.isArray(connectionOptions) ? connectionOptions : [connectionOptions];
306
326
  client = new Redis.Cluster(clusterNodes);
307
327
  } else {
308
- client = new Redis(options);
328
+ client = new Redis(connectionOptions);
309
329
  }
310
330
 
331
+ // Track error state to prevent spam
332
+ let errorReported = false;
333
+ let lastErrorTime = 0;
334
+ const ERROR_REPORT_INTERVAL = 30000; // Report errors only once per 30 seconds
335
+
311
336
  // Handle connection errors
312
337
  client.on("error", (e) => {
338
+ const now = Date.now();
339
+
340
+ // Only report errors once per interval to prevent spam
341
+ if (!errorReported || (now - lastErrorTime) > ERROR_REPORT_INTERVAL) {
342
+ let errorMsg = `Redis connection error: ${e.message}`;
343
+
344
+ // Add specific diagnostics for common SSL issues
345
+ if (e.message.includes("Protocol error") || e.message.includes("\\u0015")) {
346
+ errorMsg += "\nThis usually indicates an SSL/TLS configuration issue. Try:";
347
+ errorMsg += "\n1. Disable SSL/TLS if your Redis server doesn't support it";
348
+ errorMsg += "\n2. Use port 6380 for Redis SSL instead of 6379";
349
+ errorMsg += "\n3. Check if your Redis server is configured for SSL";
350
+ errorMsg += "\n4. Enable 'Debug: Force No SSL' option for testing";
351
+ }
352
+
353
+ if (executingNode) {
354
+ executingNode.error(errorMsg, {});
355
+ } else {
356
+ this.error(errorMsg, {});
357
+ }
358
+
359
+ errorReported = true;
360
+ lastErrorTime = now;
361
+ }
362
+ });
363
+
364
+ // Handle successful connection
365
+ client.on("connect", () => {
366
+ errorReported = false;
313
367
  if (executingNode) {
314
- executingNode.error(`Redis connection error: ${e.message}`, {});
315
- } else {
316
- this.error(`Redis connection error: ${e.message}`, {});
368
+ executingNode.status({ fill: "green", shape: "dot", text: "connected" });
369
+ }
370
+ });
371
+
372
+ // Handle disconnection
373
+ client.on("disconnect", () => {
374
+ if (executingNode) {
375
+ executingNode.status({ fill: "red", shape: "ring", text: "disconnected" });
317
376
  }
318
377
  });
319
378
 
@@ -348,6 +407,20 @@ module.exports = function (RED) {
348
407
  }
349
408
  };
350
409
 
410
+ // Force disconnect method for error recovery
411
+ this.forceDisconnect = function(nodeId) {
412
+ const id = nodeId || this.id;
413
+ if (connections[id]) {
414
+ try {
415
+ connections[id].disconnect();
416
+ } catch (e) {
417
+ // Ignore disconnect errors
418
+ }
419
+ delete connections[id];
420
+ delete usedConn[id];
421
+ }
422
+ };
423
+
351
424
  // Clean up on node close
352
425
  this.on('close', function() {
353
426
  this.disconnect();