|
3 | 3 | var _ = require('underscore'),
|
4 | 4 | async = require('async');
|
5 | 5 |
|
6 |
| -var options = {}; |
| 6 | +var options = {}; // Initialize the options - this will be populated when the csv2json function is called. |
7 | 7 |
|
8 | 8 | // Takes the parent heading and this doc's data and creates the subdocument headings (string)
|
9 | 9 | var retrieveSubHeading = function (heading, data) {
|
10 |
| - var subKeys = _.keys(data), |
11 |
| - newKey; |
| 10 | + var subKeys = _.keys(data), // retrieve the keys from the current document |
| 11 | + newKey; // temporary variable to aid in determining the heading - used to generate the 'nested' headings |
12 | 12 | _.each(subKeys, function (subKey, indx) {
|
13 |
| - newKey = heading === '' ? subKey : heading + '.' + subKey; |
14 |
| - if (typeof data[subKey] === 'object' && data[subKey] !== null) { // Another nested document |
15 |
| - subKeys[indx] = retrieveSubHeading(newKey, data[subKey]); |
| 13 | + // 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 |
| 14 | + newKey = heading === '' ? subKey : heading + '.' + subKey; |
| 15 | + if (typeof data[subKey] === 'object' && data[subKey] !== null) { // If we have another nested document |
| 16 | + subKeys[indx] = retrieveSubHeading(newKey, data[subKey]); // Recur on the subdocument to retrieve the full key name |
16 | 17 | } else {
|
17 |
| - subKeys[indx] = newKey; |
| 18 | + subKeys[indx] = newKey; // Set the key name since we don't have a sub document |
18 | 19 | }
|
19 | 20 | });
|
20 |
| - return subKeys.join(options.DELIMITER); |
| 21 | + return subKeys.join(options.DELIMITER); // Return the headings joined by our delimiter |
21 | 22 | };
|
22 | 23 |
|
| 24 | +// Retrieve the headings for all documents and return it. This checks that all documents have the same schema. |
23 | 25 | var retrieveHeading = function (data) {
|
24 |
| - return function (cb) { |
25 |
| - var keys = _.keys(data); |
26 |
| - _.each(keys, function (key, indx) { |
27 |
| - if (typeof data[key] === 'object') { |
| 26 | + return function (cb) { // Returns a function that takes a callback - the function is passed to async.parallel |
| 27 | + var keys = _.keys(data); // Retrieve the current data keys |
| 28 | + _.each(keys, function (key, indx) { // for each key |
| 29 | + if (typeof data[key] === 'object') { |
| 30 | + // if the data at the key is a document, then we retrieve the subHeading starting with an empty string heading and the doc |
28 | 31 | keys[indx] = retrieveSubHeading('', data[key]);
|
29 | 32 | }
|
30 | 33 | });
|
| 34 | + // Retrieve the unique array of headings (keys) |
31 | 35 | keys = _.uniq(keys);
|
| 36 | + // If we have more than 1 unique list, then not all docs have the same schema - report an error |
32 | 37 | if (keys.length > 1) { throw new Error('Not all documents have the same schema.', keys); }
|
33 |
| - cb(null, _.flatten(keys).join(options.DELIMITER)); |
| 38 | + cb(null, _.flatten(keys).join(options.DELIMITER)); // Return headings back |
34 | 39 | };
|
35 | 40 | };
|
36 | 41 |
|
| 42 | +// Convert the given data with the given keys |
37 | 43 | var convertData = function (data, keys) {
|
38 |
| - var output = []; |
39 |
| - _.each(keys, function (key, indx) { |
40 |
| - var value = data[key]; |
41 |
| - if (keys.indexOf(key) > -1) { |
42 |
| - if (typeof value === 'object') { |
43 |
| - output.push(convertData(value, _.keys(value))); |
| 44 | + var output = [], // Array of CSV representing converted docs |
| 45 | + value; // Temporary variable to store the current data |
| 46 | + _.each(keys, function (key, indx) { // For each key |
| 47 | + value = data[key]; // Set the current data that we are looking at |
| 48 | + if (keys.indexOf(key) > -1) { // If the keys contain the current key, then process the data |
| 49 | + if (typeof value === 'object') { // If we have an object |
| 50 | + output.push(convertData(value, _.keys(value))); // Push the recursively generated CSV |
44 | 51 | } else {
|
45 |
| - output.push(value); |
| 52 | + output.push(value); // Otherwise push the current value |
46 | 53 | }
|
47 | 54 | }
|
48 | 55 | });
|
49 |
| - return output.join(options.DELIMITER); |
| 56 | + return output.join(options.DELIMITER); // Return the data joined by our field delimiter |
50 | 57 | };
|
51 | 58 |
|
| 59 | +// Generate the CSV representing the given data. |
52 | 60 | var generateCsv = function (data) {
|
53 |
| - return function (cb) { |
| 61 | + return function (cb) { // Returns a function that takes a callback - the function is passed to async.parallel |
| 62 | + // Reduce each JSON document in data to a CSV string and append it to the CSV accumulator |
54 | 63 | return cb(null, _.reduce(data, function (csv, doc) { return csv += convertData(doc, _.keys(doc)) + options.EOL; }, ''));
|
55 | 64 | };
|
56 | 65 | };
|
57 | 66 |
|
58 | 67 | module.exports = {
|
59 | 68 |
|
| 69 | + // Function to export internally |
| 70 | + // Takes options as a document, data as a JSON document array, and a callback that will be used to report the results |
60 | 71 | json2csv: function (opts, data, callback) {
|
61 |
| - if (!callback) { throw new Error('A callback is required!'); } |
| 72 | + if (!callback) { throw new Error('A callback is required!'); } // If a callback wasn't provided, throw an error |
62 | 73 | if (!opts) { callback(new Error('Options were not passed and are required.')); return null; } // Shouldn't happen, but just in case
|
63 |
| - else { options = opts; } |
64 |
| - if (!data) { callback(new Error('Cannot call json2csv on ' + data)); return null; } |
65 |
| - if (typeof data === 'object' && !data.length) { // Single document, not an array |
| 74 | + else { options = opts; } // Options were passed, set the global options value |
| 75 | + if (!data) { callback(new Error('Cannot call json2csv on ' + data + '.')); return null; } // If we don't receive data, report an error |
| 76 | + if (typeof data !== 'object') { // If the data was not a single document or an array of documents |
| 77 | + cb(new Error('Data provided was not an array of documents.')); // Report the error back to the caller |
| 78 | + } else if (typeof data === 'object' && !data.length) { // Single document, not an array |
66 | 79 | data = [data]; // Convert to an array of the given document
|
67 | 80 | }
|
| 81 | + // Retrieve the heading and the CSV asynchronously in parallel |
68 | 82 | async.parallel([retrieveHeading(data), generateCsv(data)], function (err, res) {
|
69 | 83 | if (!err) {
|
70 |
| - callback(null, res.join(options.EOL)); |
| 84 | + // Data received with no errors, join the two responses with an end of line delimiter to setup heading and CSV body |
| 85 | + callback(null, res.join(options.EOL)); |
71 | 86 | } else {
|
72 |
| - callback(err, null); |
| 87 | + callback(err, null); // Report received error back to caller |
73 | 88 | }
|
74 | 89 | });
|
75 | 90 | }
|
|
0 commit comments