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.
- package/LICENSE +21 -0
- package/README.md +769 -0
- package/examples/example.json +427 -0
- package/package.json +46 -0
- package/redis-variable-config.html +731 -0
- package/redis-variable-config.js +363 -0
- package/redis-variable.html +284 -0
- package/redis-variable.js +621 -0
|
@@ -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
|
+
};
|