node-red-contrib-redis-variable 1.0.0 → 1.2.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.2.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;
@@ -310,10 +315,21 @@ module.exports = function (RED) {
310
315
 
311
316
  // Handle connection errors
312
317
  client.on("error", (e) => {
318
+ let errorMsg = `Redis connection error: ${e.message}`;
319
+
320
+ // Add specific diagnostics for common SSL issues
321
+ if (e.message.includes("Protocol error") || e.message.includes("\\u0015")) {
322
+ errorMsg += "\nThis usually indicates an SSL/TLS configuration issue. Try:";
323
+ errorMsg += "\n1. Disable SSL/TLS if your Redis server doesn't support it";
324
+ errorMsg += "\n2. Use port 6380 for Redis SSL instead of 6379";
325
+ errorMsg += "\n3. Check if your Redis server is configured for SSL";
326
+ errorMsg += "\n4. Enable 'Debug: Force No SSL' option for testing";
327
+ }
328
+
313
329
  if (executingNode) {
314
- executingNode.error(`Redis connection error: ${e.message}`, {});
330
+ executingNode.error(errorMsg, {});
315
331
  } else {
316
- this.error(`Redis connection error: ${e.message}`, {});
332
+ this.error(errorMsg, {});
317
333
  }
318
334
  });
319
335
 
@@ -33,6 +33,7 @@
33
33
  <option value="set">SET - Set value</option>
34
34
  <option value="del">DEL - Delete key</option>
35
35
  <option value="exists">EXISTS - Check if key exists</option>
36
+ <option value="match">MATCH - Find keys by pattern</option>
36
37
  </optgroup>
37
38
  <optgroup label="TTL Operations">
38
39
  <option value="ttl">TTL - Get time to live</option>
@@ -80,6 +81,7 @@
80
81
  <li><b>SET</b>: Store value with key (supports TTL)</li>
81
82
  <li><b>DEL</b>: Delete key(s)</li>
82
83
  <li><b>EXISTS</b>: Check if key(s) exist</li>
84
+ <li><b>MATCH</b>: Find keys by pattern using SCAN</li>
83
85
  </ul>
84
86
 
85
87
  <h4>TTL Operations</h4>
@@ -158,6 +160,37 @@
158
160
  "payload": {
159
161
  "keys": ["cache:page1", "cache:page2", "temp:data"]
160
162
  }
163
+ }
164
+
165
+ // MATCH - Find keys by pattern
166
+ {
167
+ "payload": {
168
+ "pattern": "user:*",
169
+ "count": 100
170
+ }
171
+ }
172
+
173
+ // MATCH - With pagination (cursor)
174
+ {
175
+ "payload": {
176
+ "pattern": "user:*",
177
+ "count": 50,
178
+ "cursor": "12345"
179
+ }
180
+ }
181
+
182
+ // MATCH - Skip first N keys
183
+ {
184
+ "payload": {
185
+ "pattern": "session:*",
186
+ "count": 30,
187
+ "skip": 100
188
+ }
189
+ }
190
+
191
+ // MATCH - Simple pattern
192
+ {
193
+ "payload": "session:*"
161
194
  }</pre>
162
195
 
163
196
  <h4>TTL Operations</h4>
@@ -280,5 +313,49 @@
280
313
  <li><b>INCR</b>: <code>{ key: "counter", value: 42 }</code></li>
281
314
  <li><b>LPUSH</b>: <code>{ success: true, key: "list", length: 5, operation: "lpush" }</code></li>
282
315
  <li><b>HGETALL</b>: <code>{ name: "John", age: 30, email: "john@example.com" }</code> (auto-parsed)</li>
316
+ <li><b>MATCH</b>: <code>{ pattern: "user:*", keys: ["user:1", "user:2"], count: 2, limit: 50, cursor: "12345", truncated: false }</code></li>
283
317
  </ul>
