node-red-contrib-influxdb3 1.0.6 → 1.0.8

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
@@ -52,8 +52,8 @@ A configuration node that stores connection details for your InfluxDB v3 instanc
52
52
  - **Host**: Your InfluxDB v3 host URL (e.g., `https://us-east-1-1.aws.cloud2.influxdata.com`)
53
53
  - **Token**: Your InfluxDB v3 authentication token
54
54
  - **Database**: The default database (bucket) name
55
- - **Verify TLS**: Toggle TLS certificate verification (unchecked sets `NODE_TLS_REJECT_UNAUTHORIZED=0`)
56
- - **CA Cert Path**: Optional filesystem path for a custom root CA (`NODE_EXTRA_CA_CERTS`)
55
+ - **Verify TLS**: Toggle TLS certificate verification for this connection (unchecked disables verification for this connection only)
56
+ - **CA Cert Path**: Optional filesystem path to a PEM CA certificate used to verify this connection's TLS certificate
57
57
 
58
58
  ### InfluxDB v3 Write Node
59
59
 
@@ -399,8 +399,8 @@ Simply reference them in the Node-RED UI using `${INFLUX_HOST}` syntax (if using
399
399
 
400
400
  If you are connecting to a local InfluxDB v3 instance with a custom certificate:
401
401
 
402
- - Set **CA Cert Path** in the config node to the PEM file containing your root CA. This sets `NODE_EXTRA_CA_CERTS` for the Node-RED process.
403
- - As a last resort, disable **Verify TLS** to set `NODE_TLS_REJECT_UNAUTHORIZED=0` (this disables TLS verification globally for the process).
402
+ - Set **CA Cert Path** in the config node to the PEM file containing your root CA. The certificate is read when the connection is first used and applied to this connection only.
403
+ - As a last resort, disable **Verify TLS**. This skips certificate verification for this connection only (it does not affect other connections or the rest of the Node-RED process).
404
404
 
405
405
  ### Data Not Appearing
406
406
 
@@ -416,7 +416,8 @@ The node will display error status and log details to the Node-RED debug panel:
416
416
 
417
417
  ## Requirements
418
418
 
419
- - Node-RED v2.0.0 or higher
419
+ - Node.js v18.0.0 or higher
420
+ - Node-RED v3.0.0 or higher
420
421
  - InfluxDB v3 instance (Cloud or Edge)
421
422
 
422
423
  ## License
package/influxdb3.html CHANGED
@@ -62,9 +62,9 @@
62
62
  <dt>Database <span class="property-type">string</span></dt>
63
63
  <dd>The default database (bucket) name to write to</dd>
64
64
  <dt>Verify TLS <span class="property-type">boolean</span></dt>
65
- <dd>When unchecked, sets <code>NODE_TLS_REJECT_UNAUTHORIZED=0</code> for the Node-RED process</dd>
65
+ <dd>When unchecked, disables TLS certificate verification for this connection only (insecure; use only for trusted local instances)</dd>
66
66
  <dt>CA Cert Path <span class="property-type">string</span></dt>
67
- <dd>Optional filesystem path used to set <code>NODE_EXTRA_CA_CERTS</code> for custom root CAs</dd>
67
+ <dd>Optional filesystem path to a PEM CA certificate, used to verify this connection's TLS certificate</dd>
68
68
  </dl>
69
69
  </script>
70
70
 
package/influxdb3.js CHANGED
@@ -13,6 +13,8 @@ module.exports = function(RED) {
13
13
  // Point.setTimestamp(date)
14
14
  // Point.toLineProtocol()
15
15
  const { InfluxDBClient, Point } = require('@influxdata/influxdb3-client');
16
+ const fs = require('fs');
17
+ const { validateLineProtocol } = require('./lib/line-protocol');
16
18
 
17
19
  /**
18
20
  * Normalize host URL to ensure it has trailing slash
@@ -75,23 +77,44 @@ module.exports = function(RED) {
75
77
 
76
78
  const normalizedHost = normalizeHost(configNode.host);
77
79
 
80
+ // Build per-client transport (TLS) options. These are passed only to this
81
+ // client's HTTPS requests, so they do NOT affect other connections or the
82
+ // rest of the Node-RED process (unlike NODE_TLS_REJECT_UNAUTHORIZED /
83
+ // NODE_EXTRA_CA_CERTS, which are global and read only at process startup).
84
+ const transportOptions = {};
85
+
78
86
  if (!configNode.tlsRejectUnauthorized) {
79
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
80
- RED.log.warn('InfluxDB v3: TLS certificate verification is disabled for this process.');
87
+ transportOptions.rejectUnauthorized = false;
88
+ RED.log.warn(
89
+ 'InfluxDB v3: TLS certificate verification is disabled for this connection. ' +
90
+ 'This is insecure and should only be used for trusted local instances.'
91
+ );
81
92
  }
82
93
 
83
94
  if (configNode.caCertPath) {
84
- process.env.NODE_EXTRA_CA_CERTS = configNode.caCertPath;
85
- RED.log.info(`InfluxDB v3: Using extra CA certificates from ${configNode.caCertPath}`);
95
+ try {
96
+ transportOptions.ca = fs.readFileSync(configNode.caCertPath);
97
+ RED.log.info(`InfluxDB v3: Using custom CA certificate from ${configNode.caCertPath}`);
98
+ } catch (error) {
99
+ throw new Error(
100
+ `Failed to read CA certificate from '${configNode.caCertPath}': ${error.message}`,
101
+ { cause: error }
102
+ );
103
+ }
86
104
  }
87
105
 
88
106
  RED.log.info(`InfluxDB v3: Connecting to ${normalizedHost} with database ${configNode.database}`);
89
107
 
90
- configNode.client = new InfluxDBClient({
108
+ const clientOptions = {
91
109
  host: normalizedHost,
92
110
  token: configNode.token,
93
111
  database: configNode.database
94
- });
112
+ };
113
+ if (Object.keys(transportOptions).length > 0) {
114
+ clientOptions.transportOptions = transportOptions;
115
+ }
116
+
117
+ configNode.client = new InfluxDBClient(clientOptions);
95
118
 
96
119
  RED.log.info('InfluxDB v3: Client created successfully');
97
120
  }
@@ -157,38 +180,6 @@ module.exports = function(RED) {
157
180
  }
158
181
  }
159
182
 
160
- /**
161
- * Validate that a string looks like InfluxDB line protocol.
162
- * Returns null if valid, or an error message string if invalid.
163
- * @param {string} lp - Trimmed line protocol string
164
- * @returns {string|null}
165
- */
166
- function validateLineProtocol(lp) {
167
- // Detect JSON-like strings (both valid JSON and JS object notation)
168
- if (/^\{[\s\S]*}$/.test(lp) || /^\[[\s\S]*]$/.test(lp)) {
169
- const preview = lp.length > 100 ? lp.substring(0, 100) + '...' : lp;
170
- return (
171
- 'The payload appears to be a JSON/object string, not line protocol. ' +
172
- 'If you are sending JSON, ensure msg.payload is a parsed object (not a string). ' +
173
- 'Use a JSON parse node before this node to convert the string to an object. ' +
174
- `Received string: ${preview}`
175
- );
176
- }
177
-
178
- // Line protocol must have at least: measurement field=value
179
- // i.e. at least one space and one '=' in the field set
180
- if (!lp.includes(' ') || !lp.includes('=')) {
181
- const preview = lp.length > 100 ? lp.substring(0, 100) + '...' : lp;
182
- return (
183
- 'The payload string does not appear to be valid line protocol. ' +
184
- 'Expected format: measurement[,tag=val] field=val[,field=val] [timestamp]. ' +
185
- `Received: ${preview}`
186
- );
187
- }
188
-
189
- return null;
190
- }
191
-
192
183
  /**
193
184
  * Process a field value and add it to a Point.
194
185
  * Returns true if the field was added, false if it was skipped.
@@ -253,10 +244,10 @@ module.exports = function(RED) {
253
244
  if (!Number.isInteger(value)) {
254
245
  node.warn(
255
246
  `Field '${key}' is marked as integer but value is ${value}${context}. ` +
256
- `Value will be truncated to ${Math.floor(value)} using Math.floor.`
247
+ `Value will be truncated to ${Math.trunc(value)}.`
257
248
  );
258
249
  }
259
- point.setIntegerField(key, Math.floor(value));
250
+ point.setIntegerField(key, Math.trunc(value));
260
251
  } else {
261
252
  point.setFloatField(key, value);
262
253
  }
@@ -297,7 +288,11 @@ module.exports = function(RED) {
297
288
  * @returns {{lineProtocol: string}|{error: string}} result or error
298
289
  */
299
290
  function buildLineProtocol(msg) {
300
- const measurement = msg.measurement || node.measurement;
291
+ // Trim each source before the fallback so a blank/whitespace-only
292
+ // msg.measurement falls back to the node default (instead of being used
293
+ // verbatim) and never produces a measurement made of spaces.
294
+ const trim = (m) => (typeof m === 'string' ? m.trim() : m);
295
+ const measurement = trim(msg.measurement) || trim(node.measurement);
301
296
 
302
297
  if (!measurement) {
303
298
  return { error: 'Measurement not specified' };
@@ -307,10 +302,25 @@ module.exports = function(RED) {
307
302
 
308
303
  // Add tags
309
304
  if (msg.payload.tags && typeof msg.payload.tags === 'object' && !Array.isArray(msg.payload.tags)) {
305
+ const tagContext = measurement ? ` (measurement: '${measurement}')` : '';
310
306
  for (const [key, value] of Object.entries(msg.payload.tags)) {
311
- if (value !== null && value !== undefined) {
312
- point.setTag(key, String(value));
307
+ if (value === null || value === undefined) {
308
+ continue;
309
+ }
310
+ // Guard against objects/arrays, which would otherwise be coerced to
311
+ // useless strings like "[object Object]". Mirrors addFieldToPoint.
312
+ if (typeof value === 'object') {
313
+ const typeName = Array.isArray(value)
314
+ ? 'Array'
315
+ : (value.constructor ? value.constructor.name : 'object');
316
+ node.warn(
317
+ `Skipping tag '${key}': unsupported type 'object' (${typeName})${tagContext}. ` +
318
+ `Actual value: ${safeStringify(value)}. ` +
319
+ `Tag values must be a string, number, or boolean.`
320
+ );
321
+ continue;
313
322
  }
323
+ point.setTag(key, String(value));
314
324
  }
315
325
  }
316
326
 
@@ -386,8 +396,10 @@ module.exports = function(RED) {
386
396
  try {
387
397
  const client = node.influxdb.getClient();
388
398
 
389
- // Determine the database to use
390
- const targetDatabase = msg.database || node.database || node.influxdb.database;
399
+ // Determine the database to use. Trim each source before the fallback so a
400
+ // blank/whitespace-only msg.database falls back instead of being used verbatim.
401
+ const trimDb = (d) => (typeof d === 'string' ? d.trim() : d);
402
+ const targetDatabase = trimDb(msg.database) || trimDb(node.database) || trimDb(node.influxdb.database);
391
403
 
392
404
  if (!targetDatabase) {
393
405
  throw new Error('Database not specified');
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Pure line-protocol helpers, extracted so they can be unit-tested directly
3
+ * (and shared) rather than re-implemented in test files.
4
+ * @module lib/line-protocol
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ /**
10
+ * Validate that a string looks like InfluxDB line protocol.
11
+ * Returns null if valid, or an error message string if invalid.
12
+ * @param {string} lp - Trimmed line protocol string
13
+ * @returns {string|null}
14
+ */
15
+ function validateLineProtocol(lp) {
16
+ // Detect JSON-like strings (both valid JSON and JS object notation)
17
+ if (/^\{[\s\S]*}$/.test(lp) || /^\[[\s\S]*]$/.test(lp)) {
18
+ const preview = lp.length > 100 ? lp.substring(0, 100) + '...' : lp;
19
+ return (
20
+ 'The payload appears to be a JSON/object string, not line protocol. ' +
21
+ 'If you are sending JSON, ensure msg.payload is a parsed object (not a string). ' +
22
+ 'Use a JSON parse node before this node to convert the string to an object. ' +
23
+ `Received string: ${preview}`
24
+ );
25
+ }
26
+
27
+ // Line protocol must have at least: measurement field=value
28
+ // i.e. at least one space and one '=' in the field set
29
+ if (!lp.includes(' ') || !lp.includes('=')) {
30
+ const preview = lp.length > 100 ? lp.substring(0, 100) + '...' : lp;
31
+ return (
32
+ 'The payload string does not appear to be valid line protocol. ' +
33
+ 'Expected format: measurement[,tag=val] field=val[,field=val] [timestamp]. ' +
34
+ `Received: ${preview}`
35
+ );
36
+ }
37
+
38
+ return null;
39
+ }
40
+
41
+ module.exports = { validateLineProtocol };
package/package.json CHANGED
@@ -1,9 +1,18 @@
1
1
  {
2
2
  "name": "node-red-contrib-influxdb3",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Node-RED nodes for InfluxDB v3 integration",
5
5
  "main": "influxdb3.js",
6
+ "files": [
7
+ "influxdb3.js",
8
+ "influxdb3.html",
9
+ "lib/",
10
+ "examples/",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
6
14
  "scripts": {
15
+ "lint": "eslint .",
7
16
  "test": "jest"
8
17
  },
9
18
  "keywords": [
@@ -33,9 +42,12 @@
33
42
  }
34
43
  },
35
44
  "dependencies": {
36
- "@influxdata/influxdb3-client": "^2.1.0"
45
+ "@influxdata/influxdb3-client": "^2.2.0"
37
46
  },
38
47
  "devDependencies": {
48
+ "@eslint/js": "^10.0.1",
49
+ "eslint": "^10.4.1",
50
+ "globals": "^17.6.0",
39
51
  "jest": "^30.3.0"
40
52
  }
41
53
  }