node-red-contrib-influxdb3 1.0.3 → 1.0.5

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