node-red-contrib-influxdb3 1.0.3 → 1.0.4
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/LICENSE +22 -22
- package/README.md +386 -386
- package/__tests__/influxdb3.test.js +786 -240
- package/examples/basic-flow.json +170 -170
- package/examples/mqtt-to-influx.json +134 -134
- package/influxdb3.html +243 -243
- package/influxdb3.js +400 -315
- package/package.json +41 -44
- package/renovate.json +6 -6
- package/test/point-api.test.js +56 -0
package/influxdb3.js
CHANGED
|
@@ -1,315 +1,400 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* InfluxDB v3 nodes for Node-RED
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
1
|
+
/**
|
|
2
|
+
* InfluxDB v3 nodes for Node-RED
|
|
3
|
+
* @module node-red-contrib-influxdb3
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
module.exports = function(RED) {
|
|
7
|
+
// @influxdata/influxdb3-client Point API:
|
|
8
|
+
// Point.setIntegerField(name, value)
|
|
9
|
+
// Point.setFloatField(name, value)
|
|
10
|
+
// Point.setStringField(name, value)
|
|
11
|
+
// Point.setBooleanField(name, value)
|
|
12
|
+
// Point.setTag(name, value)
|
|
13
|
+
// Point.setTimestamp(date)
|
|
14
|
+
// Point.toLineProtocol()
|
|
15
|
+
const { InfluxDBClient, Point } = require('@influxdata/influxdb3-client');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Normalize host URL to ensure it has trailing slash
|
|
19
|
+
* @param {string} host
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
function normalizeHost(host) {
|
|
23
|
+
if (!host || typeof host !== 'string') {
|
|
24
|
+
return host;
|
|
25
|
+
}
|
|
26
|
+
return host.endsWith('/') ? host : host + '/';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Configuration node to hold InfluxDB v3 connection details
|
|
31
|
+
* @param {object} config
|
|
32
|
+
*/
|
|
33
|
+
function InfluxDB3ConfigNode(config) {
|
|
34
|
+
RED.nodes.createNode(this, config);
|
|
35
|
+
|
|
36
|
+
/** @type {string} */
|
|
37
|
+
this.host = config.host;
|
|
38
|
+
/** @type {string} */
|
|
39
|
+
this.database = config.database;
|
|
40
|
+
/** @type {string} */
|
|
41
|
+
this.name = config.name;
|
|
42
|
+
/** @type {boolean} */
|
|
43
|
+
this.tlsRejectUnauthorized = config.tlsRejectUnauthorized !== false;
|
|
44
|
+
/** @type {string} */
|
|
45
|
+
this.caCertPath = config.caCertPath;
|
|
46
|
+
|
|
47
|
+
// Store token as a credential (populated by Node-RED runtime)
|
|
48
|
+
/** @type {string} */
|
|
49
|
+
if (!this.credentials) {
|
|
50
|
+
RED.log.warn('InfluxDB v3 config: credentials object is undefined');
|
|
51
|
+
}
|
|
52
|
+
this.token = this.credentials ? this.credentials.token : undefined;
|
|
53
|
+
|
|
54
|
+
// Client instance (will be created on demand)
|
|
55
|
+
/** @type {InfluxDBClient|null} */
|
|
56
|
+
this.client = null;
|
|
57
|
+
|
|
58
|
+
const configNode = this;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get or create a client instance
|
|
62
|
+
* @returns {InfluxDBClient}
|
|
63
|
+
*/
|
|
64
|
+
configNode.getClient = function() {
|
|
65
|
+
if (!configNode.client) {
|
|
66
|
+
if (!configNode.host) {
|
|
67
|
+
throw new Error('InfluxDB host is not configured');
|
|
68
|
+
}
|
|
69
|
+
if (!configNode.token) {
|
|
70
|
+
throw new Error('InfluxDB token is not configured');
|
|
71
|
+
}
|
|
72
|
+
if (!configNode.database) {
|
|
73
|
+
throw new Error('InfluxDB database is not configured');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const normalizedHost = normalizeHost(configNode.host);
|
|
77
|
+
|
|
78
|
+
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.');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
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}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
RED.log.info(`InfluxDB v3: Connecting to ${normalizedHost} with database ${configNode.database}`);
|
|
89
|
+
|
|
90
|
+
configNode.client = new InfluxDBClient({
|
|
91
|
+
host: normalizedHost,
|
|
92
|
+
token: configNode.token,
|
|
93
|
+
database: configNode.database
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
RED.log.info('InfluxDB v3: Client created successfully');
|
|
97
|
+
}
|
|
98
|
+
return configNode.client;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
configNode.on('close', function() {
|
|
102
|
+
if (configNode.client) {
|
|
103
|
+
try {
|
|
104
|
+
RED.log.info('InfluxDB v3: Closing client connection');
|
|
105
|
+
configNode.client.close();
|
|
106
|
+
} catch (error) {
|
|
107
|
+
RED.log.warn(`InfluxDB v3: Error closing client - ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
configNode.client = null;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
RED.nodes.registerType('influxdb3-config', InfluxDB3ConfigNode, {
|
|
115
|
+
credentials: {
|
|
116
|
+
token: { type: 'password' }
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* InfluxDB v3 Write Node
|
|
122
|
+
* @param {object} config
|
|
123
|
+
*/
|
|
124
|
+
function InfluxDB3WriteNode(config) {
|
|
125
|
+
RED.nodes.createNode(this, config);
|
|
126
|
+
|
|
127
|
+
this.influxdb = RED.nodes.getNode(config.influxdb);
|
|
128
|
+
this.measurement = config.measurement;
|
|
129
|
+
this.database = config.database;
|
|
130
|
+
|
|
131
|
+
const node = this;
|
|
132
|
+
let statusTimeout = null;
|
|
133
|
+
|
|
134
|
+
if (!node.influxdb) {
|
|
135
|
+
node.error('InfluxDB v3 config not set');
|
|
136
|
+
node.status({ fill: 'red', shape: 'dot', text: 'no config' });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Safely serialize a value for diagnostic logging.
|
|
142
|
+
* Handles circular references and large objects.
|
|
143
|
+
* @param {*} value
|
|
144
|
+
* @returns {string}
|
|
145
|
+
*/
|
|
146
|
+
function safeStringify(value) {
|
|
147
|
+
try {
|
|
148
|
+
return JSON.stringify(value);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
return `[unserializable: ${e.message}]`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Process a field value and add it to a Point.
|
|
156
|
+
* Returns true if the field was added, false if it was skipped.
|
|
157
|
+
*
|
|
158
|
+
* Uses the type-specific Point methods:
|
|
159
|
+
* - point.setFloatField(name, value) for numbers (default)
|
|
160
|
+
* - point.setIntegerField(name, value) for integers
|
|
161
|
+
* - point.setStringField(name, value) for strings
|
|
162
|
+
* - point.setBooleanField(name, value) for booleans
|
|
163
|
+
*
|
|
164
|
+
* @param {Point} point - The InfluxDB Point to add the field to
|
|
165
|
+
* @param {string} key - The field name
|
|
166
|
+
* @param {*} value - The field value
|
|
167
|
+
* @param {Set<string>} integerFields - Set of field names to treat as integers
|
|
168
|
+
* @param {string} measurement - Measurement name for diagnostic context
|
|
169
|
+
* @returns {boolean} true if the field was added successfully
|
|
170
|
+
*/
|
|
171
|
+
function addFieldToPoint(point, key, value, integerFields, measurement) {
|
|
172
|
+
const context = measurement ? ` (measurement: '${measurement}')` : '';
|
|
173
|
+
|
|
174
|
+
if (value === null || value === undefined) {
|
|
175
|
+
node.warn(`Skipping field '${key}': value is ${value}${context}`);
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (typeof value === 'object') {
|
|
180
|
+
const typeName = Array.isArray(value)
|
|
181
|
+
? 'Array'
|
|
182
|
+
: (value.constructor ? value.constructor.name : 'object');
|
|
183
|
+
node.warn(
|
|
184
|
+
`Skipping field '${key}': unsupported type 'object' (${typeName})${context}. ` +
|
|
185
|
+
`Actual value: ${safeStringify(value)}. ` +
|
|
186
|
+
`The value for field '${key}' must be a number, string, or boolean.`
|
|
187
|
+
);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (typeof value === 'string') {
|
|
192
|
+
// Check for integer suffix e.g. "42i"
|
|
193
|
+
if (/^-?\d+i$/.test(value)) {
|
|
194
|
+
point.setIntegerField(key, parseInt(value.slice(0, -1), 10));
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
point.setStringField(key, value);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (typeof value === 'boolean') {
|
|
202
|
+
point.setBooleanField(key, value);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (typeof value === 'number') {
|
|
207
|
+
if (!isFinite(value)) {
|
|
208
|
+
node.warn(
|
|
209
|
+
`Skipping field '${key}': numeric value is ${value} (not finite)${context}. ` +
|
|
210
|
+
`Check the source data for NaN or Infinity.`
|
|
211
|
+
);
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
if (integerFields && integerFields.has(key)) {
|
|
215
|
+
if (!Number.isInteger(value)) {
|
|
216
|
+
node.warn(
|
|
217
|
+
`Field '${key}' is marked as integer but value is ${value}${context}. ` +
|
|
218
|
+
`Value will be truncated to ${Math.floor(value)} using Math.floor.`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
point.setIntegerField(key, Math.floor(value));
|
|
222
|
+
} else {
|
|
223
|
+
point.setFloatField(key, value);
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
node.warn(
|
|
229
|
+
`Skipping field '${key}': unsupported type '${typeof value}'${context}. ` +
|
|
230
|
+
`Value: ${safeStringify(value)}`
|
|
231
|
+
);
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Set node status with optional auto-clear
|
|
237
|
+
* @param {object} status
|
|
238
|
+
* @param {number} [clearAfterMs=0]
|
|
239
|
+
*/
|
|
240
|
+
function setStatus(status, clearAfterMs) {
|
|
241
|
+
if (statusTimeout) {
|
|
242
|
+
clearTimeout(statusTimeout);
|
|
243
|
+
statusTimeout = null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
node.status(status);
|
|
247
|
+
|
|
248
|
+
if (clearAfterMs && clearAfterMs > 0) {
|
|
249
|
+
statusTimeout = setTimeout(function() {
|
|
250
|
+
node.status({});
|
|
251
|
+
statusTimeout = null;
|
|
252
|
+
}, clearAfterMs);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Build line protocol from an object payload.
|
|
258
|
+
* @param {object} msg - The incoming Node-RED message
|
|
259
|
+
* @returns {{lineProtocol: string}|{error: string}} result or error
|
|
260
|
+
*/
|
|
261
|
+
function buildLineProtocol(msg) {
|
|
262
|
+
const measurement = msg.measurement || node.measurement;
|
|
263
|
+
|
|
264
|
+
if (!measurement) {
|
|
265
|
+
return { error: 'Measurement not specified' };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const point = new Point(measurement);
|
|
269
|
+
|
|
270
|
+
// Add tags
|
|
271
|
+
if (msg.payload.tags && typeof msg.payload.tags === 'object' && !Array.isArray(msg.payload.tags)) {
|
|
272
|
+
for (const [key, value] of Object.entries(msg.payload.tags)) {
|
|
273
|
+
if (value !== null && value !== undefined) {
|
|
274
|
+
point.setTag(key, String(value));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Get list of fields that should be treated as integers
|
|
280
|
+
const integerFields = new Set(msg.payload.integers || []);
|
|
281
|
+
let fieldCount = 0;
|
|
282
|
+
|
|
283
|
+
// Add fields
|
|
284
|
+
if (msg.payload.fields && typeof msg.payload.fields === 'object' && !Array.isArray(msg.payload.fields)) {
|
|
285
|
+
// Explicit fields object
|
|
286
|
+
for (const [key, value] of Object.entries(msg.payload.fields)) {
|
|
287
|
+
if (addFieldToPoint(point, key, value, integerFields, measurement)) {
|
|
288
|
+
fieldCount++;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
// Simplified format: treat all non-reserved properties as fields
|
|
293
|
+
const reservedKeys = new Set(['tags', 'timestamp', 'integers', 'fields']);
|
|
294
|
+
for (const [key, value] of Object.entries(msg.payload)) {
|
|
295
|
+
if (!reservedKeys.has(key)) {
|
|
296
|
+
if (addFieldToPoint(point, key, value, integerFields, measurement)) {
|
|
297
|
+
fieldCount++;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (fieldCount === 0) {
|
|
304
|
+
return {
|
|
305
|
+
error: 'No valid fields to write - all fields were skipped or payload had no fields. ' +
|
|
306
|
+
'Payload was: ' + safeStringify(msg.payload)
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Handle timestamp — use nullish coalescing to preserve falsy-but-valid values like 0
|
|
311
|
+
const ts = (msg.payload.timestamp !== null && msg.payload.timestamp !== undefined)
|
|
312
|
+
? msg.payload.timestamp
|
|
313
|
+
: msg.timestamp;
|
|
314
|
+
if (ts !== null && ts !== undefined) {
|
|
315
|
+
if (ts instanceof Date && !isNaN(ts.getTime())) {
|
|
316
|
+
point.setTimestamp(ts);
|
|
317
|
+
} else if (typeof ts === 'number' && isFinite(ts) && ts >= 0) {
|
|
318
|
+
point.setTimestamp(new Date(ts));
|
|
319
|
+
} else if (typeof ts === 'string' && ts.trim() !== '') {
|
|
320
|
+
const parsed = new Date(ts);
|
|
321
|
+
if (!isNaN(parsed.getTime())) {
|
|
322
|
+
point.setTimestamp(parsed);
|
|
323
|
+
} else {
|
|
324
|
+
node.warn(`Invalid timestamp string: '${ts}'`);
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
node.warn(`Invalid timestamp: ${safeStringify(ts)} (type: ${typeof ts})`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const lp = point.toLineProtocol();
|
|
332
|
+
|
|
333
|
+
if (!lp || lp.trim() === '') {
|
|
334
|
+
return { error: 'Generated line protocol is empty' };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return { lineProtocol: lp };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Process incoming messages
|
|
341
|
+
node.on('input', async function(msg, send, done) {
|
|
342
|
+
// For Node-RED 0.x compatibility
|
|
343
|
+
send = send || function(m) { node.send(m); };
|
|
344
|
+
done = done || function(err) {
|
|
345
|
+
if (err) { node.error(err, msg); }
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
const client = node.influxdb.getClient();
|
|
350
|
+
|
|
351
|
+
// Determine the database to use
|
|
352
|
+
const targetDatabase = msg.database || node.database || node.influxdb.database;
|
|
353
|
+
|
|
354
|
+
if (!targetDatabase) {
|
|
355
|
+
throw new Error('Database not specified');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let lineProtocol;
|
|
359
|
+
|
|
360
|
+
// Check if msg.payload is already in line protocol format
|
|
361
|
+
if (typeof msg.payload === 'string') {
|
|
362
|
+
lineProtocol = msg.payload.trim();
|
|
363
|
+
if (!lineProtocol) {
|
|
364
|
+
throw new Error('Line protocol string is empty');
|
|
365
|
+
}
|
|
366
|
+
} else if (msg.payload && typeof msg.payload === 'object' && !Array.isArray(msg.payload)) {
|
|
367
|
+
const result = buildLineProtocol(msg);
|
|
368
|
+
if (result.error) {
|
|
369
|
+
throw new Error(result.error);
|
|
370
|
+
}
|
|
371
|
+
lineProtocol = result.lineProtocol;
|
|
372
|
+
} else {
|
|
373
|
+
throw new Error('Invalid payload format. Expected string (line protocol) or object with fields');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Write to InfluxDB
|
|
377
|
+
await client.write(lineProtocol, targetDatabase);
|
|
378
|
+
|
|
379
|
+
setStatus({ fill: 'green', shape: 'dot', text: 'written' }, 3000);
|
|
380
|
+
|
|
381
|
+
send(msg);
|
|
382
|
+
done();
|
|
383
|
+
|
|
384
|
+
} catch (error) {
|
|
385
|
+
setStatus({ fill: 'red', shape: 'dot', text: 'error' });
|
|
386
|
+
done(error);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
node.on('close', function() {
|
|
391
|
+
if (statusTimeout) {
|
|
392
|
+
clearTimeout(statusTimeout);
|
|
393
|
+
statusTimeout = null;
|
|
394
|
+
}
|
|
395
|
+
node.status({});
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
RED.nodes.registerType('influxdb3-write', InfluxDB3WriteNode);
|
|
400
|
+
};
|