node-red-contrib-influxdb3 1.0.0 → 1.0.2

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/influxdb3.js CHANGED
@@ -1,207 +1,303 @@
1
- /**
2
- * InfluxDB v3 nodes for Node-RED
3
- */
4
-
5
- module.exports = function(RED) {
6
- const { InfluxDBClient, Point } = require('@influxdata/influxdb3-client');
7
-
8
- /**
9
- * Configuration node to hold InfluxDB v3 connection details
10
- */
11
- function InfluxDB3ConfigNode(config) {
12
- RED.nodes.createNode(this, config);
13
-
14
- this.host = config.host;
15
- this.database = config.database;
16
- this.name = config.name;
17
-
18
- // Store token as a credential
19
- this.token = this.credentials.token;
20
-
21
- // Client instance (will be created on demand)
22
- this.client = null;
23
-
24
- // Get or create a client instance
25
- this.getClient = function() {
26
- if (!this.client) {
27
- try {
28
- this.client = new InfluxDBClient({
29
- host: this.host,
30
- token: this.token,
31
- database: this.database
32
- });
33
- } catch (error) {
34
- throw new Error(`Failed to create InfluxDB client: ${error.message}`);
35
- }
36
- }
37
- return this.client;
38
- };
39
-
40
- // Clean up on close
41
- this.on('close', function() {
42
- if (this.client) {
43
- try {
44
- this.client.close();
45
- this.client = null;
46
- } catch (error) {
47
- // Ignore errors on close
48
- }
49
- }
50
- });
51
- }
52
-
53
- RED.nodes.registerType('influxdb3-config', InfluxDB3ConfigNode, {
54
- credentials: {
55
- token: { type: 'password' }
56
- }
57
- });
58
-
59
- /**
60
- * InfluxDB v3 Write Node
61
- */
62
- function InfluxDB3WriteNode(config) {
63
- RED.nodes.createNode(this, config);
64
-
65
- this.influxdb = RED.nodes.getNode(config.influxdb);
66
- this.measurement = config.measurement;
67
- this.database = config.database;
68
-
69
- const node = this;
70
-
71
- if (!this.influxdb) {
72
- this.error('InfluxDB v3 config not set');
73
- this.status({ fill: 'red', shape: 'dot', text: 'no config' });
74
- return;
75
- }
76
-
77
- // Process incoming messages
78
- node.on('input', async function(msg, send, done) {
79
- // For Node-RED 0.x compatibility
80
- send = send || function() { node.send.apply(node, arguments); };
81
- done = done || function(err) {
82
- if (err) {
83
- node.error(err, msg);
84
- }
85
- };
86
-
87
- try {
88
- const client = node.influxdb.getClient();
89
-
90
- // Determine the database to use
91
- const targetDatabase = msg.database || node.database || node.influxdb.database;
92
-
93
- if (!targetDatabase) {
94
- throw new Error('Database not specified');
95
- }
96
-
97
- let lineProtocol;
98
-
99
- // Check if msg.payload is already in line protocol format
100
- if (typeof msg.payload === 'string') {
101
- lineProtocol = msg.payload;
102
- } else if (msg.payload && typeof msg.payload === 'object') {
103
- // Build line protocol from payload object
104
- const measurement = msg.measurement || node.measurement;
105
-
106
- if (!measurement) {
107
- throw new Error('Measurement not specified');
108
- }
109
-
110
- const point = new Point(measurement);
111
-
112
- // Add tags
113
- if (msg.payload.tags && typeof msg.payload.tags === 'object') {
114
- for (const [key, value] of Object.entries(msg.payload.tags)) {
115
- if (value !== null && value !== undefined) {
116
- point.setTag(key, String(value));
117
- }
118
- }
119
- }
120
-
121
- // Add fields
122
- if (msg.payload.fields && typeof msg.payload.fields === 'object') {
123
- for (const [key, value] of Object.entries(msg.payload.fields)) {
124
- if (value !== null && value !== undefined) {
125
- if (typeof value === 'number') {
126
- if (Number.isInteger(value)) {
127
- point.setIntegerField(key, value);
128
- } else {
129
- point.setFloatField(key, value);
130
- }
131
- } else if (typeof value === 'boolean') {
132
- point.setBooleanField(key, value);
133
- } else {
134
- point.setStringField(key, String(value));
135
- }
136
- }
137
- }
138
- } else {
139
- // If no 'fields' property, treat all non-tag properties as fields
140
- for (const [key, value] of Object.entries(msg.payload)) {
141
- if (key !== 'tags' && key !== 'timestamp' && value !== null && value !== undefined) {
142
- if (typeof value === 'number') {
143
- if (Number.isInteger(value)) {
144
- point.setIntegerField(key, value);
145
- } else {
146
- point.setFloatField(key, value);
147
- }
148
- } else if (typeof value === 'boolean') {
149
- point.setBooleanField(key, value);
150
- } else if (typeof value !== 'object') {
151
- point.setStringField(key, String(value));
152
- }
153
- }
154
- }
155
- }
156
-
157
- // Add timestamp if provided
158
- if (msg.payload.timestamp) {
159
- if (msg.payload.timestamp instanceof Date) {
160
- point.setTimestamp(msg.payload.timestamp);
161
- } else if (typeof msg.payload.timestamp === 'number') {
162
- point.setTimestamp(new Date(msg.payload.timestamp));
163
- }
164
- } else if (msg.timestamp) {
165
- if (msg.timestamp instanceof Date) {
166
- point.setTimestamp(msg.timestamp);
167
- } else if (typeof msg.timestamp === 'number') {
168
- point.setTimestamp(new Date(msg.timestamp));
169
- }
170
- }
171
-
172
- lineProtocol = point.toLineProtocol();
173
-
174
- if (!lineProtocol) {
175
- throw new Error('No fields to write - at least one field is required');
176
- }
177
- } else {
178
- throw new Error('Invalid payload format. Expected string (line protocol) or object with fields');
179
- }
180
-
181
- // Write to InfluxDB
182
- await client.write(lineProtocol, targetDatabase);
183
-
184
- node.status({ fill: 'green', shape: 'dot', text: 'written' });
185
-
186
- // Clear status after 3 seconds
187
- setTimeout(() => {
188
- node.status({});
189
- }, 3000);
190
-
191
- send(msg);
192
- done();
193
-
194
- } catch (error) {
195
- node.status({ fill: 'red', shape: 'dot', text: 'error' });
196
- done(error);
197
- }
198
- });
199
-
200
- node.on('close', function() {
201
- node.status({});
202
- });
203
- }
204
-
205
- RED.nodes.registerType('influxdb3-write', InfluxDB3WriteNode);
206
- };
207
-
1
+ /**
2
+ * InfluxDB v3 nodes for Node-RED
3
+ */
4
+
5
+ module.exports = function(RED) {
6
+ const { InfluxDBClient, Point } = require('@influxdata/influxdb3-client');
7
+
8
+ /**
9
+ * Normalize host URL to ensure it has trailing slash
10
+ */
11
+ function normalizeHost(host) {
12
+ if (!host || typeof host !== 'string') {
13
+ return host;
14
+ }
15
+ return host.endsWith('/') ? host : host + '/';
16
+ }
17
+
18
+ /**
19
+ * Process a field value and add it to a Point
20
+ */
21
+ function addFieldToPoint(point, key, value, integerFields) {
22
+ if (value === null || value === undefined) {
23
+ return false;
24
+ }
25
+
26
+ // Handle string with 'i' suffix for integers (e.g., "42i")
27
+ if (typeof value === 'string' && /^-?\d+i$/.test(value)) {
28
+ const intValue = parseInt(value.slice(0, -1), 10);
29
+ // Validate parsed value
30
+ if (!isNaN(intValue) && isFinite(intValue)) {
31
+ point.setIntegerField(key, intValue);
32
+ return true;
33
+ }
34
+ } else if (typeof value === 'number') {
35
+ // Validate number
36
+ if (!isFinite(value)) {
37
+ RED.log.warn(`Skipping field '${key}': value is not finite (${value})`);
38
+ return false;
39
+ }
40
+
41
+ // Default to float for all numbers (safe default)
42
+ // Use integer only if explicitly marked in 'integers' array
43
+ if (integerFields.has(key)) {
44
+ const intValue = Math.floor(value);
45
+ if (intValue !== value) {
46
+ RED.log.warn(`Field '${key}': truncating ${value} to ${intValue} for integer field`);
47
+ }
48
+ point.setIntegerField(key, intValue);
49
+ } else {
50
+ point.setFloatField(key, value);
51
+ }
52
+ return true;
53
+ } else if (typeof value === 'boolean') {
54
+ point.setBooleanField(key, value);
55
+ return true;
56
+ } else if (typeof value === 'string') {
57
+ point.setStringField(key, value);
58
+ return true;
59
+ } else {
60
+ // Complex types (arrays, objects) are not supported
61
+ RED.log.warn(`Skipping field '${key}': unsupported type ${typeof value}`);
62
+ return false;
63
+ }
64
+
65
+ return false;
66
+ }
67
+
68
+ /**
69
+ * Configuration node to hold InfluxDB v3 connection details
70
+ */
71
+ function InfluxDB3ConfigNode(config) {
72
+ RED.nodes.createNode(this, config);
73
+
74
+ this.host = config.host;
75
+ this.database = config.database;
76
+ this.name = config.name;
77
+
78
+ // Store token as a credential
79
+ this.token = this.credentials.token;
80
+
81
+ // Client instance (will be created on demand)
82
+ this.client = null;
83
+
84
+ // Get or create a client instance
85
+ this.getClient = function() {
86
+ if (!this.client) {
87
+ // Validate configuration
88
+ if (!this.host) {
89
+ throw new Error('InfluxDB host is not configured');
90
+ }
91
+ if (!this.token) {
92
+ throw new Error('InfluxDB token is not configured');
93
+ }
94
+ if (!this.database) {
95
+ throw new Error('InfluxDB database is not configured');
96
+ }
97
+
98
+ try {
99
+ const normalizedHost = normalizeHost(this.host);
100
+
101
+ RED.log.info(`InfluxDB v3: Connecting to ${normalizedHost} with database ${this.database}`);
102
+
103
+ this.client = new InfluxDBClient({
104
+ host: normalizedHost,
105
+ token: this.token,
106
+ database: this.database
107
+ });
108
+
109
+ RED.log.info(`InfluxDB v3: Client created successfully`);
110
+ } catch (error) {
111
+ RED.log.error(`InfluxDB v3: Failed to create client - ${error.message}`);
112
+ throw new Error(`Failed to create InfluxDB client: ${error.message}`);
113
+ }
114
+ }
115
+ return this.client;
116
+ };
117
+
118
+ // Clean up on close
119
+ this.on('close', function() {
120
+ if (this.client) {
121
+ try {
122
+ RED.log.info('InfluxDB v3: Closing client connection');
123
+ this.client.close();
124
+ this.client = null;
125
+ } catch (error) {
126
+ RED.log.warn(`InfluxDB v3: Error closing client - ${error.message}`);
127
+ }
128
+ }
129
+ });
130
+ }
131
+
132
+ RED.nodes.registerType('influxdb3-config', InfluxDB3ConfigNode, {
133
+ credentials: {
134
+ token: { type: 'password' }
135
+ }
136
+ });
137
+
138
+ /**
139
+ * InfluxDB v3 Write Node
140
+ */
141
+ function InfluxDB3WriteNode(config) {
142
+ RED.nodes.createNode(this, config);
143
+
144
+ this.influxdb = RED.nodes.getNode(config.influxdb);
145
+ this.measurement = config.measurement;
146
+ this.database = config.database;
147
+
148
+ const node = this;
149
+ let statusTimeout = null;
150
+
151
+ if (!this.influxdb) {
152
+ this.error('InfluxDB v3 config not set');
153
+ this.status({ fill: 'red', shape: 'dot', text: 'no config' });
154
+ return;
155
+ }
156
+
157
+ // Helper to set status with auto-clear
158
+ function setStatus(status, clearAfterMs = 0) {
159
+ if (statusTimeout) {
160
+ clearTimeout(statusTimeout);
161
+ statusTimeout = null;
162
+ }
163
+
164
+ node.status(status);
165
+
166
+ if (clearAfterMs > 0) {
167
+ statusTimeout = setTimeout(() => {
168
+ node.status({});
169
+ statusTimeout = null;
170
+ }, clearAfterMs);
171
+ }
172
+ }
173
+
174
+ // Process incoming messages
175
+ node.on('input', async function(msg, send, done) {
176
+ // For Node-RED 0.x compatibility
177
+ send = send || function() { node.send.apply(node, arguments); };
178
+ done = done || function(err) {
179
+ if (err) {
180
+ node.error(err, msg);
181
+ }
182
+ };
183
+
184
+ try {
185
+ const client = node.influxdb.getClient();
186
+
187
+ // Determine the database to use
188
+ const targetDatabase = msg.database || node.database || node.influxdb.database;
189
+
190
+ if (!targetDatabase) {
191
+ throw new Error('Database not specified');
192
+ }
193
+
194
+ let lineProtocol;
195
+
196
+ // Check if msg.payload is already in line protocol format
197
+ if (typeof msg.payload === 'string') {
198
+ lineProtocol = msg.payload.trim();
199
+ if (!lineProtocol) {
200
+ throw new Error('Line protocol string is empty');
201
+ }
202
+ } else if (msg.payload && typeof msg.payload === 'object' && !Array.isArray(msg.payload)) {
203
+ // Build line protocol from payload object
204
+ const measurement = msg.measurement || node.measurement;
205
+
206
+ if (!measurement) {
207
+ throw new Error('Measurement not specified');
208
+ }
209
+
210
+ const point = new Point(measurement);
211
+
212
+ // Add tags
213
+ if (msg.payload.tags && typeof msg.payload.tags === 'object' && !Array.isArray(msg.payload.tags)) {
214
+ for (const [key, value] of Object.entries(msg.payload.tags)) {
215
+ if (value !== null && value !== undefined) {
216
+ point.setTag(key, String(value));
217
+ }
218
+ }
219
+ }
220
+
221
+ // Get list of fields that should be treated as integers
222
+ const integerFields = new Set(msg.payload.integers || []);
223
+ let fieldCount = 0;
224
+
225
+ // Add fields
226
+ if (msg.payload.fields && typeof msg.payload.fields === 'object' && !Array.isArray(msg.payload.fields)) {
227
+ // Explicit fields object
228
+ for (const [key, value] of Object.entries(msg.payload.fields)) {
229
+ if (addFieldToPoint(point, key, value, integerFields)) {
230
+ fieldCount++;
231
+ }
232
+ }
233
+ } else {
234
+ // Simplified format: treat all non-reserved properties as fields
235
+ const reservedKeys = new Set(['tags', 'timestamp', 'integers', 'fields']);
236
+ for (const [key, value] of Object.entries(msg.payload)) {
237
+ if (!reservedKeys.has(key)) {
238
+ if (addFieldToPoint(point, key, value, integerFields)) {
239
+ fieldCount++;
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ if (fieldCount === 0) {
246
+ throw new Error('No valid fields to write - at least one field is required');
247
+ }
248
+
249
+ // Add timestamp if provided
250
+ if (msg.payload.timestamp) {
251
+ const ts = msg.payload.timestamp;
252
+ if (ts instanceof Date && !isNaN(ts.getTime())) {
253
+ point.setTimestamp(ts);
254
+ } else if (typeof ts === 'number' && isFinite(ts) && ts > 0) {
255
+ point.setTimestamp(new Date(ts));
256
+ } else {
257
+ node.warn(`Invalid timestamp in payload: ${ts}`);
258
+ }
259
+ } else if (msg.timestamp) {
260
+ const ts = msg.timestamp;
261
+ if (ts instanceof Date && !isNaN(ts.getTime())) {
262
+ point.setTimestamp(ts);
263
+ } else if (typeof ts === 'number' && isFinite(ts) && ts > 0) {
264
+ point.setTimestamp(new Date(ts));
265
+ } else {
266
+ node.warn(`Invalid timestamp in msg: ${ts}`);
267
+ }
268
+ }
269
+
270
+ lineProtocol = point.toLineProtocol();
271
+
272
+ if (!lineProtocol || lineProtocol.trim() === '') {
273
+ throw new Error('Generated line protocol is empty');
274
+ }
275
+ } else {
276
+ throw new Error('Invalid payload format. Expected string (line protocol) or object with fields');
277
+ }
278
+
279
+ // Write to InfluxDB
280
+ await client.write(lineProtocol, targetDatabase);
281
+
282
+ setStatus({ fill: 'green', shape: 'dot', text: 'written' }, 3000);
283
+
284
+ send(msg);
285
+ done();
286
+
287
+ } catch (error) {
288
+ setStatus({ fill: 'red', shape: 'dot', text: 'error' });
289
+ done(error);
290
+ }
291
+ });
292
+
293
+ node.on('close', function() {
294
+ if (statusTimeout) {
295
+ clearTimeout(statusTimeout);
296
+ statusTimeout = null;
297
+ }
298
+ node.status({});
299
+ });
300
+ }
301
+
302
+ RED.nodes.registerType('influxdb3-write', InfluxDB3WriteNode);
303
+ };
package/package.json CHANGED
@@ -1,38 +1,42 @@
1
- {
2
- "name": "node-red-contrib-influxdb3",
3
- "version": "1.0.0",
4
- "description": "Node-RED nodes for InfluxDB v3 integration",
5
- "main": "influxdb3.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
8
- },
9
- "keywords": [
10
- "node-red",
11
- "influxdb",
12
- "influxdb3",
13
- "time-series",
14
- "database"
15
- ],
16
- "author": "Stuart B",
17
- "license": "MIT",
18
- "repository": {
19
- "type": "git",
20
- "url": "https://github.com/stuartb55/nodered-influxdb3.git"
21
- },
22
- "homepage": "https://github.com/stuartb55/nodered-influxdb3#readme",
23
- "bugs": {
24
- "url": "https://github.com/stuartb55/nodered-influxdb3/issues"
25
- },
26
- "node-red": {
27
- "nodes": {
28
- "influxdb3": "influxdb3.js"
29
- }
30
- },
31
- "dependencies": {
32
- "@influxdata/influxdb3-client": "^1.4.0"
33
- },
34
- "peerDependencies": {
35
- "node-red": ">=2.0.0"
36
- }
37
- }
38
-
1
+ {
2
+ "name": "node-red-contrib-influxdb3",
3
+ "version": "1.0.2",
4
+ "description": "Node-RED nodes for InfluxDB v3 integration",
5
+ "main": "influxdb3.js",
6
+ "scripts": {
7
+ "test": "echo \"No tests specified\""
8
+ },
9
+ "keywords": [
10
+ "node-red",
11
+ "influxdb",
12
+ "influxdb3",
13
+ "time-series",
14
+ "database"
15
+ ],
16
+ "author": "Stuart B",
17
+ "license": "MIT",
18
+ "engines": {
19
+ "node": ">=14.0.0"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/stuartb55/nodered-influxdb3.git"
24
+ },
25
+ "homepage": "https://github.com/stuartb55/nodered-influxdb3#readme",
26
+ "bugs": {
27
+ "url": "https://github.com/stuartb55/nodered-influxdb3/issues"
28
+ },
29
+ "node-red": {
30
+ "version": ">=3.0.0",
31
+ "nodes": {
32
+ "influxdb3": "influxdb3.js"
33
+ }
34
+ },
35
+ "dependencies": {
36
+ "@influxdata/influxdb3-client": "^1.4.0"
37
+ },
38
+ "peerDependencies": {
39
+ "node-red": ">=3.0.0"
40
+ }
41
+ }
42
+
package/renovate.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": [
4
+ "config:recommended"
5
+ ]
6
+ }