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