node-red-contrib-influxdb3 1.0.8 → 1.0.9
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 +4 -2
- package/influxdb3.html +14 -3
- package/influxdb3.js +47 -11
- package/lib/line-protocol.js +30 -11
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -80,6 +80,8 @@ msg.payload = "temperature,location=room1 value=21.5";
|
|
|
80
80
|
return msg;
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
> **Note:** raw line protocol strings are passed through unmodified (each line is only sanity-checked). When data comes from an untrusted source, prefer the object formats below — the node escapes measurement, tag and field values automatically.
|
|
84
|
+
|
|
83
85
|
#### 2. Object with Fields and Tags
|
|
84
86
|
|
|
85
87
|
Send an object with explicit `fields` and `tags` properties:
|
|
@@ -102,7 +104,7 @@ return msg;
|
|
|
102
104
|
|
|
103
105
|
#### 3. Simplified Object Format
|
|
104
106
|
|
|
105
|
-
Send an object where all properties
|
|
107
|
+
Send an object where all properties are treated as fields, except the reserved keys `measurement`, `fields`, `tags`, `timestamp` and `integers`:
|
|
106
108
|
|
|
107
109
|
```javascript
|
|
108
110
|
msg.measurement = "environment";
|
|
@@ -416,7 +418,7 @@ The node will display error status and log details to the Node-RED debug panel:
|
|
|
416
418
|
|
|
417
419
|
## Requirements
|
|
418
420
|
|
|
419
|
-
- Node.js
|
|
421
|
+
- Node.js v24.0.0 or higher
|
|
420
422
|
- Node-RED v3.0.0 or higher
|
|
421
423
|
- InfluxDB v3 instance (Cloud or Edge)
|
|
422
424
|
|
package/influxdb3.html
CHANGED
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
category: 'config',
|
|
7
7
|
defaults: {
|
|
8
8
|
name: { value: '' },
|
|
9
|
-
host: {
|
|
9
|
+
host: {
|
|
10
|
+
value: '',
|
|
11
|
+
required: true,
|
|
12
|
+
validate: function(v) {
|
|
13
|
+
// Accept http(s) URLs or Node-RED ${ENV_VAR} substitution
|
|
14
|
+
return /^https?:\/\/\S+$/.test(v) || /\$\{[^}]+\}/.test(v);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
10
17
|
database: { value: '', required: true },
|
|
11
18
|
tlsRejectUnauthorized: { value: true },
|
|
12
19
|
caCertPath: { value: '' }
|
|
@@ -130,7 +137,7 @@
|
|
|
130
137
|
<dt class="optional">database <span class="property-type">string</span></dt>
|
|
131
138
|
<dd>Override the database configured in the node or connection</dd>
|
|
132
139
|
<dt class="optional">timestamp <span class="property-type">Date | number</span></dt>
|
|
133
|
-
<dd>Timestamp for the data point (if not in payload)</dd>
|
|
140
|
+
<dd>Timestamp for the data point (if not in payload). Numbers are interpreted as <b>milliseconds</b> since the epoch</dd>
|
|
134
141
|
</dl>
|
|
135
142
|
|
|
136
143
|
<h3>Outputs</h3>
|
|
@@ -145,6 +152,9 @@
|
|
|
145
152
|
<h4>Line Protocol Format</h4>
|
|
146
153
|
<p>Send a string in line protocol format:</p>
|
|
147
154
|
<pre>msg.payload = "temperature,location=room1 value=21.5";</pre>
|
|
155
|
+
<p><b>Note:</b> raw line protocol strings are passed through unmodified. When data comes from
|
|
156
|
+
an untrusted source, prefer the object format below — the node escapes measurement, tag and
|
|
157
|
+
field values automatically.</p>
|
|
148
158
|
|
|
149
159
|
<h4>Object Format</h4>
|
|
150
160
|
<p>Send an object with fields and optionally tags:</p>
|
|
@@ -161,7 +171,8 @@
|
|
|
161
171
|
}</pre>
|
|
162
172
|
|
|
163
173
|
<h4>Simplified Object Format</h4>
|
|
164
|
-
<p>Send an object where all properties
|
|
174
|
+
<p>Send an object where all properties are treated as fields, except the reserved keys
|
|
175
|
+
<code>measurement</code>, <code>fields</code>, <code>tags</code>, <code>timestamp</code> and <code>integers</code>:</p>
|
|
165
176
|
<pre>{
|
|
166
177
|
"temperature": 21.5,
|
|
167
178
|
"humidity": 65,
|
package/influxdb3.js
CHANGED
|
@@ -16,6 +16,11 @@ module.exports = function(RED) {
|
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const { validateLineProtocol } = require('./lib/line-protocol');
|
|
18
18
|
|
|
19
|
+
// Heuristic bounds for plausible millisecond timestamps. Values outside this
|
|
20
|
+
// range usually mean the source supplied seconds or nanoseconds instead.
|
|
21
|
+
const MS_TIMESTAMP_PLAUSIBLE_MIN = Date.UTC(2000, 0, 1);
|
|
22
|
+
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
23
|
+
|
|
19
24
|
/**
|
|
20
25
|
* Normalize host URL to ensure it has trailing slash
|
|
21
26
|
* @param {string} host
|
|
@@ -220,7 +225,14 @@ module.exports = function(RED) {
|
|
|
220
225
|
if (typeof value === 'string') {
|
|
221
226
|
// Check for integer suffix e.g. "42i"
|
|
222
227
|
if (/^-?\d+i$/.test(value)) {
|
|
223
|
-
|
|
228
|
+
const parsed = parseInt(value.slice(0, -1), 10);
|
|
229
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
230
|
+
node.warn(
|
|
231
|
+
`Field '${key}': integer value '${value}' exceeds JavaScript's safe integer ` +
|
|
232
|
+
`range and loses precision (stored as ${parsed})${context}.`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
point.setIntegerField(key, parsed);
|
|
224
236
|
return true;
|
|
225
237
|
}
|
|
226
238
|
point.setStringField(key, value);
|
|
@@ -337,8 +349,19 @@ module.exports = function(RED) {
|
|
|
337
349
|
}
|
|
338
350
|
}
|
|
339
351
|
} else {
|
|
340
|
-
|
|
341
|
-
|
|
352
|
+
if (msg.payload.fields !== null && msg.payload.fields !== undefined) {
|
|
353
|
+
const typeName = Array.isArray(msg.payload.fields)
|
|
354
|
+
? 'Array'
|
|
355
|
+
: typeof msg.payload.fields;
|
|
356
|
+
node.warn(
|
|
357
|
+
`'fields' is present but is not a plain object (${typeName}) (measurement: '${measurement}'). ` +
|
|
358
|
+
`It will be ignored and the other payload properties will be treated as fields.`
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
// Simplified format: treat all non-reserved properties as fields.
|
|
362
|
+
// 'measurement' is reserved too so that array items like
|
|
363
|
+
// { measurement: 'temp', value: 1 } don't write it as a string field.
|
|
364
|
+
const reservedKeys = new Set(['measurement', 'tags', 'timestamp', 'integers', 'fields']);
|
|
342
365
|
for (const [key, value] of Object.entries(msg.payload)) {
|
|
343
366
|
if (!reservedKeys.has(key)) {
|
|
344
367
|
if (addFieldToPoint(point, key, value, integerFields, measurement)) {
|
|
@@ -363,7 +386,27 @@ module.exports = function(RED) {
|
|
|
363
386
|
if (ts instanceof Date && !isNaN(ts.getTime())) {
|
|
364
387
|
point.setTimestamp(ts);
|
|
365
388
|
} else if (typeof ts === 'number' && isFinite(ts) && ts >= 0) {
|
|
366
|
-
|
|
389
|
+
const date = new Date(ts);
|
|
390
|
+
if (isNaN(date.getTime())) {
|
|
391
|
+
// Beyond the representable Date range (±8.64e15 ms) — almost
|
|
392
|
+
// always a nanosecond timestamp passed where ms are expected.
|
|
393
|
+
node.warn(
|
|
394
|
+
`Invalid timestamp: ${ts} is outside the representable date range. ` +
|
|
395
|
+
`Numeric timestamps are interpreted as milliseconds - if the source ` +
|
|
396
|
+
`supplies nanoseconds, convert to milliseconds first. The timestamp ` +
|
|
397
|
+
`was ignored; InfluxDB will assign the write time.`
|
|
398
|
+
);
|
|
399
|
+
} else {
|
|
400
|
+
// 0 is deliberately allowed without a warning (explicit epoch).
|
|
401
|
+
if (ts !== 0 && (ts < MS_TIMESTAMP_PLAUSIBLE_MIN || ts > Date.now() + ONE_DAY_MS)) {
|
|
402
|
+
node.warn(
|
|
403
|
+
`Numeric timestamp ${ts} resolves to ${date.toISOString()}. ` +
|
|
404
|
+
`Numeric timestamps are interpreted as milliseconds - if the source ` +
|
|
405
|
+
`supplies seconds or nanoseconds, convert to milliseconds first.`
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
point.setTimestamp(date);
|
|
409
|
+
}
|
|
367
410
|
} else if (typeof ts === 'string' && ts.trim() !== '') {
|
|
368
411
|
const parsed = new Date(ts);
|
|
369
412
|
if (!isNaN(parsed.getTime())) {
|
|
@@ -385,14 +428,7 @@ module.exports = function(RED) {
|
|
|
385
428
|
return { lineProtocol: lp };
|
|
386
429
|
}
|
|
387
430
|
|
|
388
|
-
// Process incoming messages
|
|
389
431
|
node.on('input', async function(msg, send, done) {
|
|
390
|
-
// For Node-RED 0.x compatibility
|
|
391
|
-
send = send || function(m) { node.send(m); };
|
|
392
|
-
done = done || function(err) {
|
|
393
|
-
if (err) { node.error(err, msg); }
|
|
394
|
-
};
|
|
395
|
-
|
|
396
432
|
try {
|
|
397
433
|
const client = node.influxdb.getClient();
|
|
398
434
|
|
package/lib/line-protocol.js
CHANGED
|
@@ -6,33 +6,52 @@
|
|
|
6
6
|
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Truncate a string for use in error messages.
|
|
11
|
+
* @param {string} str
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
function preview(str) {
|
|
15
|
+
return str.length > 100 ? str.substring(0, 100) + '...' : str;
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
/**
|
|
10
19
|
* Validate that a string looks like InfluxDB line protocol.
|
|
20
|
+
* Multi-line strings (multiple points separated by newlines) are validated
|
|
21
|
+
* line by line; blank lines are allowed and skipped.
|
|
11
22
|
* Returns null if valid, or an error message string if invalid.
|
|
12
23
|
* @param {string} lp - Trimmed line protocol string
|
|
13
24
|
* @returns {string|null}
|
|
14
25
|
*/
|
|
15
26
|
function validateLineProtocol(lp) {
|
|
16
|
-
// Detect JSON-like strings (both valid JSON and JS object notation)
|
|
27
|
+
// Detect JSON-like strings (both valid JSON and JS object notation).
|
|
28
|
+
// Checked against the whole string first, because pretty-printed JSON
|
|
29
|
+
// spans multiple lines.
|
|
17
30
|
if (/^\{[\s\S]*}$/.test(lp) || /^\[[\s\S]*]$/.test(lp)) {
|
|
18
|
-
const preview = lp.length > 100 ? lp.substring(0, 100) + '...' : lp;
|
|
19
31
|
return (
|
|
20
32
|
'The payload appears to be a JSON/object string, not line protocol. ' +
|
|
21
33
|
'If you are sending JSON, ensure msg.payload is a parsed object (not a string). ' +
|
|
22
34
|
'Use a JSON parse node before this node to convert the string to an object. ' +
|
|
23
|
-
`Received string: ${preview}`
|
|
35
|
+
`Received string: ${preview(lp)}`
|
|
24
36
|
);
|
|
25
37
|
}
|
|
26
38
|
|
|
27
|
-
//
|
|
39
|
+
// Each line must have at least: measurement field=value
|
|
28
40
|
// i.e. at least one space and one '=' in the field set
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
)
|
|
41
|
+
const lines = lp.split('\n');
|
|
42
|
+
for (let i = 0; i < lines.length; i++) {
|
|
43
|
+
const line = lines[i].trim();
|
|
44
|
+
if (!line) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (!line.includes(' ') || !line.includes('=')) {
|
|
48
|
+
const where = lines.length > 1 ? `Line ${i + 1} of the payload` : 'The payload string';
|
|
49
|
+
return (
|
|
50
|
+
`${where} does not appear to be valid line protocol. ` +
|
|
51
|
+
'Expected format: measurement[,tag=val] field=val[,field=val] [timestamp]. ' +
|
|
52
|
+
`Received: ${preview(line)}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
36
55
|
}
|
|
37
56
|
|
|
38
57
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-influxdb3",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "Node-RED nodes for InfluxDB v3 integration",
|
|
5
5
|
"main": "influxdb3.js",
|
|
6
6
|
"files": [
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"author": "Stuart B",
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"engines": {
|
|
28
|
-
"node": ">=
|
|
28
|
+
"node": ">=24.0.0"
|
|
29
29
|
},
|
|
30
30
|
"repository": {
|
|
31
31
|
"type": "git",
|