node-red-contrib-redis-variable 1.2.0 → 1.3.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 +1 -1
- package/redis-variable-config.js +135 -14
- package/redis-variable.js +378 -300
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-redis-variable",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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",
|
package/redis-variable-config.js
CHANGED
|
@@ -157,6 +157,51 @@ module.exports = function (RED) {
|
|
|
157
157
|
}
|
|
158
158
|
};
|
|
159
159
|
|
|
160
|
+
// Check if configuration exists in context
|
|
161
|
+
this.hasValidConfiguration = function(msg, executingNode) {
|
|
162
|
+
try {
|
|
163
|
+
// If using string configuration for host and port, it's always valid
|
|
164
|
+
if (this.hostType === 'str' && this.portType === 'str') {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// For context-based configuration, check if the required values exist
|
|
169
|
+
let hasHost = false;
|
|
170
|
+
let hasPort = false;
|
|
171
|
+
|
|
172
|
+
// Check host configuration
|
|
173
|
+
if (this.hostType === 'str') {
|
|
174
|
+
hasHost = !!(this.host);
|
|
175
|
+
} else {
|
|
176
|
+
const contextHost = this.parseCredentialValue(this.hostContext, this.hostType, msg, executingNode);
|
|
177
|
+
hasHost = !!(contextHost);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check port configuration
|
|
181
|
+
if (this.portType === 'str') {
|
|
182
|
+
hasPort = !!(this.port);
|
|
183
|
+
} else {
|
|
184
|
+
const contextPort = this.parseCredentialValue(this.portContext, this.portType, msg, executingNode);
|
|
185
|
+
hasPort = !!(contextPort);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// We need at least host and port to have a valid configuration
|
|
189
|
+
const result = hasHost && hasPort;
|
|
190
|
+
|
|
191
|
+
if (executingNode && process.env.NODE_RED_DEBUG) {
|
|
192
|
+
executingNode.log(`Configuration check - hasHost: ${hasHost}, hasPort: ${hasPort}, result: ${result}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return result;
|
|
196
|
+
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (executingNode) {
|
|
199
|
+
executingNode.error(`Error checking configuration: ${error.message}`);
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
160
205
|
// Get Redis connection options
|
|
161
206
|
this.getConnectionOptions = function(msg, executingNode) {
|
|
162
207
|
try {
|
|
@@ -301,35 +346,97 @@ module.exports = function (RED) {
|
|
|
301
346
|
return connections[id];
|
|
302
347
|
}
|
|
303
348
|
|
|
349
|
+
// Check if configuration is available before attempting connection
|
|
350
|
+
if (!this.hasValidConfiguration(msg, executingNode)) {
|
|
351
|
+
if (executingNode) {
|
|
352
|
+
// Only warn once per node to avoid spam
|
|
353
|
+
if (!executingNode._configWarningShown) {
|
|
354
|
+
executingNode.warn("Redis configuration not available in context. Skipping connection attempt.");
|
|
355
|
+
executingNode._configWarningShown = true;
|
|
356
|
+
|
|
357
|
+
// Reset warning flag after 30 seconds
|
|
358
|
+
setTimeout(() => {
|
|
359
|
+
if (executingNode) {
|
|
360
|
+
executingNode._configWarningShown = false;
|
|
361
|
+
}
|
|
362
|
+
}, 30000);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
304
368
|
const options = this.getConnectionOptions(msg, executingNode);
|
|
305
369
|
|
|
370
|
+
// Add connection limits to prevent infinite retry loops
|
|
371
|
+
const connectionOptions = {
|
|
372
|
+
...options,
|
|
373
|
+
maxRetriesPerRequest: 1, // Limit retries per request
|
|
374
|
+
retryDelayOnFailover: 100,
|
|
375
|
+
enableReadyCheck: false,
|
|
376
|
+
lazyConnect: true, // Don't connect immediately
|
|
377
|
+
connectTimeout: 5000, // 5 second timeout
|
|
378
|
+
commandTimeout: 3000, // 3 second command timeout
|
|
379
|
+
// Disable automatic reconnection to prevent error loops
|
|
380
|
+
retryDelayOnClusterDown: 0,
|
|
381
|
+
retryDelayOnFailover: 0,
|
|
382
|
+
maxRetriesPerRequest: 0
|
|
383
|
+
};
|
|
384
|
+
|
|
306
385
|
// Create Redis client
|
|
307
386
|
let client;
|
|
308
387
|
if (this.cluster) {
|
|
309
388
|
// For cluster mode, options should be an array of nodes
|
|
310
|
-
const clusterNodes = Array.isArray(
|
|
389
|
+
const clusterNodes = Array.isArray(connectionOptions) ? connectionOptions : [connectionOptions];
|
|
311
390
|
client = new Redis.Cluster(clusterNodes);
|
|
312
391
|
} else {
|
|
313
|
-
client = new Redis(
|
|
392
|
+
client = new Redis(connectionOptions);
|
|
314
393
|
}
|
|
315
394
|
|
|
395
|
+
// Track error state to prevent spam
|
|
396
|
+
let errorReported = false;
|
|
397
|
+
let lastErrorTime = 0;
|
|
398
|
+
const ERROR_REPORT_INTERVAL = 30000; // Report errors only once per 30 seconds
|
|
399
|
+
|
|
316
400
|
// Handle connection errors
|
|
317
401
|
client.on("error", (e) => {
|
|
318
|
-
|
|
402
|
+
const now = Date.now();
|
|
319
403
|
|
|
320
|
-
//
|
|
321
|
-
if (
|
|
322
|
-
errorMsg
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
404
|
+
// Only report errors once per interval to prevent spam
|
|
405
|
+
if (!errorReported || (now - lastErrorTime) > ERROR_REPORT_INTERVAL) {
|
|
406
|
+
let errorMsg = `Redis connection error: ${e.message}`;
|
|
407
|
+
|
|
408
|
+
// Add specific diagnostics for common SSL issues
|
|
409
|
+
if (e.message.includes("Protocol error") || e.message.includes("\\u0015")) {
|
|
410
|
+
errorMsg += "\nThis usually indicates an SSL/TLS configuration issue. Try:";
|
|
411
|
+
errorMsg += "\n1. Disable SSL/TLS if your Redis server doesn't support it";
|
|
412
|
+
errorMsg += "\n2. Use port 6380 for Redis SSL instead of 6379";
|
|
413
|
+
errorMsg += "\n3. Check if your Redis server is configured for SSL";
|
|
414
|
+
errorMsg += "\n4. Enable 'Debug: Force No SSL' option for testing";
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (executingNode) {
|
|
418
|
+
executingNode.error(errorMsg, {});
|
|
419
|
+
} else {
|
|
420
|
+
this.error(errorMsg, {});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
errorReported = true;
|
|
424
|
+
lastErrorTime = now;
|
|
327
425
|
}
|
|
328
|
-
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Handle successful connection
|
|
429
|
+
client.on("connect", () => {
|
|
430
|
+
errorReported = false;
|
|
329
431
|
if (executingNode) {
|
|
330
|
-
executingNode.
|
|
331
|
-
}
|
|
332
|
-
|
|
432
|
+
executingNode.status({ fill: "green", shape: "dot", text: "connected" });
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Handle disconnection
|
|
437
|
+
client.on("disconnect", () => {
|
|
438
|
+
if (executingNode) {
|
|
439
|
+
executingNode.status({ fill: "red", shape: "ring", text: "disconnected" });
|
|
333
440
|
}
|
|
334
441
|
});
|
|
335
442
|
|
|
@@ -364,6 +471,20 @@ module.exports = function (RED) {
|
|
|
364
471
|
}
|
|
365
472
|
};
|
|
366
473
|
|
|
474
|
+
// Force disconnect method for error recovery
|
|
475
|
+
this.forceDisconnect = function(nodeId) {
|
|
476
|
+
const id = nodeId || this.id;
|
|
477
|
+
if (connections[id]) {
|
|
478
|
+
try {
|
|
479
|
+
connections[id].disconnect();
|
|
480
|
+
} catch (e) {
|
|
481
|
+
// Ignore disconnect errors
|
|
482
|
+
}
|
|
483
|
+
delete connections[id];
|
|
484
|
+
delete usedConn[id];
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
367
488
|
// Clean up on node close
|
|
368
489
|
this.on('close', function() {
|
|
369
490
|
this.disconnect();
|
package/redis-variable.js
CHANGED
|
@@ -280,7 +280,37 @@ module.exports = function (RED) {
|
|
|
280
280
|
if (!client) {
|
|
281
281
|
client = redisConfig.getClient(msg, node, node.id);
|
|
282
282
|
if (!client) {
|
|
283
|
-
|
|
283
|
+
node.warn("Redis configuration not available. Operation skipped.");
|
|
284
|
+
node.status({ fill: "yellow", shape: "ring", text: "no config" });
|
|
285
|
+
msg.payload = { error: "Redis configuration not available" };
|
|
286
|
+
send(msg);
|
|
287
|
+
done();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Check if client is connected before proceeding
|
|
293
|
+
if (client.status !== 'ready' && client.status !== 'connect') {
|
|
294
|
+
// Try to connect if not connected
|
|
295
|
+
if (client.status === 'disconnect' || client.status === 'end') {
|
|
296
|
+
await client.connect();
|
|
297
|
+
} else {
|
|
298
|
+
// Force disconnect and recreate client for other error states
|
|
299
|
+
try {
|
|
300
|
+
redisConfig.forceDisconnect(node.id);
|
|
301
|
+
client = null;
|
|
302
|
+
client = redisConfig.getClient(msg, node, node.id);
|
|
303
|
+
if (!client) {
|
|
304
|
+
node.warn("Redis configuration not available during reconnection. Operation skipped.");
|
|
305
|
+
node.status({ fill: "yellow", shape: "ring", text: "no config" });
|
|
306
|
+
msg.payload = { error: "Redis configuration not available" };
|
|
307
|
+
send(msg);
|
|
308
|
+
done();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
} catch (reconnectError) {
|
|
312
|
+
throw new Error(`Failed to reconnect: ${reconnectError.message}`);
|
|
313
|
+
}
|
|
284
314
|
}
|
|
285
315
|
}
|
|
286
316
|
} catch (err) {
|
|
@@ -300,330 +330,378 @@ module.exports = function (RED) {
|
|
|
300
330
|
throw new Error("Missing payload");
|
|
301
331
|
}
|
|
302
332
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
333
|
+
// Wrap all Redis operations in try-catch to handle connection errors gracefully
|
|
334
|
+
try {
|
|
335
|
+
switch (node.operation) {
|
|
336
|
+
case "get":
|
|
337
|
+
let getKey = payload.key || payload;
|
|
338
|
+
if (!getKey || typeof getKey !== 'string') {
|
|
339
|
+
throw new Error("Missing or invalid key for GET operation. Use payload.key or payload as string");
|
|
340
|
+
}
|
|
341
|
+
response = await client.get(getKey);
|
|
342
|
+
msg.payload = smartParse(response);
|
|
343
|
+
break;
|
|
312
344
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
345
|
+
case "set":
|
|
346
|
+
if (!payload.key) {
|
|
347
|
+
throw new Error("Missing key for SET operation. Use payload.key");
|
|
348
|
+
}
|
|
349
|
+
let setValue = payload.value !== undefined ? payload.value : payload.data;
|
|
350
|
+
if (setValue === undefined) {
|
|
351
|
+
throw new Error("Missing value for SET operation. Use payload.value or payload.data");
|
|
352
|
+
}
|
|
353
|
+
setValue = smartSerialize(setValue);
|
|
354
|
+
|
|
355
|
+
// Support TTL
|
|
356
|
+
if (payload.ttl && payload.ttl > 0) {
|
|
357
|
+
response = await client.setex(payload.key, payload.ttl, setValue);
|
|
358
|
+
} else {
|
|
359
|
+
response = await client.set(payload.key, setValue);
|
|
360
|
+
}
|
|
361
|
+
msg.payload = { success: true, result: response, ttl: payload.ttl || null };
|
|
362
|
+
break;
|
|
331
363
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
364
|
+
case "del":
|
|
365
|
+
let delKeys = payload.keys || payload.key || payload;
|
|
366
|
+
if (!delKeys) {
|
|
367
|
+
throw new Error("Missing keys for DEL operation. Use payload.keys (array) or payload.key");
|
|
368
|
+
}
|
|
369
|
+
let keysToDelete = Array.isArray(delKeys) ? delKeys : [delKeys];
|
|
370
|
+
response = await client.del(...keysToDelete);
|
|
371
|
+
msg.payload = { success: true, deleted: response, keys: keysToDelete };
|
|
372
|
+
break;
|
|
373
|
+
|
|
374
|
+
case "exists":
|
|
375
|
+
let existsKeys = payload.keys || payload.key || payload;
|
|
376
|
+
if (!existsKeys) {
|
|
377
|
+
throw new Error("Missing keys for EXISTS operation. Use payload.keys (array) or payload.key");
|
|
378
|
+
}
|
|
379
|
+
let keysToCheck = Array.isArray(existsKeys) ? existsKeys : [existsKeys];
|
|
380
|
+
response = await client.exists(...keysToCheck);
|
|
381
|
+
msg.payload = { exists: response > 0, count: response, keys: keysToCheck };
|
|
382
|
+
break;
|
|
383
|
+
|
|
384
|
+
case "match":
|
|
385
|
+
let pattern = payload.pattern || payload;
|
|
386
|
+
if (!pattern || typeof pattern !== 'string') {
|
|
387
|
+
throw new Error("Missing pattern for MATCH operation. Use payload.pattern or payload as string");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let count = payload.count || 100; // Default scan count
|
|
391
|
+
let startCursor = payload.cursor || payload.skip || 0; // Support cursor/skip for pagination
|
|
392
|
+
let allKeys = [];
|
|
393
|
+
let cursor = startCursor;
|
|
394
|
+
let maxIterations = 1000; // Prevent infinite loops
|
|
395
|
+
let iterations = 0;
|
|
396
|
+
|
|
397
|
+
// If skip is specified, we need to scan through keys without collecting them
|
|
398
|
+
if (payload.skip && payload.skip > 0) {
|
|
399
|
+
let skipped = 0;
|
|
400
|
+
do {
|
|
401
|
+
if (iterations++ > maxIterations) {
|
|
402
|
+
throw new Error("Maximum scan iterations exceeded. Check your pattern or reduce skip value.");
|
|
403
|
+
}
|
|
404
|
+
const scanResult = await client.scan(cursor, 'MATCH', pattern, 'COUNT', count);
|
|
405
|
+
cursor = scanResult[0];
|
|
406
|
+
skipped += scanResult[1].length;
|
|
407
|
+
|
|
408
|
+
if (skipped >= payload.skip) {
|
|
409
|
+
// We've skipped enough, start collecting from this point
|
|
410
|
+
const excessSkipped = skipped - payload.skip;
|
|
411
|
+
if (excessSkipped > 0) {
|
|
412
|
+
// Add keys from current batch, excluding the excess
|
|
413
|
+
allKeys = scanResult[1].slice(excessSkipped);
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
} while (cursor !== 0 && cursor !== '0');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Continue scanning to collect keys up to the limit
|
|
368
421
|
do {
|
|
369
422
|
if (iterations++ > maxIterations) {
|
|
370
|
-
throw new Error("Maximum scan iterations exceeded. Check your pattern or reduce
|
|
423
|
+
throw new Error("Maximum scan iterations exceeded. Check your pattern or reduce count value.");
|
|
371
424
|
}
|
|
372
425
|
const scanResult = await client.scan(cursor, 'MATCH', pattern, 'COUNT', count);
|
|
373
426
|
cursor = scanResult[0];
|
|
374
|
-
|
|
427
|
+
allKeys = allKeys.concat(scanResult[1]);
|
|
375
428
|
|
|
376
|
-
if
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (excessSkipped > 0) {
|
|
380
|
-
// Add keys from current batch, excluding the excess
|
|
381
|
-
allKeys = scanResult[1].slice(excessSkipped);
|
|
382
|
-
}
|
|
429
|
+
// Break early if we've found enough keys
|
|
430
|
+
if (allKeys.length >= count) {
|
|
431
|
+
allKeys = allKeys.slice(0, count); // Trim to exact count
|
|
383
432
|
break;
|
|
384
433
|
}
|
|
385
434
|
} 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
435
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
436
|
+
msg.payload = {
|
|
437
|
+
pattern: pattern,
|
|
438
|
+
keys: allKeys,
|
|
439
|
+
count: allKeys.length,
|
|
440
|
+
limit: count,
|
|
441
|
+
cursor: cursor, // Return next cursor for pagination
|
|
442
|
+
startCursor: startCursor,
|
|
443
|
+
scanned: true,
|
|
444
|
+
truncated: allKeys.length === count,
|
|
445
|
+
iterations: iterations
|
|
446
|
+
};
|
|
447
|
+
break;
|
|
448
|
+
|
|
449
|
+
// TTL Operations
|
|
450
|
+
case "ttl":
|
|
451
|
+
let ttlKey = payload.key || payload;
|
|
452
|
+
if (!ttlKey || typeof ttlKey !== 'string') {
|
|
453
|
+
throw new Error("Missing key for TTL operation. Use payload.key or payload as string");
|
|
401
454
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
455
|
+
response = await client.ttl(ttlKey);
|
|
456
|
+
msg.payload = {
|
|
457
|
+
key: ttlKey,
|
|
458
|
+
ttl: response,
|
|
459
|
+
status: response === -1 ? "no expiration" : response === -2 ? "key not found" : "expires in " + response + " seconds"
|
|
460
|
+
};
|
|
461
|
+
break;
|
|
462
|
+
|
|
463
|
+
case "expire":
|
|
464
|
+
if (!payload.key) {
|
|
465
|
+
throw new Error("Missing key for EXPIRE operation. Use payload.key");
|
|
466
|
+
}
|
|
467
|
+
let expireSeconds = payload.ttl || payload.seconds || payload.value || 3600;
|
|
468
|
+
response = await client.expire(payload.key, expireSeconds);
|
|
469
|
+
msg.payload = {
|
|
470
|
+
success: response === 1,
|
|
471
|
+
key: payload.key,
|
|
472
|
+
ttl: expireSeconds,
|
|
473
|
+
message: response === 1 ? "Expiration set" : "Key not found"
|
|
474
|
+
};
|
|
475
|
+
break;
|
|
476
|
+
|
|
477
|
+
case "persist":
|
|
478
|
+
let persistKey = payload.key || payload;
|
|
479
|
+
if (!persistKey || typeof persistKey !== 'string') {
|
|
480
|
+
throw new Error("Missing key for PERSIST operation. Use payload.key or payload as string");
|
|
481
|
+
}
|
|
482
|
+
response = await client.persist(persistKey);
|
|
483
|
+
msg.payload = {
|
|
484
|
+
success: response === 1,
|
|
485
|
+
key: persistKey,
|
|
486
|
+
message: response === 1 ? "Expiration removed" : "Key not found or no expiration"
|
|
487
|
+
};
|
|
488
|
+
break;
|
|
489
|
+
|
|
490
|
+
// Counter Operations
|
|
491
|
+
case "incr":
|
|
492
|
+
let incrKey = payload.key || payload;
|
|
493
|
+
if (!incrKey || typeof incrKey !== 'string') {
|
|
494
|
+
throw new Error("Missing key for INCR operation. Use payload.key or payload as string");
|
|
495
|
+
}
|
|
496
|
+
response = await client.incr(incrKey);
|
|
497
|
+
msg.payload = { key: incrKey, value: response };
|
|
498
|
+
break;
|
|
499
|
+
|
|
500
|
+
case "decr":
|
|
501
|
+
let decrKey = payload.key || payload;
|
|
502
|
+
if (!decrKey || typeof decrKey !== 'string') {
|
|
503
|
+
throw new Error("Missing key for DECR operation. Use payload.key or payload as string");
|
|
504
|
+
}
|
|
505
|
+
response = await client.decr(decrKey);
|
|
506
|
+
msg.payload = { key: decrKey, value: response };
|
|
507
|
+
break;
|
|
416
508
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
509
|
+
case "incrby":
|
|
510
|
+
if (!payload.key) {
|
|
511
|
+
throw new Error("Missing key for INCRBY operation. Use payload.key");
|
|
512
|
+
}
|
|
513
|
+
let incrAmount = payload.amount || payload.value || payload.increment || 1;
|
|
514
|
+
response = await client.incrby(payload.key, incrAmount);
|
|
515
|
+
msg.payload = { key: payload.key, value: response, increment: incrAmount };
|
|
516
|
+
break;
|
|
517
|
+
|
|
518
|
+
case "decrby":
|
|
519
|
+
if (!payload.key) {
|
|
520
|
+
throw new Error("Missing key for DECRBY operation. Use payload.key");
|
|
521
|
+
}
|
|
522
|
+
let decrAmount = payload.amount || payload.value || payload.decrement || 1;
|
|
523
|
+
response = await client.decrby(payload.key, decrAmount);
|
|
524
|
+
msg.payload = { key: payload.key, value: response, decrement: decrAmount };
|
|
525
|
+
break;
|
|
526
|
+
|
|
527
|
+
// List Operations
|
|
528
|
+
case "lpush":
|
|
529
|
+
if (!payload.key) {
|
|
530
|
+
throw new Error("Missing key for LPUSH operation. Use payload.key");
|
|
531
|
+
}
|
|
532
|
+
let lpushValue = payload.value !== undefined ? payload.value : payload.data;
|
|
533
|
+
if (lpushValue === undefined) {
|
|
534
|
+
throw new Error("Missing value for LPUSH operation. Use payload.value or payload.data");
|
|
535
|
+
}
|
|
536
|
+
lpushValue = smartSerialize(lpushValue);
|
|
537
|
+
response = await client.lpush(payload.key, lpushValue);
|
|
538
|
+
msg.payload = { success: true, result: response, key: payload.key };
|
|
539
|
+
break;
|
|
540
|
+
|
|
541
|
+
case "rpush":
|
|
542
|
+
if (!payload.key) {
|
|
543
|
+
throw new Error("Missing key for RPUSH operation. Use payload.key");
|
|
544
|
+
}
|
|
545
|
+
let rpushValue = payload.value !== undefined ? payload.value : payload.data;
|
|
546
|
+
if (rpushValue === undefined) {
|
|
547
|
+
throw new Error("Missing value for RPUSH operation. Use payload.value or payload.data");
|
|
548
|
+
}
|
|
549
|
+
rpushValue = smartSerialize(rpushValue);
|
|
550
|
+
response = await client.rpush(payload.key, rpushValue);
|
|
551
|
+
msg.payload = { success: true, result: response, key: payload.key };
|
|
552
|
+
break;
|
|
553
|
+
|
|
554
|
+
case "lpop":
|
|
555
|
+
let lpopKey = payload.key || payload;
|
|
556
|
+
if (!lpopKey || typeof lpopKey !== 'string') {
|
|
557
|
+
throw new Error("Missing key for LPOP operation. Use payload.key or payload as string");
|
|
558
|
+
}
|
|
559
|
+
response = await client.lpop(lpopKey);
|
|
560
|
+
msg.payload = smartParse(response);
|
|
561
|
+
break;
|
|
562
|
+
|
|
563
|
+
case "rpop":
|
|
564
|
+
let rpopKey = payload.key || payload;
|
|
565
|
+
if (!rpopKey || typeof rpopKey !== 'string') {
|
|
566
|
+
throw new Error("Missing key for RPOP operation. Use payload.key or payload as string");
|
|
567
|
+
}
|
|
568
|
+
response = await client.rpop(rpopKey);
|
|
569
|
+
msg.payload = smartParse(response);
|
|
570
|
+
break;
|
|
571
|
+
|
|
572
|
+
case "lrange":
|
|
573
|
+
if (!payload.key) {
|
|
574
|
+
throw new Error("Missing key for LRANGE operation. Use payload.key");
|
|
575
|
+
}
|
|
576
|
+
let start = payload.start || payload.from || 0;
|
|
577
|
+
let stop = payload.stop || payload.to || -1;
|
|
578
|
+
response = await client.lrange(payload.key, start, stop);
|
|
579
|
+
msg.payload = response.map(item => smartParse(item));
|
|
580
|
+
break;
|
|
581
|
+
|
|
582
|
+
case "llen":
|
|
583
|
+
let llenKey = payload.key || payload;
|
|
584
|
+
if (!llenKey || typeof llenKey !== 'string') {
|
|
585
|
+
throw new Error("Missing key for LLEN operation. Use payload.key or payload as string");
|
|
586
|
+
}
|
|
587
|
+
response = await client.llen(llenKey);
|
|
588
|
+
msg.payload = { key: llenKey, length: response };
|
|
589
|
+
break;
|
|
590
|
+
|
|
591
|
+
// Hash Operations
|
|
592
|
+
case "hset":
|
|
593
|
+
if (!payload.key) {
|
|
594
|
+
throw new Error("Missing key for HSET operation. Use payload.key");
|
|
595
|
+
}
|
|
596
|
+
let hsetField = payload.field || payload.name;
|
|
597
|
+
let hsetValue = payload.value !== undefined ? payload.value : payload.data;
|
|
598
|
+
if (!hsetField || hsetValue === undefined) {
|
|
599
|
+
throw new Error("Missing field or value for HSET operation. Use payload.field and payload.value");
|
|
600
|
+
}
|
|
601
|
+
hsetValue = smartSerialize(hsetValue);
|
|
602
|
+
response = await client.hset(payload.key, hsetField, hsetValue);
|
|
603
|
+
msg.payload = { success: true, result: response, key: payload.key, field: hsetField };
|
|
604
|
+
break;
|
|
605
|
+
|
|
606
|
+
case "hget":
|
|
607
|
+
if (!payload.key) {
|
|
608
|
+
throw new Error("Missing key for HGET operation. Use payload.key");
|
|
609
|
+
}
|
|
610
|
+
let hgetField = payload.field || payload.name;
|
|
611
|
+
if (!hgetField) {
|
|
612
|
+
throw new Error("Missing field for HGET operation. Use payload.field");
|
|
613
|
+
}
|
|
614
|
+
response = await client.hget(payload.key, hgetField);
|
|
615
|
+
msg.payload = smartParse(response);
|
|
616
|
+
break;
|
|
617
|
+
|
|
618
|
+
case "hgetall":
|
|
619
|
+
let hgetallKey = payload.key || payload;
|
|
620
|
+
if (!hgetallKey || typeof hgetallKey !== 'string') {
|
|
621
|
+
throw new Error("Missing key for HGETALL operation. Use payload.key or payload as string");
|
|
622
|
+
}
|
|
623
|
+
response = await client.hgetall(hgetallKey);
|
|
624
|
+
// Parse all values in the hash
|
|
625
|
+
const parsedHash = {};
|
|
626
|
+
for (const [field, value] of Object.entries(response)) {
|
|
627
|
+
parsedHash[field] = smartParse(value);
|
|
628
|
+
}
|
|
629
|
+
msg.payload = parsedHash;
|
|
630
|
+
break;
|
|
631
|
+
|
|
632
|
+
case "hdel":
|
|
633
|
+
if (!payload.key) {
|
|
634
|
+
throw new Error("Missing key for HDEL operation. Use payload.key");
|
|
635
|
+
}
|
|
636
|
+
let hdelFields = payload.fields || payload.field || payload.names;
|
|
637
|
+
if (!hdelFields) {
|
|
638
|
+
throw new Error("Missing fields for HDEL operation. Use payload.fields (array) or payload.field");
|
|
639
|
+
}
|
|
640
|
+
let fieldsToDelete = Array.isArray(hdelFields) ? hdelFields : [hdelFields];
|
|
641
|
+
response = await client.hdel(payload.key, ...fieldsToDelete);
|
|
642
|
+
msg.payload = { success: true, deleted: response, key: payload.key, fields: fieldsToDelete };
|
|
643
|
+
break;
|
|
644
|
+
|
|
645
|
+
// Pub/Sub Operations
|
|
646
|
+
case "publish":
|
|
647
|
+
if (!payload.channel) {
|
|
648
|
+
throw new Error("Missing channel for PUBLISH operation. Use payload.channel");
|
|
649
|
+
}
|
|
650
|
+
let publishMessage = payload.message !== undefined ? payload.message : payload.data;
|
|
651
|
+
if (publishMessage === undefined) {
|
|
652
|
+
throw new Error("Missing message for PUBLISH operation. Use payload.message or payload.data");
|
|
653
|
+
}
|
|
654
|
+
publishMessage = smartSerialize(publishMessage);
|
|
655
|
+
response = await client.publish(payload.channel, publishMessage);
|
|
656
|
+
msg.payload = { success: true, subscribers: response, channel: payload.channel };
|
|
657
|
+
break;
|
|
430
658
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
659
|
+
default:
|
|
660
|
+
throw new Error(`Unknown operation: ${node.operation}`);
|
|
661
|
+
}
|
|
662
|
+
} catch (redisError) {
|
|
663
|
+
// Handle Redis-specific errors (connection, command errors, etc.)
|
|
664
|
+
if (redisError.message.includes('ECONNREFUSED') ||
|
|
665
|
+
redisError.message.includes('ENOTFOUND') ||
|
|
666
|
+
redisError.message.includes('ETIMEDOUT')) {
|
|
667
|
+
// Connection-related errors - force disconnect to prevent dead connections
|
|
668
|
+
if (client) {
|
|
669
|
+
try {
|
|
670
|
+
redisConfig.forceDisconnect(node.id);
|
|
671
|
+
client = null;
|
|
672
|
+
} catch (e) {
|
|
673
|
+
// Ignore disconnect errors
|
|
674
|
+
}
|
|
434
675
|
}
|
|
435
|
-
|
|
436
|
-
response = await client.expire(payload.key, expireSeconds);
|
|
676
|
+
|
|
437
677
|
msg.payload = {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
message: response === 1 ? "Expiration set" : "Key not found"
|
|
678
|
+
error: `Redis connection failed: ${redisError.message}`,
|
|
679
|
+
operation: node.operation,
|
|
680
|
+
retryable: true
|
|
442
681
|
};
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
let persistKey = payload.key || payload;
|
|
447
|
-
if (!persistKey || typeof persistKey !== 'string') {
|
|
448
|
-
throw new Error("Missing key for PERSIST operation. Use payload.key or payload as string");
|
|
449
|
-
}
|
|
450
|
-
response = await client.persist(persistKey);
|
|
682
|
+
node.status({ fill: "red", shape: "ring", text: "connection failed" });
|
|
683
|
+
} else {
|
|
684
|
+
// Other Redis errors (command errors, etc.)
|
|
451
685
|
msg.payload = {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
686
|
+
error: `Redis operation failed: ${redisError.message}`,
|
|
687
|
+
operation: node.operation,
|
|
688
|
+
retryable: false
|
|
455
689
|
};
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
let incrKey = payload.key || payload;
|
|
461
|
-
if (!incrKey || typeof incrKey !== 'string') {
|
|
462
|
-
throw new Error("Missing key for INCR operation. Use payload.key or payload as string");
|
|
463
|
-
}
|
|
464
|
-
response = await client.incr(incrKey);
|
|
465
|
-
msg.payload = { key: incrKey, value: response };
|
|
466
|
-
break;
|
|
467
|
-
|
|
468
|
-
case "decr":
|
|
469
|
-
let decrKey = payload.key || payload;
|
|
470
|
-
if (!decrKey || typeof decrKey !== 'string') {
|
|
471
|
-
throw new Error("Missing key for DECR operation. Use payload.key or payload as string");
|
|
472
|
-
}
|
|
473
|
-
response = await client.decr(decrKey);
|
|
474
|
-
msg.payload = { key: decrKey, value: response };
|
|
475
|
-
break;
|
|
476
|
-
|
|
477
|
-
case "incrby":
|
|
478
|
-
if (!payload.key) {
|
|
479
|
-
throw new Error("Missing key for INCRBY operation. Use payload.key");
|
|
480
|
-
}
|
|
481
|
-
let incrAmount = payload.amount || payload.value || payload.increment || 1;
|
|
482
|
-
response = await client.incrby(payload.key, incrAmount);
|
|
483
|
-
msg.payload = { key: payload.key, value: response, increment: incrAmount };
|
|
484
|
-
break;
|
|
485
|
-
|
|
486
|
-
case "decrby":
|
|
487
|
-
if (!payload.key) {
|
|
488
|
-
throw new Error("Missing key for DECRBY operation. Use payload.key");
|
|
489
|
-
}
|
|
490
|
-
let decrAmount = payload.amount || payload.value || payload.decrement || 1;
|
|
491
|
-
response = await client.decrby(payload.key, decrAmount);
|
|
492
|
-
msg.payload = { key: payload.key, value: response, decrement: decrAmount };
|
|
493
|
-
break;
|
|
494
|
-
|
|
495
|
-
// List Operations
|
|
496
|
-
case "lpush":
|
|
497
|
-
case "rpush":
|
|
498
|
-
if (!payload.key) {
|
|
499
|
-
throw new Error(`Missing key for ${node.operation.toUpperCase()} operation. Use payload.key`);
|
|
500
|
-
}
|
|
501
|
-
let pushValue = payload.value !== undefined ? payload.value : payload.data;
|
|
502
|
-
if (pushValue === undefined) {
|
|
503
|
-
throw new Error(`Missing value for ${node.operation.toUpperCase()} operation. Use payload.value or payload.data`);
|
|
504
|
-
}
|
|
505
|
-
pushValue = smartSerialize(pushValue);
|
|
506
|
-
response = await client[node.operation](payload.key, pushValue);
|
|
507
|
-
msg.payload = { success: true, key: payload.key, length: response, operation: node.operation };
|
|
508
|
-
break;
|
|
509
|
-
|
|
510
|
-
case "lpop":
|
|
511
|
-
case "rpop":
|
|
512
|
-
let popKey = payload.key || payload;
|
|
513
|
-
if (!popKey || typeof popKey !== 'string') {
|
|
514
|
-
throw new Error(`Missing key for ${node.operation.toUpperCase()} operation. Use payload.key or payload as string`);
|
|
515
|
-
}
|
|
516
|
-
response = await client[node.operation](popKey);
|
|
517
|
-
msg.payload = smartParse(response);
|
|
518
|
-
break;
|
|
519
|
-
|
|
520
|
-
case "llen":
|
|
521
|
-
let llenKey = payload.key || payload;
|
|
522
|
-
if (!llenKey || typeof llenKey !== 'string') {
|
|
523
|
-
throw new Error("Missing key for LLEN operation. Use payload.key or payload as string");
|
|
524
|
-
}
|
|
525
|
-
response = await client.llen(llenKey);
|
|
526
|
-
msg.payload = { key: llenKey, length: response };
|
|
527
|
-
break;
|
|
528
|
-
|
|
529
|
-
case "lrange":
|
|
530
|
-
if (!payload.key) {
|
|
531
|
-
throw new Error("Missing key for LRANGE operation. Use payload.key");
|
|
532
|
-
}
|
|
533
|
-
let start = payload.start !== undefined ? payload.start : 0;
|
|
534
|
-
let stop = payload.stop !== undefined ? payload.stop : -1;
|
|
535
|
-
response = await client.lrange(payload.key, start, stop);
|
|
536
|
-
// Auto-parse each item in the array
|
|
537
|
-
response = response.map(item => smartParse(item));
|
|
538
|
-
msg.payload = { key: payload.key, range: { start, stop }, values: response, count: response.length };
|
|
539
|
-
break;
|
|
540
|
-
|
|
541
|
-
// Hash Operations
|
|
542
|
-
case "hset":
|
|
543
|
-
if (!payload.key) {
|
|
544
|
-
throw new Error("Missing key for HSET operation. Use payload.key");
|
|
545
|
-
}
|
|
546
|
-
if (payload.field && payload.value !== undefined) {
|
|
547
|
-
// Single field
|
|
548
|
-
let hashValue = smartSerialize(payload.value);
|
|
549
|
-
response = await client.hset(payload.key, payload.field, hashValue);
|
|
550
|
-
msg.payload = { success: true, key: payload.key, field: payload.field, created: response === 1 };
|
|
551
|
-
} else if (payload.fields && typeof payload.fields === 'object') {
|
|
552
|
-
// Multiple fields from payload.fields
|
|
553
|
-
const fields = {};
|
|
554
|
-
for (const [key, value] of Object.entries(payload.fields)) {
|
|
555
|
-
fields[key] = smartSerialize(value);
|
|
556
|
-
}
|
|
557
|
-
response = await client.hset(payload.key, fields);
|
|
558
|
-
msg.payload = { success: true, key: payload.key, fields: Object.keys(fields), created: response };
|
|
559
|
-
} else {
|
|
560
|
-
throw new Error("HSET requires field and value (payload.field, payload.value) or multiple fields (payload.fields)");
|
|
561
|
-
}
|
|
562
|
-
break;
|
|
563
|
-
|
|
564
|
-
case "hget":
|
|
565
|
-
if (!payload.key) {
|
|
566
|
-
throw new Error("Missing key for HGET operation. Use payload.key");
|
|
567
|
-
}
|
|
568
|
-
let field = payload.field;
|
|
569
|
-
if (!field) {
|
|
570
|
-
throw new Error("Missing field for HGET operation. Use payload.field");
|
|
571
|
-
}
|
|
572
|
-
response = await client.hget(payload.key, field);
|
|
573
|
-
msg.payload = smartParse(response);
|
|
574
|
-
break;
|
|
575
|
-
|
|
576
|
-
case "hgetall":
|
|
577
|
-
let hgetallKey = payload.key || payload;
|
|
578
|
-
if (!hgetallKey || typeof hgetallKey !== 'string') {
|
|
579
|
-
throw new Error("Missing key for HGETALL operation. Use payload.key or payload as string");
|
|
580
|
-
}
|
|
581
|
-
response = await client.hgetall(hgetallKey);
|
|
582
|
-
// Auto-parse each field value
|
|
583
|
-
const parsed = {};
|
|
584
|
-
for (const [key, value] of Object.entries(response)) {
|
|
585
|
-
parsed[key] = smartParse(value);
|
|
586
|
-
}
|
|
587
|
-
msg.payload = parsed;
|
|
588
|
-
break;
|
|
589
|
-
|
|
590
|
-
case "hdel":
|
|
591
|
-
if (!payload.key) {
|
|
592
|
-
throw new Error("Missing key for HDEL operation. Use payload.key");
|
|
593
|
-
}
|
|
594
|
-
let fieldsToDelete = payload.fields || payload.field;
|
|
595
|
-
if (!fieldsToDelete) {
|
|
596
|
-
throw new Error("Missing fields for HDEL operation. Use payload.fields (array) or payload.field");
|
|
597
|
-
}
|
|
598
|
-
fieldsToDelete = Array.isArray(fieldsToDelete) ? fieldsToDelete : [fieldsToDelete];
|
|
599
|
-
response = await client.hdel(payload.key, ...fieldsToDelete);
|
|
600
|
-
msg.payload = { success: true, key: payload.key, deleted: response, fields: fieldsToDelete };
|
|
601
|
-
break;
|
|
602
|
-
|
|
603
|
-
case "publish":
|
|
604
|
-
let channel = payload.channel || payload.key;
|
|
605
|
-
if (!channel) {
|
|
606
|
-
throw new Error("Missing channel for PUBLISH operation. Use payload.channel or payload.key");
|
|
607
|
-
}
|
|
608
|
-
let pubValue = payload.message || payload.value || payload.data;
|
|
609
|
-
if (pubValue === undefined) {
|
|
610
|
-
throw new Error("Missing message for PUBLISH operation. Use payload.message, payload.value, or payload.data");
|
|
611
|
-
}
|
|
612
|
-
pubValue = smartSerialize(pubValue);
|
|
613
|
-
response = await client.publish(channel, pubValue);
|
|
614
|
-
msg.payload = { success: true, channel: channel, subscribers: response, message: pubValue };
|
|
615
|
-
break;
|
|
616
|
-
|
|
617
|
-
default:
|
|
618
|
-
throw new Error(`Unsupported operation: ${node.operation}`);
|
|
690
|
+
}
|
|
691
|
+
send(msg);
|
|
692
|
+
done();
|
|
693
|
+
return;
|
|
619
694
|
}
|
|
620
695
|
|
|
696
|
+
// Update node status on success
|
|
697
|
+
node.status({ fill: "green", shape: "dot", text: node.operation });
|
|
621
698
|
send(msg);
|
|
622
699
|
done();
|
|
623
700
|
|
|
624
|
-
} catch (
|
|
625
|
-
|
|
626
|
-
|
|
701
|
+
} catch (error) {
|
|
702
|
+
// Handle general errors (validation, etc.)
|
|
703
|
+
node.error(error.message, msg);
|
|
704
|
+
msg.payload = { error: error.message };
|
|
627
705
|
send(msg);
|
|
628
706
|
done();
|
|
629
707
|
}
|