node-red-contrib-aws-sqs-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,272 @@
1
+ /**
2
+ * AWS SQS Configuration Node
3
+ *
4
+ * Provides configuration for AWS SQS operations in Node-RED.
5
+ * Supports IAM roles and access key authentication with context support.
6
+ */
7
+
8
+ const { SQSClient } = require("@aws-sdk/client-sqs");
9
+
10
+ module.exports = function(RED) {
11
+ /**
12
+ * Helper function to get value from different contexts
13
+ * @param {Object} node - Node instance
14
+ * @param {string} value - Value to get
15
+ * @param {string} type - Type of value (str, flow, global, env)
16
+ * @param {Object} msg - Message object
17
+ * @returns {string} Retrieved value
18
+ */
19
+ function getValueFromContext(node, value, type, msg) {
20
+ if (value === null || value === undefined) return null;
21
+
22
+ try {
23
+ let result;
24
+ switch (type) {
25
+ case 'flow':
26
+ const flowContext = node.context().flow;
27
+ if (!flowContext) {
28
+ return null;
29
+ }
30
+ result = getNestedValue(flowContext, value);
31
+ break;
32
+ case 'global':
33
+ const globalContext = node.context().global;
34
+ if (!globalContext) {
35
+ return null;
36
+ }
37
+ result = getNestedValue(globalContext, value);
38
+ break;
39
+ case 'env':
40
+ result = process.env[value];
41
+ break;
42
+ case 'msg':
43
+ result = RED.util.getMessageProperty(msg, value);
44
+ break;
45
+ default:
46
+ result = value;
47
+ }
48
+
49
+ return result !== undefined ? result : null;
50
+ } catch (err) {
51
+ throw new Error(`Failed to get value for type: ${type}, value: ${value}. Error: ${err.message}`);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Helper function to get nested values like "all_vars.host"
57
+ * @param {Object} context - Context object
58
+ * @param {string} path - Dot-separated path
59
+ * @returns {*} Retrieved value
60
+ */
61
+ function getNestedValue(context, path) {
62
+ if (!context) return undefined;
63
+
64
+ if (path.includes('.')) {
65
+ const parts = path.split('.');
66
+ let result = context.get(parts[0]);
67
+ for (let i = 1; i < parts.length; i++) {
68
+ if (result && typeof result === 'object') {
69
+ result = result[parts[i]];
70
+ } else {
71
+ return undefined;
72
+ }
73
+ }
74
+ return result;
75
+ } else {
76
+ return context.get(path);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * AWS SQS Configuration Node constructor
82
+ *
83
+ * @param {Object} config - Node configuration
84
+ * @param {string} config.name - Node name
85
+ * @param {boolean} config.useIAMRole - Whether to use IAM role
86
+ */
87
+ function AWSSQSConfigNode(config) {
88
+ RED.nodes.createNode(this, config);
89
+
90
+ this.name = config.name || "AWS SQS Config";
91
+ this.region = config.region || "eu-central-1";
92
+
93
+ // Store credential types and values
94
+ this.useIAMRole = config.useIAMRole === true || config.useIAMRole === "true" || config.useIAMRole === 1;
95
+ this.deferMissingConfig = config.deferMissingConfig === undefined ? true : (config.deferMissingConfig === true || config.deferMissingConfig === "true");
96
+ this.regionType = config.regionType || 'str';
97
+ this.regionContext = config.regionContext;
98
+ this.accessKeyIdType = config.accessKeyIdType || 'str';
99
+ this.secretAccessKeyType = config.secretAccessKeyType || 'str';
100
+ this.accessKeyId = config.accessKeyId;
101
+ this.accessKeyIdContext = config.accessKeyIdContext;
102
+ this.secretAccessKey = config.secretAccessKey;
103
+ this.secretAccessKeyContext = config.secretAccessKeyContext;
104
+
105
+ /**
106
+ * Parse credential values based on type
107
+ * @param {string} value - Value to parse
108
+ * @param {string} type - Type of value
109
+ * @param {Object} msg - Message object
110
+ * @param {Object} executingNode - Node executing the request
111
+ * @returns {string|null} Parsed value
112
+ */
113
+ this.parseCredentialValue = function(value, type, msg, executingNode) {
114
+ if (!value) {
115
+ return null;
116
+ }
117
+
118
+ try {
119
+ let result;
120
+ switch (type) {
121
+ case 'str':
122
+ result = value;
123
+ break;
124
+ case 'flow':
125
+ result = getValueFromContext(executingNode || this, value, 'flow', msg);
126
+ break;
127
+ case 'global':
128
+ result = getValueFromContext(executingNode || this, value, 'global', msg);
129
+ break;
130
+ case 'env':
131
+ result = process.env[value] || null;
132
+ break;
133
+ default:
134
+ result = value;
135
+ }
136
+
137
+ return result;
138
+ } catch (error) {
139
+ if (executingNode) {
140
+ executingNode.error(`Error parsing credential value: ${error.message}`);
141
+ }
142
+ return null;
143
+ }
144
+ };
145
+
146
+ /**
147
+ * Resolve region based on configuration
148
+ * @param {Object} msg - Message object
149
+ * @param {Object} executingNode - Node executing the request
150
+ * @returns {string} Region string
151
+ */
152
+ this.resolveRegion = function(msg, executingNode) {
153
+ if (this.regionType === 'str') {
154
+ return this.region || process.env.AWS_REGION || "eu-central-1";
155
+ }
156
+
157
+ const resolved = this.parseCredentialValue(this.regionContext, this.regionType, msg, executingNode);
158
+ return resolved || process.env.AWS_REGION || "eu-central-1";
159
+ };
160
+
161
+ /**
162
+ * Get credentials based on configuration
163
+ * @param {Object} msg - Message object
164
+ * @param {Object} executingNode - Node executing the request
165
+ * @returns {Object} Credentials object
166
+ */
167
+ this.getCredentials = function(msg, executingNode) {
168
+ if (this.useIAMRole) {
169
+ return {
170
+ useIAMRole: true,
171
+ region: this.resolveRegion(msg, executingNode)
172
+ };
173
+ }
174
+
175
+ try {
176
+ // Handle Access Key ID
177
+ let accessKeyId;
178
+ if (this.accessKeyIdType === 'str') {
179
+ accessKeyId = this.credentials?.accessKeyId;
180
+ } else {
181
+ accessKeyId = this.parseCredentialValue(this.accessKeyIdContext, this.accessKeyIdType, msg, executingNode);
182
+ }
183
+
184
+ // Handle Secret Access Key
185
+ let secretAccessKey;
186
+ if (this.secretAccessKeyType === 'str') {
187
+ secretAccessKey = this.credentials?.secretAccessKey;
188
+ } else {
189
+ secretAccessKey = this.parseCredentialValue(this.secretAccessKeyContext, this.secretAccessKeyType, msg, executingNode);
190
+ }
191
+
192
+ if (!accessKeyId || !secretAccessKey) {
193
+ const missingFields = [];
194
+ if (!accessKeyId) missingFields.push('Access Key ID');
195
+ if (!secretAccessKey) missingFields.push('Secret Access Key');
196
+ if (this.deferMissingConfig) {
197
+ return null;
198
+ }
199
+ throw new Error(`Missing required credentials: ${missingFields.join(', ')}`);
200
+ }
201
+
202
+ return {
203
+ accessKeyId,
204
+ secretAccessKey,
205
+ region: this.resolveRegion(msg, executingNode)
206
+ };
207
+ } catch (err) {
208
+ if (this.deferMissingConfig) {
209
+ return null;
210
+ }
211
+ throw new Error(`Failed to get AWS credentials: ${err.message}`);
212
+ }
213
+ };
214
+
215
+ /**
216
+ * Get AWS SQS client
217
+ * @param {Object} msg - Message object
218
+ * @param {Object} executingNode - Node executing the request
219
+ * @returns {SQSClient} AWS SQS client
220
+ */
221
+ this.getClient = (msg, executingNode) => {
222
+ try {
223
+ if (this.useIAMRole) {
224
+ return new SQSClient({
225
+ region: this.resolveRegion(msg, executingNode)
226
+ });
227
+ }
228
+
229
+ const credentials = this.getCredentials(msg, executingNode);
230
+ if (!credentials) {
231
+ return null;
232
+ }
233
+
234
+ return new SQSClient({
235
+ region: credentials.region,
236
+ credentials: {
237
+ accessKeyId: credentials.accessKeyId,
238
+ secretAccessKey: credentials.secretAccessKey
239
+ }
240
+ });
241
+
242
+ } catch (error) {
243
+ if (executingNode) {
244
+ executingNode.error(`Failed to create SQS client: ${error.message}`);
245
+ }
246
+ throw error;
247
+ }
248
+ };
249
+ }
250
+
251
+ // Register node type with credentials
252
+ RED.nodes.registerType("aws-sqs-config", AWSSQSConfigNode, {
253
+ credentials: {
254
+ accessKeyId: { type: "text" },
255
+ secretAccessKey: { type: "password" }
256
+ },
257
+ defaults: {
258
+ name: { value: "" },
259
+ region: { value: "eu-central-1", required: true },
260
+ regionType: { value: "str" },
261
+ regionContext: { value: "" },
262
+ useIAMRole: { value: false, required: true },
263
+ deferMissingConfig: { value: true },
264
+ accessKeyId: { value: "" },
265
+ accessKeyIdType: { value: "str" },
266
+ accessKeyIdContext: { value: "" },
267
+ secretAccessKey: { value: "" },
268
+ secretAccessKeyType: { value: "str" },
269
+ secretAccessKeyContext: { value: "" }
270
+ }
271
+ });
272
+ };
package/aws-sqs.html ADDED
@@ -0,0 +1,253 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType("aws-sqs", {
3
+ category: "AWS",
4
+ color: "#b2e2b2",
5
+ defaults: {
6
+ name: { value: "" },
7
+ awsConfig: { value: "", type: "aws-sqs-config", required: true },
8
+ operation: { value: "send", required: true },
9
+ queueUrl: { value: "" },
10
+ queueUrlType: { value: "str" },
11
+ messageBody: { value: "payload" },
12
+ messageBodyType: { value: "msg" },
13
+ messageGroupId: { value: "" },
14
+ messageGroupIdType: { value: "str" },
15
+ messageDeduplicationId: { value: "" },
16
+ messageDeduplicationIdType: { value: "str" },
17
+ maxNumberOfMessages: { value: 1 },
18
+ waitTimeSeconds: { value: 0 },
19
+ visibilityTimeout: { value: 30 },
20
+ deleteAfterReceive: { value: true },
21
+ parseJsonBody: { value: false },
22
+ pollEnabled: { value: false },
23
+ pollIntervalSeconds: { value: 30 }
24
+ },
25
+ inputs: 1,
26
+ outputs: 1,
27
+ icon: "font-awesome/fa-exchange",
28
+ label: function() {
29
+ return this.name || "AWS SQS (" + this.operation + ")";
30
+ },
31
+ oneditprepare: function() {
32
+ // Initialize typedInput for Queue URL
33
+ $("#node-input-queueUrl").typedInput({
34
+ default: 'str',
35
+ types: ['str', 'msg', 'flow', 'global', 'env'],
36
+ typeField: "#node-input-queueUrlType"
37
+ });
38
+ $("#node-input-queueUrl").typedInput('type', this.queueUrlType || 'str');
39
+ $("#node-input-queueUrl").typedInput('value', this.queueUrl || '');
40
+
41
+ // Initialize typedInput for Message Body
42
+ $("#node-input-messageBody").typedInput({
43
+ default: 'msg',
44
+ types: ['msg', 'str', 'flow', 'global', 'env'],
45
+ typeField: "#node-input-messageBodyType"
46
+ });
47
+ $("#node-input-messageBody").typedInput('type', this.messageBodyType || 'msg');
48
+ $("#node-input-messageBody").typedInput('value', this.messageBody || 'payload');
49
+
50
+ // Initialize typedInput for Message Group ID (FIFO)
51
+ $("#node-input-messageGroupId").typedInput({
52
+ default: 'str',
53
+ types: ['str', 'msg', 'flow', 'global', 'env'],
54
+ typeField: "#node-input-messageGroupIdType"
55
+ });
56
+ $("#node-input-messageGroupId").typedInput('type', this.messageGroupIdType || 'str');
57
+ $("#node-input-messageGroupId").typedInput('value', this.messageGroupId || '');
58
+
59
+ // Initialize typedInput for Message Deduplication ID (FIFO)
60
+ $("#node-input-messageDeduplicationId").typedInput({
61
+ default: 'str',
62
+ types: ['str', 'msg', 'flow', 'global', 'env'],
63
+ typeField: "#node-input-messageDeduplicationIdType"
64
+ });
65
+ $("#node-input-messageDeduplicationId").typedInput('type', this.messageDeduplicationIdType || 'str');
66
+ $("#node-input-messageDeduplicationId").typedInput('value', this.messageDeduplicationId || '');
67
+
68
+ // Initialize tooltips
69
+ $('.node-input').tooltip({
70
+ delay: { show: 500, hide: 100 },
71
+ trigger: 'hover'
72
+ });
73
+
74
+ var updateOperationFields = function() {
75
+ var operation = $("#node-input-operation").val();
76
+ if (operation === "send") {
77
+ $("#sendFields").show();
78
+ $("#receiveFields").hide();
79
+ } else {
80
+ $("#sendFields").hide();
81
+ $("#receiveFields").show();
82
+ }
83
+ };
84
+
85
+ $("#node-input-operation").on("change", updateOperationFields);
86
+ updateOperationFields();
87
+ },
88
+ oneditsave: function() {
89
+ this.queueUrlType = $("#node-input-queueUrlType").val();
90
+ this.queueUrl = $("#node-input-queueUrl").typedInput('value');
91
+ this.messageBodyType = $("#node-input-messageBodyType").val();
92
+ this.messageBody = $("#node-input-messageBody").typedInput('value');
93
+ this.messageGroupIdType = $("#node-input-messageGroupIdType").val();
94
+ this.messageGroupId = $("#node-input-messageGroupId").typedInput('value');
95
+ this.messageDeduplicationIdType = $("#node-input-messageDeduplicationIdType").val();
96
+ this.messageDeduplicationId = $("#node-input-messageDeduplicationId").typedInput('value');
97
+ }
98
+ });
99
+ </script>
100
+
101
+ <script type="text/x-red" data-template-name="aws-sqs">
102
+ <div class="form-row">
103
+ <label for="node-input-name">
104
+ <i class="fa fa-tag"></i> Name
105
+ </label>
106
+ <input type="text" id="node-input-name" placeholder="AWS SQS" style="width: 70%;">
107
+ </div>
108
+ <div class="form-row">
109
+ <label for="node-input-awsConfig">
110
+ <i class="fa fa-cog"></i> AWS Config
111
+ </label>
112
+ <input type="text" id="node-input-awsConfig" style="width: 70%;">
113
+ </div>
114
+ <div class="form-row">
115
+ <label for="node-input-operation">
116
+ <i class="fa fa-exchange"></i> Operation
117
+ </label>
118
+ <select id="node-input-operation" style="width: 70%;">
119
+ <option value="send">Send Message</option>
120
+ <option value="receive">Receive Messages</option>
121
+ </select>
122
+ </div>
123
+ <div class="form-row">
124
+ <label for="node-input-queueUrl">
125
+ <i class="fa fa-link"></i> Queue URL
126
+ </label>
127
+ <input type="text" id="node-input-queueUrl" style="width: 70%;">
128
+ <input type="hidden" id="node-input-queueUrlType">
129
+ </div>
130
+
131
+ <div id="sendFields">
132
+ <div class="form-row">
133
+ <label for="node-input-messageBody">
134
+ <i class="fa fa-envelope"></i> Message Body
135
+ </label>
136
+ <input type="text" id="node-input-messageBody" style="width: 70%;">
137
+ <input type="hidden" id="node-input-messageBodyType">
138
+ </div>
139
+ <div class="form-row">
140
+ <label for="node-input-messageGroupId">
141
+ <i class="fa fa-object-group"></i> Message Group ID
142
+ </label>
143
+ <input type="text" id="node-input-messageGroupId" style="width: 70%;">
144
+ <input type="hidden" id="node-input-messageGroupIdType">
145
+ </div>
146
+ <div class="form-row">
147
+ <label for="node-input-messageDeduplicationId">
148
+ <i class="fa fa-clone"></i> Deduplication ID
149
+ </label>
150
+ <input type="text" id="node-input-messageDeduplicationId" style="width: 70%;">
151
+ <input type="hidden" id="node-input-messageDeduplicationIdType">
152
+ </div>
153
+ </div>
154
+
155
+ <div id="receiveFields" style="display: none;">
156
+ <div class="form-row">
157
+ <label for="node-input-pollEnabled">
158
+ <i class="fa fa-refresh"></i> Polling
159
+ </label>
160
+ <input type="checkbox" id="node-input-pollEnabled" style="display: inline-block; width: auto; vertical-align: middle;">
161
+ <span style="margin-left: 5px; vertical-align: middle;">Enable regular polling</span>
162
+ </div>
163
+ <div class="form-row">
164
+ <label for="node-input-pollIntervalSeconds">
165
+ <i class="fa fa-clock-o"></i> Poll Interval (s)
166
+ </label>
167
+ <input type="number" id="node-input-pollIntervalSeconds" min="1" max="86400" style="width: 70%;">
168
+ </div>
169
+ <div class="form-row">
170
+ <label for="node-input-maxNumberOfMessages">
171
+ <i class="fa fa-list"></i> Max Messages
172
+ </label>
173
+ <input type="number" id="node-input-maxNumberOfMessages" min="1" max="10" style="width: 70%;">
174
+ </div>
175
+ <div class="form-row">
176
+ <label for="node-input-waitTimeSeconds">
177
+ <i class="fa fa-clock-o"></i> Wait Time (s)
178
+ </label>
179
+ <input type="number" id="node-input-waitTimeSeconds" min="0" max="20" style="width: 70%;">
180
+ </div>
181
+ <div class="form-row">
182
+ <label for="node-input-visibilityTimeout">
183
+ <i class="fa fa-eye-slash"></i> Visibility Timeout (s)
184
+ </label>
185
+ <input type="number" id="node-input-visibilityTimeout" min="0" max="43200" style="width: 70%;">
186
+ </div>
187
+ <div class="form-row">
188
+ <label for="node-input-deleteAfterReceive">
189
+ <i class="fa fa-trash"></i> Delete After Receive
190
+ </label>
191
+ <input type="checkbox" id="node-input-deleteAfterReceive" style="display: inline-block; width: auto; vertical-align: middle;">
192
+ <span style="margin-left: 5px; vertical-align: middle;">Delete messages from queue after read</span>
193
+ </div>
194
+ <div class="form-row">
195
+ <label for="node-input-parseJsonBody">
196
+ <i class="fa fa-code"></i> Parse JSON Body
197
+ </label>
198
+ <input type="checkbox" id="node-input-parseJsonBody" style="display: inline-block; width: auto; vertical-align: middle;">
199
+ <span style="margin-left: 5px; vertical-align: middle;">Parse message body as JSON when possible</span>
200
+ </div>
201
+ </div>
202
+ </script>
203
+
204
+ <script type="text/x-red" data-help-name="aws-sqs">
205
+ <p>A Node-RED node for AWS SQS operations.</p>
206
+
207
+ <h3>Configuration</h3>
208
+ <p>Select or create an AWS SQS configuration that contains your AWS credentials and region settings.</p>
209
+
210
+ <h3>Queue URL Input</h3>
211
+ <p>The Queue URL field supports multiple input types:</p>
212
+ <ul>
213
+ <li><b>String</b>: Direct Queue URL</li>
214
+ <li><b>Message</b>: Retrieved from message property (e.g., <code>payload.queueUrl</code>)</li>
215
+ <li><b>Flow Context</b>: Retrieved from flow context</li>
216
+ <li><b>Global Context</b>: Retrieved from global context</li>
217
+ <li><b>Environment Variable</b>: Retrieved from environment variable</li>
218
+ </ul>
219
+
220
+ <h3>Operations</h3>
221
+ <ul>
222
+ <li><b>Send Message</b>: Sends a message to the queue</li>
223
+ <li><b>Receive Messages</b>: Receives messages from the queue</li>
224
+ </ul>
225
+
226
+ <h3>FIFO Notes</h3>
227
+ <ul>
228
+ <li>For FIFO queues, <b>Message Group ID</b> is required</li>
229
+ <li>Deduplication ID is optional if content-based deduplication is enabled</li>
230
+ </ul>
231
+
232
+ <h3>Output</h3>
233
+ <p>Send operation returns message metadata. Receive operation returns array of messages in <code>msg.payload</code>.</p>
234
+ </script>
235
+
236
+ <style>
237
+ .help-text {
238
+ font-size: 0.8em;
239
+ color: #666;
240
+ margin-top: 4px;
241
+ }
242
+ .error-text {
243
+ color: #d00;
244
+ font-size: 0.8em;
245
+ margin-top: 4px;
246
+ }
247
+ .input-error {
248
+ border-color: #d00 !important;
249
+ }
250
+ .form-row {
251
+ margin-bottom: 10px;
252
+ }
253
+ </style>