node-red-contrib-influxdb3 1.0.0 → 1.0.1

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/README.md CHANGED
@@ -118,14 +118,55 @@ return msg;
118
118
 
119
119
  ### Data Types
120
120
 
121
- When using object format, the node automatically handles data types:
122
- - **Numbers**:
123
- - Integers are written as integer fields
124
- - Floats are written as float fields
121
+ **Important:** By default, **all numbers are written as floats** to avoid schema conflicts in InfluxDB. This is because JavaScript doesn't distinguish between `1.0` and `1` (both equal `1`), which can cause issues when InfluxDB expects a float but receives an integer.
122
+
123
+ When using object format, the node handles data types as follows:
124
+ - **Numbers**: Written as **float fields** by default
125
+ - **Integers**: Must be explicitly marked (see below)
125
126
  - **Booleans**: Written as boolean fields
126
127
  - **Strings**: Written as string fields
127
128
  - **Tags**: Always converted to strings
128
129
 
130
+ #### Writing Integer Fields
131
+
132
+ To write integers explicitly, use one of these methods:
133
+
134
+ **Method 1: Using the `integers` array**
135
+ ```javascript
136
+ msg.payload = {
137
+ fields: {
138
+ temperature: 21.5, // float
139
+ count: 42, // will be float by default
140
+ total: 100 // will be float by default
141
+ },
142
+ integers: ['count', 'total'] // mark these as integers
143
+ };
144
+ ```
145
+
146
+ **Method 2: Using the `i` suffix**
147
+ ```javascript
148
+ msg.payload = {
149
+ fields: {
150
+ temperature: 21.5, // float
151
+ count: "42i", // integer (note the string with 'i' suffix)
152
+ total: "100i" // integer
153
+ }
154
+ };
155
+ ```
156
+
157
+ **Example with both floats and integers:**
158
+ ```javascript
159
+ msg.measurement = "sensor_data";
160
+ msg.payload = {
161
+ temperature: 21.5, // float
162
+ humidity: 65.0, // float (even though it looks like an integer)
163
+ event_count: "50i", // integer
164
+ tags: {
165
+ location: "room1"
166
+ }
167
+ };
168
+ ```
169
+
129
170
  ### Message Properties
130
171
 
131
172
  The following message properties can be used to override node configuration:
@@ -133,6 +174,7 @@ The following message properties can be used to override node configuration:
133
174
  - `msg.measurement` - Override the measurement name
134
175
  - `msg.database` - Override the database name
135
176
  - `msg.timestamp` - Set the timestamp for the data point (Date object or milliseconds)
177
+ - `msg.payload.integers` - Array of field names to write as integers (e.g., `['count', 'total']`)
136
178
 
137
179
  ## Examples
138
180
 
@@ -323,10 +365,6 @@ The node will display error status and log details to the Node-RED debug panel:
323
365
 
324
366
  MIT
325
367
 
326
- ## Author
327
-
328
- Your Name
329
-
330
368
  ## Links
331
369
 
332
370
  - [InfluxDB v3 JavaScript Client](https://github.com/InfluxCommunity/influxdb3-js)
package/influxdb3.html CHANGED
@@ -154,6 +154,30 @@
154
154
  }
155
155
  }</pre>
156
156
 
157
+ <h3>Data Types</h3>
158
+ <p><strong>Important:</strong> By default, <strong>all numbers are written as floats</strong> to avoid schema conflicts.
159
+ JavaScript doesn't distinguish between <code>1.0</code> and <code>1</code>, which can cause issues when InfluxDB expects a float.</p>
160
+
161
+ <h4>Writing Integer Fields</h4>
162
+ <p>To explicitly write integers, use one of these methods:</p>
163
+
164
+ <p><strong>Method 1: Using the <code>integers</code> array</strong></p>
165
+ <pre>msg.payload = {
166
+ fields: {
167
+ temperature: 21.5, // float
168
+ count: 42 // will be float by default
169
+ },
170
+ integers: ['count'] // mark count as integer
171
+ };</pre>
172
+
173
+ <p><strong>Method 2: Using the <code>i</code> suffix</strong></p>
174
+ <pre>msg.payload = {
175
+ fields: {
176
+ temperature: 21.5, // float
177
+ count: "42i" // integer (string with 'i' suffix)
178
+ }
179
+ };</pre>
180
+
157
181
  <h3>Configuration</h3>
