1 | import fs from 'fs';
|
2 | import os from 'os';
|
3 | import path from 'path';
|
4 |
|
5 | function escapeCellForTabSeperatedOutput(cell) {
|
6 | if (cell === undefined || cell === null) {
|
7 | return '';
|
8 | }
|
9 | const cellAsString = `${cell}`;
|
10 | if (cellAsString.match(/("|\r|\n|\t)/)) {
|
11 | return `"${cellAsString.replace(/"/g, '""')}"`;
|
12 | }
|
13 | return cell;
|
14 | }
|
15 |
|
16 | function tabSeperated(tableData) {
|
17 | return (
|
18 | [tableData.columns.map((col) => col.label)]
|
19 | .concat(tableData.rows)
|
20 | .map((row) => {
|
21 | return row.map(escapeCellForTabSeperatedOutput).join('\t');
|
22 | })
|
23 | .join(os.EOL) + os.EOL
|
24 | );
|
25 | }
|
26 |
|
27 | function escapeCellForCommaSeperatedOutput(cell) {
|
28 | if (cell === undefined || cell === null) {
|
29 | return '""';
|
30 | }
|
31 | const cellAsString = `${cell}`;
|
32 | return `"${cellAsString.replace(/"/g, '""')}"`;
|
33 | }
|
34 |
|
35 | function commaSeperated(tableData) {
|
36 | return `${
|
37 | [tableData.columns.map((col) => col.label)]
|
38 | .concat(tableData.rows)
|
39 | .map((row) => {
|
40 | return row.map(escapeCellForCommaSeperatedOutput).join(',');
|
41 | })
|
42 | .join(os.EOL)
|
43 | // Use \r\n rather than os.EOL as per RFC 4180
|
44 | }\r\n`;
|
45 | }
|
46 |
|
47 | function jsonFormatted(tableData) {
|
48 | return (
|
49 | JSON.stringify(
|
50 | tableData.rows.map((row) =>
|
51 | tableData.columns.reduce((obj, col, i) => {
|
52 | obj[col.name] = row[i];
|
53 | return obj;
|
54 | }, {})
|
55 | ),
|
56 | null,
|
57 | '\t'
|
58 | ) + os.EOL
|
59 | );
|
60 | }
|
61 |
|
62 | function getSortIndexForInput(sortColumnIndex, visibleColumnDefinitions) {
|
63 | return isNaN(parseInt(sortColumnIndex, 10))
|
64 | ? Math.max(
|
65 | visibleColumnDefinitions.findIndex(
|
66 | (column) => column.name === sortColumnIndex
|
67 | ),
|
68 | 0
|
69 | )
|
70 | : parseInt(sortColumnIndex, 10);
|
71 | }
|
72 |
|
73 | function getColumnDefinitionsForInput(columns, allColumns) {
|
74 | return columns
|
75 | .map((columnName) =>
|
76 | allColumns.find((column) => column.name === columnName)
|
77 | )
|
78 | .filter((column) => !!column);
|
79 | }
|
80 |
|
81 | function getData(visibleColumnDefinitions, data, sortIndex) {
|
82 | return {
|
83 | columns: visibleColumnDefinitions.map((column, i) =>
|
84 | Object.assign(column, { isSorted: i === sortIndex })
|
85 | ),
|
86 | rows: data
|
87 | .map((operation) =>
|
88 | visibleColumnDefinitions.map((column) =>
|
89 | column.value(operation)
|
90 | )
|
91 | )
|
92 | .sort((a, b) =>
|
93 | a[sortIndex] === b[sortIndex]
|
94 | ? 0
|
95 | : a[sortIndex] < b[sortIndex]
|
96 | ? -1
|
97 | : 1
|
98 | ),
|
99 | };
|
100 | }
|
101 |
|
102 | const exportTransformers = {
|
103 | csv: commaSeperated,
|
104 | xls: tabSeperated,
|
105 | json: jsonFormatted,
|
106 | };
|
107 |
|
108 | export default class FdtTable {
|
109 | constructor(moduleRegistration, columns) {
|
110 | this.columns = columns;
|
111 |
|
112 | this.sortOption = new moduleRegistration.Option('sort')
|
113 | .setShort('S')
|
114 | .setDescription(
|
115 | 'Column name or number to sort by (defaults to 0, first column).'
|
116 | )
|
117 | .setDefault('0', true);
|
118 |
|
119 | const columnNames = columns
|
120 | .map((col) => col.name + (col.default ? '*' : ''))
|
121 | .join('|');
|
122 | this.columnsOption = new moduleRegistration.MultiOption('columns')
|
123 | .setDescription(
|
124 | `One or more space-separated column names to output (${columnNames}), only works when not exporting.`
|
125 | )
|
126 | .setShort('C')
|
127 | .setDefault(
|
128 | columns.filter((col) => col.default).map((col) => col.name),
|
129 | true
|
130 | );
|
131 |
|
132 | const exportFormats = Object.keys(exportTransformers)
|
133 | .map((col, i) => col + (!i ? '*' : ''))
|
134 | .join('|');
|
135 | this.exportOption = new moduleRegistration.Option('export')
|
136 | .setDescription(
|
137 | `Export table to a file; the export type is determined by the file extension (${exportFormats}), and ignores the columns option.`
|
138 | )
|
139 | .setShort('E');
|
140 | }
|
141 |
|
142 | print(res, columnsInput, data, sortInput, exportLocation) {
|
143 | const visibleColumnDefinitions = exportLocation
|
144 | ? this.columns
|
145 | : getColumnDefinitionsForInput(columnsInput, this.columns);
|
146 | const tableData = getData(
|
147 | visibleColumnDefinitions,
|
148 | data,
|
149 | getSortIndexForInput(sortInput, visibleColumnDefinitions)
|
150 | );
|
151 |
|
152 | if (!exportLocation) {
|
153 | res.table(
|
154 | tableData.columns.map(
|
155 | (col) => col.label + (col.isSorted ? '*' : '')
|
156 | ),
|
157 | tableData.rows.map((row) => row.map((cell) => cell || '-'))
|
158 | );
|
159 |
|
160 | res.break();
|
161 | const columnLabels = visibleColumnDefinitions
|
162 | .map((col, _i) => col.label + (col.isSorted ? '*' : ''))
|
163 | .join(', ')
|
164 | .toLowerCase();
|
165 | res.success(
|
166 | `Printed ${columnLabels} for ${tableData.rows.length} results`
|
167 | );
|
168 |
|
169 | return;
|
170 | }
|
171 |
|
172 | const ext = path
|
173 | .extname(path.basename(exportLocation))
|
174 | .replace('.', '');
|
175 |
|
176 | if (!exportTransformers[ext]) {
|
177 | throw new res.InputError(
|
178 | `Unknown export type "${ext}".`,
|
179 | `You can export a table by using the "export" option to specify a file with one of the following extensions: ${Object.keys(
|
180 | exportTransformers
|
181 | ).join('|')}.`
|
182 | );
|
183 | }
|
184 |
|
185 | res.debug(`Exporting to "${exportLocation}"`);
|
186 | const exported = exportTransformers[ext](tableData);
|
187 |
|
188 |
|
189 | fs.writeFileSync(exportLocation, exported);
|
190 |
|
191 | res.debug(`Exported file: ${exported.length} characters`);
|
192 | }
|
193 | }
|