diff --git a/src/core/operations/JSONToCSV.mjs b/src/core/operations/JSONToCSV.mjs index e846f164..c363acbe 100644 --- a/src/core/operations/JSONToCSV.mjs +++ b/src/core/operations/JSONToCSV.mjs @@ -6,6 +6,8 @@ import Operation from "../Operation.mjs"; import OperationError from "../errors/OperationError.mjs"; +import * as flat from "flat"; +const flatten = flat.default ? flat.default.flatten : flat.flatten; /** * JSON to CSV operation @@ -38,6 +40,40 @@ class JSONToCSV extends Operation { ]; } + /** + * Converts a JSON to csv equivalent. + * + * @returns {string} + */ + toCsv() { + const self = this; + // If the JSON is an array of arrays, this is easy + if (this.flattened[0] instanceof Array) { + return this.flattened + .map(row => row + .map(self.escapeCellContents.bind(self)) + .join(this.cellDelim) + ) + .join(this.rowDelim) + + this.rowDelim; + } + + // If it's an array of dictionaries... + const header = Object.keys(this.flattened[0]); + return header + .map(self.escapeCellContents.bind(self)) + .join(this.cellDelim) + + this.rowDelim + + this.flattened + .map(row => header + .map(h => row[h]) + .map(self.escapeCellContents.bind(self)) + .join(this.cellDelim) + ) + .join(this.rowDelim) + + this.rowDelim; + } + /** * @param {JSON} input * @param {Object[]} args @@ -49,40 +85,23 @@ class JSONToCSV extends Operation { // Record values so they don't have to be passed to other functions explicitly this.cellDelim = cellDelim; this.rowDelim = rowDelim; - const self = this; - - if (!(input instanceof Array)) { - input = [input]; + this.flattened = input; + if (!(this.flattened instanceof Array)) { + this.flattened = [input]; } try { - // If the JSON is an array of arrays, this is easy - if (input[0] instanceof Array) { - return input - .map(row => row - .map(self.escapeCellContents.bind(self)) - .join(cellDelim) - ) - .join(rowDelim) + - rowDelim; - } - - // If it's an array of dictionaries... - const header = Object.keys(input[0]); - return header - .map(self.escapeCellContents.bind(self)) - .join(cellDelim) + - rowDelim + - input - .map(row => header - .map(h => row[h]) - .map(self.escapeCellContents.bind(self)) - .join(cellDelim) - ) - .join(rowDelim) + - rowDelim; + return this.toCsv(); } catch (err) { - throw new OperationError("Unable to parse JSON to CSV: " + err.toString()); + try { + this.flattened = flatten(input); + if (!(this.flattened instanceof Array)) { + this.flattened = [this.flattened]; + } + return this.toCsv(); + } catch (err) { + throw new OperationError("Unable to parse JSON to CSV: " + err.toString()); + } } } diff --git a/tests/operations/tests/JSONtoCSV.mjs b/tests/operations/tests/JSONtoCSV.mjs index 195bce3d..a9a0867e 100644 --- a/tests/operations/tests/JSONtoCSV.mjs +++ b/tests/operations/tests/JSONtoCSV.mjs @@ -89,5 +89,71 @@ TestRegister.addTests([ args: [",", "\\r\\n"] }, ], + }, + { + name: "JSON to CSV: nested JSON", + input: JSON.stringify({a: 1, b: {c: 2, d: 3}}), + expectedOutput: "a,b.c,b.d\r\n1,2,3\r\n", + recipeConfig: [ + { + op: "JSON to CSV", + args: [",", "\\r\\n"] + }, + ], + }, + { + name: "JSON to CSV: nested array", + input: JSON.stringify({a: 1, b: [2, 3]}), + expectedOutput: "a,b.0,b.1\r\n1,2,3\r\n", + recipeConfig: [ + { + op: "JSON to CSV", + args: [",", "\\r\\n"] + }, + ], + }, + { + name: "JSON to CSV: nested JSON, nested array", + input: JSON.stringify({a: 1, b: {c: [2, 3], d: 4}}), + expectedOutput: "a,b.c.0,b.c.1,b.d\r\n1,2,3,4\r\n", + recipeConfig: [ + { + op: "JSON to CSV", + args: [",", "\\r\\n"] + }, + ], + }, + { + name: "JSON to CSV: nested array, nested JSON", + input: JSON.stringify({a: 1, b: [{c: 3, d: 4}]}), + expectedOutput: "a,b.0.c,b.0.d\r\n1,3,4\r\n", + recipeConfig: [ + { + op: "JSON to CSV", + args: [",", "\\r\\n"] + }, + ], + }, + { + name: "JSON to CSV: nested array, nested array", + input: JSON.stringify({a: 1, b: [[2, 3]]}), + expectedOutput: "a,b.0.0,b.0.1\r\n1,2,3\r\n", + recipeConfig: [ + { + op: "JSON to CSV", + args: [",", "\\r\\n"] + }, + ], + }, + { + name: "JSON to CSV: nested JSON, nested JSON", + input: JSON.stringify({a: 1, b: { c: { d: 2, e: 3}}}), + expectedOutput: "a,b.c.d,b.c.e\r\n1,2,3\r\n", + recipeConfig: [ + { + op: "JSON to CSV", + args: [",", "\\r\\n"] + }, + ], } ]);