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

Add support for flattening arrays and change transforms arguments to … #432

Merged
merged 1 commit into from
Oct 19, 2019
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
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@ Options:
-b, --with-bom Includes BOM character at the beginning of the CSV.
-p, --pretty Print output as a pretty table. Use only when printing to console.
-u, --unwind <paths> Creates multiple rows from a single JSON document similar to MongoDB unwind.
-B, --unwind-blank When unwinding, blank out instead of repeating data.
-F, --flatten Flatten nested objects.
-S, --flatten-separator <separator> Flattened keys separator. Defaults to '.'.
-B, --unwind-blank When unwinding, blank out instead of repeating data. Defaults to false.
-F, --flatten-objects Flatten nested objects. Defaults to false.
-F, --flatten-arrays Flatten nested arrays. Defaults to false.
-S, --flatten-separator <separator> Flattened keys separator. Defaults to '.'. (default: ".")
-h, --help output usage information
```

Expand Down Expand Up @@ -394,30 +395,35 @@ const { transforms: { unwind, flatten } } = require('json2csv');

The unwind transform deconstructs an array field from the input item to output a row for each element. Is's similar to MongoDB's $unwind aggregation.

The transform needs to be instantiated and takes 2 arguments:
The transform needs to be instantiated and takes an options object as arguments containing:
- `paths` - Array of String, list the paths to the fields to be unwound. It's mandatory and should not be empty.
- `blank` - Boolean, unwind using blank values instead of repeating data. Defaults to `false`.
- `blankOut` - Boolean, unwind using blank values instead of repeating data. Defaults to `false`.

```js
// Default
unwind(['fieldToUnwind']);
unwind({ paths: ['fieldToUnwind'] });

// Blanking out repeated data
unwind(['fieldToUnwind'], true);
unwind({ paths: ['fieldToUnwind'], blankOut: true });
```

##### Flatten
Flatten nested javascript objects into a single level object.

The transform needs to be instantiated and takes 1 argument:
The transform needs to be instantiated and takes an options object as arguments containing:
- `objects` - Boolean, whether to flatten JSON objects or not. Defaults to `true`.
- `arrays`- Boolean, whether to flatten Arrays or not. Defaults to `false`.
- `separator` - String, separator to use between nested JSON keys when flattening a field. Defaults to `.`.

```js
// Default
flatten();

// Custom separator '__'
flatten('_');
flatten({ separator: '_' });

