-
Notifications
You must be signed in to change notification settings - Fork 39
/
index.js
141 lines (123 loc) · 4.25 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
'use strict';
const csv = require('async-csv');
const fs = require('fs');
/**
* Converts an array of objects into a CSV file.
*/
class ObjectsToCsv {
/**
* Creates a new instance of the object array to csv converter.
* @param {object[]} objectArray
*/
constructor(objectArray) {
if (!Array.isArray(objectArray)) {
throw new Error('The input to objects-to-csv must be an array of objects.');
}
if (objectArray.length > 0) {
if (objectArray.some(row => typeof row !== 'object')) {
throw new Error('The array must contain objects, not other data types.');
}
}
this.data = objectArray;
}
/**
* Saves the CSV file to the specified file.
* @param {string} filename - The path and filename of the new CSV file.
* @param {object} options - The options for writing to disk.
* @param {boolean} [options.append] - Whether to append to file. Default is overwrite (false).
* @param {boolean} [options.bom] - Append the BOM mark so that Excel shows
* @param {boolean} [options.allColumns] - Whether to check all items for column names or only the first. Default is the first.
* Unicode correctly.
*/
async toDisk(filename, options) {
if (!filename) {
throw new Error('Empty filename when trying to write to disk.');
}
let addHeader = false;
// If the file didn't exist yet or is empty, add the column headers
// as the first line of the file. Do not add it when we are appending
// to an existing file.
const fileNotExists = !fs.existsSync(filename) || fs.statSync(filename).size === 0;
if (fileNotExists || !options || !options.append) {
addHeader = true;
}
const allColumns = options && options.allColumns
? options.allColumns
: false;
let data = await this.toString(addHeader, allColumns);
// Append the BOM mark if requested at the beginning of the file, otherwise
// Excel won't show Unicode correctly. The actual BOM mark will be EF BB BF,
// see https://stackoverflow.com/a/27975629/6269864 for details.
if (options && options.bom && fileNotExists) {
data = '\ufeff' + data;
}
if (options && options.append) {
return new Promise((resolve, reject) => {
fs.appendFile(filename, data, 'utf8', (error) => {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
} else {
return new Promise((resolve, reject) => {
fs.writeFile(filename, data, 'utf8', (error) => {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
}
}
/**
* Returns the CSV file as string.
* @param {boolean} header - If false, omit the first row containing the
* column names.
* @param {boolean} allColumns - Whether to check all items for column names.
* Uses only the first item if false.
* @returns {Promise<string>}
*/
async toString(header = true, allColumns = false) {
return await convert(this.data, header, allColumns);
}
}
/**
* Private method to run the actual conversion of array of objects to CSV data.
* @param {object[]} data
* @param {boolean} header - Whether the first line should contain column headers.
* @param {boolean} allColumns - Whether to check all items for column names.
* Uses only the first item if false.
* @returns {string}
*/
async function convert(data, header = true, allColumns = false) {
if (data.length === 0) {
return '';
}
const columnNames =
allColumns
? [...data
.reduce((columns, row) => { // check each object to compile a full list of column names
Object.keys(row).map(rowKey => columns.add(rowKey));
return columns;
}, new Set())]
: Object.keys(data[0]); // just figure out columns from the first item in array
if (allColumns) {
columnNames.sort(); // for predictable order of columns
}
// This will hold data in the format that `async-csv` can accept, i.e.
// an array of arrays.
let csvInput = [];
if (header) {
csvInput.push(columnNames);
}
// Add all other rows:
csvInput.push(
...data.map(row => columnNames.map(column => row[column])),
);
return await csv.stringify(csvInput);
}
module.exports = ObjectsToCsv;