158
182
  <dl class="message-properties">
159
183
  <dt>Connection</dt>
package/influxdb3.js CHANGED
@@ -5,6 +5,66 @@
5
5
  module.exports = function(RED) {
6
6
  const { InfluxDBClient, Point } = require('@influxdata/influxdb3-client');
7
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
+
8
68
  /**
9
69
  * Configuration node to hold InfluxDB v3 connection details
10
70
  */
@@ -24,13 +84,31 @@ module.exports = function(RED) {
24
84
  // Get or create a client instance
25
85
  this.getClient = function() {
26
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
+
27
98
  try {
99
+ const normalizedHost = normalizeHost(this.host);
100
+
101
+ RED.log.info(`InfluxDB v3: Connecting to ${normalizedHost} with database ${this.database}`);
102
+
28
103
  this.client = new InfluxDBClient({
29
- host: this.host,
104
+ host: normalizedHost,
30
105
  token: this.token,
31
106
  database: this.database
32
107
  });
108
+
109
+ RED.log.info(`InfluxDB v3: Client created successfully`);
33
110
  } catch (error) {
111
+ RED.log.error(`InfluxDB v3: Failed to create client - ${error.message}`);
34
112
  throw new Error(`Failed to create InfluxDB client: ${error.message}`);
35
113
  }
36
114
  }
@@ -41,10 +119,11 @@ module.exports = function(RED) {
41
119
  this.on('close', function() {
42
120
  if (this.client) {
43
121
  try {
122
+ RED.log.info('InfluxDB v3: Closing client connection');
44
123
  this.client.close();
45
124
  this.client = null;
46
125
  } catch (error) {
47
- // Ignore errors on close
126
+ RED.log.warn(`InfluxDB v3: Error closing client - ${error.message}`);
48
127
  }
49
128
  }
50
129
  });
@@ -67,6 +146,7 @@ module.exports = function(RED) {
67
146
  this.database = config.database;
68
147
 
69
148
  const node = this;
149
+ let statusTimeout = null;
70
150
 
71
151
  if (!this.influxdb) {
72
152
  this.error('InfluxDB v3 config not set');
@@ -74,6 +154,23 @@ module.exports = function(RED) {
74
154
  return;
75
155
  }
76
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
+
77
174
  // Process incoming messages
78
175
  node.on('input', async function(msg, send, done) {
79
176
  // For Node-RED 0.x compatibility
@@ -98,8 +195,11 @@ module.exports = function(RED) {
98
195
 
99
196
  // Check if msg.payload is already in line protocol format
100
197
  if (typeof msg.payload === 'string') {
101
- lineProtocol = msg.payload;
102
- } else if (msg.payload && typeof msg.payload === 'object') {
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)) {
103
203
  // Build line protocol from payload object
104
204
  const measurement = msg.measurement || node.measurement;
105
205
 
@@ -110,7 +210,7 @@ module.exports = function(RED) {
110
210
  const point = new Point(measurement);
111
211
 
112
212
  // Add tags
113
- if (msg.payload.tags && typeof msg.payload.tags === 'object') {
213
+ if (msg.payload.tags && typeof msg.payload.tags === 'object' && !Array.isArray(msg.payload.tags)) {
114
214
  for (const [key, value] of Object.entries(msg.payload.tags)) {
115
215
  if (value !== null && value !== undefined) {
116
216
  point.setTag(key, String(value));
@@ -118,61 +218,59 @@ module.exports = function(RED) {
118
218
  }
119
219
  }
120
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
+
121
225
  // Add fields
122
- if (msg.payload.fields && typeof msg.payload.fields === 'object') {
226
+ if (msg.payload.fields && typeof msg.payload.fields === 'object' && !Array.isArray(msg.payload.fields)) {
227
+ // Explicit fields object
123
228
  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
- }
229
+ if (addFieldToPoint(point, key, value, integerFields)) {
230
+ fieldCount++;
136
231
  }
137
232
  }
138
233
  } else {
139
- // If no 'fields' property, treat all non-tag properties as fields
234
+ // Simplified format: treat all non-reserved properties as fields
235
+ const reservedKeys = new Set(['tags', 'timestamp', 'integers', 'fields']);
140
236
  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));
237
+ if (!reservedKeys.has(key)) {
238
+ if (addFieldToPoint(point, key, value, integerFields)) {
239
+ fieldCount++;
152
240
  }
153
241
  }
154
242
  }
155
243
  }
156
244
 
245
+ if (fieldCount === 0) {
246
+ throw new Error('No valid fields to write - at least one field is required');
247
+ }
248
+
157
249
  // Add timestamp if provided
158
250
  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));
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}`);
163
258
  }
164
259
  } 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));
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}`);
169
267
  }
170
268
  }
171
269
 
172
270
  lineProtocol = point.toLineProtocol();
173
271
 
174
- if (!lineProtocol) {
175
- throw new Error('No fields to write - at least one field is required');
272
+ if (!lineProtocol || lineProtocol.trim() === '') {
273
+ throw new Error('Generated line protocol is empty');
176
274
  }
177
275
  } else {
178
276
  throw new Error('Invalid payload format. Expected string (line protocol) or object with fields');
@@ -181,27 +279,25 @@ module.exports = function(RED) {
181
279
  // Write to InfluxDB
182
280
  await client.write(lineProtocol, targetDatabase);
183
281
 
184
- node.status({ fill: 'green', shape: 'dot', text: 'written' });
185
-
186
- // Clear status after 3 seconds
187
- setTimeout(() => {
188
- node.status({});
189
- }, 3000);
282
+ setStatus({ fill: 'green', shape: 'dot', text: 'written' }, 3000);
190
283
 
191
284
  send(msg);
192
285
  done();
193
286
 
194
287
  } catch (error) {
195
- node.status({ fill: 'red', shape: 'dot', text: 'error' });
288
+ setStatus({ fill: 'red', shape: 'dot', text: 'error' });
196
289
  done(error);
197
290
  }
198
291
  });
199
292
 
200
293
  node.on('close', function() {
294
+ if (statusTimeout) {
295
+ clearTimeout(statusTimeout);
296
+ statusTimeout = null;
297
+ }
201
298
  node.status({});
202
299
  });
203
300
  }
204
301
 
205
302
  RED.nodes.registerType('influxdb3-write', InfluxDB3WriteNode);
206
303
  };
207
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-influxdb3",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Node-RED nodes for InfluxDB v3 integration",
5
5
  "main": "influxdb3.js",
6
6
  "scripts": {
@@ -15,6 +15,9 @@
15
15
  ],
16
16
  "author": "Stuart B",
17
17
  "license": "MIT",
18
+ "engines": {
19
+ "node": ">=14.0.0"
20
+ },
18
21
  "repository": {
19
22
  "type": "git",
20
23
  "url": "https://github.com/stuartb55/nodered-influxdb3.git"
@@ -24,6 +27,7 @@
24
27
  "url": "https://github.com/stuartb55/nodered-influxdb3/issues"
25
28
  },
26
29
  "node-red": {
30
+ "version": ">=3.0.0",
27
31
  "nodes": {
28
32
  "influxdb3": "influxdb3.js"
29
33
  }
@@ -32,7 +36,7 @@
32
36
  "@influxdata/influxdb3-client": "^1.4.0"
33
37
  },
34
38
  "peerDependencies": {
35
- "node-red": ">=2.0.0"
39
+ "node-red": ">=3.0.0"
36
40
  }
37
41
  }
38
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
+ }