UNPKG

7.99 kBJavaScriptView Raw
1'use strict';
2
3let path = require('doc-path'),
4 constants = require('./constants.json');
5
6const dateStringRegex = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
7
8module.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 */
37function 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 */
54function 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 */
73function 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 */
101function 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 */
123function 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 */
135function 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 */
150function 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 */
160function 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 */
170function 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 */
179function 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 */
193function 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 */
214function 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 */
234function 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
246function isString(value) {
247 return typeof value === 'string';
248}
249
250function isObject(value) {
251 return typeof value === 'object';
252}
253
254function isFunction(value) {
255 return typeof value === 'function';
256}
257
258function isNull(value) {
259 return value === null;
260}
261
262function isDate(value) {
263 return value instanceof Date;
264}
265
266function isUndefined(value) {
267 return typeof value === 'undefined';
268}
269
270function isError(value) {
271 return Object.prototype.toString.call(value) === '[object Error]';
272}
273
274function arrayDifference(a, b) {
275 return a.filter((x) => !b.includes(x));
276}
277
278function unique(array) {
279 return [...new Set(array)];
280}
281
282function flatten(array) {
283 return [].concat(...array);
284}