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 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 (except 'tags' and 'timestamp') are treated as fields:
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 v18.0.0 or higher
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: { value: '', required: true },
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 (except 'tags' and 'timestamp') are treated as fields:</p>
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
- point.setIntegerField(key, parseInt(value.slice(0, -1), 10));
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
- // Simplified format: treat all non-reserved properties as fields
341
- const reservedKeys = new Set(['tags', 'timestamp', 'integers', 'fields']);
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
- point.setTimestamp(new Date(ts));
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
 
@@ -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
- // Line protocol must have at least: measurement field=value
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
- 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
- );
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.8",
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": ">=18.0.0"
28
+ "node": ">=24.0.0"
29
29
  },
30
30
  "repository": {
31
31
  "type": "git",