json2csv_bundle.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963
  1. (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
  2. 'use strict';
  3. const utils = require('./utils.js');
  4. module.exports = {
  5. deepKeys: deepKeys,
  6. deepKeysFromList: deepKeysFromList
  7. };
  8. /**
  9. * Return the deep keys list for a single document
  10. * @param object
  11. * @param options
  12. * @returns {Array}
  13. */
  14. function deepKeys(object, options) {
  15. options = mergeOptions(options);
  16. if (utils.isObject(object)) {
  17. return generateDeepKeysList('', object, options);
  18. }
  19. return [];
  20. }
  21. /**
  22. * Return the deep keys list for all documents in the provided list
  23. * @param list
  24. * @param options
  25. * @returns Array[Array[String]]
  26. */
  27. function deepKeysFromList(list, options) {
  28. options = mergeOptions(options);
  29. return list.map((document) => { // for each document
  30. if (utils.isObject(document)) {
  31. // if the data at the key is a document, then we retrieve the subHeading starting with an empty string heading and the doc
  32. return deepKeys(document, options);
  33. }
  34. return [];
  35. });
  36. }
  37. function generateDeepKeysList(heading, data, options) {
  38. let keys = Object.keys(data).map((currentKey) => {
  39. // If the given heading is empty, then we set the heading to be the subKey, otherwise set it as a nested heading w/ a dot
  40. let keyName = buildKeyName(heading, currentKey);
  41. // If we have another nested document, recur on the sub-document to retrieve the full key name
  42. if (isDocumentToRecurOn(data[currentKey])) {
  43. return generateDeepKeysList(keyName, data[currentKey], options);
  44. } else if (options.expandArrayObjects && isArrayToRecurOn(data[currentKey])) {
  45. // If we have a nested array that we need to recur on
  46. return processArrayKeys(data[currentKey], keyName, options);
  47. }
  48. // Otherwise return this key name since we don't have a sub document
  49. return keyName;
  50. });
  51. return utils.flatten(keys);
  52. }
  53. /**
  54. * Helper function to handle the processing of arrays when the expandArrayObjects
  55. * option is specified.
  56. * @param subArray
  57. * @param currentKeyPath
  58. * @param options
  59. * @returns {*}
  60. */
  61. function processArrayKeys(subArray, currentKeyPath, options) {
  62. let subArrayKeys = deepKeysFromList(subArray);
  63. if (!subArray.length) {
  64. return options.ignoreEmptyArraysWhenExpanding ? [] : [currentKeyPath];
  65. } else if (subArray.length && utils.flatten(subArrayKeys).length === 0) {
  66. // Has items in the array, but no objects
  67. return [currentKeyPath];
  68. } else {
  69. subArrayKeys = subArrayKeys.map((schemaKeys) => {
  70. if (isEmptyArray(schemaKeys)) {
  71. return [currentKeyPath];
  72. }
  73. return schemaKeys.map((subKey) => buildKeyName(currentKeyPath, subKey));
  74. });
  75. return utils.unique(utils.flatten(subArrayKeys));
  76. }
  77. }
  78. /**
  79. * Function used to generate the key path
  80. * @param upperKeyName String accumulated key path
  81. * @param currentKeyName String current key name
  82. * @returns String
  83. */
  84. function buildKeyName(upperKeyName, currentKeyName) {
  85. if (upperKeyName) {
  86. return upperKeyName + '.' + currentKeyName;
  87. }
  88. return currentKeyName;
  89. }
  90. /**
  91. * Returns whether this value is a document to recur on or not
  92. * @param val Any item whose type will be evaluated
  93. * @returns {boolean}
  94. */
  95. function isDocumentToRecurOn(val) {
  96. return utils.isObject(val) && !utils.isNull(val) && !Array.isArray(val) && Object.keys(val).length;
  97. }
  98. /**
  99. * Returns whether this value is an array to recur on or not
  100. * @param val Any item whose type will be evaluated
  101. * @returns {boolean}
  102. */
  103. function isArrayToRecurOn(val) {
  104. return Array.isArray(val);
  105. }
  106. /**
  107. * Helper function that determines whether or not a value is an empty array
  108. * @param val
  109. * @returns {boolean}
  110. */
  111. function isEmptyArray(val) {
  112. return Array.isArray(val) && !val.length;
  113. }
  114. function mergeOptions(options) {
  115. return {
  116. expandArrayObjects: false,
  117. ignoreEmptyArraysWhenExpanding: false,
  118. ...options || {}
  119. };
  120. }
  121. },{"./utils.js":2}],2:[function(require,module,exports){
  122. 'use strict';
  123. module.exports = {
  124. // underscore replacements:
  125. isString,
  126. isNull,
  127. isError,
  128. isDate,
  129. isFunction,
  130. isUndefined,
  131. isObject,
  132. unique,
  133. flatten
  134. };
  135. /*
  136. * Helper functions which were created to remove underscorejs from this package.
  137. */
  138. function isString(value) {
  139. return typeof value === 'string';
  140. }
  141. function isObject(value) {
  142. return typeof value === 'object';
  143. }
  144. function isFunction(value) {
  145. return typeof value === 'function';
  146. }
  147. function isNull(value) {
  148. return value === null;
  149. }
  150. function isDate(value) {
  151. return value instanceof Date;
  152. }
  153. function isUndefined(value) {
  154. return typeof value === 'undefined';
  155. }
  156. function isError(value) {
  157. return Object.prototype.toString.call(value) === '[object Error]';
  158. }
  159. function unique(array) {
  160. return [...new Set(array)];
  161. }
  162. function flatten(array) {
  163. return [].concat(...array);
  164. }
  165. },{}],3:[function(require,module,exports){
  166. /**
  167. * @license MIT
  168. * doc-path <https://github.com/mrodrig/doc-path>
  169. * Copyright (c) 2015-present, Michael Rodrigues.
  170. */
  171. "use strict";function evaluatePath(t,r){if(!t)return null;let{dotIndex:e,key:a,remaining:i}=state(r);return e>=0&&!t[r]?Array.isArray(t[a])?t[a].map(t=>evaluatePath(t,i)):evaluatePath(t[a],i):Array.isArray(t)?t.map(t=>evaluatePath(t,r)):t[r]}function setPath(t,r,e){if(!t)throw new Error("No object was provided.");if(!r)throw new Error("No keyPath was provided.");return r.startsWith("__proto__")||r.startsWith("constructor")||r.startsWith("prototype")?t:_sp(t,r,e)}function _sp(t,r,e){let{dotIndex:a,key:i,remaining:s}=state(r);if(a>=0){if(!t[i]&&Array.isArray(t))return t.forEach(t=>_sp(t,r,e));t[i]||(t[i]={}),_sp(t[i],s,e)}else{if(Array.isArray(t))return t.forEach(t=>_sp(t,s,e));t[r]=e}return t}function state(t){let r=t.indexOf(".");return{dotIndex:r,key:t.slice(0,r>=0?r:void 0),remaining:t.slice(r+1)}}module.exports={evaluatePath:evaluatePath,setPath:setPath};
  172. },{}],4:[function(require,module,exports){
  173. module.exports={
  174. "errors" : {
  175. "callbackRequired": "A callback is required!",
  176. "optionsRequired": "Options were not passed and are required.",
  177. "json2csv": {
  178. "cannotCallOn": "Cannot call json2csv on ",
  179. "dataCheckFailure": "Data provided was not an array of documents.",
  180. "notSameSchema": "Not all documents have the same schema."
  181. },
  182. "csv2json": {
  183. "cannotCallOn": "Cannot call csv2json on ",
  184. "dataCheckFailure": "CSV is not a string."
  185. }
  186. },
  187. "defaultOptions" : {
  188. "delimiter" : {
  189. "field" : ",",
  190. "wrap" : "\"",
  191. "eol" : "\n"
  192. },
  193. "excelBOM": false,
  194. "prependHeader" : true,
  195. "trimHeaderFields": false,
  196. "trimFieldValues" : false,
  197. "sortHeader" : false,
  198. "parseCsvNumbers" : false,
  199. "keys" : null,
  200. "checkSchemaDifferences": false,
  201. "expandArrayObjects": false,
  202. "unwindArrays": false,
  203. "useDateIso8601Format": false,
  204. "useLocaleFormat": false
  205. },
  206. "values" : {
  207. "excelBOM": "\ufeff"
  208. }
  209. }
  210. },{}],5:[function(require,module,exports){
  211. 'use strict';
  212. let path = require('doc-path'),
  213. deeks = require('deeks'),
  214. constants = require('./constants.json'),
  215. utils = require('./utils');
  216. const Json2Csv = function(options) {
  217. const wrapDelimiterCheckRegex = new RegExp(options.delimiter.wrap, 'g'),
  218. crlfSearchRegex = /\r?\n|\r/,
  219. expandingWithoutUnwinding = options.expandArrayObjects && !options.unwindArrays,
  220. deeksOptions = {
  221. expandArrayObjects: expandingWithoutUnwinding,
  222. ignoreEmptyArraysWhenExpanding: expandingWithoutUnwinding
  223. };
  224. /** HEADER FIELD FUNCTIONS **/
  225. /**
  226. * Returns the list of data field names of all documents in the provided list
  227. * @param data {Array<Object>} Data to be converted
  228. * @returns {Promise.<Array[String]>}
  229. */
  230. function getFieldNameList(data) {
  231. // If keys weren't specified, then we'll use the list of keys generated by the deeks module
  232. return Promise.resolve(deeks.deepKeysFromList(data, deeksOptions));
  233. }
  234. /**
  235. * Processes the schemas by checking for schema differences, if so desired.
  236. * If schema differences are not to be checked, then it resolves the unique
  237. * list of field names.
  238. * @param documentSchemas
  239. * @returns {Promise.<Array[String]>}
  240. */
  241. function processSchemas(documentSchemas) {
  242. // If the user wants to check for the same schema (regardless of schema ordering)
  243. if (options.checkSchemaDifferences) {
  244. return checkSchemaDifferences(documentSchemas);
  245. } else {
  246. // Otherwise, we do not care if the schemas are different, so we should get the unique list of keys
  247. let uniqueFieldNames = utils.unique(utils.flatten(documentSchemas));
  248. return Promise.resolve(uniqueFieldNames);
  249. }
  250. }
  251. /**
  252. * This function performs the schema difference check, if the user specifies that it should be checked.
  253. * If there are no field names, then there are no differences.
  254. * Otherwise, we get the first schema and the remaining list of schemas
  255. * @param documentSchemas
  256. * @returns {*}
  257. */
  258. function checkSchemaDifferences(documentSchemas) {
  259. // have multiple documents - ensure only one schema (regardless of field ordering)
  260. let firstDocSchema = documentSchemas[0],
  261. restOfDocumentSchemas = documentSchemas.slice(1),
  262. schemaDifferences = computeNumberOfSchemaDifferences(firstDocSchema, restOfDocumentSchemas);
  263. // If there are schema inconsistencies, throw a schema not the same error
  264. if (schemaDifferences) {
  265. return Promise.reject(new Error(constants.errors.json2csv.notSameSchema));
  266. }
  267. return Promise.resolve(firstDocSchema);
  268. }
  269. /**
  270. * Computes the number of schema differences
  271. * @param firstDocSchema
  272. * @param restOfDocumentSchemas
  273. * @returns {*}
  274. */
  275. function computeNumberOfSchemaDifferences(firstDocSchema, restOfDocumentSchemas) {
  276. return restOfDocumentSchemas.reduce((schemaDifferences, documentSchema) => {
  277. // If there is a difference between the schemas, increment the counter of schema inconsistencies
  278. let numberOfDifferences = utils.computeSchemaDifferences(firstDocSchema, documentSchema).length;
  279. return numberOfDifferences > 0
  280. ? schemaDifferences + 1
  281. : schemaDifferences;
  282. }, 0);
  283. }
  284. /**
  285. * If so specified, this sorts the header field names alphabetically
  286. * @param fieldNames {Array<String>}
  287. * @returns {Array<String>} sorted field names, or unsorted if sorting not specified
  288. */
  289. function sortHeaderFields(fieldNames) {
  290. if (options.sortHeader) {
  291. return fieldNames.sort();
  292. }
  293. return fieldNames;
  294. }
  295. /**
  296. * Trims the header fields, if the user desires them to be trimmed.
  297. * @param params
  298. * @returns {*}
  299. */
  300. function trimHeaderFields(params) {
  301. if (options.trimHeaderFields) {
  302. params.headerFields = params.headerFields.map((field) => field.split('.')
  303. .map((component) => component.trim())
  304. .join('.')
  305. );
  306. }
  307. return params;
  308. }
  309. /**
  310. * Wrap the headings, if desired by the user.
  311. * @param params
  312. * @returns {*}
  313. */
  314. function wrapHeaderFields(params) {
  315. // only perform this if we are actually prepending the header
  316. if (options.prependHeader) {
  317. params.headerFields = params.headerFields.map(function(headingKey) {
  318. return wrapFieldValueIfNecessary(headingKey);
  319. });
  320. }
  321. return params;
  322. }
  323. /**
  324. * Generates the CSV header string by joining the headerFields by the field delimiter
  325. * @param params
  326. * @returns {*}
  327. */
  328. function generateCsvHeader(params) {
  329. params.header = params.headerFields.join(options.delimiter.field);
  330. return params;
  331. }
  332. /**
  333. * Retrieve the headings for all documents and return it.
  334. * This checks that all documents have the same schema.
  335. * @param data
  336. * @returns {Promise}
  337. */
  338. function retrieveHeaderFields(data) {
  339. if (options.keys && !options.unwindArrays) {
  340. return Promise.resolve(options.keys)
  341. .then(sortHeaderFields);
  342. }
  343. return getFieldNameList(data)
  344. .then(processSchemas)
  345. .then(sortHeaderFields);
  346. }
  347. /** RECORD FIELD FUNCTIONS **/
  348. /**
  349. * Unwinds objects in arrays within record objects if the user specifies the
  350. * expandArrayObjects option. If not specified, this passes the params
  351. * argument through to the next function in the promise chain.
  352. * @param params {Object}
  353. * @returns {Promise}
  354. */
  355. function unwindRecordsIfNecessary(params, finalPass = false) {
  356. if (options.unwindArrays) {
  357. const originalRecordsLength = params.records.length;
  358. // Unwind each of the documents at the given headerField
  359. params.headerFields.forEach((headerField) => {
  360. params.records = utils.unwind(params.records, headerField);
  361. });
  362. return retrieveHeaderFields(params.records)
  363. .then((headerFields) => {
  364. params.headerFields = headerFields;
  365. // If we were able to unwind more arrays, then try unwinding again...
  366. if (originalRecordsLength !== params.records.length) {
  367. return unwindRecordsIfNecessary(params);
  368. }
  369. // Otherwise, we didn't unwind any additional arrays, so continue...
  370. // Run a final time in case the earlier unwinding exposed additional
  371. // arrays to unwind...
  372. if (!finalPass) {
  373. return unwindRecordsIfNecessary(params, true);
  374. }
  375. // If keys were provided, set the headerFields to the provided keys:
  376. if (options.keys) {
  377. params.headerFields = options.keys;
  378. }
  379. return params;
  380. });
  381. }
  382. return params;
  383. }
  384. /**
  385. * Main function which handles the processing of a record, or document to be converted to CSV format
  386. * This function specifies and performs the necessary operations in the necessary order
  387. * in order to obtain the data and convert it to CSV form while maintaining RFC 4180 compliance.
  388. * * Order of operations:
  389. * - Get fields from provided key list (as array of actual values)
  390. * - Convert the values to csv/string representation [possible option here for custom converters?]
  391. * - Trim fields
  392. * - Determine if they need to be wrapped (& wrap if necessary)
  393. * - Combine values for each line (by joining by field delimiter)
  394. * @param params
  395. * @returns {*}
  396. */
  397. function processRecords(params) {
  398. params.records = params.records.map((record) => {
  399. // Retrieve data for each of the headerFields from this record
  400. let recordFieldData = retrieveRecordFieldData(record, params.headerFields),
  401. // Process the data in this record and return the
  402. processedRecordData = recordFieldData.map((fieldValue) => {
  403. fieldValue = trimRecordFieldValue(fieldValue);
  404. fieldValue = recordFieldValueToString(fieldValue);
  405. fieldValue = wrapFieldValueIfNecessary(fieldValue);
  406. return fieldValue;
  407. });
  408. // Join the record data by the field delimiter
  409. return generateCsvRowFromRecord(processedRecordData);
  410. }).join(options.delimiter.eol);
  411. return params;
  412. }
  413. /**
  414. * Helper function intended to process *just* array values when the expandArrayObjects setting is set to true
  415. * @param recordFieldValue
  416. * @returns {*} processed array value
  417. */
  418. function processRecordFieldDataForExpandedArrayObject(recordFieldValue) {
  419. let filteredRecordFieldValue = utils.removeEmptyFields(recordFieldValue);
  420. // If we have an array and it's either empty of full of empty values, then use an empty value representation
  421. if (!recordFieldValue.length || !filteredRecordFieldValue.length) {
  422. return options.emptyFieldValue || '';
  423. } else if (filteredRecordFieldValue.length === 1) {
  424. // Otherwise, we have an array of actual values...
  425. // Since we are expanding array objects, we will want to key in on values of objects.
  426. return filteredRecordFieldValue[0]; // Extract the single value in the array
  427. }
  428. return recordFieldValue;
  429. }
  430. /**
  431. * Gets all field values from a particular record for the given list of fields
  432. * @param record
  433. * @param fields
  434. * @returns {Array}
  435. */
  436. function retrieveRecordFieldData(record, fields) {
  437. let recordValues = [];
  438. fields.forEach((field) => {
  439. let recordFieldValue = path.evaluatePath(record, field);
  440. if (!utils.isUndefined(options.emptyFieldValue) && utils.isEmptyField(recordFieldValue)) {
  441. recordFieldValue = options.emptyFieldValue;
  442. } else if (options.expandArrayObjects && Array.isArray(recordFieldValue)) {
  443. recordFieldValue = processRecordFieldDataForExpandedArrayObject(recordFieldValue);
  444. }
  445. recordValues.push(recordFieldValue);
  446. });
  447. return recordValues;
  448. }
  449. /**
  450. * Converts a record field value to its string representation
  451. * @param fieldValue
  452. * @returns {*}
  453. */
  454. function recordFieldValueToString(fieldValue) {
  455. const isDate = utils.isDate(fieldValue); // store to avoid checking twice
  456. if (utils.isNull(fieldValue) || Array.isArray(fieldValue) || utils.isObject(fieldValue) && !isDate) {
  457. return JSON.stringify(fieldValue);
  458. } else if (utils.isUndefined(fieldValue)) {
  459. return 'undefined';
  460. } else if (isDate && options.useDateIso8601Format) {
  461. return fieldValue.toISOString();
  462. } else {
  463. return !options.useLocaleFormat ? fieldValue.toString() : fieldValue.toLocaleString();
  464. }
  465. }
  466. /**
  467. * Trims the record field value, if specified by the user's provided options
  468. * @param fieldValue
  469. * @returns {*}
  470. */
  471. function trimRecordFieldValue(fieldValue) {
  472. if (options.trimFieldValues) {
  473. if (Array.isArray(fieldValue)) {
  474. return fieldValue.map(trimRecordFieldValue);
  475. } else if (utils.isString(fieldValue)) {
  476. return fieldValue.trim();
  477. }
  478. return fieldValue;
  479. }
  480. return fieldValue;
  481. }
  482. /**
  483. * Escapes quotation marks in the field value, if necessary, and appropriately
  484. * wraps the record field value if it contains a comma (field delimiter),
  485. * quotation mark (wrap delimiter), or a line break (CRLF)
  486. * @param fieldValue
  487. * @returns {*}
  488. */
  489. function wrapFieldValueIfNecessary(fieldValue) {
  490. const wrapDelimiter = options.delimiter.wrap;
  491. // eg. includes quotation marks (default delimiter)
  492. if (fieldValue.includes(options.delimiter.wrap)) {
  493. // add an additional quotation mark before each quotation mark appearing in the field value
  494. fieldValue = fieldValue.replace(wrapDelimiterCheckRegex, wrapDelimiter + wrapDelimiter);
  495. }
  496. // if the field contains a comma (field delimiter), quotation mark (wrap delimiter), line break, or CRLF
  497. // then enclose it in quotation marks (wrap delimiter)
  498. if (fieldValue.includes(options.delimiter.field) ||
  499. fieldValue.includes(options.delimiter.wrap) ||
  500. fieldValue.match(crlfSearchRegex)) {
  501. // wrap the field's value in a wrap delimiter (quotation marks by default)
  502. fieldValue = wrapDelimiter + fieldValue + wrapDelimiter;
  503. }
  504. return fieldValue;
  505. }
  506. /**
  507. * Generates the CSV record string by joining the field values together by the field delimiter
  508. * @param recordFieldValues
  509. */
  510. function generateCsvRowFromRecord(recordFieldValues) {
  511. return recordFieldValues.join(options.delimiter.field);
  512. }
  513. /** CSV COMPONENT COMBINER/FINAL PROCESSOR **/
  514. /**
  515. * Performs the final CSV construction by combining the fields in the appropriate
  516. * order depending on the provided options values and sends the generated CSV
  517. * back to the user
  518. * @param params
  519. */
  520. function generateCsvFromComponents(params) {
  521. let header = params.header,
  522. records = params.records,
  523. // If we are prepending the header, then add an EOL, otherwise just return the records
  524. csv = (options.excelBOM ? constants.values.excelBOM : '') +
  525. (options.prependHeader ? header + options.delimiter.eol : '') +
  526. records;
  527. return params.callback(null, csv);
  528. }
  529. /** MAIN CONVERTER FUNCTION **/
  530. /**
  531. * Internally exported json2csv function
  532. * Takes data as either a document or array of documents and a callback that will be used to report the results
  533. * @param data {Object|Array<Object>} documents to be converted to csv
  534. * @param callback {Function} callback function
  535. */
  536. function convert(data, callback) {
  537. // Single document, not an array
  538. if (utils.isObject(data) && !data.length) {
  539. data = [data]; // Convert to an array of the given document
  540. }
  541. // Retrieve the heading and then generate the CSV with the keys that are identified
  542. retrieveHeaderFields(data)
  543. .then((headerFields) => ({
  544. headerFields,
  545. callback,
  546. records: data
  547. }))
  548. .then(unwindRecordsIfNecessary)
  549. .then(processRecords)
  550. .then(wrapHeaderFields)
  551. .then(trimHeaderFields)
  552. .then(generateCsvHeader)
  553. .then(generateCsvFromComponents)
  554. .catch(callback);
  555. }
  556. return {
  557. convert,
  558. validationFn: utils.isObject,
  559. validationMessages: constants.errors.json2csv
  560. };
  561. };
  562. module.exports = { Json2Csv };
  563. },{"./constants.json":4,"./utils":6,"deeks":1,"doc-path":3}],6:[function(require,module,exports){
  564. 'use strict';
  565. let path = require('doc-path'),
  566. constants = require('./constants.json');
  567. const dateStringRegex = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/,
  568. MAX_ARRAY_LENGTH = 100000;
  569. module.exports = {
  570. isStringRepresentation,
  571. isDateRepresentation,
  572. computeSchemaDifferences,
  573. deepCopy,
  574. convert,
  575. isEmptyField,
  576. removeEmptyFields,
  577. getNCharacters,
  578. unwind,
  579. isInvalid,
  580. // underscore replacements:
  581. isString,
  582. isNull,
  583. isError,
  584. isDate,
  585. isUndefined,
  586. isObject,
  587. unique,
  588. flatten
  589. };
  590. /**
  591. * Build the options to be passed to the appropriate function
  592. * If a user does not provide custom options, then we use our default
  593. * If options are provided, then we set each valid key that was passed
  594. * @param opts {Object} options object
  595. * @return {Object} options object
  596. */
  597. function buildOptions(opts) {
  598. opts = {...constants.defaultOptions, ...opts || {}};
  599. // Note: Object.assign does a shallow default, we need to deep copy the delimiter object
  600. opts.delimiter = {...constants.defaultOptions.delimiter, ...opts.delimiter};
  601. // Otherwise, send the options back
  602. return opts;
  603. }
  604. /**
  605. * When promisified, the callback and options argument ordering is swapped, so
  606. * this function is intended to determine which argument is which and return
  607. * them in the correct order
  608. * @param arg1 {Object|Function} options or callback
  609. * @param arg2 {Object|Function} options or callback
  610. */
  611. function parseArguments(arg1, arg2) {
  612. // If this was promisified (callback and opts are swapped) then fix the argument order.
  613. if (isObject(arg1) && !isFunction(arg1)) {
  614. return {
  615. options: arg1,
  616. callback: arg2
  617. };
  618. }
  619. // Regular ordering where the callback is provided before the options object
  620. return {
  621. options: arg2,
  622. callback: arg1
  623. };
  624. }
  625. /**
  626. * Validates the parameters passed in to json2csv and csv2json
  627. * @param config {Object} of the form: { data: {Any}, callback: {Function}, dataCheckFn: Function, errorMessages: {Object} }
  628. */
  629. function validateParameters(config) {
  630. // If a callback wasn't provided, throw an error
  631. if (!config.callback) {
  632. throw new Error(constants.errors.callbackRequired);
  633. }
  634. // If we don't receive data, report an error
  635. if (!config.data) {
  636. config.callback(new Error(config.errorMessages.cannotCallOn + config.data + '.'));
  637. return false;
  638. }
  639. // The data provided data does not meet the type check requirement
  640. if (!config.dataCheckFn(config.data)) {
  641. config.callback(new Error(config.errorMessages.dataCheckFailure));
  642. return false;
  643. }
  644. // If we didn't hit any known error conditions, then the data is so far determined to be valid
  645. // Note: json2csv/csv2json may perform additional validity checks on the data
  646. return true;
  647. }
  648. /**
  649. * Abstracted function to perform the conversion of json-->csv or csv-->json
  650. * depending on the converter class that is passed via the params object
  651. * @param params {Object}
  652. */
  653. function convert(params) {
  654. let {options, callback} = parseArguments(params.callback, params.options);
  655. options = buildOptions(options);
  656. let converter = new params.converter(options),
  657. // Validate the parameters before calling the converter's convert function
  658. valid = validateParameters({
  659. data: params.data,
  660. callback,
  661. errorMessages: converter.validationMessages,
  662. dataCheckFn: converter.validationFn
  663. });
  664. if (valid) converter.convert(params.data, callback);
  665. }
  666. /**
  667. * Utility function to deep copy an object, used by the module tests
  668. * @param obj
  669. * @returns {any}
  670. */
  671. function deepCopy(obj) {
  672. return JSON.parse(JSON.stringify(obj));
  673. }
  674. /**
  675. * Helper function that determines whether the provided value is a representation
  676. * of a string. Given the RFC4180 requirements, that means that the value is
  677. * wrapped in value wrap delimiters (usually a quotation mark on each side).
  678. * @param fieldValue
  679. * @param options
  680. * @returns {boolean}
  681. */
  682. function isStringRepresentation(fieldValue, options) {
  683. const firstChar = fieldValue[0],
  684. lastIndex = fieldValue.length - 1,
  685. lastChar = fieldValue[lastIndex];
  686. // If the field starts and ends with a wrap delimiter
  687. return firstChar === options.delimiter.wrap && lastChar === options.delimiter.wrap;
  688. }
  689. /**
  690. * Helper function that determines whether the provided value is a representation
  691. * of a date.
  692. * @param fieldValue
  693. * @returns {boolean}
  694. */
  695. function isDateRepresentation(fieldValue) {
  696. return dateStringRegex.test(fieldValue);
  697. }
  698. /**
  699. * Helper function that determines the schema differences between two objects.
  700. * @param schemaA
  701. * @param schemaB
  702. * @returns {*}
  703. */
  704. function computeSchemaDifferences(schemaA, schemaB) {
  705. return arrayDifference(schemaA, schemaB)
  706. .concat(arrayDifference(schemaB, schemaA));
  707. }
  708. /**
  709. * Utility function to check if a field is considered empty so that the emptyFieldValue can be used instead
  710. * @param fieldValue
  711. * @returns {boolean}
  712. */
  713. function isEmptyField(fieldValue) {
  714. return isUndefined(fieldValue) || isNull(fieldValue) || fieldValue === '';
  715. }
  716. /**
  717. * Helper function that removes empty field values from an array.
  718. * @param fields
  719. * @returns {Array}
  720. */
  721. function removeEmptyFields(fields) {
  722. return fields.filter((field) => !isEmptyField(field));
  723. }
  724. /**
  725. * Helper function that retrieves the next n characters from the start index in
  726. * the string including the character at the start index. This is used to
  727. * check if are currently at an EOL value, since it could be multiple
  728. * characters in length (eg. '\r\n')
  729. * @param str
  730. * @param start
  731. * @param n
  732. * @returns {string}
  733. */
  734. function getNCharacters(str, start, n) {
  735. return str.substring(start, start + n);
  736. }
  737. /**
  738. * The following unwind functionality is a heavily modified version of @edwincen's
  739. * unwind extension for lodash. Since lodash is a large package to require in,
  740. * and all of the required functionality was already being imported, either
  741. * natively or with doc-path, I decided to rewrite the majority of the logic
  742. * so that an additional dependency would not be required. The original code
  743. * with the lodash dependency can be found here:
  744. *
  745. * https://github.com/edwincen/unwind/blob/master/index.js
  746. */
  747. /**
  748. * Core function that unwinds an item at the provided path
  749. * @param accumulator {Array<any>}
  750. * @param item {any}
  751. * @param fieldPath {String}
  752. */
  753. function unwindItem(accumulator, item, fieldPath) {
  754. const valueToUnwind = path.evaluatePath(item, fieldPath);
  755. let cloned = deepCopy(item);
  756. if (Array.isArray(valueToUnwind) && valueToUnwind.length) {
  757. valueToUnwind.forEach((val) => {
  758. cloned = deepCopy(item);
  759. accumulator.push(path.setPath(cloned, fieldPath, val));
  760. });
  761. } else if (Array.isArray(valueToUnwind) && valueToUnwind.length === 0) {
  762. // Push an empty string so the value is empty since there are no values
  763. path.setPath(cloned, fieldPath, '');
  764. accumulator.push(cloned);
  765. } else {
  766. accumulator.push(cloned);
  767. }
  768. }
  769. /**
  770. * Main unwind function which takes an array and a field to unwind.
  771. * @param array {Array<any>}
  772. * @param field {String}
  773. * @returns {Array<any>}
  774. */
  775. function unwind(array, field) {
  776. const result = [];
  777. array.forEach((item) => {
  778. unwindItem(result, item, field);
  779. });
  780. return result;
  781. }
  782. /*
  783. * Helper functions which were created to remove underscorejs from this package.
  784. */
  785. function isString(value) {
  786. return typeof value === 'string';
  787. }
  788. function isObject(value) {
  789. return typeof value === 'object';
  790. }
  791. function isFunction(value) {
  792. return typeof value === 'function';
  793. }
  794. function isNull(value) {
  795. return value === null;
  796. }
  797. function isDate(value) {
  798. return value instanceof Date;
  799. }
  800. function isUndefined(value) {
  801. return typeof value === 'undefined';
  802. }
  803. function isError(value) {
  804. return Object.prototype.toString.call(value) === '[object Error]';
  805. }
  806. function arrayDifference(a, b) {
  807. return a.filter((x) => !b.includes(x));
  808. }
  809. function unique(array) {
  810. return [...new Set(array)];
  811. }
  812. function flatten(array) {
  813. // Node 11+ - use the native array flattening function
  814. if (array.flat) {
  815. return array.flat();
  816. }
  817. // #167 - allow browsers to flatten very long 200k+ element arrays
  818. if (array.length > MAX_ARRAY_LENGTH) {
  819. let safeArray = [];
  820. for (let a = 0; a < array.length; a += MAX_ARRAY_LENGTH) {
  821. safeArray = safeArray.concat(...array.slice(a, a + MAX_ARRAY_LENGTH));
  822. }
  823. return safeArray;
  824. }
  825. return [].concat(...array);
  826. }
  827. /**
  828. * Used to help avoid incorrect values returned by JSON.parse when converting
  829. * CSV back to JSON, such as '39e1804' which JSON.parse converts to Infinity
  830. */
  831. function isInvalid(parsedJson) {
  832. return parsedJson === Infinity ||
  833. parsedJson === -Infinity;
  834. }
  835. },{"./constants.json":4,"doc-path":3}]},{},[5]);