@engine9-io/input-tools 1.6.8 → 1.7.0
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/ForEachEntry.js +46 -28
- package/package.json +1 -1
- package/test/packet/forEach.js +30 -18
- package/timelineTypes.js +5 -0
- package/test/packet/timelineStream.js +0 -26
package/ForEachEntry.js
CHANGED
|
@@ -23,30 +23,28 @@ const {
|
|
|
23
23
|
|
|
24
24
|
class ForEachEntry {
|
|
25
25
|
constructor({ accountId } = {}) {
|
|
26
|
-
this.timelineOutputMutex = new Mutex();
|
|
27
26
|
this.fileUtilities = new FileUtilities({ accountId });
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
getOutputStream({ name, postfix = '.timeline.csv', validatorFunction = () => true }) {
|
|
30
|
+
this.outputStreams = this.outputStreams || {};
|
|
31
|
+
if (this.outputStreams[name]?.items) return this.outputStreams[name].items;
|
|
32
|
+
|
|
33
|
+
this.outputStreams[name] = this.outputStreams[name] || {
|
|
34
|
+
mutex: new Mutex(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return this.outputStreams[name].mutex.runExclusive(async () => {
|
|
38
|
+
const outputFile = await getTempFilename({ postfix });
|
|
39
|
+
debug(`Output file requested, writing output to to: ${outputFile}`);
|
|
40
|
+
const outputStream = new ValidatingReadable({
|
|
36
41
|
objectMode: true,
|
|
37
|
-
},
|
|
38
|
-
if (!data) return true;
|
|
39
|
-
if (typeof data !== 'object') throw new Error('Invalid timeline data push, must be an object');
|
|
40
|
-
// Is this necessary?
|
|
41
|
-
if (!data.person_id) throw new Error('Invalid timeline data push, must have a person_id, even if 0');
|
|
42
|
-
if (!data.ts) data.ts = new Date().toISOString();
|
|
43
|
-
return true;
|
|
44
|
-
});
|
|
42
|
+
}, validatorFunction);
|
|
45
43
|
// eslint-disable-next-line no-underscore-dangle
|
|
46
|
-
|
|
44
|
+
outputStream._read = () => {};
|
|
47
45
|
|
|
48
|
-
const writeStream = fs.createWriteStream(
|
|
49
|
-
const
|
|
46
|
+
const writeStream = fs.createWriteStream(outputFile);
|
|
47
|
+
const finishWritingOutputPromise = new Promise((resolve, reject) => {
|
|
50
48
|
writeStream.on('finish', () => {
|
|
51
49
|
resolve();
|
|
52
50
|
}).on('error', (err) => {
|
|
@@ -54,16 +52,16 @@ class ForEachEntry {
|
|
|
54
52
|
});
|
|
55
53
|
});
|
|
56
54
|
|
|
57
|
-
|
|
55
|
+
outputStream
|
|
58
56
|
.pipe(csv.stringify({ header: true }))
|
|
59
57
|
.pipe(writeStream);
|
|
60
58
|
|
|
61
|
-
this.
|
|
62
|
-
stream:
|
|
63
|
-
promises: [
|
|
64
|
-
files: [
|
|
59
|
+
this.outputStreams[name].items = {
|
|
60
|
+
stream: outputStream,
|
|
61
|
+
promises: [finishWritingOutputPromise],
|
|
62
|
+
files: [outputFile],
|
|
65
63
|
};
|
|
66
|
-
return this.
|
|
64
|
+
return this.outputStreams[name].items;
|
|
67
65
|
});
|
|
68
66
|
}
|
|
69
67
|
|
|
@@ -90,7 +88,7 @@ class ForEachEntry {
|
|
|
90
88
|
let records = 0;
|
|
91
89
|
let batches = 0;
|
|
92
90
|
|
|
93
|
-
|
|
91
|
+
const outputFiles = {};
|
|
94
92
|
|
|
95
93
|
const transformArguments = {};
|
|
96
94
|
// An array of promises that must be completed, such as writing to disk
|
|
@@ -105,11 +103,31 @@ class ForEachEntry {
|
|
|
105
103
|
const binding = bindings[bindingName];
|
|
106
104
|
if (!binding.path) throw new Error(`Invalid binding: path is required for binding ${bindingName}`);
|
|
107
105
|
if (binding.path === 'output.timeline') {
|
|
108
|
-
const { stream: streamImpl, promises, files } = await this.
|
|
106
|
+
const { stream: streamImpl, promises, files } = await this.getOutputStream({
|
|
107
|
+
name: bindingName,
|
|
108
|
+
postfix: '.timeline.csv',
|
|
109
|
+
validatorFunction: (data) => {
|
|
110
|
+
if (!data) return true;
|
|
111
|
+
if (typeof data !== 'object') throw new Error('Invalid timeline data push, must be an object');
|
|
112
|
+
// Is this necessary?
|
|
113
|
+
if (!data.person_id) throw new Error('Invalid timeline data push, must have a person_id, even if 0');
|
|
114
|
+
if (!data.ts) data.ts = new Date().toISOString();
|
|
115
|
+
return true;
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
newStreams.push(streamImpl);
|
|
119
|
+
transformArguments[bindingName] = streamImpl;
|
|
120
|
+
bindingPromises = bindingPromises.concat(promises || []);
|
|
121
|
+
outputFiles[bindingName] = files;
|
|
122
|
+
} else if (binding.path === 'output.stream') {
|
|
123
|
+
const { stream: streamImpl, promises, files } = await this.getOutputStream({
|
|
124
|
+
name: bindingName,
|
|
125
|
+
postfix: '.output.csv',
|
|
126
|
+
});
|
|
109
127
|
newStreams.push(streamImpl);
|
|
110
128
|
transformArguments[bindingName] = streamImpl;
|
|
111
129
|
bindingPromises = bindingPromises.concat(promises || []);
|
|
112
|
-
|
|
130
|
+
outputFiles[bindingName] = files;
|
|
113
131
|
} else if (binding.path === 'file') {
|
|
114
132
|
transformArguments[bindingName] = await getFile(binding);
|
|
115
133
|
} else if (binding.path === 'handlebars') {
|
|
@@ -152,7 +170,7 @@ class ForEachEntry {
|
|
|
152
170
|
newStreams.forEach((s) => s.push(null));
|
|
153
171
|
await Promise.all(bindingPromises);
|
|
154
172
|
|
|
155
|
-
return {
|
|
173
|
+
return { outputFiles };
|
|
156
174
|
}
|
|
157
175
|
}
|
|
158
176
|
|
package/package.json
CHANGED
package/test/packet/forEach.js
CHANGED
|
@@ -10,41 +10,53 @@ describe('Test Person Packet For Each', async () => {
|
|
|
10
10
|
it('forEachPerson Should loop through 1000 sample people', async () => {
|
|
11
11
|
let counter = 0;
|
|
12
12
|
const forEach = new ForEachEntry();
|
|
13
|
-
await forEach.process(
|
|
13
|
+
const result = await forEach.process(
|
|
14
14
|
{
|
|
15
15
|
packet: 'test/sample/1000_message.packet.zip',
|
|
16
16
|
batchSize: 50,
|
|
17
17
|
bindings: {
|
|
18
|
-
|
|
18
|
+
timelineOutputFileStream: {
|
|
19
19
|
path: 'output.timeline',
|
|
20
20
|
options: {
|
|
21
|
-
entry_type: '
|
|
21
|
+
entry_type: 'ENTRY_OPTION',
|
|
22
22
|
},
|
|
23
23
|
},
|
|
24
|
+
sampleOutputFileStream: {
|
|
25
|
+
path: 'output.stream',
|
|
26
|
+
},
|
|
24
27
|
},
|
|
25
28
|
async transform(props) {
|
|
26
29
|
const {
|
|
27
30
|
batch,
|
|
28
|
-
|
|
31
|
+
timelineOutputFileStream,
|
|
32
|
+
sampleOutputFileStream,
|
|
29
33
|
} = props;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
|
|
35
|
+
batch.forEach((p) => {
|
|
36
|
+
if (Math.random() > 0.9) {
|
|
37
|
+
sampleOutputFileStream.push({
|
|
38
|
+
// for testing we don't need real person_ids
|
|
39
|
+
person_id: p.person_id || Math.floor(Math.random() * 1000000),
|
|
40
|
+
email: p.email,
|
|
41
|
+
entry_type: 'SAMPLE_OUTPUT',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
timelineOutputFileStream.push(
|
|
45
|
+
{
|
|
46
|
+
// for testing we don't need real person_ids
|
|
47
|
+
person_id: p.person_id || Math.floor(Math.random() * 1000000),
|
|
48
|
+
email: p.email,
|
|
49
|
+
entry_type: 'EMAIL_DELIVERED',
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
44
54
|
batch.forEach(() => { counter += 1; });
|
|
45
55
|
},
|
|
46
56
|
},
|
|
47
57
|
);
|
|
58
|
+
assert(result.outputFiles?.timelineOutputFileStream?.length);
|
|
59
|
+
assert(result.outputFiles?.sampleOutputFileStream?.length);
|
|
48
60
|
assert.equal(counter, 1000, `Expected to loop through 1000 people, actual:${counter}`);
|
|
49
61
|
});
|
|
50
62
|
debug('Completed tests');
|
package/timelineTypes.js
CHANGED
|
@@ -60,6 +60,9 @@ const MESSAGE_CONVERSION_ADVOCACY = 64;
|
|
|
60
60
|
// unknown transaction conversion on a message
|
|
61
61
|
const MESSAGE_CONVERSION_TRANSACTION = 65;
|
|
62
62
|
|
|
63
|
+
const MESSAGE_DELIVERY_FAILURE_SHOULD_RETRY = 66;
|
|
64
|
+
const MESSAGE_DELIVERY_FAILURE_SHOULD_NOT_RETRY = 66;
|
|
65
|
+
|
|
63
66
|
const FORM_ADVOCACY = 66;
|
|
64
67
|
|
|
65
68
|
const FILE_IMPORT = 70;
|
|
@@ -129,6 +132,8 @@ const TIMELINE_ENTRY_TYPES = {
|
|
|
129
132
|
MESSAGE_CONVERSION,
|
|
130
133
|
MESSAGE_CONVERSION_ADVOCACY,
|
|
131
134
|
MESSAGE_CONVERSION_TRANSACTION,
|
|
135
|
+
MESSAGE_DELIVERY_FAILURE_SHOULD_RETRY,
|
|
136
|
+
MESSAGE_DELIVERY_FAILURE_SHOULD_NOT_RETRY,
|
|
132
137
|
|
|
133
138
|
FILE_IMPORT,
|
|
134
139
|
EXPORT,
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
const {
|
|
2
|
-
describe, it,
|
|
3
|
-
} = require('node:test');
|
|
4
|
-
const fs = require('node:fs');
|
|
5
|
-
const assert = require('node:assert');
|
|
6
|
-
|
|
7
|
-
const { getTimelineOutputStream } = require('../../index');
|
|
8
|
-
|
|
9
|
-
describe('TimelineOutputStream', async () => {
|
|
10
|
-
it('timeline: It should save items to a csv file', async () => {
|
|
11
|
-
const {
|
|
12
|
-
stream: timelineStream, promises,
|
|
13
|
-
files,
|
|
14
|
-
} = await getTimelineOutputStream({});
|
|
15
|
-
timelineStream.push({ foo: 'bar' });
|
|
16
|
-
// finish the input stream
|
|
17
|
-
timelineStream.push(null);
|
|
18
|
-
await promises[0];
|
|
19
|
-
const content = fs.readFileSync(files[0]).toString().split('\n').map((d) => d.trim())
|
|
20
|
-
.filter(Boolean);
|
|
21
|
-
|
|
22
|
-
const s = 'uuid,entry_type,person_id,reference_id';
|
|
23
|
-
assert.equal(content[0].slice(0, s.length), s, "Beginning of first line doesn't match expected timeline csv header");
|
|
24
|
-
assert.equal(content.length, 2, `There are ${content.length}, not 2 lines in the CSV file`);
|
|
25
|
-
});
|
|
26
|
-
});
|