Skip to content

Commit

Permalink
feat: Parameter unwindPath for multiple fields (#174) (#183)
Browse files Browse the repository at this point in the history
* Add unwind multiple times as array

* Change small details as requested
  • Loading branch information
eduardomourar authored and knownasilya committed Jul 10, 2017
1 parent 62e8b0c commit fbcaa10
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 34 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ published
*.log
dist
_docpress
.vscode
74 changes: 73 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,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
- `unwindPath` - Array of Strings, 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`.
- `preserveNewLinesInValues` - Boolean, preserve \r and \n in values. Defaults to `false`.
Expand Down Expand Up @@ -317,6 +317,77 @@ The content of the "file.csv" should be
"Porsche",30000,"aqua"
```

### Example 8

You can also unwind arrays multiple times or with nested objects.

```javascript
var json2csv = require('json2csv');
var fs = require('fs');
var fields = ['carModel', 'price', 'items.name', 'items.color', 'items.items.position', 'items.items.color'];
var myCars = [
{
"carModel": "BMW",
"price": 15000,
"items": [
{
"name": "airbag",
"color": "white"
}, {
"name": "dashboard",
"color": "black"
}
]
}, {
"carModel": "Porsche",
"price": 30000,
"items": [
{
"name": "airbag",
"items": [
{
"position": "left",
"color": "white"
}, {
"position": "right",
"color": "gray"
}
]
}, {
"name": "dashboard",
"items": [
{
"position": "left",
"color": "gray"
}, {
"position": "right",
"color": "black"
}
]
}
]
}
];
var csv = json2csv({ data: myCars, fields: fields, unwindPath: ['items', 'items.items'] });

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","items.name","items.color","items.items.position","items.items.color"
"BMW",15000,"airbag","white",,
"BMW",15000,"dashboard","black",,
"Porsche",30000,"airbag",,"left","white"
"Porsche",30000,"airbag",,"right","gray"
"Porsche",30000,"dashboard",,"left","gray"
"Porsche",30000,"dashboard",,"right","black"
```

## Command Line Interface

`json2csv` can also be called from the command line if installed with `-g`.
Expand All @@ -338,6 +409,7 @@ Usage: json2csv [options]
-q, --quote [value] Specify an alternate quote value.
-n, --no-header Disable the column name header
-F, --flatten Flatten nested objects
-u, --unwindPath <paths> Creates multiple rows from a single JSON document similar to MongoDB unwind.
-L, --ldjson Treat the input as Line-Delimited JSON.
-p, --pretty Use only when printing to console. Logs output in pretty tables.
-a, --include-empty-rows Includes empty rows in the resulting CSV output.
Expand Down
5 changes: 5 additions & 0 deletions bin/json2csv.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ program
.option('-q, --quote [value]', 'Specify an alternate quote value.')
.option('-n, --no-header', 'Disable the column name header')
.option('-F, --flatten', 'Flatten nested objects')
.option('-u, --unwindPath <paths>', 'Creates multiple rows from a single JSON document similar to MongoDB unwind.')
.option('-L, --ldjson', 'Treat the input as Line-Delimited JSON.')
.option('-p, --pretty', 'Use only when printing to console. Logs output in pretty tables.')
.option('-a, --include-empty-rows', 'Includes empty rows in the resulting CSV output.')
Expand Down Expand Up @@ -126,6 +127,10 @@ getFields(function (err, fields) {
opts.newLine = program.newLine;
}

if (program.unwindPath) {
opts.unwindPath = program.fields.split(',');
}

var csv = json2csv(opts);

if (program.output) {
Expand Down
85 changes: 58 additions & 27 deletions lib/json2csv.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ var flatten = require('flat');
* @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 {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
*/
Expand Down Expand Up @@ -137,6 +137,14 @@ function checkParams(params) {

//#check include empty rows, defaults to false
params.includeEmptyRows = params.includeEmptyRows || false;

//#check unwindPath, defaults to empty array
params.unwindPath = params.unwindPath || [];

// if unwindPath is not in array [{}], then just create 1 item array.
if (!Array.isArray(params.unwindPath)) {
params.unwindPath = [params.unwindPath];
}
}

/**
Expand Down Expand Up @@ -194,8 +202,7 @@ function replaceQuotationMarks(stringifiedElement, quotes) {
* @returns {String} csv string
*/
function createColumnContent(params, str) {
var dataRows = createDataRows(params);
dataRows.forEach(function (dataElement) {
createDataRows(params.data, params.unwindPath).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 @@ -280,33 +287,57 @@ function createColumnContent(params, str) {
}

/**
* Performs the unwind logic if necessary to convert single JSON document into multiple rows
* @param params
* Performs the unwind recursively in specified sequence
*
* @param {Array} originalData The params.data value. Original array of JSON objects
* @param {String[]} unwindPaths The params.unwindPath value. Unwind strings to be used to deconstruct array
* @returns {Array} Array of objects containing all rows after unwind of chosen paths
*/
function createDataRows(params) {
var dataRows = params.data;

if (params.unwindPath) {
dataRows = [];
params.data.forEach(function(dataEl) {
var unwindArray = lodashGet(dataEl, params.unwindPath);
var isArr = Array.isArray(unwindArray);

if (isArr && unwindArray.length) {
unwindArray.forEach(function(unwindEl) {
var dataCopy = lodashCloneDeep(dataEl);
lodashSet(dataCopy, params.unwindPath, unwindEl);
dataRows.push(dataCopy);
});
} else if (isArr && !unwindArray.length) {
var dataCopy = lodashCloneDeep(dataEl);
lodashSet(dataCopy, params.unwindPath, undefined);
dataRows.push(dataCopy);
} else {
dataRows.push(dataEl);
}
function createDataRows(originalData, unwindPaths) {
var dataRows = [];
if (unwindPaths.length) {
originalData.forEach(function(dataElement) {
var dataRow = [dataElement];

unwindPaths.forEach(function(unwindPath) {
dataRow = unwindRows(dataRow, unwindPath);
});

Array.prototype.push.apply(dataRows, dataRow);
});
} else {
dataRows = originalData;
}

return dataRows;
}

/**
* Performs the unwind logic if necessary to convert single JSON document into multiple rows
*
* @param {Array} inputRows Array contaning single or multiple rows to unwind
* @param {String} unwindPath Single path to do unwind
* @returns {Array} Array of rows processed
*/
function unwindRows(inputRows, unwindPath) {
var outputRows = [];
inputRows.forEach(function(dataEl) {
var unwindArray = lodashGet(dataEl, unwindPath);
var isArr = Array.isArray(unwindArray);

if (isArr && unwindArray.length) {
unwindArray.forEach(function(unwindEl) {
var dataCopy = lodashCloneDeep(dataEl);
lodashSet(dataCopy, unwindPath, unwindEl);
outputRows.push(dataCopy);
});
} else if (isArr && !unwindArray.length) {
var dataCopy = lodashCloneDeep(dataEl);
lodashSet(dataCopy, unwindPath, undefined);
outputRows.push(dataCopy);
} else {
outputRows.push(dataEl);
}
});
return outputRows;
}
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@
"commander": "^2.8.1",
"debug": "^2.2.0",
"flat": "^2.0.0",
"lodash.flatten": "^4.2.0",
"lodash.get": "^4.3.0",
"lodash.flatten": "^4.4.0",
"lodash.get": "^4.4.0",
"lodash.set": "^4.3.0",
"lodash.uniq": "^4.3.0",
"lodash.clonedeep": "^4.3.0",
"lodash.uniq": "^4.5.0",
"lodash.clonedeep": "^4.5.0",
"path-is-absolute": "^1.0.0"
},
"devDependencies": {
Expand Down
7 changes: 7 additions & 0 deletions test/fixtures/csv/unwind2.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"carModel","price","items.name","items.color","items.items.position","items.items.color"
"BMW",15000,"airbag","white",,
"BMW",15000,"dashboard","black",,
"Porsche",30000,"airbag",,"left","white"
"Porsche",30000,"airbag",,"right","gray"
"Porsche",30000,"dashboard",,"left","gray"
"Porsche",30000,"dashboard",,"right","black"
44 changes: 44 additions & 0 deletions test/fixtures/json/unwind2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[
{
"carModel": "BMW",
"price": 15000,
"items": [
{
"name": "airbag",
"color": "white"
}, {
"name": "dashboard",
"color": "black"
}
]
}, {
"carModel": "Porsche",
"price": 30000,
"items": [
{
"name": "airbag",
"items": [
{
"position": "left",
"color": "white"
}, {
"position": "right",
"color": "gray"
}
]
},
{
"name": "dashboard",
"items": [
{
"position": "left",
"color": "gray"
}, {
"position": "right",
"color": "black"
}
]
}
]
}
]
3 changes: 2 additions & 1 deletion test/helpers/load-fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ var fixtures = [
'emptyRow',
'emptyRowNotIncluded',
'emptyRowDefaultValues',
'unwind'
'unwind',
'unwind2'
];

/*eslint-disable no-console*/
Expand Down
17 changes: 16 additions & 1 deletion test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ 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 jsonUnwind2 = require('./fixtures/json/unwind2');
var jsonNewLine = require('./fixtures/json/newLine');
var csvFixtures = {};

Expand Down Expand Up @@ -409,7 +410,8 @@ async.parallel(loadFixtures(csvFixtures), function (err) {

test('should escape " when preceeded by \\', function (t){
json2csv({
data: [{field: '\\"'}]
data: [{field: '\\"'}],
newLine: '\n'
}, function (error, csv){
t.error(error);
t.equal(csv, '"field"\n"\\""');
Expand Down Expand Up @@ -618,6 +620,19 @@ async.parallel(loadFixtures(csvFixtures), function (err) {
})
});


test('should unwind twice an array into multiple rows', function(t) {
json2csv({
data: jsonUnwind2,
fields: ['carModel', 'price', 'items.name', 'items.color', 'items.items.position', 'items.items.color'],
unwindPath: ['items', 'items.items']
}, function(error, csv) {
t.error(error);
t.equal(csv, csvFixtures.unwind2);
t.end()
})
});

test('should not preserve new lines in values by default', function(t) {
json2csv({
data: jsonNewLine,
Expand Down

0 comments on commit fbcaa10

Please sign in to comment.