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,363 @@
1
+ const Redis = require("ioredis");
2
+
3
+ module.exports = function (RED) {
4
+ let connections = {};
5
+ let usedConn = {};
6
+
7
+ /**
8
+ * Helper function to get value from different contexts
9
+ * @param {Object} node - Node instance
10
+ * @param {string} value - Value to get
11
+ * @param {string} type - Type of value (str, flow, global, env)
12
+ * @param {Object} msg - Message object
13
+ * @returns {string} Retrieved value
14
+ */
15
+ function getValueFromContext(node, value, type, msg) {
16
+ if (value === null || value === undefined) return null;
17
+
18
+ try {
19
+ let result;
20
+ switch (type) {
21
+ case 'flow':
22
+ const flowContext = node.context().flow;
23
+ if (!flowContext) {
24
+ return null;
25
+ }
26
+ result = getNestedValue(flowContext, value);
27
+ break;
28
+ case 'global':
29
+ const globalContext = node.context().global;
30
+ if (!globalContext) {
31
+ return null;
32
+ }
33
+ result = getNestedValue(globalContext, value);
34
+ break;
35
+ case 'env':
36
+ result = process.env[value];
37
+ break;
38
+ case 'msg':
39
+ result = RED.util.getMessageProperty(msg, value);
40
+ break;
41
+ default:
42
+ result = value;
43
+ }
44
+
45
+ return result !== undefined ? result : null;
46
+ } catch (err) {
47
+ throw new Error(`Failed to get value for type: ${type}, value: ${value}. Error: ${err.message}`);
48
+ }
49
+ }
50
+
51
+ // Helper function to get nested values like "redis_config.host"
52
+ function getNestedValue(context, path) {
53
+ if (!context) return undefined;
54
+
55
+ if (path.includes('.')) {
56
+ const parts = path.split('.');
57
+ let result = context.get(parts[0]);
58
+ for (let i = 1; i < parts.length; i++) {
59
+ if (result && typeof result === 'object') {
60
+ result = result[parts[i]];
61
+ } else {
62
+ return undefined;
63
+ }
64
+ }
65
+ return result;
66
+ } else {
67
+ return context.get(path);
68
+ }
69
+ }
70
+
71
+ function RedisConfigNode(config) {
72
+ RED.nodes.createNode(this, config);
73
+ this.name = config.name || "Redis Config";
74
+ this.cluster = config.cluster || false;
75
+
76
+ // Connection configuration
77
+ this.hostType = config.hostType || 'str';
78
+ this.host = config.host || 'localhost';
79
+ this.hostContext = config.hostContext;
80
+ this.port = config.port || 6379;
81
+ this.portType = config.portType || 'str';
82
+ this.portContext = config.portContext;
83
+
84
+ // Authentication
85
+ this.passwordType = config.passwordType || 'str';
86
+ this.password = config.password;
87
+ this.passwordContext = config.passwordContext;
88
+ this.username = config.username;
89
+ this.usernameType = config.usernameType || 'str';
90
+ this.usernameContext = config.usernameContext;
91
+
92
+ // SSL/TLS Configuration
93
+ this.enableTLS = config.enableTLS || false;
94
+ this.tlsRejectUnauthorized = config.tlsRejectUnauthorized !== false; // Default to true
95
+ this.tlsCertType = config.tlsCertType || 'str';
96
+ this.tlsCertContext = config.tlsCertContext;
97
+ this.tlsKeyType = config.tlsKeyType || 'str';
98
+ this.tlsKeyContext = config.tlsKeyContext;
99
+ this.tlsCaType = config.tlsCaType || 'str';
100
+ this.tlsCaContext = config.tlsCaContext;
101
+
102
+ // Database and other options
103
+ this.database = config.database || 0;
104
+ this.databaseType = config.databaseType || 'str';
105
+ this.databaseContext = config.databaseContext;
106
+
107
+ // Advanced options
108
+ this.optionsType = config.optionsType || 'json';
109
+ this.options = config.options || '{}';
110
+ this.optionsContext = config.optionsContext;
111
+
112
+ // Get credentials for string passwords
113
+ const credentials = this.credentials || {};
114
+
115
+ // Helper method to parse credential values
116
+ this.parseCredentialValue = function(value, type, msg, executingNode) {
117
+ if (!value && value !== 0) {
118
+ return null;
119
+ }
120
+
121
+ try {
122
+ let result;
123
+ switch (type) {
124
+ case 'str':
125
+ result = value;
126
+ break;
127
+ case 'flow':
128
+ result = getValueFromContext(executingNode || this, value, 'flow', msg);
129
+ break;
130
+ case 'global':
131
+ result = getValueFromContext(executingNode || this, value, 'global', msg);
132
+ break;
133
+ case 'env':
134
+ result = process.env[value] || null;
135
+ break;
136
+ case 'json':
137
+ try {
138
+ result = JSON.parse(value);
139
+ } catch (e) {
140
+ result = value;
141
+ }
142
+ break;
143
+ case 'num':
144
+ result = Number(value);
145
+ break;
146
+ default:
147
+ result = value;
148
+ }
149
+ return result;
150
+ } catch (error) {
151
+ if (executingNode) {
152
+ executingNode.error(`Error parsing credential value: ${error.message}`);
153
+ }
154
+ return null;
155
+ }
156
+ };
157
+
158
+ // Get Redis connection options
159
+ this.getConnectionOptions = function(msg, executingNode) {
160
+ try {
161
+ // Parse host
162
+ let host = this.parseCredentialValue(
163
+ this.hostType === 'str' ? this.host : this.hostContext,
164
+ this.hostType,
165
+ msg,
166
+ executingNode
167
+ ) || 'localhost';
168
+
169
+ // Parse port
170
+ let port = this.parseCredentialValue(
171
+ this.portType === 'str' ? this.port : this.portContext,
172
+ this.portType,
173
+ msg,
174
+ executingNode
175
+ ) || 6379;
176
+
177
+ // Parse database
178
+ let database = this.parseCredentialValue(
179
+ this.databaseType === 'str' ? this.database : this.databaseContext,
180
+ this.databaseType,
181
+ msg,
182
+ executingNode
183
+ ) || 0;
184
+
185
+ // Parse password
186
+ let password = null;
187
+ if (this.passwordType === 'str') {
188
+ password = this.credentials?.password;
189
+ } else {
190
+ password = this.parseCredentialValue(this.passwordContext, this.passwordType, msg, executingNode);
191
+ }
192
+
193
+ // Parse username
194
+ let username = null;
195
+ if (this.usernameType === 'str') {
196
+ username = this.credentials?.username;
197
+ } else {
198
+ username = this.parseCredentialValue(this.usernameContext, this.usernameType, msg, executingNode);
199
+ }
200
+
201
+ // Parse additional options
202
+ let additionalOptions = {};
203
+ if (this.optionsType === 'json') {
204
+ try {
205
+ additionalOptions = JSON.parse(this.options || '{}');
206
+ } catch (e) {
207
+ additionalOptions = {};
208
+ }
209
+ } else {
210
+ additionalOptions = this.parseCredentialValue(this.optionsContext, this.optionsType, msg, executingNode) || {};
211
+ }
212
+
213
+ // Build connection options
214
+ const connectionOptions = {
215
+ host: host,
216
+ port: parseInt(port),
217
+ db: parseInt(database),
218
+ retryDelayOnFailover: 100,
219
+ enableReadyCheck: false,
220
+ maxRetriesPerRequest: null,
221
+ ...additionalOptions
222
+ };
223
+
224
+ // Add authentication if provided
225
+ if (password) {
226
+ connectionOptions.password = password;
227
+ }
228
+ if (username) {
229
+ connectionOptions.username = username;
230
+ }
231
+
232
+ // Add SSL/TLS configuration if enabled
233
+ if (this.enableTLS) {
234
+ connectionOptions.tls = {
235
+ rejectUnauthorized: this.tlsRejectUnauthorized
236
+ };
237
+
238
+ // Parse TLS Certificate
239
+ let tlsCert = null;
240
+ if (this.tlsCertType === 'str') {
241
+ tlsCert = this.credentials?.tlsCert;
242
+ } else {
243
+ tlsCert = this.parseCredentialValue(this.tlsCertContext, this.tlsCertType, msg, executingNode);
244
+ }
245
+
246
+ // Parse TLS Key
247
+ let tlsKey = null;
248
+ if (this.tlsKeyType === 'str') {
249
+ tlsKey = this.credentials?.tlsKey;
250
+ } else {
251
+ tlsKey = this.parseCredentialValue(this.tlsKeyContext, this.tlsKeyType, msg, executingNode);
252
+ }
253
+
254
+ // Parse TLS CA
255
+ let tlsCa = null;
256
+ if (this.tlsCaType === 'str') {
257
+ tlsCa = this.credentials?.tlsCa;
258
+ } else {
259
+ tlsCa = this.parseCredentialValue(this.tlsCaContext, this.tlsCaType, msg, executingNode);
260
+ }
261
+
262
+ // Add TLS options if provided
263
+ if (tlsCert && tlsKey) {
264
+ connectionOptions.tls.cert = tlsCert;
265
+ connectionOptions.tls.key = tlsKey;
266
+ }
267
+
268
+ if (tlsCa) {
269
+ connectionOptions.tls.ca = tlsCa;
270
+ }
271
+
272
+ // If using custom CA or self-signed certificates, might need to disable rejection
273
+ if (!this.tlsRejectUnauthorized) {
274
+ connectionOptions.tls.rejectUnauthorized = false;
275
+ }
276
+ }
277
+
278
+ return connectionOptions;
279
+
280
+ } catch (error) {
281
+ if (executingNode) {
282
+ executingNode.error(`Failed to get Redis connection options: ${error.message}`);
283
+ }
284
+ throw error;
285
+ }
286
+ };
287
+
288
+ // Get Redis client
289
+ this.getClient = function(msg, executingNode, nodeId) {
290
+ try {
291
+ const id = nodeId || this.id;
292
+
293
+ // Return existing connection if available
294
+ if (connections[id]) {
295
+ usedConn[id]++;
296
+ return connections[id];
297
+ }
298
+
299
+ const options = this.getConnectionOptions(msg, executingNode);
300
+
301
+ // Create Redis client
302
+ let client;
303
+ if (this.cluster) {
304
+ // For cluster mode, options should be an array of nodes
305
+ const clusterNodes = Array.isArray(options) ? options : [options];
306
+ client = new Redis.Cluster(clusterNodes);
307
+ } else {
308
+ client = new Redis(options);
309
+ }
310
+
311
+ // Handle connection errors
312
+ client.on("error", (e) => {
313
+ if (executingNode) {
314
+ executingNode.error(`Redis connection error: ${e.message}`, {});
315
+ } else {
316
+ this.error(`Redis connection error: ${e.message}`, {});
317
+ }
318
+ });
319
+
320
+ // Store connection
321
+ connections[id] = client;
322
+ if (usedConn[id] === undefined) {
323
+ usedConn[id] = 1;
324
+ } else {
325
+ usedConn[id]++;
326
+ }
327
+
328
+ return client;
329
+
330
+ } catch (error) {
331
+ if (executingNode) {
332
+ executingNode.error(`Failed to create Redis client: ${error.message}`);
333
+ }
334
+ throw error;
335
+ }
336
+ };
337
+
338
+ // Disconnect method
339
+ this.disconnect = function(nodeId) {
340
+ const id = nodeId || this.id;
341
+ if (usedConn[id] !== undefined) {
342
+ usedConn[id]--;
343
+ }
344
+ if (connections[id] && usedConn[id] <= 0) {
345
+ connections[id].disconnect();
346
+ delete connections[id];
347
+ delete usedConn[id];
348
+ }
349
+ };
350
+
351
+ // Clean up on node close
352
+ this.on('close', function() {
353
+ this.disconnect();
354
+ });
355
+ }
356
+
357
+ RED.nodes.registerType("redis-variable-config", RedisConfigNode, {
358
+ credentials: {
359
+ password: { type: "password" },
360
+ username: { type: "text" }
361
+ }
362
+ });
363
+ };
@@ -0,0 +1,284 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType("redis-variable", {
3
+ category: "Redis",
4
+ color: "#D8BFD8",
5
+ defaults: {
6
+ name: { value: "" },
7
+ redisConfig: { value: "", type: "redis-variable-config" },
8
+ operation: { value: "get" }
9
+ },
10
+ inputs: 1,
11
+ outputs: 1,
12
+ icon: "font-awesome/fa-database",
13
+ label: function() {
14
+ return this.name || "Redis (" + (this.operation || "get") + ")";
15
+ }
16
+ });
17
+ </script>
18
+
19
+ <script type="text/html" data-template-name="redis-variable">
20
+ <div class="form-row">
21
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
22
+ <input type="text" id="node-input-name" placeholder="Name">
23
+ </div>
24
+ <div class="form-row">
25
+ <label for="node-input-redisConfig"><i class="fa fa-cog"></i> Redis Config</label>
26
+ <input type="text" id="node-input-redisConfig">
27
+ </div>
28
+ <div class="form-row">
29
+ <label for="node-input-operation"><i class="fa fa-tasks"></i> Operation</label>
30
+ <select id="node-input-operation">
31
+ <optgroup label="Basic Operations">
32
+ <option value="get">GET - Get value</option>
33
+ <option value="set">SET - Set value</option>
34
+ <option value="del">DEL - Delete key</option>
35
+ <option value="exists">EXISTS - Check if key exists</option>
36
+ </optgroup>
37
+ <optgroup label="TTL Operations">
38
+ <option value="ttl">TTL - Get time to live</option>
39
+ <option value="expire">EXPIRE - Set expiration</option>
40
+ <option value="persist">PERSIST - Remove expiration</option>
41
+ </optgroup>
42
+ <optgroup label="Counter Operations">
43
+ <option value="incr">INCR - Increment by 1</option>
44
+ <option value="decr">DECR - Decrement by 1</option>
45
+ <option value="incrby">INCRBY - Increment by N</option>
46
+ <option value="decrby">DECRBY - Decrement by N</option>
47
+ </optgroup>
48
+ <optgroup label="List Operations">
49
+ <option value="lpush">LPUSH - Push to left</option>
50
+ <option value="rpush">RPUSH - Push to right</option>
51
+ <option value="lpop">LPOP - Pop from left</option>
52
+ <option value="rpop">RPOP - Pop from right</option>
53
+ <option value="llen">LLEN - List length</option>
54
+ <option value="lrange">LRANGE - Get range</option>
55
+ </optgroup>
56
+ <optgroup label="Hash Operations">
57
+ <option value="hset">HSET - Set hash field</option>
58
+ <option value="hget">HGET - Get hash field</option>
59
+ <option value="hgetall">HGETALL - Get all fields</option>
60
+ <option value="hdel">HDEL - Delete hash field</option>
61
+ </optgroup>
62
+ <optgroup label="Pub/Sub Operations">
63
+ <option value="publish">PUBLISH - Publish message</option>
64
+ </optgroup>
65
+ </select>
66
+ </div>
67
+ </script>
68
+
69
+ <script type="text/x-red" data-help-name="redis-variable">
70
+ <p>Redis node for comprehensive operations with universal payload-based configuration.</p>
71
+
72
+ <h3>Configuration</h3>
73
+ <p>Select or create a Redis configuration that contains your connection settings.</p>
74
+
75
+ <h3>Operations</h3>
76
+
77
+ <h4>Basic Operations</h4>
78
+ <ul>
79
+ <li><b>GET</b>: Retrieve value by key</li>
80
+ <li><b>SET</b>: Store value with key (supports TTL)</li>
81
+ <li><b>DEL</b>: Delete key(s)</li>
82
+ <li><b>EXISTS</b>: Check if key(s) exist</li>
83
+ </ul>
84
+
85
+ <h4>TTL Operations</h4>
86
+ <ul>
87
+ <li><b>TTL</b>: Get remaining time to live in seconds</li>
88
+ <li><b>EXPIRE</b>: Set expiration time for existing key</li>
89
+ <li><b>PERSIST</b>: Remove expiration from key</li>
90
+ </ul>
91
+
92
+ <h4>Counter Operations</h4>
93
+ <ul>
94
+ <li><b>INCR</b>: Increment value by 1</li>
95
+ <li><b>DECR</b>: Decrement value by 1</li>
96
+ <li><b>INCRBY</b>: Increment value by N</li>
97
+ <li><b>DECRBY</b>: Decrement value by N</li>
98
+ </ul>
99
+
100
+ <h4>List Operations</h4>
101
+ <ul>
102
+ <li><b>LPUSH</b>: Add element to beginning of list</li>
103
+ <li><b>RPUSH</b>: Add element to end of list</li>
104
+ <li><b>LPOP</b>: Remove and return first element</li>
105
+ <li><b>RPOP</b>: Remove and return last element</li>
106
+ <li><b>LLEN</b>: Get list length</li>
107
+ <li><b>LRANGE</b>: Get range of elements</li>
108
+ </ul>
109
+
110
+ <h4>Hash Operations</h4>
111
+ <ul>
112
+ <li><b>HSET</b>: Set hash field value</li>
113
+ <li><b>HGET</b>: Get hash field value</li>
114
+ <li><b>HGETALL</b>: Get all hash fields</li>
115
+ <li><b>HDEL</b>: Delete hash field(s)</li>
116
+ </ul>
117
+
118
+ <h4>Pub/Sub Operations</h4>
119
+ <ul>
120
+ <li><b>PUBLISH</b>: Publish message to channel</li>
121
+ </ul>
122
+
123
+ <h3>Universal Payload Format</h3>
124
+ <p>All parameters are passed via <code>msg.payload</code>. The payload can be a string (for simple operations) or an object with specific properties.</p>
125
+
126
+ <h3>Examples</h3>
127
+
128
+ <h4>Basic Operations</h4>
129
+ <pre>// GET - simple format
130
+ {
131
+ "payload": "user:123"
132
+ }
133
+
134
+ // GET - object format
135
+ {
136
+ "payload": {"key": "user:123"}
137
+ }
138
+
139
+ // SET with value
140
+ {
141
+ "payload": {
142
+ "key": "user:123",
143
+ "value": "John Doe"
144
+ }
145
+ }
146
+
147
+ // SET with TTL (expires in 1 hour)
148
+ {
149
+ "payload": {
150
+ "key": "session:abc123",
151
+ "value": {"userId": 42, "role": "admin"},
152
+ "ttl": 3600
153
+ }
154
+ }
155
+
156
+ // DELETE multiple keys
157
+ {
158
+ "payload": {
159
+ "keys": ["cache:page1", "cache:page2", "temp:data"]
160
+ }
161
+ }</pre>
162
+
163
+ <h4>TTL Operations</h4>
164
+ <pre>// Check TTL
165
+ {
166
+ "payload": "session:abc123"
167
+ }
168
+
169
+ // Set expiration
170
+ {
171
+ "payload": {
172
+ "key": "temp:data",
173
+ "ttl": 1800
174
+ }
175
+ }
176
+
177
+ // Remove expiration
178
+ {
179
+ "payload": "permanent:key"
180
+ }</pre>
181
+
182
+ <h4>Counter Operations</h4>
183
+ <pre>// Increment by 1
184
+ {
185
+ "payload": "page:views"
186
+ }
187
+
188
+ // Increment by amount
189
+ {
190
+ "payload": {
191
+ "key": "score:player1",
192
+ "amount": 100
193
+ }
194
+ }</pre>
195
+
196
+ <h4>List Operations</h4>
197
+ <pre>// Add to list
198
+ {
199
+ "payload": {
200
+ "key": "queue:tasks",
201
+ "value": {"task": "process_order", "id": 123}
202
+ }
203
+ }
204
+
205
+ // Get range
206
+ {
207
+ "payload": {
208
+ "key": "queue:tasks",
209
+ "start": 0,
210
+ "stop": 9
211
+ }
212
+ }
213
+
214
+ // Pop from list
215
+ {
216
+ "payload": "queue:tasks"
217
+ }</pre>
218
+
219
+ <h4>Hash Operations</h4>
220
+ <pre>// Set single field
221
+ {
222
+ "payload": {
223
+ "key": "user:123",
224
+ "field": "email",
225
+ "value": "user@example.com"
226
+ }
227
+ }
228
+
229
+ // Set multiple fields
230
+ {
231
+ "payload": {
232
+ "key": "user:123",
233
+ "fields": {
234
+ "name": "John Doe",
235
+ "age": 30,
236
+ "city": "New York"
237
+ }
238
+ }
239
+ }
240
+
241
+ // Get field
242
+ {
243
+ "payload": {
244
+ "key": "user:123",
245
+ "field": "email"
246
+ }
247
+ }
248
+
249
+ // Get all fields
250
+ {
251
+ "payload": "user:123"
252
+ }</pre>
253
+
254
+ <h4>Pub/Sub Operations</h4>
255
+ <pre>// Publish message
256
+ {
257
+ "payload": {
258
+ "channel": "notifications",
259
+ "message": {
260
+ "type": "alert",
261
+ "text": "System maintenance"
262
+ }
263
+ }
264
+ }</pre>
265
+
266
+ <h3>Automatic JSON Handling</h3>
267
+ <p>The node automatically detects and handles JSON data:</p>
268
+ <ul>
269
+ <li><b>When storing</b>: JavaScript objects are automatically serialized to JSON strings</li>
270
+ <li><b>When retrieving</b>: Valid JSON strings are automatically parsed back to objects</li>
271
+ <li><b>Simple values</b>: Strings, numbers, and other simple types are handled as-is</li>
272
+ <li>Applies to all operations that handle values (GET, SET, lists, hashes, pub/sub)</li>
273
+ </ul>
274
+
275
+ <h3>Output Examples</h3>
276
+ <ul>
277
+ <li><b>GET</b>: Returns the stored value (auto-parsed if JSON)</li>
278
+ <li><b>SET</b>: <code>{ success: true, result: "OK", ttl: 3600 }</code></li>
279
+ <li><b>TTL</b>: <code>{ key: "mykey", ttl: 3600, status: "expires in 3600 seconds" }</code></li>
280
+ <li><b>INCR</b>: <code>{ key: "counter", value: 42 }</code></li>
281
+ <li><b>LPUSH</b>: <code>{ success: true, key: "list", length: 5, operation: "lpush" }</code></li>
282
+ <li><b>HGETALL</b>: <code>{ name: "John", age: 30, email: "john@example.com" }</code> (auto-parsed)</li>
283
+ </ul>
284
+ </script>