318
+
319
+ <h4>MATCH Operation Details</h4>
320
+ <p>The MATCH operation supports advanced features for efficient key discovery:</p>
321
+ <ul>
322
+ <li><b>Pattern matching</b>: Uses Redis SCAN with pattern matching (e.g., <code>user:*</code>, <code>session:ci_session:*</code>)</li>
323
+ <li><b>Count limit</b>: Limit the number of keys returned (default: 100)</li>
324
+ <li><b>Cursor pagination</b>: Use <code>cursor</code> parameter for efficient pagination through large datasets</li>
325
+ <li><b>Skip keys</b>: Use <code>skip</code> parameter to skip the first N matching keys</li>
326
+ <li><b>Performance optimized</b>: Uses Redis SCAN for non-blocking operation on large datasets</li>
327
+ </ul>
328
+
329
+ <p><b>Response format:</b></p>
330
+ <pre>{
331
+ "pattern": "user:*",
332
+ "keys": ["user:1", "user:2", "user:3"],
333
+ "count": 3,
334
+ "limit": 50,
335
+ "cursor": "67890", // Next cursor for pagination
336
+ "startCursor": "0", // Starting cursor
337
+ "scanned": true,
338
+ "truncated": false // true if results were limited by count
339
+ }</pre>
340
+
341
+ <p><b>Pagination example:</b></p>
342
+ <pre>// First request
343
+ {
344
+ "payload": {
345
+ "pattern": "session:*",
346
+ "count": 30
347
+ }
348
+ }
349
+
350
+ // Response contains cursor for next page
351
+ // Use that cursor in next request
352
+ {
353
+ "payload": {
354
+ "pattern": "session:*",
355
+ "count": 30,
356
+ "cursor": "67890" // from previous response
357
+ }
358
+ }
359
+
360
+ // Continue until cursor becomes "0" (end of results)</pre>
284
361
  </script>
