1 | ;
|
2 |
|
3 | let path = require('doc-path'),
|
4 | constants = require('./constants.json');
|
5 |
|
6 | const dateStringRegex = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
|
7 |
|
8 | module.exports = {
|
9 | isStringRepresentation,
|
10 | isDateRepresentation,
|
11 | computeSchemaDifferences,
|
12 | deepCopy,
|
13 | convert,
|
14 | isEmptyField,
|
15 | removeEmptyFields,
|
16 | getNCharacters,
|
17 | unwind,
|
18 |
|
19 | // underscore replacements:
|
20 | isString,
|
21 | isNull,
|
22 | isError,
|
23 | isDate,
|
24 | isUndefined,
|
25 | isObject,
|
26 | unique,
|
27 | flatten
|
28 | };
|
29 |
|
30 | /**
|
31 | * Build the options to be passed to the appropriate function
|
32 | * If a user does not provide custom options, then we use our default
|
33 | * If options are provided, then we set each valid key that was passed
|
34 | * @param opts {Object} options object
|
35 | * @return {Object} options object
|
36 | */
|
37 | function buildOptions(opts) {
|
38 | opts = {...constants.defaultOptions, ...opts || {}};
|
39 |
|
40 | // Note: Object.assign does a shallow default, we need to deep copy the delimiter object
|
41 | opts.delimiter = {...constants.defaultOptions.delimiter, ...opts.delimiter};
|
42 |
|
43 | // Otherwise, send the options back
|
44 | return opts;
|
45 | }
|
46 |
|
47 | /**
|
48 | * When promisified, the callback and options argument ordering is swapped, so
|
49 | * this function is intended to determine which argument is which and return
|
50 | * them in the correct order
|
51 | * @param arg1 {Object|Function} options or callback
|
52 | * @param arg2 {Object|Function} options or callback
|
53 | */
|
54 | function parseArguments(arg1, arg2) {
|
55 | // If this was promisified (callback and opts are swapped) then fix the argument order.
|
56 | if (isObject(arg1) && !isFunction(arg1)) {
|
57 | return {
|
58 | options: arg1,
|
59 | callback: arg2
|
60 | };
|
61 | }
|
62 | // Regular ordering where the callback is provided before the options object
|
63 | return {
|
64 | options: arg2,
|
65 | callback: arg1
|
66 | };
|
67 | }
|
68 |
|
69 | /**
|
70 | * Validates the parameters passed in to json2csv and csv2json
|
71 | * @param config {Object} of the form: { data: {Any}, callback: {Function}, dataCheckFn: Function, errorMessages: {Object} }
|
72 | */
|
73 | function validateParameters(config) {
|
74 | // If a callback wasn't provided, throw an error
|
75 | if (!config.callback) {
|
76 | throw new Error(constants.errors.callbackRequired);
|
77 | }
|
78 |
|
79 | // If we don't receive data, report an error
|
80 | if (!config.data) {
|
81 | config.callback(new Error(config.errorMessages.cannotCallOn + config.data + '.'));
|
82 | return false;
|
83 | }
|
84 |
|
85 | // The data provided data does not meet the type check requirement
|
86 | if (!config.dataCheckFn(config.data)) {
|
87 | config.callback(new Error(config.errorMessages.dataCheckFailure));
|
88 | return false;
|
89 | }
|
90 |
|
91 | // If we didn't hit any known error conditions, then the data is so far determined to be valid
|
92 | // Note: json2csv/csv2json may perform additional validity checks on the data
|
93 | return true;
|
94 | }
|
95 |
|
96 | /**
|
97 | * Abstracted function to perform the conversion of json-->csv or csv-->json
|
98 | * depending on the converter class that is passed via the params object
|
99 | * @param params {Object}
|
100 | */
|
101 | function convert(params) {
|
102 | let {options, callback} = parseArguments(params.callback, params.options);
|
103 | options = buildOptions(options);
|
104 |
|
105 | let converter = new params.converter(options),
|
106 |
|
107 | // Validate the parameters before calling the converter's convert function
|
108 | valid = validateParameters({
|
109 | data: params.data,
|
110 | callback,
|
111 | errorMessages: converter.validationMessages,
|
112 | dataCheckFn: converter.validationFn
|
113 | });
|
114 |
|
115 | if (valid) converter.convert(params.data, callback);
|
116 | }
|
117 |
|
118 | /**
|
119 | * Utility function to deep copy an object, used by the module tests
|
120 | * @param obj
|
121 | * @returns {any}
|
122 | */
|
123 | function deepCopy(obj) {
|
124 | return JSON.parse(JSON.stringify(obj));
|
125 | }
|
126 |
|
127 | /**
|
128 | * Helper function that determines whether the provided value is a representation
|
129 | * of a string. Given the RFC4180 requirements, that means that the value is
|
130 | * wrapped in value wrap delimiters (usually a quotation mark on each side).
|
131 | * @param fieldValue
|
132 | * @param options
|
133 | * @returns {boolean}
|
134 | */
|
135 | function isStringRepresentation(fieldValue, options) {
|
136 | const firstChar = fieldValue[0],
|
137 | lastIndex = fieldValue.length - 1,
|
138 | lastChar = fieldValue[lastIndex];
|
139 |
|
140 | // If the field starts and ends with a wrap delimiter
|
141 | return firstChar === options.delimiter.wrap && lastChar === options.delimiter.wrap;
|
142 | }
|
143 |
|
144 | /**
|
145 | * Helper function that determines whether the provided value is a representation
|
146 | * of a date.
|
147 | * @param fieldValue
|
148 | * @returns {boolean}
|
149 | */
|
150 | function isDateRepresentation(fieldValue) {
|
151 | return dateStringRegex.test(fieldValue);
|
152 | }
|
153 |
|
154 | /**
|
155 | * Helper function that determines the schema differences between two objects.
|
156 | * @param schemaA
|
157 | * @param schemaB
|
158 | * @returns {*}
|
159 | */
|
160 | function computeSchemaDifferences(schemaA, schemaB) {
|
161 | return arrayDifference(schemaA, schemaB)
|
162 | .concat(arrayDifference(schemaB, schemaA));
|
163 | }
|
164 |
|
165 | /**
|
166 | * Utility function to check if a field is considered empty so that the emptyFieldValue can be used instead
|
167 | * @param fieldValue
|
168 | * @returns {boolean}
|
169 | */
|
170 | function isEmptyField(fieldValue) {
|
171 | return isUndefined(fieldValue) || isNull(fieldValue) || fieldValue === '';
|
172 | }
|
173 |
|
174 | /**
|
175 | * Helper function that removes empty field values from an array.
|
176 | * @param fields
|
177 | * @returns {Array}
|
178 | */
|
179 | function removeEmptyFields(fields) {
|
180 | return fields.filter((field) => !isEmptyField(field));
|
181 | }
|
182 |
|
183 | /**
|
184 | * Helper function that retrieves the next n characters from the start index in
|
185 | * the string including the character at the start index. This is used to
|
186 | * check if are currently at an EOL value, since it could be multiple
|
187 | * characters in length (eg. '\r\n')
|
188 | * @param str
|
189 | * @param start
|
190 | * @param n
|
191 | * @returns {string}
|
192 | */
|
193 | function getNCharacters(str, start, n) {
|
194 | return str.substring(start, start + n);
|
195 | }
|
196 |
|
197 | /**
|
198 | * The following unwind functionality is a heavily modified version of @edwincen's
|
199 | * unwind extension for lodash. Since lodash is a large package to require in,
|
200 | * and all of the required functionality was already being imported, either
|
201 | * natively or with doc-path, I decided to rewrite the majority of the logic
|
202 | * so that an additional dependency would not be required. The original code
|
203 | * with the lodash dependency can be found here:
|
204 | *
|
205 | * https://github.com/edwincen/unwind/blob/master/index.js
|
206 | */
|
207 |
|
208 | /**
|
209 | * Core function that unwinds an item at the provided path
|
210 | * @param accumulator {Array<any>}
|
211 | * @param item {any}
|
212 | * @param fieldPath {String}
|
213 | */
|
214 | function unwindItem(accumulator, item, fieldPath) {
|
215 | const valueToUnwind = path.evaluatePath(item, fieldPath);
|
216 | let cloned = deepCopy(item);
|
217 |
|
218 | if (Array.isArray(valueToUnwind)) {
|
219 | valueToUnwind.forEach((val) => {
|
220 | cloned = deepCopy(item);
|
221 | accumulator.push(path.setPath(cloned, fieldPath, val));
|
222 | });
|
223 | } else {
|
224 | accumulator.push(cloned);
|
225 | }
|
226 | }
|
227 |
|
228 | /**
|
229 | * Main unwind function which takes an array and a field to unwind.
|
230 | * @param array {Array<any>}
|
231 | * @param field {String}
|
232 | * @returns {Array<any>}
|
233 | */
|
234 | function unwind(array, field) {
|
235 | const result = [];
|
236 | array.forEach((item) => {
|
237 | unwindItem(result, item, field);
|
238 | });
|
239 | return result;
|
240 | }
|
241 |
|
242 | /*
|
243 | * Helper functions which were created to remove underscorejs from this package.
|
244 | */
|
245 |
|
246 | function isString(value) {
|
247 | return typeof value === 'string';
|
248 | }
|
249 |
|
250 | function isObject(value) {
|
251 | return typeof value === 'object';
|
252 | }
|
253 |
|
254 | function isFunction(value) {
|
255 | return typeof value === 'function';
|
256 | }
|
257 |
|
258 | function isNull(value) {
|
259 | return value === null;
|
260 | }
|
261 |
|
262 | function isDate(value) {
|
263 | return value instanceof Date;
|
264 | }
|
265 |
|
266 | function isUndefined(value) {
|
267 | return typeof value === 'undefined';
|
268 | }
|
269 |
|
270 | function isError(value) {
|
271 | return Object.prototype.toString.call(value) === '[object Error]';
|
272 | }
|
273 |
|
274 | function arrayDifference(a, b) {
|
275 | return a.filter((x) => !b.includes(x));
|
276 | }
|
277 |
|
278 | function unique(array) {
|
279 | return [...new Set(array)];
|
280 | }
|
281 |
|
282 | function flatten(array) {
|
283 | return [].concat(...array);
|
284 | }
|