node-red-contrib-influxdb3 1.0.2 → 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/README.md +9 -0
- package/__tests__/influxdb3.test.js +786 -0
- package/influxdb3.html +16 -1
- package/influxdb3.js +285 -188
- package/package.json +6 -7
- package/test/point-api.test.js +56 -0
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
let mockLastClientOptions;
|
|
4
|
+
let mockLastClientInstance;
|
|
5
|
+
let mockLastPoint;
|
|
6
|
+
|
|
7
|
+
jest.mock('@influxdata/influxdb3-client', () => {
|
|
8
|
+
class MockInfluxDBClient {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
mockLastClientOptions = options;
|
|
11
|
+
mockLastClientInstance = this;
|
|
12
|
+
this.write = jest.fn().mockResolvedValue(undefined);
|
|
13
|
+
this.close = jest.fn();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class MockPoint {
|
|
18
|
+
constructor(measurement) {
|
|
19
|
+
this.measurement = measurement;
|
|
20
|
+
this.tags = {};
|
|
21
|
+
this.integerFields = {};
|
|
22
|
+
this.floatFields = {};
|
|
23
|
+
this.stringFields = {};
|
|
24
|
+
this.booleanFields = {};
|
|
25
|
+
this.timestamp = null;
|
|
26
|
+
mockLastPoint = this;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setTag(key, value) {
|
|
30
|
+
this.tags[key] = value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setIntegerField(key, value) {
|
|
34
|
+
this.integerFields[key] = value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setFloatField(key, value) {
|
|
38
|
+
this.floatFields[key] = value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setStringField(key, value) {
|
|
42
|
+
this.stringFields[key] = value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setBooleanField(key, value) {
|
|
46
|
+
this.booleanFields[key] = value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setTimestamp(ts) {
|
|
50
|
+
this.timestamp = ts;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
toLineProtocol() {
|
|
54
|
+
return `lp:${this.measurement}`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
InfluxDBClient: MockInfluxDBClient,
|
|
60
|
+
Point: MockPoint,
|
|
61
|
+
__getLastClientOptions: () => mockLastClientOptions,
|
|
62
|
+
__getLastClientInstance: () => mockLastClientInstance,
|
|
63
|
+
__getLastPoint: () => mockLastPoint
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
function buildRED() {
|
|
68
|
+
const types = {};
|
|
69
|
+
return {
|
|
70
|
+
log: {
|
|
71
|
+
info: jest.fn(),
|
|
72
|
+
warn: jest.fn(),
|
|
73
|
+
error: jest.fn()
|
|
74
|
+
},
|
|
75
|
+
nodes: {
|
|
76
|
+
createNode(node, config) {
|
|
77
|
+
node.credentials = config.credentials || {};
|
|
78
|
+
node.status = jest.fn();
|
|
79
|
+
node.error = jest.fn();
|
|
80
|
+
node.warn = jest.fn();
|
|
81
|
+
node.send = jest.fn();
|
|
82
|
+
node.on = jest.fn((event, handler) => {
|
|
83
|
+
node._handlers = node._handlers || {};
|
|
84
|
+
node._handlers[event] = handler;
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
registerType(name, ctor) {
|
|
88
|
+
types[name] = ctor;
|
|
89
|
+
},
|
|
90
|
+
getNode(id) {
|
|
91
|
+
return id;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
_types: types
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function setup() {
|
|
99
|
+
jest.resetModules();
|
|
100
|
+
const RED = buildRED();
|
|
101
|
+
require('../influxdb3.js')(RED);
|
|
102
|
+
const influxModule = require('@influxdata/influxdb3-client');
|
|
103
|
+
return { RED, influxModule };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
jest.useFakeTimers();
|
|
108
|
+
mockLastClientOptions = undefined;
|
|
109
|
+
mockLastClientInstance = undefined;
|
|
110
|
+
mockLastPoint = undefined;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
afterEach(() => {
|
|
114
|
+
jest.runOnlyPendingTimers();
|
|
115
|
+
jest.useRealTimers();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('InfluxDB v3 config node', () => {
|
|
119
|
+
test('normalizes host and uses provided config', () => {
|
|
120
|
+
const { RED, influxModule } = setup();
|
|
121
|
+
const ConfigCtor = RED._types['influxdb3-config'];
|
|
122
|
+
|
|
123
|
+
const configNode = new ConfigCtor({
|
|
124
|
+
host: 'https://example.com',
|
|
125
|
+
database: 'metrics',
|
|
126
|
+
name: 'Test',
|
|
127
|
+
credentials: { token: 'token' }
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
configNode.getClient();
|
|
131
|
+
|
|
132
|
+
const options = influxModule.__getLastClientOptions();
|
|
133
|
+
expect(options.host).toBe('https://example.com/');
|
|
134
|
+
expect(options.database).toBe('metrics');
|
|
135
|
+
expect(options.token).toBe('token');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('sets extra CA certificate path when configured', () => {
|
|
139
|
+
const original = process.env.NODE_EXTRA_CA_CERTS;
|
|
140
|
+
const { RED } = setup();
|
|
141
|
+
const ConfigCtor = RED._types['influxdb3-config'];
|
|
142
|
+
|
|
143
|
+
const configNode = new ConfigCtor({
|
|
144
|
+
host: 'https://example.com',
|
|
145
|
+
database: 'metrics',
|
|
146
|
+
name: 'Test',
|
|
147
|
+
caCertPath: path.join('C:', 'certs', 'root.pem'),
|
|
148
|
+
credentials: { token: 'token' }
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
configNode.getClient();
|
|
152
|
+
|
|
153
|
+
expect(process.env.NODE_EXTRA_CA_CERTS).toBe(path.join('C:', 'certs', 'root.pem'));
|
|
154
|
+
process.env.NODE_EXTRA_CA_CERTS = original;
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('InfluxDB v3 write node', () => {
|
|
159
|
+
test('writes line protocol from object payload', async () => {
|
|
160
|
+
const { RED, influxModule } = setup();
|
|
161
|
+
const ConfigCtor = RED._types['influxdb3-config'];
|
|
162
|
+
const WriteCtor = RED._types['influxdb3-write'];
|
|
163
|
+
|
|
164
|
+
const configNode = new ConfigCtor({
|
|
165
|
+
host: 'https://example.com',
|
|
166
|
+
database: 'metrics',
|
|
167
|
+
name: 'Test',
|
|
168
|
+
credentials: { token: 'token' }
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const writeNode = new WriteCtor({
|
|
172
|
+
influxdb: configNode,
|
|
173
|
+
measurement: 'cpu',
|
|
174
|
+
database: ''
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const msg = {
|
|
178
|
+
measurement: 'cpu',
|
|
179
|
+
payload: {
|
|
180
|
+
fields: {
|
|
181
|
+
temperature: 21.5,
|
|
182
|
+
count: 5
|
|
183
|
+
},
|
|
184
|
+
tags: { location: 'lab' },
|
|
185
|
+
integers: ['count'],
|
|
186
|
+
timestamp: 1700000000000
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const send = jest.fn();
|
|
191
|
+
const done = jest.fn();
|
|
192
|
+
await writeNode._handlers.input(msg, send, done);
|
|
193
|
+
|
|
194
|
+
const point = influxModule.__getLastPoint();
|
|
195
|
+
const client = influxModule.__getLastClientInstance();
|
|
196
|
+
|
|
197
|
+
expect(point.floatFields.temperature).toBe(21.5);
|
|
198
|
+
expect(point.integerFields.count).toBe(5);
|
|
199
|
+
expect(point.tags.location).toBe('lab');
|
|
200
|
+
|
|
201
|
+
expect(client.write).toHaveBeenCalledWith('lp:cpu', 'metrics');
|
|
202
|
+
expect(send).toHaveBeenCalledWith(msg);
|
|
203
|
+
expect(done).toHaveBeenCalled();
|
|
204
|
+
|
|
205
|
+
if (writeNode._handlers.close) {
|
|
206
|
+
writeNode._handlers.close();
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('writes raw line protocol string with database override', async () => {
|
|
211
|
+
const { RED, influxModule } = setup();
|
|
212
|
+
const ConfigCtor = RED._types['influxdb3-config'];
|
|
213
|
+
const WriteCtor = RED._types['influxdb3-write'];
|
|
214
|
+
|
|
215
|
+
const configNode = new ConfigCtor({
|
|
216
|
+
host: 'https://example.com',
|
|
217
|
+
database: 'metrics',
|
|
218
|
+
name: 'Test',
|
|
219
|
+
credentials: { token: 'token' }
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const writeNode = new WriteCtor({
|
|
223
|
+
influxdb: configNode,
|
|
224
|
+
measurement: '',
|
|
225
|
+
database: ''
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const msg = {
|
|
229
|
+
payload: ' weather,location=lab temperature=18.5 ',
|
|
230
|
+
database: 'override-db'
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const send = jest.fn();
|
|
234
|
+
const done = jest.fn();
|
|
235
|
+
await writeNode._handlers.input(msg, send, done);
|
|
236
|
+
|
|
237
|
+
const client = influxModule.__getLastClientInstance();
|
|
238
|
+
expect(client.write).toHaveBeenCalledWith('weather,location=lab temperature=18.5', 'override-db');
|
|
239
|
+
expect(send).toHaveBeenCalledWith(msg);
|
|
240
|
+
expect(done).toHaveBeenCalled();
|
|
241
|
+
|
|
242
|
+
if (writeNode._handlers.close) {
|
|
243
|
+
writeNode._handlers.close();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Helper to create a write node for addFieldToPoint / buildLineProtocol tests
|
|
249
|
+
function createWriteNode() {
|
|
250
|
+
const { RED, influxModule } = setup();
|
|
251
|
+
const ConfigCtor = RED._types['influxdb3-config'];
|
|
252
|
+
const WriteCtor = RED._types['influxdb3-write'];
|
|
253
|
+
|
|
254
|
+
const configNode = new ConfigCtor({
|
|
255
|
+
host: 'https://example.com',
|
|
256
|
+
database: 'metrics',
|
|
257
|
+
name: 'Test',
|
|
258
|
+
credentials: { token: 'token' }
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const writeNode = new WriteCtor({
|
|
262
|
+
influxdb: configNode,
|
|
263
|
+
measurement: 'test_measurement',
|
|
264
|
+
database: ''
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
return { RED, influxModule, configNode, writeNode };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
describe('addFieldToPoint – field type handling', () => {
|
|
271
|
+
test('float fields by default for numbers', async () => {
|
|
272
|
+
const { influxModule, writeNode } = createWriteNode();
|
|
273
|
+
const msg = {
|
|
274
|
+
measurement: 'sensor',
|
|
275
|
+
payload: { fields: { temperature: 21.5, humidity: 60 } }
|
|
276
|
+
};
|
|
277
|
+
const send = jest.fn();
|
|
278
|
+
const done = jest.fn();
|
|
279
|
+
await writeNode._handlers.input(msg, send, done);
|
|
280
|
+
|
|
281
|
+
const point = influxModule.__getLastPoint();
|
|
282
|
+
expect(point.floatFields.temperature).toBe(21.5);
|
|
283
|
+
expect(point.floatFields.humidity).toBe(60);
|
|
284
|
+
expect(done).toHaveBeenCalled();
|
|
285
|
+
expect(done.mock.calls[0][0]).toBeUndefined();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('integer fields when listed in msg.payload.integers', async () => {
|
|
289
|
+
const { influxModule, writeNode } = createWriteNode();
|
|
290
|
+
const msg = {
|
|
291
|
+
measurement: 'sensor',
|
|
292
|
+
payload: {
|
|
293
|
+
fields: { count: 42, temperature: 21.5 },
|
|
294
|
+
integers: ['count']
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const send = jest.fn();
|
|
298
|
+
const done = jest.fn();
|
|
299
|
+
await writeNode._handlers.input(msg, send, done);
|
|
300
|
+
|
|
301
|
+
const point = influxModule.__getLastPoint();
|
|
302
|
+
expect(point.integerFields.count).toBe(42);
|
|
303
|
+
expect(point.floatFields.temperature).toBe(21.5);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('integer suffix string "42i" is parsed as integer', async () => {
|
|
307
|
+
const { influxModule, writeNode } = createWriteNode();
|
|
308
|
+
const msg = {
|
|
309
|
+
measurement: 'sensor',
|
|
310
|
+
payload: { fields: { count: '42i' } }
|
|
311
|
+
};
|
|
312
|
+
const send = jest.fn();
|
|
313
|
+
const done = jest.fn();
|
|
314
|
+
await writeNode._handlers.input(msg, send, done);
|
|
315
|
+
|
|
316
|
+
const point = influxModule.__getLastPoint();
|
|
317
|
+
expect(point.integerFields.count).toBe(42);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('negative integer suffix string "-7i" is parsed as integer', async () => {
|
|
321
|
+
const { influxModule, writeNode } = createWriteNode();
|
|
322
|
+
const msg = {
|
|
323
|
+
measurement: 'sensor',
|
|
324
|
+
payload: { fields: { offset: '-7i' } }
|
|
325
|
+
};
|
|
326
|
+
const send = jest.fn();
|
|
327
|
+
const done = jest.fn();
|
|
328
|
+
await writeNode._handlers.input(msg, send, done);
|
|
329
|
+
|
|
330
|
+
const point = influxModule.__getLastPoint();
|
|
331
|
+
expect(point.integerFields.offset).toBe(-7);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('regular strings are set as string fields', async () => {
|
|
335
|
+
const { influxModule, writeNode } = createWriteNode();
|
|
336
|
+
const msg = {
|
|
337
|
+
measurement: 'sensor',
|
|
338
|
+
payload: { fields: { status: 'ok' } }
|
|
339
|
+
};
|
|
340
|
+
const send = jest.fn();
|
|
341
|
+
const done = jest.fn();
|
|
342
|
+
await writeNode._handlers.input(msg, send, done);
|
|
343
|
+
|
|
344
|
+
const point = influxModule.__getLastPoint();
|
|
345
|
+
expect(point.stringFields.status).toBe('ok');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('boolean fields are set correctly', async () => {
|
|
349
|
+
const { influxModule, writeNode } = createWriteNode();
|
|
350
|
+
const msg = {
|
|
351
|
+
measurement: 'sensor',
|
|
352
|
+
payload: { fields: { active: true, disabled: false } }
|
|
353
|
+
};
|
|
354
|
+
const send = jest.fn();
|
|
355
|
+
const done = jest.fn();
|
|
356
|
+
await writeNode._handlers.input(msg, send, done);
|
|
357
|
+
|
|
358
|
+
const point = influxModule.__getLastPoint();
|
|
359
|
+
expect(point.booleanFields.active).toBe(true);
|
|
360
|
+
expect(point.booleanFields.disabled).toBe(false);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('non-integer float is truncated with warning when marked as integer', async () => {
|
|
364
|
+
const { influxModule, writeNode } = createWriteNode();
|
|
365
|
+
const msg = {
|
|
366
|
+
measurement: 'sensor',
|
|
367
|
+
payload: {
|
|
368
|
+
fields: { value: 3.7 },
|
|
369
|
+
integers: ['value']
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
const send = jest.fn();
|
|
373
|
+
const done = jest.fn();
|
|
374
|
+
await writeNode._handlers.input(msg, send, done);
|
|
375
|
+
|
|
376
|
+
const point = influxModule.__getLastPoint();
|
|
377
|
+
expect(point.integerFields.value).toBe(3);
|
|
378
|
+
expect(writeNode.warn).toHaveBeenCalledWith(
|
|
379
|
+
expect.stringContaining("marked as integer but value is 3.7")
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe('addFieldToPoint – enhanced error messages (issue #16)', () => {
|
|
385
|
+
test('object field value produces detailed warning with type and value', async () => {
|
|
386
|
+
const { writeNode } = createWriteNode();
|
|
387
|
+
const msg = {
|
|
388
|
+
measurement: 'sensor',
|
|
389
|
+
payload: {
|
|
390
|
+
fields: {
|
|
391
|
+
good: 42,
|
|
392
|
+
nested: { a: 1, b: 2 }
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
const send = jest.fn();
|
|
397
|
+
const done = jest.fn();
|
|
398
|
+
await writeNode._handlers.input(msg, send, done);
|
|
399
|
+
|
|
400
|
+
expect(writeNode.warn).toHaveBeenCalledWith(
|
|
401
|
+
expect.stringContaining("Skipping field 'nested': unsupported type 'object' (Object)")
|
|
402
|
+
);
|
|
403
|
+
expect(writeNode.warn).toHaveBeenCalledWith(
|
|
404
|
+
expect.stringContaining('Actual value: {"a":1,"b":2}')
|
|
405
|
+
);
|
|
406
|
+
expect(writeNode.warn).toHaveBeenCalledWith(
|
|
407
|
+
expect.stringContaining("must be a number, string, or boolean")
|
|
408
|
+
);
|
|
409
|
+
// Should still succeed for the valid field
|
|
410
|
+
expect(send).toHaveBeenCalled();
|
|
411
|
+
expect(done).toHaveBeenCalled();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test('array field value shows Array type name in warning', async () => {
|
|
415
|
+
const { writeNode } = createWriteNode();
|
|
416
|
+
const msg = {
|
|
417
|
+
measurement: 'sensor',
|
|
418
|
+
payload: {
|
|
419
|
+
fields: {
|
|
420
|
+
good: 1,
|
|
421
|
+
values: [1, 2, 3]
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
const send = jest.fn();
|
|
426
|
+
const done = jest.fn();
|
|
427
|
+
await writeNode._handlers.input(msg, send, done);
|
|
428
|
+
|
|
429
|
+
expect(writeNode.warn).toHaveBeenCalledWith(
|
|
430
|
+
expect.stringContaining("unsupported type 'object' (Array)")
|
|
431
|
+
);
|
|
432
|
+
expect(writeNode.warn).toHaveBeenCalledWith(
|
|
433
|
+
expect.stringContaining('Actual value: [1,2,3]')
|
|
434
|
+
);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test('null field value produces clear warning', async () => {
|
|
438
|
+
const { writeNode } = createWriteNode();
|
|
439
|
+
const msg = {
|
|
440
|
+
measurement: 'sensor',
|
|
441
|
+
payload: {
|
|
442
|
+
fields: {
|
|
443
|
+
good: 1,
|
|
444
|
+
broken: null
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
const send = jest.fn();
|
|
449
|
+
const done = jest.fn();
|
|
450
|
+
await writeNode._handlers.input(msg, send, done);
|
|
451
|
+
|
|
452
|
+
expect(writeNode.warn).toHaveBeenCalledWith(
|
|
453
|
+
expect.stringContaining("Skipping field 'broken': value is null")
|
|
454
|
+
);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test('undefined field value produces clear warning', async () => {
|
|
458
|
+
const { writeNode } = createWriteNode();
|
|
459
|
+
const msg = {
|
|
460
|
+
measurement: 'sensor',
|
|
461
|
+
payload: {
|
|
462
|
+
fields: {
|
|
463
|
+
good: 1,
|
|
464
|
+
missing: undefined
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
const send = jest.fn();
|
|
469
|
+
const done = jest.fn();
|
|
470
|
+
await writeNode._handlers.input(msg, send, done);
|
|
471
|
+
|
|
472
|
+
expect(writeNode.warn).toHaveBeenCalledWith(
|
|
473
|
+
expect.stringContaining("Skipping field 'missing': value is undefined")
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test('NaN field value is skipped with warning', async () => {
|
|
478
|
+
const { writeNode } = createWriteNode();
|
|
479
|
+
const msg = {
|
|
480
|
+
measurement: 'sensor',
|
|
481
|
+
payload: {
|
|
482
|
+
fields: {
|
|
483
|
+
good: 1,
|
|
484
|
+
bad: NaN
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
const send = jest.fn();
|
|
489
|
+
const done = jest.fn();
|
|
490
|
+
await writeNode._handlers.input(msg, send, done);
|
|
491
|
+
|
|
492
|
+
expect(writeNode.warn).toHaveBeenCalledWith(
|
|
493
|
+
expect.stringContaining("Skipping field 'bad': numeric value is NaN (not finite)")
|
|
494
|
+
);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test('Infinity field value is skipped with warning', async () => {
|
|
498
|
+
const { writeNode } = createWriteNode();
|
|
499
|
+
const msg = {
|
|
500
|
+
measurement: 'sensor',
|
|
501
|
+
payload: {
|
|
502
|
+
fields: {
|
|
503
|
+
good: 1,
|
|
504
|
+
bad: Infinity
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
const send = jest.fn();
|
|
509
|
+
const done = jest.fn();
|
|
510
|
+
await writeNode._handlers.input(msg, send, done);
|
|
511
|
+
|
|
512
|
+
expect(writeNode.warn).toHaveBeenCalledWith(
|
|
513
|
+
expect.stringContaining("Skipping field 'bad': numeric value is Infinity (not finite)")
|
|
514
|
+
);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test('warning includes measurement name as context', async () => {
|
|
518
|
+
const { writeNode } = createWriteNode();
|
|
519
|
+
const msg = {
|
|
520
|
+
measurement: 'my_sensor',
|
|
521
|
+
payload: {
|
|
522
|
+
fields: {
|
|
523
|
+
good: 1,
|
|
524
|
+
broken: { nested: true }
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
const send = jest.fn();
|
|
529
|
+
const done = jest.fn();
|
|
530
|
+
await writeNode._handlers.input(msg, send, done);
|
|
531
|
+
|
|
532
|
+
expect(writeNode.warn).toHaveBeenCalledWith(
|
|
533
|
+
expect.stringContaining("(measurement: 'my_sensor')")
|
|
534
|
+
);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test('all fields skipped produces error with payload dump', async () => {
|
|
538
|
+
const { writeNode } = createWriteNode();
|
|
539
|
+
const msg = {
|
|
540
|
+
measurement: 'sensor',
|
|
541
|
+
payload: {
|
|
542
|
+
fields: {
|
|
543
|
+
bad1: null,
|
|
544
|
+
bad2: { nested: true }
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
const send = jest.fn();
|
|
549
|
+
const done = jest.fn();
|
|
550
|
+
await writeNode._handlers.input(msg, send, done);
|
|
551
|
+
|
|
552
|
+
// done is called with an error when no valid fields remain
|
|
553
|
+
expect(done).toHaveBeenCalledWith(expect.any(Error));
|
|
554
|
+
expect(done.mock.calls[0][0].message).toContain('No valid fields to write');
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
describe('buildLineProtocol – simplified payload format', () => {
|
|
559
|
+
test('non-reserved keys are used as fields', async () => {
|
|
560
|
+
const { influxModule, writeNode } = createWriteNode();
|
|
561
|
+
const msg = {
|
|
562
|
+
measurement: 'sensor',
|
|
563
|
+
payload: {
|
|
564
|
+
temperature: 21.5,
|
|
565
|
+
humidity: 60,
|
|
566
|
+
tags: { location: 'lab' }
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
const send = jest.fn();
|
|
570
|
+
const done = jest.fn();
|
|
571
|
+
await writeNode._handlers.input(msg, send, done);
|
|
572
|
+
|
|
573
|
+
const point = influxModule.__getLastPoint();
|
|
574
|
+
expect(point.floatFields.temperature).toBe(21.5);
|
|
575
|
+
expect(point.floatFields.humidity).toBe(60);
|
|
576
|
+
expect(point.tags.location).toBe('lab');
|
|
577
|
+
// Reserved keys should NOT appear as fields
|
|
578
|
+
expect(point.floatFields.tags).toBeUndefined();
|
|
579
|
+
expect(point.stringFields.tags).toBeUndefined();
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test('reserved keys (tags, timestamp, integers, fields) are excluded from fields', async () => {
|
|
583
|
+
const { influxModule, writeNode } = createWriteNode();
|
|
584
|
+
const msg = {
|
|
585
|
+
measurement: 'sensor',
|
|
586
|
+
payload: {
|
|
587
|
+
value: 42,
|
|
588
|
+
tags: { location: 'lab' },
|
|
589
|
+
timestamp: 1700000000000,
|
|
590
|
+
integers: ['value']
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
const send = jest.fn();
|
|
594
|
+
const done = jest.fn();
|
|
595
|
+
await writeNode._handlers.input(msg, send, done);
|
|
596
|
+
|
|
597
|
+
const point = influxModule.__getLastPoint();
|
|
598
|
+
expect(point.integerFields.value).toBe(42);
|
|
599
|
+
// None of the reserved keys should appear as field entries
|
|
600
|
+
expect(point.floatFields.tags).toBeUndefined();
|
|
601
|
+
expect(point.floatFields.timestamp).toBeUndefined();
|
|
602
|
+
expect(point.floatFields.integers).toBeUndefined();
|
|
603
|
+
expect(point.floatFields.fields).toBeUndefined();
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
describe('buildLineProtocol – timestamp handling', () => {
|
|
608
|
+
test('numeric timestamp from msg.payload.timestamp', async () => {
|
|
609
|
+
const { influxModule, writeNode } = createWriteNode();
|
|
610
|
+
const msg = {
|
|
611
|
+
measurement: 'sensor',
|
|
612
|
+
payload: {
|
|
613
|
+
fields: { value: 1 },
|
|
614
|
+
timestamp: 1700000000000
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
const send = jest.fn();
|
|
618
|
+
const done = jest.fn();
|
|
619
|
+
await writeNode._handlers.input(msg, send, done);
|
|
620
|
+
|
|
621
|
+
const point = influxModule.__getLastPoint();
|
|
622
|
+
expect(point.timestamp).toEqual(new Date(1700000000000));
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test('fallback to msg.timestamp when payload.timestamp is absent', async () => {
|
|
626
|
+
const { influxModule, writeNode } = createWriteNode();
|
|
627
|
+
const msg = {
|
|
628
|
+
measurement: 'sensor',
|
|
629
|
+
timestamp: 1700000000000,
|
|
630
|
+
payload: {
|
|
631
|
+
fields: { value: 1 }
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
const send = jest.fn();
|
|
635
|
+
const done = jest.fn();
|
|
636
|
+
await writeNode._handlers.input(msg, send, done);
|
|
637
|
+
|
|
638
|
+
const point = influxModule.__getLastPoint();
|
|
639
|
+
expect(point.timestamp).toEqual(new Date(1700000000000));
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
test('Date object timestamp is used directly', async () => {
|
|
643
|
+
const { influxModule, writeNode } = createWriteNode();
|
|
644
|
+
const date = new Date('2025-01-01T00:00:00Z');
|
|
645
|
+
const msg = {
|
|
646
|
+
measurement: 'sensor',
|
|
647
|
+
payload: {
|
|
648
|
+
fields: { value: 1 },
|
|
649
|
+
timestamp: date
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
const send = jest.fn();
|
|
653
|
+
const done = jest.fn();
|
|
654
|
+
await writeNode._handlers.input(msg, send, done);
|
|
655
|
+
|
|
656
|
+
const point = influxModule.__getLastPoint();
|
|
657
|
+
expect(point.timestamp).toEqual(date);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test('invalid timestamp string produces warning', async () => {
|
|
661
|
+
const { writeNode } = createWriteNode();
|
|
662
|
+
const msg = {
|
|
663
|
+
measurement: 'sensor',
|
|
664
|
+
payload: {
|
|
665
|
+
fields: { value: 1 },
|
|
666
|
+
timestamp: 'not-a-date'
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
const send = jest.fn();
|
|
670
|
+
const done = jest.fn();
|
|
671
|
+
await writeNode._handlers.input(msg, send, done);
|
|
672
|
+
|
|
673
|
+
expect(writeNode.warn).toHaveBeenCalledWith(
|
|
674
|
+
expect.stringContaining("Invalid timestamp string: 'not-a-date'")
|
|
675
|
+
);
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
describe('buildLineProtocol – error cases', () => {
|
|
680
|
+
test('missing measurement produces error', async () => {
|
|
681
|
+
const { RED } = setup();
|
|
682
|
+
const ConfigCtor = RED._types['influxdb3-config'];
|
|
683
|
+
const WriteCtor = RED._types['influxdb3-write'];
|
|
684
|
+
|
|
685
|
+
const configNode = new ConfigCtor({
|
|
686
|
+
host: 'https://example.com',
|
|
687
|
+
database: 'metrics',
|
|
688
|
+
name: 'Test',
|
|
689
|
+
credentials: { token: 'token' }
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// No measurement on node
|
|
693
|
+
const writeNode = new WriteCtor({
|
|
694
|
+
influxdb: configNode,
|
|
695
|
+
measurement: '',
|
|
696
|
+
database: ''
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
const msg = {
|
|
700
|
+
// No measurement on msg either
|
|
701
|
+
payload: { fields: { value: 1 } }
|
|
702
|
+
};
|
|
703
|
+
const send = jest.fn();
|
|
704
|
+
const done = jest.fn();
|
|
705
|
+
await writeNode._handlers.input(msg, send, done);
|
|
706
|
+
|
|
707
|
+
expect(done).toHaveBeenCalledWith(expect.any(Error));
|
|
708
|
+
expect(done.mock.calls[0][0].message).toContain('Measurement not specified');
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
test('empty line protocol string produces error', async () => {
|
|
712
|
+
const { RED } = setup();
|
|
713
|
+
const ConfigCtor = RED._types['influxdb3-config'];
|
|
714
|
+
const WriteCtor = RED._types['influxdb3-write'];
|
|
715
|
+
|
|
716
|
+
const configNode = new ConfigCtor({
|
|
717
|
+
host: 'https://example.com',
|
|
718
|
+
database: 'metrics',
|
|
719
|
+
name: 'Test',
|
|
720
|
+
credentials: { token: 'token' }
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
const writeNode = new WriteCtor({
|
|
724
|
+
influxdb: configNode,
|
|
725
|
+
measurement: '',
|
|
726
|
+
database: ''
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
const msg = {
|
|
730
|
+
payload: ' '
|
|
731
|
+
};
|
|
732
|
+
const send = jest.fn();
|
|
733
|
+
const done = jest.fn();
|
|
734
|
+
await writeNode._handlers.input(msg, send, done);
|
|
735
|
+
|
|
736
|
+
expect(done).toHaveBeenCalledWith(expect.any(Error));
|
|
737
|
+
expect(done.mock.calls[0][0].message).toContain('Line protocol string is empty');
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
test('invalid payload type (array) produces error', async () => {
|
|
741
|
+
const { writeNode } = createWriteNode();
|
|
742
|
+
const msg = {
|
|
743
|
+
payload: [1, 2, 3]
|
|
744
|
+
};
|
|
745
|
+
const send = jest.fn();
|
|
746
|
+
const done = jest.fn();
|
|
747
|
+
await writeNode._handlers.input(msg, send, done);
|
|
748
|
+
|
|
749
|
+
expect(done).toHaveBeenCalledWith(expect.any(Error));
|
|
750
|
+
expect(done.mock.calls[0][0].message).toContain('Invalid payload format');
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
describe('InfluxDB v3 config node – credentials warning', () => {
|
|
755
|
+
test('logs warning when credentials object is undefined', () => {
|
|
756
|
+
const RED = buildRED();
|
|
757
|
+
|
|
758
|
+
// Override createNode to NOT set credentials
|
|
759
|
+
RED.nodes.createNode = function(node, _config) {
|
|
760
|
+
node.status = jest.fn();
|
|
761
|
+
node.error = jest.fn();
|
|
762
|
+
node.warn = jest.fn();
|
|
763
|
+
node.send = jest.fn();
|
|
764
|
+
node.on = jest.fn((event, handler) => {
|
|
765
|
+
node._handlers = node._handlers || {};
|
|
766
|
+
node._handlers[event] = handler;
|
|
767
|
+
});
|
|
768
|
+
// Deliberately NOT setting node.credentials
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
require('../influxdb3.js')(RED);
|
|
772
|
+
const ConfigCtor = RED._types['influxdb3-config'];
|
|
773
|
+
|
|
774
|
+
const configNode = new ConfigCtor({
|
|
775
|
+
host: 'https://example.com',
|
|
776
|
+
database: 'metrics',
|
|
777
|
+
name: 'Test'
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
expect(RED.log.warn).toHaveBeenCalledWith(
|
|
781
|
+
'InfluxDB v3 config: credentials object is undefined'
|
|
782
|
+
);
|
|
783
|
+
expect(configNode.token).toBeUndefined();
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
|