package/redis-variable.js CHANGED
@@ -16,6 +16,8 @@ module.exports = function (RED) {
16
16
  this.location = config.location || 'flow';
17
17
  this.sha1 = "";
18
18
 
19
+ // Save redisConfig once in the constructor
20
+ let redisConfig = RED.nodes.getNode(config.redisConfig);
19
21
  let client = null;
20
22
  let running = true;
21
23
 
@@ -49,49 +51,6 @@ module.exports = function (RED) {
49
51
  return str;
50
52
  }
51
53
 
52
- // Try to get Redis configuration, but don't fail if not found
53
- const redisConfig = RED.nodes.getNode(config.redisConfig);
54
-
55
- if (!redisConfig) {
56
- node.warn("Redis configuration not found. Node will be in standby mode.");
57
- node.status({
58
- fill: "yellow",
59
- shape: "dot",
60
- text: "no config"
61
- });
62
-
63
- // Still handle input, but just pass through with warning
64
- node.on('input', function(msg) {
65
- msg.payload = { error: "Redis configuration not found" };
66
- node.send(msg);
67
- });
68
- return;
69
- }
70
-
71
- // Initialize Redis client only if config is available
72
- try {
73
- const nodeId = this.block ? node.id : redisConfig.id;
74
- client = redisConfig.getClient({}, node, nodeId);
75
-
76
- if (!client) {
77
- throw new Error("Failed to initialize Redis client");
78
- }
79
- } catch (error) {
80
- node.error(`Failed to initialize Redis client: ${error.message}`);
81
- node.status({
82
- fill: "red",
83
- shape: "dot",
84
- text: "connection error"
85
- });
86
-
87
- // Handle input with error
88
- node.on('input', function(msg) {
89
- msg.payload = { error: error.message };
90
- node.send(msg);
91
- });
92
- return;
93
- }
94
-
95
54
  // Handle different operations
96
55
  switch (this.operation) {
97
56
  case "subscribe":
@@ -303,6 +262,35 @@ module.exports = function (RED) {
303
262
  send = send || function() { node.send.apply(node, arguments) };
304
263
  done = done || function(err) { if(err) node.error(err, msg); };
305
264
 
265
+ if (!running) {
266
+ running = true;
267
+ }
268
+
269
+ // Use redisConfig saved at construction
270
+ if (!redisConfig) {
271
+ node.error("Redis configuration not found", msg);
272
+ msg.payload = { error: "Redis configuration not found" };
273
+ send(msg);
274
+ done();
275
+ return;
276
+ }
277
+
278
+ // Only create client if not already created
279
+ try {
280
+ if (!client) {
281
+ client = redisConfig.getClient(msg, node, node.id);
282
+ if (!client) {
283
+ throw new Error("Failed to initialize Redis client");
284
+ }
285
+ }
286
+ } catch (err) {
287
+ node.error(err.message, msg);
288
+ msg.payload = { error: err.message };
289
+ send(msg);
290
+ done();
291
+ return;
292
+ }
293
+
306
294
  try {
307
295
  let response;
308
296
  let payload = msg.payload;
@@ -361,6 +349,71 @@ module.exports = function (RED) {
361
349
  msg.payload = { exists: response > 0, count: response, keys: keysToCheck };
362
350
  break;
363
351
 
352
+ case "match":
353
+ let pattern = payload.pattern || payload;
354
+ if (!pattern || typeof pattern !== 'string') {
355
+ throw new Error("Missing pattern for MATCH operation. Use payload.pattern or payload as string");
356
+ }
357
+
358
+ let count = payload.count || 100; // Default scan count
359
+ let startCursor = payload.cursor || payload.skip || 0; // Support cursor/skip for pagination
360
+ let allKeys = [];
361
+ let cursor = startCursor;
362
+ let maxIterations = 1000; // Prevent infinite loops
363
+ let iterations = 0;
364
+
365
+ // If skip is specified, we need to scan through keys without collecting them
366
+ if (payload.skip && payload.skip > 0) {
367
+ let skipped = 0;
368
+ do {
369
+ if (iterations++ > maxIterations) {
370
+ throw new Error("Maximum scan iterations exceeded. Check your pattern or reduce skip value.");
371
+ }
372
+ const scanResult = await client.scan(cursor, 'MATCH', pattern, 'COUNT', count);
373
+ cursor = scanResult[0];
374
+ skipped += scanResult[1].length;
375
+
376
+ if (skipped >= payload.skip) {
377
+ // We've skipped enough, start collecting from this point
378
+ const excessSkipped = skipped - payload.skip;
379
+ if (excessSkipped > 0) {
380
+ // Add keys from current batch, excluding the excess
381
+ allKeys = scanResult[1].slice(excessSkipped);
382
+ }
383
+ break;
384
+ }
385
+ } while (cursor !== 0 && cursor !== '0');
386
+ }
387
+
388
+ // Continue scanning to collect keys up to the limit
389
+ do {
390
+ if (iterations++ > maxIterations) {
391
+ throw new Error("Maximum scan iterations exceeded. Check your pattern or reduce count value.");
392
+ }
393
+ const scanResult = await client.scan(cursor, 'MATCH', pattern, 'COUNT', count);
394
+ cursor = scanResult[0];
395
+ allKeys = allKeys.concat(scanResult[1]);
396
+
397
+ // Break early if we've found enough keys
398
+ if (allKeys.length >= count) {
399
+ allKeys = allKeys.slice(0, count); // Trim to exact count
400
+ break;
401
+ }
402
+ } while (cursor !== 0 && cursor !== '0');
403
+
404
+ msg.payload = {
405
+ pattern: pattern,
406
+ keys: allKeys,
407
+ count: allKeys.length,
408
+ limit: count,
409
+ cursor: cursor, // Return next cursor for pagination
410
+ startCursor: startCursor,
411
+ scanned: true,
412
+ truncated: allKeys.length === count,
413
+ iterations: iterations
414
+ };
415
+ break;
416
+
364
417
  // TTL Operations
365
418
  case "ttl":
366
419
  let ttlKey = payload.key || payload;
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 node-red-contrib-redis-variable
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.