@yarkivaev/source-to-sink 1.1.0 → 1.2.2
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/.github/workflows/ci.yml +1 -1
- package/eslint.config.js +176 -0
- package/index.js +1 -0
- package/package.json +11 -5
- package/src/batch.js +2 -2
- package/src/lokiSource.js +3 -3
- package/src/modbusSource.js +52 -9
- package/src/mqttSource.js +5 -5
- package/src/pollingSource.js +4 -3
- package/src/postgresSink.js +7 -7
- package/src/sqliteSink.js +55 -0
- package/src/stompSource.js +20 -11
- package/src/timedBatch.js +4 -4
- package/test/integration_clickhouseSink.js +5 -5
- package/test/integration_lokiSource.js +13 -13
- package/test/integration_mqttSink.js +12 -12
- package/test/integration_mqttSource.js +19 -19
- package/test/test_batch.js +21 -35
- package/test/test_circuit.js +12 -12
- package/test/test_clickhouseSink.js +8 -8
- package/test/test_lokiSource.js +12 -12
- package/test/test_modbusSource.js +37 -14
- package/test/test_mqttSink.js +6 -6
- package/test/test_mqttSource.js +8 -8
- package/test/test_pollingSource.js +23 -23
- package/test/test_postgresSink.js +12 -12
- package/test/test_sqliteSink.js +176 -0
- package/test/test_stompSource.js +8 -8
- package/test/test_timedBatch.js +13 -13
|
@@ -8,8 +8,8 @@ describe('modbusSource', () => {
|
|
|
8
8
|
const collector = { accept: () => {} };
|
|
9
9
|
const clk = fakeClock(Math.floor(Math.random() * 10000));
|
|
10
10
|
assert.throws(
|
|
11
|
-
() => modbusSource('', 502, 4000, 14, 5, collector, clk),
|
|
12
|
-
/Host must be a non-empty string
|
|
11
|
+
() => {return modbusSource('', 502, 4000, 14, 5, collector, clk)},
|
|
12
|
+
/Host must be a non-empty string/u,
|
|
13
13
|
'Should reject empty host'
|
|
14
14
|
);
|
|
15
15
|
});
|
|
@@ -18,8 +18,8 @@ describe('modbusSource', () => {
|
|
|
18
18
|
const collector = { accept: () => {} };
|
|
19
19
|
const clk = fakeClock(Math.floor(Math.random() * 10000));
|
|
20
20
|
assert.throws(
|
|
21
|
-
() => modbusSource(null, 502, 4000, 14, 5, collector, clk),
|
|
22
|
-
/Host must be a non-empty string
|
|
21
|
+
() => {return modbusSource(null, 502, 4000, 14, 5, collector, clk)},
|
|
22
|
+
/Host must be a non-empty string/u,
|
|
23
23
|
'Should reject null host'
|
|
24
24
|
);
|
|
25
25
|
});
|
|
@@ -28,8 +28,8 @@ describe('modbusSource', () => {
|
|
|
28
28
|
const collector = { accept: () => {} };
|
|
29
29
|
const clk = fakeClock(Math.floor(Math.random() * 10000));
|
|
30
30
|
assert.throws(
|
|
31
|
-
() => modbusSource('192.168.2.148', -1, 4000, 14, 5, collector, clk),
|
|
32
|
-
/Port must be a positive number
|
|
31
|
+
() => {return modbusSource('192.168.2.148', -1, 4000, 14, 5, collector, clk)},
|
|
32
|
+
/Port must be a positive number/u,
|
|
33
33
|
'Should reject negative port'
|
|
34
34
|
);
|
|
35
35
|
});
|
|
@@ -38,8 +38,8 @@ describe('modbusSource', () => {
|
|
|
38
38
|
const collector = { accept: () => {} };
|
|
39
39
|
const clk = fakeClock(Math.floor(Math.random() * 10000));
|
|
40
40
|
assert.throws(
|
|
41
|
-
() => modbusSource('192.168.2.148', 502, -1, 14, 5, collector, clk),
|
|
42
|
-
/Address must be a non-negative number
|
|
41
|
+
() => {return modbusSource('192.168.2.148', 502, -1, 14, 5, collector, clk)},
|
|
42
|
+
/Address must be a non-negative number/u,
|
|
43
43
|
'Should reject negative address'
|
|
44
44
|
);
|
|
45
45
|
});
|
|
@@ -48,8 +48,8 @@ describe('modbusSource', () => {
|
|
|
48
48
|
const collector = { accept: () => {} };
|
|
49
49
|
const clk = fakeClock(Math.floor(Math.random() * 10000));
|
|
50
50
|
assert.throws(
|
|
51
|
-
() => modbusSource('192.168.2.148', 502, 4000, 0, 5, collector, clk),
|
|
52
|
-
/Count must be a positive number
|
|
51
|
+
() => {return modbusSource('192.168.2.148', 502, 4000, 0, 5, collector, clk)},
|
|
52
|
+
/Count must be a positive number/u,
|
|
53
53
|
'Should reject zero count'
|
|
54
54
|
);
|
|
55
55
|
});
|
|
@@ -57,8 +57,8 @@ describe('modbusSource', () => {
|
|
|
57
57
|
it('throws on missing collector', () => {
|
|
58
58
|
const clk = fakeClock(Math.floor(Math.random() * 10000));
|
|
59
59
|
assert.throws(
|
|
60
|
-
() => modbusSource('192.168.2.148', 502, 4000, 14, 5, null, clk),
|
|
61
|
-
/Collector must have an accept\(\) method
|
|
60
|
+
() => {return modbusSource('192.168.2.148', 502, 4000, 14, 5, null, clk)},
|
|
61
|
+
/Collector must have an accept\(\) method/u,
|
|
62
62
|
'Should reject null collector'
|
|
63
63
|
);
|
|
64
64
|
});
|
|
@@ -66,8 +66,8 @@ describe('modbusSource', () => {
|
|
|
66
66
|
it('throws on missing clock', () => {
|
|
67
67
|
const collector = { accept: () => {} };
|
|
68
68
|
assert.throws(
|
|
69
|
-
() => modbusSource('192.168.2.148', 502, 4000, 14, 5, collector, null),
|
|
70
|
-
/Clock must have a millis\(\) method
|
|
69
|
+
() => {return modbusSource('192.168.2.148', 502, 4000, 14, 5, collector, null)},
|
|
70
|
+
/Clock must have a millis\(\) method/u,
|
|
71
71
|
'Should reject null clock'
|
|
72
72
|
);
|
|
73
73
|
});
|
|
@@ -93,4 +93,27 @@ describe('modbusSource', () => {
|
|
|
93
93
|
source.stop();
|
|
94
94
|
assert.strictEqual(true, true, 'Should not throw when stopping without start');
|
|
95
95
|
});
|
|
96
|
+
|
|
97
|
+
it('retries connection on failure', async () => {
|
|
98
|
+
let logged = false;
|
|
99
|
+
const log = { error: () => { logged = true; } };
|
|
100
|
+
const collector = { accept: () => {} };
|
|
101
|
+
const clk = fakeClock(Math.floor(Math.random() * 10000));
|
|
102
|
+
const source = modbusSource(`\u00df${Math.random()}`, 502, 4000, 14, 5, collector, clk, log);
|
|
103
|
+
source.start();
|
|
104
|
+
await new Promise(resolve => { setTimeout(resolve, 200); });
|
|
105
|
+
source.stop();
|
|
106
|
+
assert.strictEqual(logged, true, 'Should log connection failure and retry');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('does not crash process on connection failure', async () => {
|
|
110
|
+
const log = { error: () => {} };
|
|
111
|
+
const collector = { accept: () => {} };
|
|
112
|
+
const clk = fakeClock(Math.floor(Math.random() * 10000));
|
|
113
|
+
const source = modbusSource(`\u0420${Math.random()}`, 502, 4000, 14, 5, collector, clk, log);
|
|
114
|
+
source.start();
|
|
115
|
+
await new Promise(resolve => { setTimeout(resolve, 200); });
|
|
116
|
+
source.stop();
|
|
117
|
+
assert.strictEqual(true, true, 'Should not crash on unreachable host');
|
|
118
|
+
});
|
|
96
119
|
});
|
package/test/test_mqttSink.js
CHANGED
|
@@ -5,16 +5,16 @@ import mqttSink from '../src/mqttSink.js';
|
|
|
5
5
|
describe('mqttSink', () => {
|
|
6
6
|
it('throws on empty url', () => {
|
|
7
7
|
assert.throws(
|
|
8
|
-
() => mqttSink(''),
|
|
9
|
-
/URL must be a non-empty string
|
|
8
|
+
() => {return mqttSink('')},
|
|
9
|
+
/URL must be a non-empty string/u,
|
|
10
10
|
'Should reject empty URL'
|
|
11
11
|
);
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
it('throws on missing url', () => {
|
|
15
15
|
assert.throws(
|
|
16
|
-
() => mqttSink(null),
|
|
17
|
-
/URL must be a non-empty string
|
|
16
|
+
() => {return mqttSink(null)},
|
|
17
|
+
/URL must be a non-empty string/u,
|
|
18
18
|
'Should reject null URL'
|
|
19
19
|
);
|
|
20
20
|
});
|
|
@@ -37,8 +37,8 @@ describe('mqttSink', () => {
|
|
|
37
37
|
it('throws on write when not connected', () => {
|
|
38
38
|
const sink = mqttSink(`mqtt://\u00df${Math.random()}`);
|
|
39
39
|
assert.throws(
|
|
40
|
-
() => sink.write([{ topic: `\u00e9${Math.random()}`, payload: `${Math.random()}` }]),
|
|
41
|
-
/Sink is not connected
|
|
40
|
+
() => {return sink.write([{ topic: `\u00e9${Math.random()}`, payload: `${Math.random()}` }])},
|
|
41
|
+
/Sink is not connected/u,
|
|
42
42
|
'Should reject write when not connected'
|
|
43
43
|
);
|
|
44
44
|
});
|
package/test/test_mqttSource.js
CHANGED
|
@@ -6,8 +6,8 @@ describe('mqttSource', () => {
|
|
|
6
6
|
it('throws on missing url', () => {
|
|
7
7
|
const collector = { accept: () => {} };
|
|
8
8
|
assert.throws(
|
|
9
|
-
() => mqttSource(null, 'test/topic', collector),
|
|
10
|
-
/URL must be a non-empty string
|
|
9
|
+
() => {return mqttSource(null, 'test/topic', collector)},
|
|
10
|
+
/URL must be a non-empty string/u,
|
|
11
11
|
'Should reject missing url'
|
|
12
12
|
);
|
|
13
13
|
});
|
|
@@ -15,8 +15,8 @@ describe('mqttSource', () => {
|
|
|
15
15
|
it('throws on empty url', () => {
|
|
16
16
|
const collector = { accept: () => {} };
|
|
17
17
|
assert.throws(
|
|
18
|
-
() => mqttSource('', 'test/topic', collector),
|
|
19
|
-
/URL must be a non-empty string
|
|
18
|
+
() => {return mqttSource('', 'test/topic', collector)},
|
|
19
|
+
/URL must be a non-empty string/u,
|
|
20
20
|
'Should reject empty url'
|
|
21
21
|
);
|
|
22
22
|
});
|
|
@@ -24,16 +24,16 @@ describe('mqttSource', () => {
|
|
|
24
24
|
it('throws on empty topic', () => {
|
|
25
25
|
const collector = { accept: () => {} };
|
|
26
26
|
assert.throws(
|
|
27
|
-
() => mqttSource('mqtt://localhost:1883', '', collector),
|
|
28
|
-
/Topic must be a non-empty string
|
|
27
|
+
() => {return mqttSource('mqtt://localhost:1883', '', collector)},
|
|
28
|
+
/Topic must be a non-empty string/u,
|
|
29
29
|
'Should reject empty topic'
|
|
30
30
|
);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
it('throws on missing collector', () => {
|
|
34
34
|
assert.throws(
|
|
35
|
-
() => mqttSource('mqtt://localhost:1883', 'test', null),
|
|
36
|
-
/Collector must have an accept\(\) method
|
|
35
|
+
() => {return mqttSource('mqtt://localhost:1883', 'test', null)},
|
|
36
|
+
/Collector must have an accept\(\) method/u,
|
|
37
37
|
'Should reject missing collector'
|
|
38
38
|
);
|
|
39
39
|
});
|
|
@@ -6,7 +6,7 @@ import fakeClock from './fakeClock.js';
|
|
|
6
6
|
describe('pollingSource', () => {
|
|
7
7
|
it('polls fetch after interval', async () => {
|
|
8
8
|
let polled = false;
|
|
9
|
-
const fetch =
|
|
9
|
+
const fetch = () => {
|
|
10
10
|
polled = true;
|
|
11
11
|
return [];
|
|
12
12
|
};
|
|
@@ -14,7 +14,7 @@ describe('pollingSource', () => {
|
|
|
14
14
|
const clk = fakeClock(Math.floor(Math.random() * 10000));
|
|
15
15
|
const source = pollingSource(fetch, 0.05, collector, clk);
|
|
16
16
|
source.start();
|
|
17
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
17
|
+
await new Promise(resolve => { setTimeout(resolve, 100); });
|
|
18
18
|
source.stop();
|
|
19
19
|
assert.strictEqual(polled, true, 'Should poll fetch after interval');
|
|
20
20
|
});
|
|
@@ -25,19 +25,19 @@ describe('pollingSource', () => {
|
|
|
25
25
|
{ ts: Math.random(), line: `\u00e9${Math.random()}` },
|
|
26
26
|
{ ts: Math.random(), line: `\u4e2d${Math.random()}` }
|
|
27
27
|
];
|
|
28
|
-
const fetch =
|
|
29
|
-
const collector = { accept: (entry) => received.push(entry) };
|
|
28
|
+
const fetch = () => {return entries};
|
|
29
|
+
const collector = { accept: (entry) => {return received.push(entry)} };
|
|
30
30
|
const clk = fakeClock(Math.floor(Math.random() * 10000));
|
|
31
31
|
const source = pollingSource(fetch, 0.05, collector, clk);
|
|
32
32
|
source.start();
|
|
33
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
33
|
+
await new Promise(resolve => { setTimeout(resolve, 100); });
|
|
34
34
|
source.stop();
|
|
35
35
|
assert.strictEqual(received.length, entries.length, 'Should forward all entries to collector');
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
it('passes time window to fetch', async () => {
|
|
39
39
|
let captured = null;
|
|
40
|
-
const fetch =
|
|
40
|
+
const fetch = (since, until) => {
|
|
41
41
|
captured = { since, until };
|
|
42
42
|
return [];
|
|
43
43
|
};
|
|
@@ -45,14 +45,14 @@ describe('pollingSource', () => {
|
|
|
45
45
|
const clk = fakeClock(1000);
|
|
46
46
|
const source = pollingSource(fetch, 0.05, collector, clk);
|
|
47
47
|
source.start();
|
|
48
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
48
|
+
await new Promise(resolve => { setTimeout(resolve, 100); });
|
|
49
49
|
source.stop();
|
|
50
50
|
assert.strictEqual(captured !== null, true, 'Should pass time window to fetch');
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
it('ignores duplicate start calls', () => {
|
|
54
54
|
let count = 0;
|
|
55
|
-
const fetch =
|
|
55
|
+
const fetch = () => {
|
|
56
56
|
count += 1;
|
|
57
57
|
return [];
|
|
58
58
|
};
|
|
@@ -66,7 +66,7 @@ describe('pollingSource', () => {
|
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
it('ignores stop when not started', () => {
|
|
69
|
-
const fetch =
|
|
69
|
+
const fetch = () => {return []};
|
|
70
70
|
const collector = { accept: () => {} };
|
|
71
71
|
const clk = fakeClock(0);
|
|
72
72
|
const source = pollingSource(fetch, 60, collector, clk);
|
|
@@ -76,7 +76,7 @@ describe('pollingSource', () => {
|
|
|
76
76
|
|
|
77
77
|
it('stops polling on stop', async () => {
|
|
78
78
|
let count = 0;
|
|
79
|
-
const fetch =
|
|
79
|
+
const fetch = () => {
|
|
80
80
|
count += 1;
|
|
81
81
|
return [];
|
|
82
82
|
};
|
|
@@ -84,10 +84,10 @@ describe('pollingSource', () => {
|
|
|
84
84
|
const clk = fakeClock(0);
|
|
85
85
|
const source = pollingSource(fetch, 0.03, collector, clk);
|
|
86
86
|
source.start();
|
|
87
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
87
|
+
await new Promise(resolve => { setTimeout(resolve, 50); });
|
|
88
88
|
source.stop();
|
|
89
89
|
const before = count;
|
|
90
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
90
|
+
await new Promise(resolve => { setTimeout(resolve, 100); });
|
|
91
91
|
assert.strictEqual(count, before, 'Should stop polling after stop');
|
|
92
92
|
});
|
|
93
93
|
|
|
@@ -95,39 +95,39 @@ describe('pollingSource', () => {
|
|
|
95
95
|
const collector = { accept: () => {} };
|
|
96
96
|
const clk = fakeClock(0);
|
|
97
97
|
assert.throws(
|
|
98
|
-
() => pollingSource(null, 10, collector, clk),
|
|
99
|
-
/Fetch must be a function
|
|
98
|
+
() => {return pollingSource(null, 10, collector, clk)},
|
|
99
|
+
/Fetch must be a function/u,
|
|
100
100
|
'Should reject missing fetch'
|
|
101
101
|
);
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
it('throws on invalid interval', () => {
|
|
105
|
-
const fetch =
|
|
105
|
+
const fetch = () => {return []};
|
|
106
106
|
const collector = { accept: () => {} };
|
|
107
107
|
const clk = fakeClock(0);
|
|
108
108
|
assert.throws(
|
|
109
|
-
() => pollingSource(fetch, -1, collector, clk),
|
|
110
|
-
/Interval must be a positive number
|
|
109
|
+
() => {return pollingSource(fetch, -1, collector, clk)},
|
|
110
|
+
/Interval must be a positive number/u,
|
|
111
111
|
'Should reject invalid interval'
|
|
112
112
|
);
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
it('throws on missing collector', () => {
|
|
116
|
-
const fetch =
|
|
116
|
+
const fetch = () => {return []};
|
|
117
117
|
const clk = fakeClock(0);
|
|
118
118
|
assert.throws(
|
|
119
|
-
() => pollingSource(fetch, 10, null, clk),
|
|
120
|
-
/Collector must have an accept\(\) method
|
|
119
|
+
() => {return pollingSource(fetch, 10, null, clk)},
|
|
120
|
+
/Collector must have an accept\(\) method/u,
|
|
121
121
|
'Should reject missing collector'
|
|
122
122
|
);
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
it('throws on missing clock', () => {
|
|
126
|
-
const fetch =
|
|
126
|
+
const fetch = () => {return []};
|
|
127
127
|
const collector = { accept: () => {} };
|
|
128
128
|
assert.throws(
|
|
129
|
-
() => pollingSource(fetch, 10, collector, null),
|
|
130
|
-
/Clock must have a millis\(\) method
|
|
129
|
+
() => {return pollingSource(fetch, 10, collector, null)},
|
|
130
|
+
/Clock must have a millis\(\) method/u,
|
|
131
131
|
'Should reject missing clock'
|
|
132
132
|
);
|
|
133
133
|
});
|
|
@@ -5,48 +5,48 @@ import postgresSink, { deduplicate } from '../src/postgresSink.js';
|
|
|
5
5
|
describe('postgresSink', () => {
|
|
6
6
|
it('throws on missing url', () => {
|
|
7
7
|
assert.throws(
|
|
8
|
-
() => postgresSink(null, 'metrics', ['ts', 'value']),
|
|
9
|
-
/URL must be a non-empty string
|
|
8
|
+
() => {return postgresSink(null, 'metrics', ['ts', 'value'])},
|
|
9
|
+
/URL must be a non-empty string/u,
|
|
10
10
|
'Should reject missing url'
|
|
11
11
|
);
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
it('throws on empty url', () => {
|
|
15
15
|
assert.throws(
|
|
16
|
-
() => postgresSink('', 'metrics', ['ts', 'value']),
|
|
17
|
-
/URL must be a non-empty string
|
|
16
|
+
() => {return postgresSink('', 'metrics', ['ts', 'value'])},
|
|
17
|
+
/URL must be a non-empty string/u,
|
|
18
18
|
'Should reject empty url'
|
|
19
19
|
);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
it('throws on missing table', () => {
|
|
23
23
|
assert.throws(
|
|
24
|
-
() => postgresSink('postgresql://localhost:5432/db', null, ['ts', 'value']),
|
|
25
|
-
/Table must be a non-empty string
|
|
24
|
+
() => {return postgresSink('postgresql://localhost:5432/db', null, ['ts', 'value'])},
|
|
25
|
+
/Table must be a non-empty string/u,
|
|
26
26
|
'Should reject missing table'
|
|
27
27
|
);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
it('throws on empty table', () => {
|
|
31
31
|
assert.throws(
|
|
32
|
-
() => postgresSink('postgresql://localhost:5432/db', '', ['ts', 'value']),
|
|
33
|
-
/Table must be a non-empty string
|
|
32
|
+
() => {return postgresSink('postgresql://localhost:5432/db', '', ['ts', 'value'])},
|
|
33
|
+
/Table must be a non-empty string/u,
|
|
34
34
|
'Should reject empty table'
|
|
35
35
|
);
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
it('throws on missing columns', () => {
|
|
39
39
|
assert.throws(
|
|
40
|
-
() => postgresSink('postgresql://localhost:5432/db', 'metrics', null),
|
|
41
|
-
/Columns must be a non-empty array
|
|
40
|
+
() => {return postgresSink('postgresql://localhost:5432/db', 'metrics', null)},
|
|
41
|
+
/Columns must be a non-empty array/u,
|
|
42
42
|
'Should reject missing columns'
|
|
43
43
|
);
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
it('throws on empty columns', () => {
|
|
47
47
|
assert.throws(
|
|
48
|
-
() => postgresSink('postgresql://localhost:5432/db', 'metrics', []),
|
|
49
|
-
/Columns must be a non-empty array
|
|
48
|
+
() => {return postgresSink('postgresql://localhost:5432/db', 'metrics', [])},
|
|
49
|
+
/Columns must be a non-empty array/u,
|
|
50
50
|
'Should reject empty columns'
|
|
51
51
|
);
|
|
52
52
|
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'mocha';
|
|
3
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import Database from 'better-sqlite3';
|
|
7
|
+
import sqliteSink from '../src/sqliteSink.js';
|
|
8
|
+
|
|
9
|
+
describe('sqliteSink', () => {
|
|
10
|
+
it('throws on missing path', () => {
|
|
11
|
+
assert.throws(
|
|
12
|
+
() => {return sqliteSink(null, 'metrics', ['ts', 'value'])},
|
|
13
|
+
/Path must be a non-empty string/u,
|
|
14
|
+
'Should reject missing path'
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('throws on empty path', () => {
|
|
19
|
+
assert.throws(
|
|
20
|
+
() => {return sqliteSink('', 'metrics', ['ts', 'value'])},
|
|
21
|
+
/Path must be a non-empty string/u,
|
|
22
|
+
'Should reject empty path'
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('throws on missing table', () => {
|
|
27
|
+
const dir = mkdtempSync(join(tmpdir(), 'sink-'));
|
|
28
|
+
try {
|
|
29
|
+
assert.throws(
|
|
30
|
+
() => {return sqliteSink(join(dir, 'db.sqlite'), null, ['ts', 'value'])},
|
|
31
|
+
/Table must be a non-empty string/u,
|
|
32
|
+
'Should reject missing table'
|
|
33
|
+
);
|
|
34
|
+
} finally {
|
|
35
|
+
rmSync(dir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('throws on empty table', () => {
|
|
40
|
+
const dir = mkdtempSync(join(tmpdir(), 'sink-'));
|
|
41
|
+
try {
|
|
42
|
+
assert.throws(
|
|
43
|
+
() => {return sqliteSink(join(dir, 'db.sqlite'), '', ['ts', 'value'])},
|
|
44
|
+
/Table must be a non-empty string/u,
|
|
45
|
+
'Should reject empty table'
|
|
46
|
+
);
|
|
47
|
+
} finally {
|
|
48
|
+
rmSync(dir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('throws on missing columns', () => {
|
|
53
|
+
const dir = mkdtempSync(join(tmpdir(), 'sink-'));
|
|
54
|
+
try {
|
|
55
|
+
assert.throws(
|
|
56
|
+
() => {return sqliteSink(join(dir, 'db.sqlite'), 'metrics', null)},
|
|
57
|
+
/Columns must be a non-empty array/u,
|
|
58
|
+
'Should reject missing columns'
|
|
59
|
+
);
|
|
60
|
+
} finally {
|
|
61
|
+
rmSync(dir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('throws on empty columns', () => {
|
|
66
|
+
const dir = mkdtempSync(join(tmpdir(), 'sink-'));
|
|
67
|
+
try {
|
|
68
|
+
assert.throws(
|
|
69
|
+
() => {return sqliteSink(join(dir, 'db.sqlite'), 'metrics', [])},
|
|
70
|
+
/Columns must be a non-empty array/u,
|
|
71
|
+
'Should reject empty columns'
|
|
72
|
+
);
|
|
73
|
+
} finally {
|
|
74
|
+
rmSync(dir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('returns sink with write method', () => {
|
|
79
|
+
const dir = mkdtempSync(join(tmpdir(), 'sink-'));
|
|
80
|
+
const path = join(dir, 'db.sqlite');
|
|
81
|
+
const db = new Database(path);
|
|
82
|
+
db.exec('CREATE TABLE metrics (topic TEXT, ts REAL, value REAL)');
|
|
83
|
+
db.close();
|
|
84
|
+
try {
|
|
85
|
+
const sink = sqliteSink(path, 'metrics', ['topic', 'ts', 'value']);
|
|
86
|
+
assert.strictEqual(typeof sink.write, 'function', 'Should have write method');
|
|
87
|
+
sink.close();
|
|
88
|
+
} finally {
|
|
89
|
+
rmSync(dir, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('inserts records into the database', () => {
|
|
94
|
+
const dir = mkdtempSync(join(tmpdir(), 'sink-'));
|
|
95
|
+
const path = join(dir, `db-${Math.random()}.sqlite`);
|
|
96
|
+
const db = new Database(path);
|
|
97
|
+
db.exec('CREATE TABLE metrics (topic TEXT, ts REAL, value REAL)');
|
|
98
|
+
db.close();
|
|
99
|
+
const topic = `sénsör/${Math.random()}`;
|
|
100
|
+
const ts = Date.now();
|
|
101
|
+
const value = Math.random() * 1000;
|
|
102
|
+
try {
|
|
103
|
+
const sink = sqliteSink(path, 'metrics', ['topic', 'ts', 'value']);
|
|
104
|
+
sink.write([{ topic, ts, value }]);
|
|
105
|
+
sink.close();
|
|
106
|
+
const reader = new Database(path);
|
|
107
|
+
const rows = reader.prepare('SELECT topic, ts, value FROM metrics').all();
|
|
108
|
+
reader.close();
|
|
109
|
+
assert.strictEqual(rows.length, 1, 'Should insert exactly one record');
|
|
110
|
+
} finally {
|
|
111
|
+
rmSync(dir, { recursive: true });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('inserts batch of records atomically', () => {
|
|
116
|
+
const dir = mkdtempSync(join(tmpdir(), 'sink-'));
|
|
117
|
+
const path = join(dir, `db-${Math.random()}.sqlite`);
|
|
118
|
+
const db = new Database(path);
|
|
119
|
+
db.exec('CREATE TABLE metrics (topic TEXT, ts REAL, value REAL)');
|
|
120
|
+
db.close();
|
|
121
|
+
const count = 5 + Math.floor(Math.random() * 10);
|
|
122
|
+
const records = Array.from({ length: count }, (_, i) => {return {
|
|
123
|
+
topic: `tøpic/${Math.random()}`,
|
|
124
|
+
ts: Date.now() + i,
|
|
125
|
+
value: Math.random()
|
|
126
|
+
}});
|
|
127
|
+
try {
|
|
128
|
+
const sink = sqliteSink(path, 'metrics', ['topic', 'ts', 'value']);
|
|
129
|
+
sink.write(records);
|
|
130
|
+
sink.close();
|
|
131
|
+
const reader = new Database(path);
|
|
132
|
+
const rows = reader.prepare('SELECT COUNT(*) as cnt FROM metrics').get();
|
|
133
|
+
reader.close();
|
|
134
|
+
assert.strictEqual(rows.cnt, count, 'Should insert all records in batch');
|
|
135
|
+
} finally {
|
|
136
|
+
rmSync(dir, { recursive: true });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('preserves record values', () => {
|
|
141
|
+
const dir = mkdtempSync(join(tmpdir(), 'sink-'));
|
|
142
|
+
const path = join(dir, `db-${Math.random()}.sqlite`);
|
|
143
|
+
const db = new Database(path);
|
|
144
|
+
db.exec('CREATE TABLE metrics (topic TEXT, ts REAL, value REAL)');
|
|
145
|
+
db.close();
|
|
146
|
+
const topic = `données/capteur-${Math.random()}`;
|
|
147
|
+
const ts = Date.now() + Math.floor(Math.random() * 100000);
|
|
148
|
+
const value = Math.random() * 500;
|
|
149
|
+
try {
|
|
150
|
+
const sink = sqliteSink(path, 'metrics', ['topic', 'ts', 'value']);
|
|
151
|
+
sink.write([{ topic, ts, value }]);
|
|
152
|
+
sink.close();
|
|
153
|
+
const reader = new Database(path);
|
|
154
|
+
const row = reader.prepare('SELECT topic, ts, value FROM metrics').get();
|
|
155
|
+
reader.close();
|
|
156
|
+
assert.strictEqual(row.topic, topic, 'Should preserve topic value');
|
|
157
|
+
} finally {
|
|
158
|
+
rmSync(dir, { recursive: true });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('returns sink with close method', () => {
|
|
163
|
+
const dir = mkdtempSync(join(tmpdir(), 'sink-'));
|
|
164
|
+
const path = join(dir, 'db.sqlite');
|
|
165
|
+
const db = new Database(path);
|
|
166
|
+
db.exec('CREATE TABLE metrics (topic TEXT, ts REAL, value REAL)');
|
|
167
|
+
db.close();
|
|
168
|
+
try {
|
|
169
|
+
const sink = sqliteSink(path, 'metrics', ['topic', 'ts', 'value']);
|
|
170
|
+
assert.strictEqual(typeof sink.close, 'function', 'Should have close method');
|
|
171
|
+
sink.close();
|
|
172
|
+
} finally {
|
|
173
|
+
rmSync(dir, { recursive: true });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
package/test/test_stompSource.js
CHANGED
|
@@ -6,8 +6,8 @@ describe('stompSource', () => {
|
|
|
6
6
|
it('throws on missing url', () => {
|
|
7
7
|
const collector = { accept: () => {} };
|
|
8
8
|
assert.throws(
|
|
9
|
-
() => stompSource(null, '/queue/test', collector),
|
|
10
|
-
/URL must be a non-empty string
|
|
9
|
+
() => {return stompSource(null, '/queue/test', collector)},
|
|
10
|
+
/URL must be a non-empty string/u,
|
|
11
11
|
'Should reject missing url'
|
|
12
12
|
);
|
|
13
13
|
});
|
|
@@ -15,8 +15,8 @@ describe('stompSource', () => {
|
|
|
15
15
|
it('throws on empty url', () => {
|
|
16
16
|
const collector = { accept: () => {} };
|
|
17
17
|
assert.throws(
|
|
18
|
-
() => stompSource('', '/queue/test', collector),
|
|
19
|
-
/URL must be a non-empty string
|
|
18
|
+
() => {return stompSource('', '/queue/test', collector)},
|
|
19
|
+
/URL must be a non-empty string/u,
|
|
20
20
|
'Should reject empty url'
|
|
21
21
|
);
|
|
22
22
|
});
|
|
@@ -24,16 +24,16 @@ describe('stompSource', () => {
|
|
|
24
24
|
it('throws on empty destination', () => {
|
|
25
25
|
const collector = { accept: () => {} };
|
|
26
26
|
assert.throws(
|
|
27
|
-
() => stompSource('stomp://localhost:61613', '', collector),
|
|
28
|
-
/Destination must be a non-empty string
|
|
27
|
+
() => {return stompSource('stomp://localhost:61613', '', collector)},
|
|
28
|
+
/Destination must be a non-empty string/u,
|
|
29
29
|
'Should reject empty destination'
|
|
30
30
|
);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
it('throws on missing collector', () => {
|
|
34
34
|
assert.throws(
|
|
35
|
-
() => stompSource('stomp://localhost:61613', '/queue/test', null),
|
|
36
|
-
/Collector must have an accept\(\) method
|
|
35
|
+
() => {return stompSource('stomp://localhost:61613', '/queue/test', null)},
|
|
36
|
+
/Collector must have an accept\(\) method/u,
|
|
37
37
|
'Should reject missing collector'
|
|
38
38
|
);
|
|
39
39
|
});
|