Skip to content

Commit

Permalink
Adding "unwindPath" option that enables data transformation prior to … (
Browse files Browse the repository at this point in the history
#140)

* Adding "unwindPath" option that enables data transformation prior to writing CSV similar to MongoDB's $unwind operation

* Fixing up README.md spacing in unwind section
  • Loading branch information
jonathan-fulton authored and Ilya Radchenko committed Sep 20, 2016
1 parent ab3865d commit 6d14a8d
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 10 deletions.
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ try {
- `eol` - String, it gets added to each row of data. Defaults to `` if not specified.
- `newLine` - String, overrides the default OS line ending (i.e. `\n` on Unix and `\r\n` on Windows).
- `flatten` - Boolean, flattens nested JSON using [flat]. Defaults to `false`.
- `unwindPath` - String, creates multiple rows from a single JSON document similar to MongoDB's $unwind
- `excelStrings` - Boolean, converts string data into normalized Excel style data.
- `includeEmptyRows` - Boolean, includes empty rows. Defaults to `false`.
- `callback` - `function (error, csvString) {}`. If provided, will callback asynchronously. Only supported for compatibility reasons.
Expand Down Expand Up @@ -264,7 +265,55 @@ car.make, car.model, price, color
"Porsche", "9PA AF1", 60000, "green"
```

### Example 7

You can unwind arrays similar to MongoDB's $unwind operation using the `unwindPath` option.

```javascript
var json2csv = require('json2csv');
var fs = require('fs');
var fields = ['carModel', 'price', 'colors'];
var myCars = [
{
"carModel": "Audi",
"price": 0,
"colors": ["blue","green","yellow"]
}, {
"carModel": "BMW",
"price": 15000,
"colors": ["red","blue"]
}, {
"carModel": "Mercedes",
"price": 20000,
"colors": "yellow"
}, {
"carModel": "Porsche",
"price": 30000,
"colors": ["green","teal","aqua"]
}
];
var csv = json2csv({ data: myCars, fields: fields, unwindPath: 'colors' });

fs.writeFile('file.csv', csv, function(err) {
if (err) throw err;
console.log('file saved');
});
```

The content of the "file.csv" should be

```
"carModel","price","colors"
"Audi",0,"blue"
"Audi",0,"green"
"Audi",0,"yellow"
"BMW",15000,"red"
"BMW",15000,"blue"
"Mercedes",20000,"yellow"
"Porsche",30000,"green"
"Porsche",30000,"teal"
"Porsche",30000,"aqua"
```

## Command Line Interface

Expand Down
55 changes: 51 additions & 4 deletions lib/json2csv.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,34 @@ var os = require('os');
var lodashGet = require('lodash.get');
var lodashFlatten = require('lodash.flatten');
var lodashUniq = require('lodash.uniq');
var lodashSet = require('lodash.set');
var lodashCloneDeep = require('lodash.clonedeep');
var flatten = require('flat');

/**
* @name Json2CsvParams
* @type {Object}
* @property {Array} data - array of JSON objects
* @property {Array} fields - see documentation for details
* @property {String[]} [fieldNames] - names for fields at the same indexes. Must be same length as fields array
* (Optional. Maintained for backwards compatibility. Use fields config object for more features)
* @property {String} [del=","] - delimiter of columns
* @property {String} [defaultValue="<empty>"] - default value to use when missing data
* @property {String} [quotes='"'] - quotes around cell values and column names
* @property {String} [doubleQuotes='"""'] - the value to replace double quotes in strings
* @property {Boolean} [hasCSVColumnTitle=true] - determines whether or not CSV file will contain a title column
* @property {String} [eol=''] - it gets added to each row of data
* @property {String} [newLine] - overrides the default OS line ending (\n on Unix \r\n on Windows)
* @property {Boolean} [flatten=false] - flattens nested JSON using flat (https://www.npmjs.com/package/flat)
* @property {String} [unwindPath] - similar to MongoDB's $unwind, Deconstructs an array field from the input JSON to output a row for each element
* @property {Boolean} [excelStrings] - converts string data into normalized Excel style data
* @property {Boolean} [includeEmptyRows=false] - includes empty rows
*/

/**
* Main function that converts json to csv.
*
* @param {Object} params Function parameters containing data, fields,
* @param {Json2CsvParams} params Function parameters containing data, fields,
* delimiter (default is ','), hasCSVColumnTitle (default is true)
* and default value (default is '')
* @param {Function} [callback] Callback function
Expand Down Expand Up @@ -51,7 +73,7 @@ module.exports = function (params, callback) {
*
* Note that this modifies params.
*
* @param {Object} params Function parameters containing data, fields,
* @param {Json2CsvParams} params Function parameters containing data, fields,
* delimiter, default value, mark quotes and hasCSVColumnTitle
*/
function checkParams(params) {
Expand Down Expand Up @@ -120,7 +142,7 @@ function checkParams(params) {
/**
* Create the title row with all the provided fields as column headings
*
* @param {Object} params Function parameters containing data, fields and delimiter
* @param {Json2CsvParams} params Function parameters containing data, fields and delimiter
* @returns {String} titles as a string
*/
function createColumnTitles(params) {
Expand Down Expand Up @@ -172,7 +194,8 @@ function replaceQuotationMarks(stringifiedElement, quotes) {
* @returns {String} csv string
*/
function createColumnContent(params, str) {
params.data.forEach(function (dataElement) {
var dataRows = createDataRows(params);
dataRows.forEach(function (dataElement) {
//if null do nothing, if empty object without includeEmptyRows do nothing
if (dataElement && (Object.getOwnPropertyNames(dataElement).length > 0 || params.includeEmptyRows)) {
var line = '';
Expand Down Expand Up @@ -243,3 +266,27 @@ function createColumnContent(params, str) {

return str;
}

/**
* Performs the unwind logic if necessary to convert single JSON document into multiple rows
* @param params
*/
function createDataRows(params) {
var dataRows = params.data;
if (params.unwindPath) {
dataRows = [];
params.data.forEach(function(dataEl) {
var unwindArray = lodashGet(dataEl, params.unwindPath);
if (Array.isArray(unwindArray)) {
unwindArray.forEach(function(unwindEl) {
var dataCopy = lodashCloneDeep(dataEl);
lodashSet(dataCopy, params.unwindPath, unwindEl);
dataRows.push(dataCopy);
});
} else {
dataRows.push(dataEl);
}
});
}
return dataRows;
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
"flat": "^2.0.0",
"lodash.flatten": "^4.2.0",
"lodash.get": "^4.3.0",
"lodash.set": "^4.3.0",
"lodash.uniq": "^4.3.0",
"lodash.clonedeep": "^4.3.0",
"path-is-absolute": "^1.0.0"
},
"devDependencies": {
Expand Down
10 changes: 10 additions & 0 deletions test/fixtures/csv/unwind.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"carModel","price","colors"
"Audi",0,"blue"
"Audi",0,"green"
"Audi",0,"yellow"
"BMW",15000,"red"
"BMW",15000,"blue"
"Mercedes",20000,"yellow"
"Porsche",30000,"green"
"Porsche",30000,"teal"
"Porsche",30000,"aqua"
6 changes: 6 additions & 0 deletions test/fixtures/json/unwind.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{ "carModel": "Audi", "price": 0, "colors": ["blue","green","yellow"] },
{ "carModel": "BMW", "price": 15000, "colors": ["red","blue"] },
{ "carModel": "Mercedes", "price": 20000, "colors": "yellow" },
{ "carModel": "Porsche", "price": 30000, "colors": ["green","teal","aqua"] }
]
1 change: 1 addition & 0 deletions test/helpers/load-fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var fixtures = [
'emptyRow',
'emptyRowNotIncluded',
'emptyRowDefaultValues',
'unwind'
];

/*eslint-disable no-console*/
Expand Down
25 changes: 19 additions & 6 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ var jsonDefaultValueEmpty = require('./fixtures/json/defaultValueEmpty');
var jsonTrailingBackslash = require('./fixtures/json/trailingBackslash');
var jsonOverriddenDefaultValue = require('./fixtures/json/overridenDefaultValue');
var jsonEmptyRow = require('./fixtures/json/emptyRow');
var jsonUnwind = require('./fixtures/json/unwind');
var csvFixtures = {};

async.parallel(loadFixtures(csvFixtures), function (err) {
Expand Down Expand Up @@ -366,12 +367,12 @@ async.parallel(loadFixtures(csvFixtures), function (err) {
label: 'NEST1',
value: 'bird.nest1'
},
'bird.nest2',
{
label: 'nonexistent',
value: 'fake.path',
default: 'col specific default value'
}
'bird.nest2',
{
label: 'nonexistent',
value: 'fake.path',
default: 'col specific default value'
}
],
defaultValue: 'NULL'
}, function (error, csv) {
Expand Down Expand Up @@ -590,4 +591,16 @@ async.parallel(loadFixtures(csvFixtures), function (err) {
t.end();
});
});

test('should unwind an array into multiple rows', function(t) {
json2csv({
data: jsonUnwind,
fields: ['carModel', 'price', 'colors'],
unwindPath: 'colors'
}, function(error, csv) {
t.error(error);
t.equal(csv, csvFixtures.unwind);
t.end()
})
});
});

0 comments on commit 6d14a8d

Please sign in to comment.