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 +6 -5
- package/influxdb3.html +2 -2
- package/influxdb3.js +57 -45
- package/lib/line-protocol.js +41 -0
- package/package.json +14 -2
- package/__tests__/influxdb3.test.js +0 -972
- package/renovate.json +0 -6
- package/test/line-protocol-validation.test.js +0 -83
- package/test/point-api.test.js +0 -56
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
|
|
56
|
-
- **CA Cert Path**: Optional filesystem path
|
|
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.
|
|
403
|
-
- As a last resort, disable **Verify TLS
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
80
|
-
RED.log.warn(
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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.
|
|
247
|
+
`Value will be truncated to ${Math.trunc(value)}.`
|
|
257
248
|
);
|
|
258
249
|
}
|
|
259
|
-
point.setIntegerField(key, Math.
|
|
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
|
-
|
|
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
|
|
312
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|