Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parameter unwindPath for multiple fields (#174) #183

Merged
merged 2 commits into from
Jul 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this part of code, make the change compatible with old single string parameter. I have not extensively tested, but it should work fine. So I would say no breaking changes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, excellent!

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