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 +259 -2
- package/package.json +6 -3
- package/redis-variable-config.html +17 -1
- package/redis-variable-config.js +55 -39
- package/redis-variable.html +77 -0
- package/redis-variable.js +96 -43
- package/LICENSE +0 -21
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.
|
|
4
|
-
"description": "A comprehensive Node-RED node for Redis operations with universal payload-based configuration, automatic JSON handling,
|
|
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>
|
package/redis-variable-config.js
CHANGED
|
@@ -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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
163
|
-
|
|
164
|
-
this.
|
|
165
|
-
|
|
166
|
-
executingNode
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// Parse port
|
|
170
|
-
let port
|
|
171
|
-
|
|
172
|
-
this.
|
|
173
|
-
|
|
174
|
-
executingNode
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// Parse database
|
|
178
|
-
let database
|
|
179
|
-
|
|
180
|
-
this.
|
|
181
|
-
|
|
182
|
-
executingNode
|
|
183
|
-
|
|
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(
|
|
330
|
+
executingNode.error(errorMsg, {});
|
|
315
331
|
} else {
|
|
316
|
-
this.error(
|
|
332
|
+
this.error(errorMsg, {});
|
|
317
333
|
}
|
|
318
334
|
});
|
|
319
335
|
|
package/redis-variable.html
CHANGED
|
@@ -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.
|