// Flatten only arrays
flatten({ objects: false, arrays: true });
```

### Javascript module examples
Expand Down
25 changes: 19 additions & 6 deletions bin/json2csv.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ program
.option('-b, --with-bom', 'Includes BOM character at the beginning of the CSV.')
.option('-p, --pretty', 'Print output as a pretty table. Use only when printing to console.')
// Built-in transforms
.option('-u, --unwind <paths>', 'Creates multiple rows from a single JSON document similar to MongoDB unwind.')
.option('-B, --unwind-blank', 'When unwinding, blank out instead of repeating data.')
.option('-F, --flatten', 'Flatten nested objects.')
.option('-S, --flatten-separator <separator>', 'Flattened keys separator. Defaults to \'.\'.')
.option('--unwind <paths>', 'Creates multiple rows from a single JSON document similar to MongoDB unwind.')
.option('--unwind-blank', 'When unwinding, blank out instead of repeating data. Defaults to false.', false)
.option('--flatten-objects', 'Flatten nested objects. Defaults to false.', false)
.option('--flatten-arrays', 'Flatten nested arrays. Defaults to false.', false)
.option('--flatten-separator <separator>', 'Flattened keys separator. Defaults to \'.\'.', '.')
.parse(process.argv);

function makePathAbsolute(filePath) {
Expand Down Expand Up @@ -136,8 +137,20 @@ async function processStream(config, opts) {
const config = Object.assign({}, program.config ? require(program.config) : {}, program);

const transforms = [];
if (config.unwind) transforms.push(unwind(config.unwind.split(','), config.unwindBlank || false));
if (config.flatten) transforms.push(flatten(config.flattenSeparator || '.'));
if (config.unwind) {
transforms.push(unwind({
paths: config.unwind.split(','),
blankOut: config.unwindBlank
}));
}

if (config.flattenObjects || config.flattenArrays) {
transforms.push(flatten({
objects: config.flattenObjects,
arrays: config.flattenArrays,
separator: config.flattenSeparator
}));
}

const opts = {
transforms,
Expand Down
22 changes: 14 additions & 8 deletions lib/transforms/flatten.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,28 @@
* @param {String} separator Separator to be used as the flattened field name
* @returns {Object => Object} Flattened object
*/
function flatten(separator = '.') {
function flatten({ objects = true, arrays = false, separator = '.' } = {}) {
function step (obj, flatDataRow, currentPath) {
Object.keys(obj).forEach((key) => {
const newPath = currentPath ? `${currentPath}${separator}${key}` : key;
const value = obj[key];

if (typeof value !== 'object'
|| value === null
|| Array.isArray(value)
|| Object.prototype.toString.call(value.toJSON) === '[object Function]'
|| !Object.keys(value).length) {
flatDataRow[newPath] = value;
if (objects
&& typeof value === 'object'
&& value !== null
&& !Array.isArray(value)
&& Object.prototype.toString.call(value.toJSON) !== '[object Function]'
&& Object.keys(value).length) {
step(value, flatDataRow, newPath);
return;
}

step(value, flatDataRow, newPath);
if (arrays && Array.isArray(value)) {
step(value, flatDataRow, newPath);
return;
}

flatDataRow[newPath] = value;
});

return flatDataRow;
Expand Down
2 changes: 1 addition & 1 deletion lib/transforms/unwind.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const { setProp, flattenReducer } = require('../utils');
* @param {String[]} unwindPaths The paths as strings to be used to deconstruct the array
* @returns {Object => Array} Array of objects containing all rows after unwind of chosen paths
*/
function unwind(paths, blankOut = false) {
function unwind({ paths = [], blankOut = false } = {}) {
function unwindReducer(rows, unwindPath) {
return rows
.map(row => {
Expand Down
16 changes: 13 additions & 3 deletions test/CLI.js
Original file line number Diff line number Diff line change
Expand Up @@ -777,7 +777,7 @@ module.exports = (testRunner, jsonFixtures, csvFixtures) => {
});

testRunner.add('should support flattening deep JSON using the flatten transform', (t) => {
const opts = '--flatten';
const opts = '--flatten-objects';

exec(`${cli} -i "${getFixturePath('/json/deepJSON.json')}" ${opts}`, (err, stdout, stderr) => {
t.notOk(stderr);
Expand All @@ -786,9 +786,19 @@ module.exports = (testRunner, jsonFixtures, csvFixtures) => {
t.end();
});
});
testRunner.add('should support flattening JSON with nested arrays using the flatten transform', (t) => {
const opts = '--flatten-objects --flatten-arrays';

exec(`${cli} -i "${getFixturePath('/json/flattenArrays.json')}" ${opts}`, (err, stdout, stderr) => {
t.notOk(stderr);
const csv = stdout;
t.equal(csv, csvFixtures.flattenedArrays);
t.end();
});
});

testRunner.add('should support custom flatten separator using the flatten transform', (t) => {
const opts = '--flatten --flatten-separator __';
const opts = '--flatten-objects --flatten-separator __';

exec(`${cli} -i "${getFixturePath('/json/deepJSON.json')}" ${opts}`, (err, stdout, stderr) => {
t.notOk(stderr);
Expand All @@ -799,7 +809,7 @@ module.exports = (testRunner, jsonFixtures, csvFixtures) => {
});

testRunner.add('should support multiple transforms and honor the order in which they are declared', (t) => {
const opts = '--unwind items --flatten';
const opts = '--unwind items --flatten-objects';

exec(`${cli} -i "${getFixturePath('/json/unwindAndFlatten.json')}" ${opts}`, (err, stdout, stderr) => {
t.notOk(stderr);
Expand Down
26 changes: 21 additions & 5 deletions test/JSON2CSVAsyncParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -1131,7 +1131,7 @@ module.exports = (testRunner, jsonFixtures, csvFixtures, inMemoryJsonFixtures) =
testRunner.add('should support unwinding an object into multiple rows using the unwind transform', async (t) => {
const opts = {
fields: ['carModel', 'price', 'colors'],
transforms: [unwind(['colors'])],
transforms: [unwind({ paths: ['colors'] })],
};
const parser = new AsyncParser(opts);

Expand All @@ -1148,7 +1148,7 @@ module.exports = (testRunner, jsonFixtures, csvFixtures, inMemoryJsonFixtures) =
testRunner.add('should support multi-level unwind using the unwind transform', async (t) => {
const opts = {
fields: ['carModel', 'price', 'extras.items.name', 'extras.items.color', 'extras.items.items.position', 'extras.items.items.color'],
transforms: [unwind(['extras.items', 'extras.items.items'])],
transforms: [unwind({ paths: ['extras.items', 'extras.items.items'] })],
};
const parser = new AsyncParser(opts);

Expand All @@ -1165,7 +1165,7 @@ module.exports = (testRunner, jsonFixtures, csvFixtures, inMemoryJsonFixtures) =
testRunner.add('should support unwind and blank out repeated data using the unwind transform', async (t) => {
const opts = {
fields: ['carModel', 'price', 'extras.items.name', 'extras.items.color', 'extras.items.items.position', 'extras.items.items.color'],
transforms: [unwind(['extras.items', 'extras.items.items'], true)],
transforms: [unwind({ paths: ['extras.items', 'extras.items.items'], blankOut: true })],
};
const parser = new AsyncParser(opts);

Expand Down Expand Up @@ -1195,9 +1195,25 @@ module.exports = (testRunner, jsonFixtures, csvFixtures, inMemoryJsonFixtures) =
t.end();
});

testRunner.add('should support flattening JSON with nested arrays using the flatten transform', async (t) => {
const opts = {
transforms: [flatten({ arrays: true })],
};
const parser = new AsyncParser(opts);

try {
const csv = await parser.fromInput(jsonFixtures.flattenArrays()).promise();
t.equal(csv, csvFixtures.flattenedArrays);
} catch(err) {
t.fail(err.message);
}

t.end();
});

testRunner.add('should support custom flatten separator using the flatten transform', async (t) => {
const opts = {
transforms: [flatten('__')],
transforms: [flatten({ separator: '__' })],
};
const parser = new AsyncParser(opts);

Expand All @@ -1213,7 +1229,7 @@ module.exports = (testRunner, jsonFixtures, csvFixtures, inMemoryJsonFixtures) =

testRunner.add('should support multiple transforms and honor the order in which they are declared', async (t) => {
const opts = {
transforms: [unwind(['items']), flatten()],
transforms: [unwind({ paths: ['items'] }), flatten()],
};
const parser = new AsyncParser(opts);

Expand Down
22 changes: 17 additions & 5 deletions test/JSON2CSVParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,7 @@ module.exports = (testRunner, jsonFixtures, csvFixtures) => {
testRunner.add('should support unwinding an object into multiple rows using the unwind transform', (t) => {
const opts = {
fields: ['carModel', 'price', 'colors'],
transforms: [unwind(['colors'])],
transforms: [unwind({ paths: ['colors'] })],
};

const parser = new Json2csvParser(opts);
Expand All @@ -685,7 +685,7 @@ module.exports = (testRunner, jsonFixtures, csvFixtures) => {
testRunner.add('should support multi-level unwind using the unwind transform', (t) => {
const opts = {
fields: ['carModel', 'price', 'extras.items.name', 'extras.items.color', 'extras.items.items.position', 'extras.items.items.color'],
transforms: [unwind(['extras.items', 'extras.items.items'])],
transforms: [unwind({ paths: ['extras.items', 'extras.items.items'] })],
};

const parser = new Json2csvParser(opts);
Expand All @@ -698,7 +698,7 @@ module.exports = (testRunner, jsonFixtures, csvFixtures) => {
testRunner.add('should support unwind and blank out repeated data using the unwind transform', (t) => {
const opts = {
fields: ['carModel', 'price', 'extras.items.name', 'extras.items.color', 'extras.items.items.position', 'extras.items.items.color'],
transforms: [unwind(['extras.items', 'extras.items.items'], true)],
transforms: [unwind({ paths: ['extras.items', 'extras.items.items'], blankOut: true })],
};

const parser = new Json2csvParser(opts);
Expand Down Expand Up @@ -733,9 +733,21 @@ module.exports = (testRunner, jsonFixtures, csvFixtures) => {
t.end();
});

testRunner.add('should support flattening JSON with nested arrays using the flatten transform', (t) => {
const opts = {
transforms: [flatten({ arrays: true })],
};

const parser = new Json2csvParser(opts);
const csv = parser.parse(jsonFixtures.flattenArrays);

t.equal(csv, csvFixtures.flattenedArrays);
t.end();
});

testRunner.add('should support custom flatten separator using the flatten transform', (t) => {
const opts = {
transforms: [flatten('__')],
transforms: [flatten({ separator: '__' })],
};

const parser = new Json2csvParser(opts);
Expand All @@ -747,7 +759,7 @@ module.exports = (testRunner, jsonFixtures, csvFixtures) => {

testRunner.add('should support multiple transforms and honor the order in which they are declared', (t) => {
const opts = {
transforms: [unwind('items'), flatten()],
transforms: [unwind({ paths: ['items'] }), flatten()],
};

const parser = new Json2csvParser(opts);
Expand Down
33 changes: 27 additions & 6 deletions test/JSON2CSVTransform.js
Original file line number Diff line number Diff line change
Expand Up @@ -1126,7 +1126,7 @@ module.exports = (testRunner, jsonFixtures, csvFixtures, inMemoryJsonFixtures) =
testRunner.add('should support unwinding an object into multiple rows using the unwind transform', (t) => {
const opts = {
fields: ['carModel', 'price', 'colors'],
transforms: [unwind(['colors'])],
transforms: [unwind({ paths: ['colors'] })],
};

const transform = new Json2csvTransform(opts);
Expand All @@ -1148,7 +1148,7 @@ module.exports = (testRunner, jsonFixtures, csvFixtures, inMemoryJsonFixtures) =
testRunner.add('should support multi-level unwind using the unwind transform', (t) => {
const opts = {
fields: ['carModel', 'price', 'extras.items.name', 'extras.items.color', 'extras.items.items.position', 'extras.items.items.color'],
transforms: [unwind(['extras.items', 'extras.items.items'])],
transforms: [unwind({ paths: ['extras.items', 'extras.items.items'] })],
};

const transform = new Json2csvTransform(opts);
Expand All @@ -1172,7 +1172,7 @@ module.exports = (testRunner, jsonFixtures, csvFixtures, inMemoryJsonFixtures) =
testRunner.add('should support unwind and blank out repeated data using the unwind transform', (t) => {
const opts = {
fields: ['carModel', 'price', 'extras.items.name', 'extras.items.color', 'extras.items.items.position', 'extras.items.items.color'],
transforms: [unwind(['extras.items', 'extras.items.items'], true)],
transforms: [unwind({ paths: ['extras.items', 'extras.items.items'], blankOut: true })],
};

const transform = new Json2csvTransform(opts);
Expand Down Expand Up @@ -1212,9 +1212,30 @@ module.exports = (testRunner, jsonFixtures, csvFixtures, inMemoryJsonFixtures) =
});
});

testRunner.add('should support flattening JSON with nested arrays using the flatten transform', (t) => {
const opts = {
transforms: [flatten({ arrays: true })],
};

const transform = new Json2csvTransform(opts);
const processor = jsonFixtures.flattenArrays().pipe(transform);

let csv = '';
processor
.on('data', chunk => (csv += chunk.toString()))
.on('end', () => {
t.equal(csv, csvFixtures.flattenedArrays);
t.end();
})
.on('error', err => {
t.fail(err.message);
t.end();
});
});

testRunner.add('should support custom flatten separator using the flatten transform', (t) => {
const opts = {
transforms: [flatten('__')],
transforms: [flatten({ separator: '__' })],
};

const transform = new Json2csvTransform(opts);
Expand All @@ -1235,7 +1256,7 @@ module.exports = (testRunner, jsonFixtures, csvFixtures, inMemoryJsonFixtures) =

testRunner.add('should support multiple transforms and honor the order in which they are declared', (t) => {
const opts = {
transforms: [unwind(['items']), flatten()],
transforms: [unwind({ paths: ['items'] }), flatten()],
};

const transform = new Json2csvTransform(opts);
Expand All @@ -1254,7 +1275,7 @@ module.exports = (testRunner, jsonFixtures, csvFixtures, inMemoryJsonFixtures) =
});
});

testRunner.add('should support custom transforms', async (t) => {
testRunner.add('should support custom transforms', (t) => {
const opts = {
transforms: [row => ({
model: row.carModel,
Expand Down
3 changes: 3 additions & 0 deletions test/fixtures/csv/flattenedArrays.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"name","age","friends.0.name","friends.0.age","friends.1.name","friends.1.age"
"Jack",39,"Oliver",40,"Harry",50
"Thomas",40,"Harry",35,,
12 changes: 12 additions & 0 deletions test/fixtures/json/flattenArrays.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"name": "Jack",
"age": 39,
"friends": [{ "name": "Oliver", "age": 40 }, { "name": "Harry", "age": 50 }]
},
{
"name": "Thomas",
"age": 40,
"friends": [{ "name": "Harry", "age": 35 }]
}
]