@engine9-io/input-tools 1.6.9 → 1.7.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  const fs = require('node:fs');
2
2
 
3
- const { Writable } = require('node:stream');
3
+ const { Transform, Writable } = require('node:stream');
4
4
  const { pipeline } = require('node:stream/promises');
5
5
  const { throttle } = require('throttle-debounce');
6
6
  const parallelTransform = require('parallel-transform');
@@ -23,30 +23,32 @@ 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
- getTimelineOutputStream() {
31
- return this.timelineOutputMutex.runExclusive(async () => {
32
- if (this.outputStream) return this.outputStream;
33
- const timelineFile = await getTempFilename({ postfix: '.timeline.csv' });
34
- debug(`Timeline output requested, writing timeline file to: ${timelineFile}`);
35
- const timelineOutputStream = new ValidatingReadable({
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 fileInfo = {
39
+ filename: await getTempFilename({ postfix }),
40
+ records: 0,
41
+ };
42
+
43
+ debug(`Output file requested, writing output to to: ${fileInfo.filename}`);
44
+ const outputStream = new ValidatingReadable({
36
45
  objectMode: true,
37
- }, (data) => {
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
- });
46
+ }, validatorFunction);
45
47
  // eslint-disable-next-line no-underscore-dangle
46
- timelineOutputStream._read = () => {};
48
+ outputStream._read = () => {};
47
49
 
48
- const writeStream = fs.createWriteStream(timelineFile);
49
- const finishWritingTimelinePromise = new Promise((resolve, reject) => {
50
+ const writeStream = fs.createWriteStream(fileInfo.filename);
51
+ const finishWritingOutputPromise = new Promise((resolve, reject) => {
50
52
  writeStream.on('finish', () => {
51
53
  resolve();
52
54
  }).on('error', (err) => {
@@ -54,16 +56,24 @@ class ForEachEntry {
54
56
  });
55
57
  });
56
58
 
57
- timelineOutputStream
59
+ this.outputStreams[name].items = {
60
+ stream: outputStream,
61
+ promises: [finishWritingOutputPromise],
62
+ files: [fileInfo],
63
+ };
64
+
65
+ outputStream
66
+ .pipe(new Transform({
67
+ objectMode: true,
68
+ transform(o, enc, cb) {
69
+ fileInfo.records += 1;
70
+ cb();
71
+ },
72
+ }))
58
73
  .pipe(csv.stringify({ header: true }))
59
74
  .pipe(writeStream);
60
75
 
61
- this.outputStream = {
62
- stream: timelineOutputStream,
63
- promises: [finishWritingTimelinePromise],
64
- files: [timelineFile],
65
- };
66
- return this.outputStream;
76
+ return this.outputStreams[name].items;
67
77
  });
68
78
  }
69
79
 
@@ -90,7 +100,7 @@ class ForEachEntry {
90
100
  let records = 0;
91
101
  let batches = 0;
92
102
 
93
- let timelineFiles = [];
103
+ const outputFiles = {};
94
104
 
95
105
  const transformArguments = {};
96
106
  // An array of promises that must be completed, such as writing to disk
@@ -105,11 +115,31 @@ class ForEachEntry {
105
115
  const binding = bindings[bindingName];
106
116
  if (!binding.path) throw new Error(`Invalid binding: path is required for binding ${bindingName}`);
107
117
  if (binding.path === 'output.timeline') {
108
- const { stream: streamImpl, promises, files } = await this.getTimelineOutputStream({});
118
+ const { stream: streamImpl, promises, files } = await this.getOutputStream({
119
+ name: bindingName,
120
+ postfix: '.timeline.csv',
121
+ validatorFunction: (data) => {
122
+ if (!data) return true;
123
+ if (typeof data !== 'object') throw new Error('Invalid timeline data push, must be an object');
124
+ // Is this necessary?
125
+ if (!data.person_id) throw new Error('Invalid timeline data push, must have a person_id, even if 0');
126
+ if (!data.ts) data.ts = new Date().toISOString();
127
+ return true;
128
+ },
129
+ });
130
+ newStreams.push(streamImpl);
131
+ transformArguments[bindingName] = streamImpl;
132
+ bindingPromises = bindingPromises.concat(promises || []);
133
+ outputFiles[bindingName] = files;
134
+ } else if (binding.path === 'output.stream') {
135
+ const { stream: streamImpl, promises, files } = await this.getOutputStream({
136
+ name: bindingName,
137
+ postfix: '.output.csv',
138
+ });
109
139
  newStreams.push(streamImpl);
110
140
  transformArguments[bindingName] = streamImpl;
111
141
  bindingPromises = bindingPromises.concat(promises || []);
112
- timelineFiles = timelineFiles.concat(files);
142
+ outputFiles[bindingName] = files;
113
143
  } else if (binding.path === 'file') {
114
144
  transformArguments[bindingName] = await getFile(binding);
115
145
  } else if (binding.path === 'handlebars') {
@@ -152,7 +182,7 @@ class ForEachEntry {
152
182
  newStreams.forEach((s) => s.push(null));
153
183
  await Promise.all(bindingPromises);
154
184
 
155
- return { timelineFiles };
185
+ return { outputFiles };
156
186
  }
157
187
  }
158
188
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engine9-io/input-tools",
3
- "version": "1.6.9",
3
+ "version": "1.7.1",
4
4
  "description": "Tools for dealing with Engine9 inputs",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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
- timelineOutputStream: {
18
+ timelineOutputFileStream: {
19
19
  path: 'output.timeline',
20
20
  options: {
21
- entry_type: 'SAMPLE',
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
- timelineOutputStream,
31
+ timelineOutputFileStream,
32
+ sampleOutputFileStream,
29
33
  } = props;
30
- if (timelineOutputStream) {
31
- batch.forEach((p) => {
32
- timelineOutputStream.push(
33
- {
34
- // for testing we don't need real person_ids
35
- person_id: p.person_id || Math.floor(Math.random() * 1000000),
36
- email: p.email,
37
- entry_type: 'EMAIL_DELIVERED',
38
- },
39
- );
40
- });
41
- } else {
42
- throw new Error(`output.timeline did not put a timelineOutputStream into the bindings:${Object.keys(props)}`);
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?.[0]?.records);
59
+ assert(result.outputFiles?.sampleOutputFileStream?.[0]?.records);
48
60
  assert.equal(counter, 1000, `Expected to loop through 1000 people, actual:${counter}`);
49
61
  });
50
62
  debug('Completed tests');
@@ -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
- });