|
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