node-red-contrib-redis-variable 1.0.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.
@@ -0,0 +1,621 @@
1
+ const async = require("async");
2
+
3
+ module.exports = function (RED) {
4
+ function RedisVariableNode(config) {
5
+ RED.nodes.createNode(this, config);
6
+ const node = this;
7
+
8
+ // Node configuration
9
+ this.operation = config.operation || "get";
10
+ this.timeout = config.timeout || 0;
11
+ this.block = config.block || false;
12
+ this.keyval = config.keyval || 0;
13
+ this.func = config.func;
14
+ this.stored = config.stored || false;
15
+ this.params = config.params;
16
+ this.location = config.location || 'flow';
17
+ this.sha1 = "";
18
+
19
+ let client = null;
20
+ let running = true;
21
+
22
+ // Helper functions for automatic JSON handling
23
+ function isJsonString(str) {
24
+ if (typeof str !== 'string') return false;
25
+ try {
26
+ const parsed = JSON.parse(str);
27
+ return typeof parsed === 'object' && parsed !== null;
28
+ } catch (e) {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ function smartSerialize(value) {
34
+ if (typeof value === 'object' && value !== null) {
35
+ return JSON.stringify(value);
36
+ }
37
+ return String(value);
38
+ }
39
+
40
+ function smartParse(str) {
41
+ if (typeof str !== 'string') return str;
42
+ if (isJsonString(str)) {
43
+ try {
44
+ return JSON.parse(str);
45
+ } catch (e) {
46
+ return str;
47
+ }
48
+ }
49
+ return str;
50
+ }
51
+
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
+ // Handle different operations
96
+ switch (this.operation) {
97
+ case "subscribe":
98
+ case "psubscribe":
99
+ handleSubscription();
100
+ break;
101
+ case "blpop":
102
+ case "brpop":
103
+ handleBlockingPop();
104
+ break;
105
+ case "lua-script":
106
+ handleLuaScript();
107
+ break;
108
+ case "instance":
109
+ handleInstance();
110
+ break;
111
+ default:
112
+ handleInput();
113
+ break;
114
+ }
115
+
116
+ // Subscription operations (subscribe, psubscribe)
117
+ function handleSubscription() {
118
+ try {
119
+ if (node.operation === "psubscribe") {
120
+ client.on("pmessage", function (pattern, channel, message) {
121
+ var payload = smartParse(message);
122
+ node.send({
123
+ pattern: pattern,
124
+ topic: channel,
125
+ payload: payload,
126
+ });
127
+ });
128
+ client[node.operation](node.topic, (err, count) => {
129
+ if (err) {
130
+ node.error(err.message);
131
+ node.status({
132
+ fill: "red",
133
+ shape: "dot",
134
+ text: "error",
135
+ });
136
+ } else {
137
+ node.status({
138
+ fill: "green",
139
+ shape: "dot",
140
+ text: "connected",
141
+ });
142
+ }
143
+ });
144
+ } else if (node.operation === "subscribe") {
145
+ client.on("message", function (channel, message) {
146
+ var payload = smartParse(message);
147
+ node.send({
148
+ topic: channel,
149
+ payload: payload,
150
+ });
151
+ });
152
+ client[node.operation](node.topic, (err, count) => {
153
+ if (err) {
154
+ node.error(err.message);
155
+ node.status({
156
+ fill: "red",
157
+ shape: "dot",
158
+ text: "error",
159
+ });
160
+ } else {
161
+ node.status({
162
+ fill: "green",
163
+ shape: "dot",
164
+ text: "connected",
165
+ });
166
+ }
167
+ });
168
+ }
169
+ } catch (error) {
170
+ node.error(`Subscription error: ${error.message}`);
171
+ node.status({
172
+ fill: "red",
173
+ shape: "dot",
174
+ text: "error",
175
+ });
176
+ }
177
+ }
178
+
179
+ // Blocking pop operations (blpop, brpop)
180
+ function handleBlockingPop() {
181
+ try {
182
+ async.whilst(
183
+ (cb) => {
184
+ cb(null, running);
185
+ },
186
+ (cb) => {
187
+ client[node.operation](node.topic, Number(node.timeout))
188
+ .then((data) => {
189
+ if (data !== null && data.length == 2) {
190
+ var payload = smartParse(data[1]);
191
+ node.send({
192
+ topic: node.topic,
193
+ payload: payload,
194
+ });
195
+ }
196
+ cb(null);
197
+ })
198
+ .catch((e) => {
199
+ node.error(e.message);
200
+ running = false;
201
+ cb(e);
202
+ });
203
+ },
204
+ () => {}
205
+ );
206
+ } catch (error) {
207
+ node.error(`Blocking pop error: ${error.message}`);
208
+ node.status({
209
+ fill: "red",
210
+ shape: "dot",
211
+ text: "error",
212
+ });
213
+ }
214
+ }
215
+
216
+ // Lua script operations
217
+ function handleLuaScript() {
218
+ try {
219
+ if (node.stored) {
220
+ client.script("load", node.func, function (err, res) {
221
+ if (err) {
222
+ node.status({
223
+ fill: "red",
224
+ shape: "dot",
225
+ text: "script not loaded",
226
+ });
227
+ node.error(err.message);
228
+ } else {
229
+ node.status({
230
+ fill: "green",
231
+ shape: "dot",
232
+ text: "script loaded",
233
+ });
234
+ node.sha1 = res;
235
+ }
236
+ });
237
+ }
238
+
239
+ node.on("input", function (msg, send, done) {
240
+ send = send || function() { node.send.apply(node, arguments) };
241
+ done = done || function(err) { if(err) node.error(err, msg); };
242
+
243
+ try {
244
+ if (node.keyval > 0 && !Array.isArray(msg.payload)) {
245
+ done(new Error("Payload is not Array"));
246
+ return;
247
+ }
248
+
249
+ var args = null;
250
+ var command = "eval";
251
+ if (node.stored) {
252
+ command = "evalsha";
253
+ args = [node.sha1, node.keyval].concat(msg.payload || []);
254
+ } else {
255
+ args = [node.func, node.keyval].concat(msg.payload || []);
256
+ }
257
+
258
+ client[command](args, function (err, res) {
259
+ if (err) {
260
+ done(err);
261
+ } else {
262
+ msg.payload = res;
263
+ send(msg);
264
+ done();
265
+ }
266
+ });
267
+ } catch (error) {
268
+ done(error);
269
+ }
270
+ });
271
+ } catch (error) {
272
+ node.error(`Lua script error: ${error.message}`);
273
+ node.status({
274
+ fill: "red",
275
+ shape: "dot",
276
+ text: "error",
277
+ });
278
+ }
279
+ }
280
+
281
+ // Instance operation - store client in context
282
+ function handleInstance() {
283
+ try {
284
+ node.context()[node.location].set(node.topic, client);
285
+ node.status({
286
+ fill: "green",
287
+ shape: "dot",
288
+ text: "ready",
289
+ });
290
+ } catch (error) {
291
+ node.error(`Failed to store Redis instance: ${error.message}`);
292
+ node.status({
293
+ fill: "red",
294
+ shape: "dot",
295
+ text: "error",
296
+ });
297
+ }
298
+ }
299
+
300
+ // Handle input for other operations
301
+ function handleInput() {
302
+ node.on('input', async (msg, send, done) => {
303
+ send = send || function() { node.send.apply(node, arguments) };
304
+ done = done || function(err) { if(err) node.error(err, msg); };
305
+
306
+ try {
307
+ let response;
308
+ let payload = msg.payload;
309
+
310
+ // Validate payload
311
+ if (!payload) {
312
+ throw new Error("Missing payload");
313
+ }
314
+
315
+ switch (node.operation) {
316
+ case "get":
317
+ let getKey = payload.key || payload;
318
+ if (!getKey || typeof getKey !== 'string') {
319
+ throw new Error("Missing or invalid key for GET operation. Use payload.key or payload as string");
320
+ }
321
+ response = await client.get(getKey);
322
+ msg.payload = smartParse(response);
323
+ break;
324
+
325
+ case "set":
326
+ if (!payload.key) {
327
+ throw new Error("Missing key for SET operation. Use payload.key");
328
+ }
329
+ let setValue = payload.value !== undefined ? payload.value : payload.data;
330
+ if (setValue === undefined) {
331
+ throw new Error("Missing value for SET operation. Use payload.value or payload.data");
332
+ }
333
+ setValue = smartSerialize(setValue);
334
+
335
+ // Support TTL
336
+ if (payload.ttl && payload.ttl > 0) {
337
+ response = await client.setex(payload.key, payload.ttl, setValue);
338
+ } else {
339
+ response = await client.set(payload.key, setValue);
340
+ }
341
+ msg.payload = { success: true, result: response, ttl: payload.ttl || null };
342
+ break;
343
+
344
+ case "del":
345
+ let delKeys = payload.keys || payload.key || payload;
346
+ if (!delKeys) {
347
+ throw new Error("Missing keys for DEL operation. Use payload.keys (array) or payload.key");
348
+ }
349
+ let keysToDelete = Array.isArray(delKeys) ? delKeys : [delKeys];
350
+ response = await client.del(...keysToDelete);
351
+ msg.payload = { success: true, deleted: response, keys: keysToDelete };
352
+ break;
353
+
354
+ case "exists":
355
+ let existsKeys = payload.keys || payload.key || payload;
356
+ if (!existsKeys) {
357
+ throw new Error("Missing keys for EXISTS operation. Use payload.keys (array) or payload.key");
358
+ }
359
+ let keysToCheck = Array.isArray(existsKeys) ? existsKeys : [existsKeys];
360
+ response = await client.exists(...keysToCheck);
361
+ msg.payload = { exists: response > 0, count: response, keys: keysToCheck };
362
+ break;
363
+
364
+ // TTL Operations
365
+ case "ttl":
366
+ let ttlKey = payload.key || payload;
367
+ if (!ttlKey || typeof ttlKey !== 'string') {
368
+ throw new Error("Missing key for TTL operation. Use payload.key or payload as string");
369
+ }
370
+ response = await client.ttl(ttlKey);
371
+ msg.payload = {
372
+ key: ttlKey,
373
+ ttl: response,
374
+ status: response === -1 ? "no expiration" : response === -2 ? "key not found" : "expires in " + response + " seconds"
375
+ };
376
+ break;
377
+
378
+ case "expire":
379
+ if (!payload.key) {
380
+ throw new Error("Missing key for EXPIRE operation. Use payload.key");
381
+ }
382
+ let expireSeconds = payload.ttl || payload.seconds || payload.value || 3600;
383
+ response = await client.expire(payload.key, expireSeconds);
384
+ msg.payload = {
385
+ success: response === 1,
386
+ key: payload.key,
387
+ ttl: expireSeconds,
388
+ message: response === 1 ? "Expiration set" : "Key not found"
389
+ };
390
+ break;
391
+
392
+ case "persist":
393
+ let persistKey = payload.key || payload;
394
+ if (!persistKey || typeof persistKey !== 'string') {
395
+ throw new Error("Missing key for PERSIST operation. Use payload.key or payload as string");
396
+ }
397
+ response = await client.persist(persistKey);
398
+ msg.payload = {
399
+ success: response === 1,
400
+ key: persistKey,
401
+ message: response === 1 ? "Expiration removed" : "Key not found or no expiration"
402
+ };
403
+ break;
404
+
405
+ // Counter Operations
406
+ case "incr":
407
+ let incrKey = payload.key || payload;
408
+ if (!incrKey || typeof incrKey !== 'string') {
409
+ throw new Error("Missing key for INCR operation. Use payload.key or payload as string");
410
+ }
411
+ response = await client.incr(incrKey);
412
+ msg.payload = { key: incrKey, value: response };
413
+ break;
414
+
415
+ case "decr":
416
+ let decrKey = payload.key || payload;
417
+ if (!decrKey || typeof decrKey !== 'string') {
418
+ throw new Error("Missing key for DECR operation. Use payload.key or payload as string");
419
+ }
420
+ response = await client.decr(decrKey);
421
+ msg.payload = { key: decrKey, value: response };
422
+ break;
423
+
424
+ case "incrby":
425
+ if (!payload.key) {
426
+ throw new Error("Missing key for INCRBY operation. Use payload.key");
427
+ }
428
+ let incrAmount = payload.amount || payload.value || payload.increment || 1;
429
+ response = await client.incrby(payload.key, incrAmount);
430
+ msg.payload = { key: payload.key, value: response, increment: incrAmount };
431
+ break;
432
+
433
+ case "decrby":
434
+ if (!payload.key) {
435
+ throw new Error("Missing key for DECRBY operation. Use payload.key");
436
+ }
437
+ let decrAmount = payload.amount || payload.value || payload.decrement || 1;
438
+ response = await client.decrby(payload.key, decrAmount);
439
+ msg.payload = { key: payload.key, value: response, decrement: decrAmount };
440
+ break;
441
+
442
+ // List Operations
443
+ case "lpush":
444
+ case "rpush":
445
+ if (!payload.key) {
446
+ throw new Error(`Missing key for ${node.operation.toUpperCase()} operation. Use payload.key`);
447
+ }
448
+ let pushValue = payload.value !== undefined ? payload.value : payload.data;
449
+ if (pushValue === undefined) {
450
+ throw new Error(`Missing value for ${node.operation.toUpperCase()} operation. Use payload.value or payload.data`);
451
+ }
452
+ pushValue = smartSerialize(pushValue);
453
+ response = await client[node.operation](payload.key, pushValue);
454
+ msg.payload = { success: true, key: payload.key, length: response, operation: node.operation };
455
+ break;
456
+
457
+ case "lpop":
458
+ case "rpop":
459
+ let popKey = payload.key || payload;
460
+ if (!popKey || typeof popKey !== 'string') {
461
+ throw new Error(`Missing key for ${node.operation.toUpperCase()} operation. Use payload.key or payload as string`);
462
+ }
463
+ response = await client[node.operation](popKey);
464
+ msg.payload = smartParse(response);
465
+ break;
466
+
467
+ case "llen":
468
+ let llenKey = payload.key || payload;
469
+ if (!llenKey || typeof llenKey !== 'string') {
470
+ throw new Error("Missing key for LLEN operation. Use payload.key or payload as string");
471
+ }
472
+ response = await client.llen(llenKey);
473
+ msg.payload = { key: llenKey, length: response };
474
+ break;
475
+
476
+ case "lrange":
477
+ if (!payload.key) {
478
+ throw new Error("Missing key for LRANGE operation. Use payload.key");
479
+ }
480
+ let start = payload.start !== undefined ? payload.start : 0;
481
+ let stop = payload.stop !== undefined ? payload.stop : -1;
482
+ response = await client.lrange(payload.key, start, stop);
483
+ // Auto-parse each item in the array
484
+ response = response.map(item => smartParse(item));
485
+ msg.payload = { key: payload.key, range: { start, stop }, values: response, count: response.length };
486
+ break;
487
+
488
+ // Hash Operations
489
+ case "hset":
490
+ if (!payload.key) {
491
+ throw new Error("Missing key for HSET operation. Use payload.key");
492
+ }
493
+ if (payload.field && payload.value !== undefined) {
494
+ // Single field
495
+ let hashValue = smartSerialize(payload.value);
496
+ response = await client.hset(payload.key, payload.field, hashValue);
497
+ msg.payload = { success: true, key: payload.key, field: payload.field, created: response === 1 };
498
+ } else if (payload.fields && typeof payload.fields === 'object') {
499
+ // Multiple fields from payload.fields
500
+ const fields = {};
501
+ for (const [key, value] of Object.entries(payload.fields)) {
502
+ fields[key] = smartSerialize(value);
503
+ }
504
+ response = await client.hset(payload.key, fields);
505
+ msg.payload = { success: true, key: payload.key, fields: Object.keys(fields), created: response };
506
+ } else {
507
+ throw new Error("HSET requires field and value (payload.field, payload.value) or multiple fields (payload.fields)");
508
+ }
509
+ break;
510
+
511
+ case "hget":
512
+ if (!payload.key) {
513
+ throw new Error("Missing key for HGET operation. Use payload.key");
514
+ }
515
+ let field = payload.field;
516
+ if (!field) {
517
+ throw new Error("Missing field for HGET operation. Use payload.field");
518
+ }
519
+ response = await client.hget(payload.key, field);
520
+ msg.payload = smartParse(response);
521
+ break;
522
+
523
+ case "hgetall":
524
+ let hgetallKey = payload.key || payload;
525
+ if (!hgetallKey || typeof hgetallKey !== 'string') {
526
+ throw new Error("Missing key for HGETALL operation. Use payload.key or payload as string");
527
+ }
528
+ response = await client.hgetall(hgetallKey);
529
+ // Auto-parse each field value
530
+ const parsed = {};
531
+ for (const [key, value] of Object.entries(response)) {
532
+ parsed[key] = smartParse(value);
533
+ }
534
+ msg.payload = parsed;
535
+ break;
536
+
537
+ case "hdel":
538
+ if (!payload.key) {
539
+ throw new Error("Missing key for HDEL operation. Use payload.key");
540
+ }
541
+ let fieldsToDelete = payload.fields || payload.field;
542
+ if (!fieldsToDelete) {
543
+ throw new Error("Missing fields for HDEL operation. Use payload.fields (array) or payload.field");
544
+ }
545
+ fieldsToDelete = Array.isArray(fieldsToDelete) ? fieldsToDelete : [fieldsToDelete];
546
+ response = await client.hdel(payload.key, ...fieldsToDelete);
547
+ msg.payload = { success: true, key: payload.key, deleted: response, fields: fieldsToDelete };
548
+ break;
549
+
550
+ case "publish":
551
+ let channel = payload.channel || payload.key;
552
+ if (!channel) {
553
+ throw new Error("Missing channel for PUBLISH operation. Use payload.channel or payload.key");
554
+ }
555
+ let pubValue = payload.message || payload.value || payload.data;
556
+ if (pubValue === undefined) {
557
+ throw new Error("Missing message for PUBLISH operation. Use payload.message, payload.value, or payload.data");
558
+ }
559
+ pubValue = smartSerialize(pubValue);
560
+ response = await client.publish(channel, pubValue);
561
+ msg.payload = { success: true, channel: channel, subscribers: response, message: pubValue };
562
+ break;
563
+
564
+ default:
565
+ throw new Error(`Unsupported operation: ${node.operation}`);
566
+ }
567
+
568
+ send(msg);
569
+ done();
570
+
571
+ } catch (err) {
572
+ node.error(err.message, msg);
573
+ msg.payload = { error: err.message };
574
+ send(msg);
575
+ done();
576
+ }
577
+ });
578
+ }
579
+
580
+ // Set initial status
581
+ if (["subscribe", "psubscribe", "blpop", "brpop"].includes(node.operation)) {
582
+ node.status({
583
+ fill: "green",
584
+ shape: "dot",
585
+ text: "connected",
586
+ });
587
+ } else if (node.operation === "instance") {
588
+ // Status set in handleInstance
589
+ } else {
590
+ node.status({
591
+ fill: "blue",
592
+ shape: "dot",
593
+ text: "ready",
594
+ });
595
+ }
596
+
597
+ // Clean up on node close
598
+ node.on("close", async (undeploy, done) => {
599
+ node.status({});
600
+ running = false;
601
+
602
+ if (node.operation === "instance" && node.location && node.topic) {
603
+ try {
604
+ node.context()[node.location].set(node.topic, null);
605
+ } catch (e) {
606
+ // Ignore errors when cleaning up context
607
+ }
608
+ }
609
+
610
+ if (redisConfig) {
611
+ const nodeId = node.block ? node.id : redisConfig.id;
612
+ redisConfig.disconnect(nodeId);
613
+ }
614
+
615
+ client = null;
616
+ done();
617
+ });
618
+ }
619
+
620
+ RED.nodes.registerType("redis-variable", RedisVariableNode);
621
+ };