├── .eslintrc ├── .gitattributes ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .prettierrc.js ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bin ├── json2csv.js └── utils │ ├── TablePrinter.js │ └── parseNdjson.js ├── docs ├── cli-examples.md └── parser-examples.md ├── lib ├── JSON2CSVAsyncParser.js ├── JSON2CSVBase.js ├── JSON2CSVParser.js ├── JSON2CSVStreamParser.js ├── JSON2CSVTransform.js ├── formatters │ ├── default.js │ ├── number.js │ ├── object.js │ ├── string.js │ ├── stringExcel.js │ ├── stringQuoteOnlyIfNecessary.js │ └── symbol.js ├── json2csv.js ├── transforms │ ├── flatten.js │ └── unwind.js └── utils.js ├── package-lock.json ├── package.json ├── rollup.config.js └── test ├── CLI.js ├── JSON2CSVAsyncParser.js ├── JSON2CSVAsyncParserInMemory.js ├── JSON2CSVParser.js ├── JSON2CSVStreamParser.js ├── JSON2CSVTransform.js ├── fixtures ├── csv │ ├── backslashAtEnd.csv │ ├── backslashAtEndInMiddleColumn.csv │ ├── backslashBeforeNewLine.csv │ ├── customHeaderQuotes.csv │ ├── date.csv │ ├── deepJSON.csv │ ├── default.csv │ ├── defaultCustomTransform.csv │ ├── defaultStream.csv │ ├── defaultValue.csv │ ├── defaultValueEmpty.csv │ ├── delimiter.csv │ ├── embeddedjson.csv │ ├── emptyObject.csv │ ├── emptyRow.csv │ ├── emptyRowDefaultValues.csv │ ├── emptyRowNotIncluded.csv │ ├── eol.csv │ ├── escapeCustomQuotes.csv │ ├── escapeDoubleBackslashedEscapedQuote.csv │ ├── escapeEOL.csv │ ├── escapeTab.csv │ ├── escapedQuotes.csv │ ├── escapedQuotesUnescaped.csv │ ├── excelStrings.csv │ ├── excelStringsWithEscapedQuoted.csv │ ├── fancyfields.csv │ ├── fieldNames.csv │ ├── flattenToJSON.csv │ ├── flattenedArrays.csv │ ├── flattenedCustomSeparatorDeepJSON.csv │ ├── flattenedDeepJSON.csv │ ├── flattenedEmbeddedJson.csv │ ├── functionField.csv │ ├── functionNoStringify.csv │ ├── functionStringifyByDefault.csv │ ├── ndjson.csv │ ├── nested.csv │ ├── numberCustomSeparator.csv │ ├── numberFixedDecimals.csv │ ├── numberFixedDecimalsAndCustomSeparator.csv │ ├── overriddenDefaultValue.csv │ ├── prettyprint.txt │ ├── prettyprintWithoutHeader.txt │ ├── prettyprintWithoutRows.txt │ ├── quoteOnlyIfNecessary.csv │ ├── quotes.csv │ ├── reversed.csv │ ├── selected.csv │ ├── symbol.csv │ ├── trailingBackslash.csv │ ├── tsv.csv │ ├── unwind.csv │ ├── unwind2.csv │ ├── unwind2Blank.csv │ ├── unwindAndFlatten.csv │ ├── unwindComplexObject.csv │ ├── withBOM.csv │ ├── withNotExistField.csv │ ├── withSimpleQuotes.csv │ ├── withoutHeader.csv │ └── withoutQuotes.csv ├── fields │ ├── emptyRowDefaultValues.json │ ├── fancyfields.js │ ├── fieldNames.json │ ├── functionNoStringify.js │ ├── functionStringifyByDefault.js │ ├── functionWithCheck.js │ ├── nested.json │ ├── overriddenDefaultValue.json │ └── overriddenDefaultValue2.js └── json │ ├── arrayWithNull.json │ ├── backslashAtEnd.json │ ├── backslashAtEndInMiddleColumn.json │ ├── backslashBeforeNewLine.json │ ├── date.js │ ├── deepJSON.json │ ├── default.json │ ├── defaultInvalid.json │ ├── defaultValue.json │ ├── defaultValueEmpty.json │ ├── delimiter.json │ ├── empty.json │ ├── emptyArray.json │ ├── emptyObject.json │ ├── emptyRow.json │ ├── eol.json │ ├── escapeCustomQuotes.json │ ├── escapeDoubleBackslashedEscapedQuote.json │ ├── escapeEOL.json │ ├── escapeTab.json │ ├── escapedQuotes.json │ ├── fancyfields.json │ ├── flattenArrays.json │ ├── flattenToJSON.js │ ├── functionField.js │ ├── functionNoStringify.json │ ├── functionStringifyByDefault.json │ ├── invalidNoToken.json │ ├── ndjson.json │ ├── ndjsonInvalid.json │ ├── nested.json │ ├── notAnObject.json │ ├── numberFormatter.json │ ├── overriddenDefaultValue.json │ ├── quoteOnlyIfNecessary.json │ ├── quotes.json │ ├── specialCharacters.json │ ├── symbol.js │ ├── trailingBackslash.json │ ├── unwind.json │ ├── unwind2.json │ ├── unwindAndFlatten.json │ └── unwindComplexObject.json ├── helpers └── loadFixtures.js ├── index.js └── parseNdjson.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": 9 8 | }, 9 | "extends": "eslint:recommended" 10 | } 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | test/fixtures/csv/eol.csv text eol=crlf 3 | test/fixtures/csv/escapeEOL.csv text eol=crlf 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | dist 4 | 5 | coverage 6 | .DS_Store 7 | .tm_properties 8 | server.* 9 | published 10 | *.log 11 | _docpress 12 | .vscode 13 | 14 | test/fixtures/results -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .travis.yml 2 | .github/ 3 | devtools 4 | CHANGELOG.md 5 | test 6 | coverage 7 | .nyc_output 8 | _docpress 9 | webpack.config.js 10 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | }; 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | - "13" 5 | - "14" 6 | - "15" 7 | script: 8 | - "npm run lint" 9 | - "npm run test-with-coverage" 10 | after_success: 11 | - "npm run coveralls" 12 | sudo: false 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [6.0.0-alpha.2](https://github.com/zemirco/json2csv/compare/v6.0.0-alpha.1...v6.0.0-alpha.2) (2023-01-21) 6 | 7 | ## [6.0.0-alpha.1](https://github.com/zemirco/json2csv/compare/v6.0.0-alpha.0...v6.0.0-alpha.1) (2022-02-23) 8 | 9 | 10 | ### Features 11 | 12 | * expose JSON2CSVStreamParser ([d476707](https://github.com/zemirco/json2csv/commit/d47670780f3dd07299ece99c7a5de409f714d21f)) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * fix missing ndjson option to CLI ([885e28b](https://github.com/zemirco/json2csv/commit/885e28bef66777d715f6df575f971525391efbbe)) 18 | * fix some issues in the AsyncParser tests ([695f116](https://github.com/zemirco/json2csv/commit/695f116cca80f316a802b85c6065ad5db3b9d2d8)) 19 | * reset lockfile due to changes in url patterns for github ([b589372](https://github.com/zemirco/json2csv/commit/b58937294b383946067f0eb415e484f645c420f0)) 20 | * unwind transform issue with nested arrays ([#548](https://github.com/zemirco/json2csv/issues/548)) ([3cb57f3](https://github.com/zemirco/json2csv/commit/3cb57f3357b053ce0d4a26e474dae10e07d14ac4)) 21 | * update engines and volta ([98984dd](https://github.com/zemirco/json2csv/commit/98984ddd479439c904c8434cafac6bdbabf2e6f2)) 22 | 23 | ## [6.0.0-alpha.0](https://github.com/zemirco/json2csv/compare/v5.0.3...v6.0.0-alpha.0) (2021-04-14) 24 | 25 | 26 | ### ⚠ BREAKING CHANGES 27 | 28 | * Drop support for Node < v12 29 | * AsyncParser API has changed, see the `Upgrading from 5.X to 6.X` section for details. 30 | 31 | * fix: consolidate the API of AsyncParser and parseAsync 32 | 33 | * feat: simplify AsyncParser 34 | 35 | * chore: drop support for node 11 36 | 37 | * refactor: improve AsyncParser parse method 38 | 39 | * docs: add links to node docs and fix few small issues 40 | * In the JavaScript modules, `formatters` are introduced and the `quote`, `escapedQuote` and `excelStrings` options are removed. See the migration notes in the readme. CLI hasn't changed. 41 | 42 | ### Features 43 | 44 | * Introduce formatters ([#455](https://github.com/zemirco/json2csv/issues/455)) ([88ed6ee](https://github.com/zemirco/json2csv/commit/88ed6ee780b439d394235c9e8fac7e42b0d614dd)) 45 | * use jsonparse for ND-JSON instead of the custom made implementation ([#493](https://github.com/zemirco/json2csv/issues/493)) ([55aa0c7](https://github.com/zemirco/json2csv/commit/55aa0c70374def0dafa342d2a122d077eb87d5e1)) 46 | 47 | 48 | ### Bug Fixes 49 | 50 | * consolidate the API of AsyncParser and parseAsync ([#492](https://github.com/zemirco/json2csv/issues/492)) ([bcce91f](https://github.com/zemirco/json2csv/commit/bcce91f953625bb6a3b401d839670bb3cb5ba11a)) 51 | * issue with unwind and empty arrays creating an extra column ([#497](https://github.com/zemirco/json2csv/issues/497)) ([3b74735](https://github.com/zemirco/json2csv/commit/3b747359b086ec212a0f6ecb92ec0a40511f75c3)) 52 | * Performance optimizations ([#491](https://github.com/zemirco/json2csv/issues/491)) ([471f5a7](https://github.com/zemirco/json2csv/commit/471f5a7a55375a06a66ce4b0438583d719d6db8f)) 53 | * prevents Parser and AsyncParser from caching the fields option between executions causing issues and inconsistencies ([#498](https://github.com/zemirco/json2csv/issues/498)) ([4d8a81a](https://github.com/zemirco/json2csv/commit/4d8a81a3139024c31377fc62e4e39ece29e72c8c)) 54 | * simplify stringExcel formatter and support proper escaping ([#513](https://github.com/zemirco/json2csv/issues/513)) ([50062c3](https://github.com/zemirco/json2csv/commit/50062c3e155ff2c12b1bb417085188a2156885a8)) 55 | 56 | ### [5.0.3](https://github.com/zemirco/json2csv/compare/v5.0.2...v5.0.3) (2020-09-24) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * audit dependencies fix ([d6d0fc7](https://github.com/zemirco/json2csv/commit/d6d0fc78128e01e021414aaf52a65cbcd09a1225)) 62 | * update commander dep ([322e568](https://github.com/zemirco/json2csv/commit/322e568793ec4a64f43ec2ac82c9886177bcc4ed)) 63 | 64 | ### [5.0.2](https://github.com/zemirco/json2csv/compare/v5.0.1...v5.0.2) (2020-09-24) 65 | 66 | 67 | ### Bug Fixes 68 | 69 | * **cli:** fix relative paths issue in CLI when not streaming ([#488](https://github.com/zemirco/json2csv/issues/488)) ([06079e8](https://github.com/zemirco/json2csv/commit/06079e840128030eacfecde66da11295eb162234)) 70 | 71 | ### [5.0.1](https://github.com/zemirco/json2csv/compare/v5.0.0...v5.0.1) (2020-04-28) 72 | 73 | 74 | ### Bug Fixes 75 | 76 | * wrong call to processValue ([#454](https://github.com/zemirco/json2csv/issues/454)) ([66abd45](https://github.com/zemirco/json2csv/commit/66abd45)) 77 | 78 | ## [5.0.0](https://github.com/zemirco/json2csv/compare/v4.5.2...v5.0.0) (2020-03-15) 79 | 80 | 81 | ### ⚠ BREAKING CHANGES 82 | 83 | * Node 8 and 9 no longer supported, use Node 10 or greater. It might still work, but it has reached End-Of-Life. 84 | * module no longer takes `unwind`, `unwindBlank`, `flatten` or the `flattenSeparator` options, instead see the new `transforms` option. CLI options are unchanged from the callers side, but use the built in transforms under the hood. 85 | 86 | * Add support for transforms 87 | 88 | * Add documentation about transforms 89 | * remove extra commonjs build, use starting point in package.json `main` field. 90 | * Renamed `doubleQuote` to `escapedQuote` 91 | * remove `stringify` option 92 | * `--fields-config` option has been removed, use the new `--config` option for all configuration, not just fields. 93 | * Drop node 6 and 7, and add node 11 and 12 94 | 95 | ### Bug Fixes 96 | 97 | * Always error asynchronously from parseAsync method ([#412](https://github.com/zemirco/json2csv/issues/412)) ([16cc044](https://github.com/zemirco/json2csv/commit/16cc044)) 98 | * audit deps ([15992cf](https://github.com/zemirco/json2csv/commit/15992cf)) 99 | * drop Node 8 and 9 ([7295465](https://github.com/zemirco/json2csv/commit/7295465)) 100 | * Make some CLI options mandatory ([#433](https://github.com/zemirco/json2csv/issues/433)) ([bd51527](https://github.com/zemirco/json2csv/commit/bd51527)) 101 | * Remove CommonJS build ([#422](https://github.com/zemirco/json2csv/issues/422)) ([5ce0089](https://github.com/zemirco/json2csv/commit/5ce0089)) 102 | * Remove stringify option ([#419](https://github.com/zemirco/json2csv/issues/419)) ([39f303d](https://github.com/zemirco/json2csv/commit/39f303d)) 103 | * Rename doubleQuote to escapedQuote ([#418](https://github.com/zemirco/json2csv/issues/418)) ([f99408c](https://github.com/zemirco/json2csv/commit/f99408c)) 104 | * update CI node versions ([#413](https://github.com/zemirco/json2csv/issues/413)) ([6fd6c09](https://github.com/zemirco/json2csv/commit/6fd6c09)) 105 | * update commander cli dep ([74aa40a](https://github.com/zemirco/json2csv/commit/74aa40a)) 106 | * update commander dep ([272675b](https://github.com/zemirco/json2csv/commit/272675b)) 107 | * **deps:** audit dependencies ([bf9877a](https://github.com/zemirco/json2csv/commit/bf9877a)) 108 | * **deps:** update commander ([3f099f2](https://github.com/zemirco/json2csv/commit/3f099f2)) 109 | * **security:** fix audit vulnerabilities ([b57715b](https://github.com/zemirco/json2csv/commit/b57715b)) 110 | 111 | 112 | ### Features 113 | 114 | * Add support for flattening arrays and change transforms arguments to an object. ([#432](https://github.com/zemirco/json2csv/issues/432)) ([916e448](https://github.com/zemirco/json2csv/commit/916e448)) 115 | * Add support for transforms ([#431](https://github.com/zemirco/json2csv/issues/431)) ([f1d04d0](https://github.com/zemirco/json2csv/commit/f1d04d0)) 116 | * Improve async promise to optionally not return ([#421](https://github.com/zemirco/json2csv/issues/421)) ([3e296f6](https://github.com/zemirco/json2csv/commit/3e296f6)) 117 | * Improves the unwind transform so it unwinds all unwindable fields if … ([#434](https://github.com/zemirco/json2csv/issues/434)) ([ec1f301](https://github.com/zemirco/json2csv/commit/ec1f301)) 118 | * replace fields config by a global config ([#338](https://github.com/zemirco/json2csv/issues/338)) ([d6c1c5f](https://github.com/zemirco/json2csv/commit/d6c1c5f)) 119 | 120 | ## [4.5.2](https://github.com/zemirco/json2csv/compare/v4.5.1...v4.5.2) (2019-07-05) 121 | 122 | 123 | ### Bug Fixes 124 | 125 | * Improve the inference of the header name when using function as value ([#395](https://github.com/zemirco/json2csv/issues/395)) ([590d19a](https://github.com/zemirco/json2csv/commit/590d19a)) 126 | 127 | 128 | 129 | 130 | ## [4.4.0](https://github.com/zemirco/json2csv/compare/v4.3.5...v4.4.0) (2019-03-25) 131 | 132 | 133 | ### Features 134 | 135 | * Performance improvements and new async api ([#360](https://github.com/zemirco/json2csv/issues/360)) ([d59dea1](https://github.com/zemirco/json2csv/commit/d59dea1)) 136 | 137 | 138 | 139 | ## [4.3.5](https://github.com/zemirco/json2csv/compare/v4.3.4...v4.3.5) (2019-02-22) 140 | 141 | 142 | ### Bug Fixes 143 | 144 | * audit deps ([3182707](https://github.com/zemirco/json2csv/commit/3182707)) 145 | * unwind of nested fields ([#357](https://github.com/zemirco/json2csv/issues/357)) ([2d69281](https://github.com/zemirco/json2csv/commit/2d69281)) 146 | 147 | 148 | 149 | 150 | ## [4.3.4](https://github.com/zemirco/json2csv/compare/v4.3.3...v4.3.4) (2019-02-11) 151 | 152 | 153 | ### Bug Fixes 154 | 155 | * issue with fields.value function not receiving correct fields ([#353](https://github.com/zemirco/json2csv/issues/353)) ([851c02f](https://github.com/zemirco/json2csv/commit/851c02f)) 156 | 157 | 158 | 159 | 160 | ## [4.3.3](https://github.com/zemirco/json2csv/compare/v4.3.2...v4.3.3) (2019-01-11) 161 | 162 | 163 | ### Bug Fixes 164 | 165 | * audit dep fix ([1ef4bcd](https://github.com/zemirco/json2csv/commit/1ef4bcd)) 166 | * Remove invalid reference to flat ([#347](https://github.com/zemirco/json2csv/issues/347)) ([130ef7d](https://github.com/zemirco/json2csv/commit/130ef7d)) 167 | * Remove preferGlobal from package.json ([#346](https://github.com/zemirco/json2csv/issues/346)) ([2b6ad3a](https://github.com/zemirco/json2csv/commit/2b6ad3a)) 168 | 169 | 170 | 171 | 172 | ## [4.3.2](https://github.com/zemirco/json2csv/compare/v4.3.1...v4.3.2) (2018-12-08) 173 | 174 | 175 | ### Bug Fixes 176 | 177 | * Remove lodash.clonedeep dependency ([#339](https://github.com/zemirco/json2csv/issues/339)) ([d28955a](https://github.com/zemirco/json2csv/commit/d28955a)), closes [#333](https://github.com/zemirco/json2csv/issues/333) 178 | 179 | 180 | 181 | 182 | ## [4.3.1](https://github.com/zemirco/json2csv/compare/v4.3.0...v4.3.1) (2018-11-17) 183 | 184 | 185 | ### Bug Fixes 186 | 187 | * Return correct exit code on error ([#337](https://github.com/zemirco/json2csv/issues/337)) ([a793de5](https://github.com/zemirco/json2csv/commit/a793de5)) 188 | 189 | 190 | 191 | 192 | # [4.3.0](https://github.com/zemirco/json2csv/compare/v4.2.1...v4.3.0) (2018-11-05) 193 | 194 | 195 | ### Bug Fixes 196 | 197 | * Optimize performance around the usage of fields ([#328](https://github.com/zemirco/json2csv/issues/328)) ([d9e4463](https://github.com/zemirco/json2csv/commit/d9e4463)) 198 | * Remove wrong submodule ([#326](https://github.com/zemirco/json2csv/issues/326)) ([6486bb0](https://github.com/zemirco/json2csv/commit/6486bb0)) 199 | 200 | 201 | ### Features 202 | 203 | * Add support for objectMode in the stream API ([#325](https://github.com/zemirco/json2csv/issues/325)) ([8f0ae55](https://github.com/zemirco/json2csv/commit/8f0ae55)) 204 | 205 | 206 | 207 | 208 | ## [4.2.1](https://github.com/zemirco/json2csv/compare/v4.2.0...v4.2.1) (2018-08-06) 209 | 210 | 211 | ### Bug Fixes 212 | 213 | * bug that modifies opts after parsing an object/stream ([#318](https://github.com/zemirco/json2csv/issues/318)) ([f0a4830](https://github.com/zemirco/json2csv/commit/f0a4830)) 214 | * Clean up the flattening separator feature ([#315](https://github.com/zemirco/json2csv/issues/315)) ([ee3d181](https://github.com/zemirco/json2csv/commit/ee3d181)) 215 | 216 | 217 | 218 | 219 | # [4.2.0](https://github.com/zemirco/json2csv/compare/v4.1.6...v4.2.0) (2018-07-31) 220 | 221 | 222 | ### Features 223 | 224 | * Added flattenSeparator option ([#314](https://github.com/zemirco/json2csv/issues/314)) ([5c5de9f](https://github.com/zemirco/json2csv/commit/5c5de9f)) 225 | 226 | 227 | 228 | 229 | ## [4.1.6](https://github.com/zemirco/json2csv/compare/v4.1.5...v4.1.6) (2018-07-12) 230 | 231 | 232 | ### Bug Fixes 233 | 234 | * Update dependencies and remove cli-table2 dependency ([#312](https://github.com/zemirco/json2csv/issues/312)) ([5981ba3](https://github.com/zemirco/json2csv/commit/5981ba3)) 235 | 236 | 237 | 238 | 239 | ## [4.1.5](https://github.com/zemirco/json2csv/compare/v4.1.4...v4.1.5) (2018-06-26) 240 | 241 | 242 | ### Bug Fixes 243 | 244 | * Process stdin as a stream ([#308](https://github.com/zemirco/json2csv/issues/308)) ([2b186b6](https://github.com/zemirco/json2csv/commit/2b186b6)) 245 | 246 | 247 | 248 | 249 | ## [4.1.4](https://github.com/zemirco/json2csv/compare/v4.1.3...v4.1.4) (2018-06-23) 250 | 251 | 252 | ### Bug Fixes 253 | 254 | * don't escape tabs ([#305](https://github.com/zemirco/json2csv/issues/305)) ([a36c8e3](https://github.com/zemirco/json2csv/commit/a36c8e3)) 255 | 256 | 257 | 258 | 259 | ## [4.1.3](https://github.com/zemirco/json2csv/compare/v4.1.2...v4.1.3) (2018-05-23) 260 | 261 | 262 | ### Bug Fixes 263 | 264 | * Escape custom quotes correctly ([#301](https://github.com/zemirco/json2csv/issues/301)) ([7d57208](https://github.com/zemirco/json2csv/commit/7d57208)) 265 | 266 | 267 | 268 | 269 | ## [4.1.2](https://github.com/zemirco/json2csv/compare/v4.1.1...v4.1.2) (2018-04-16) 270 | 271 | 272 | ### Bug Fixes 273 | 274 | * **tests:** Skip bogus pretty print tests only in old node versions ([#290](https://github.com/zemirco/json2csv/issues/290)) ([0f3b885](https://github.com/zemirco/json2csv/commit/0f3b885)) 275 | 276 | 277 | 278 | 279 | ## [4.1.1](https://github.com/zemirco/json2csv/compare/v4.1.0...v4.1.1) (2018-04-16) 280 | 281 | 282 | ### Bug Fixes 283 | 284 | * readme CLI's info ([#289](https://github.com/zemirco/json2csv/issues/289)) ([9fe65b3](https://github.com/zemirco/json2csv/commit/9fe65b3)) 285 | * Add tests and docs to unwind-blank feature ([#287](https://github.com/zemirco/json2csv/issues/287)) ([e3d4a05](https://github.com/zemirco/json2csv/commit/e3d4a05)) 286 | * **perf:** Improve unwind performance and maintainability ([#288](https://github.com/zemirco/json2csv/issues/288)) ([80e496d](https://github.com/zemirco/json2csv/commit/80e496d)) 287 | 288 | 289 | 290 | 291 | ## [4.1.0](https://github.com/zemirco/json2csv/compare/v4.0.4...v4.1.0) (2018-04-16) 292 | 293 | 294 | ### Bug Fixes 295 | 296 | * Avoid redundant deep cloning when unwinding. ([#286](https://github.com/zemirco/json2csv/issues/286)) ([95a6ca9](https://github.com/zemirco/json2csv/commit/95a6ca9)) 297 | 298 | 299 | ### Features 300 | 301 | * Add ability to unwind by blanking out instead of repeating data ([#285](https://github.com/zemirco/json2csv/issues/285)) ([61d9808](https://github.com/zemirco/json2csv/commit/61d9808)) 302 | 303 | 304 | 305 | 306 | ## [4.0.4](https://github.com/zemirco/json2csv/compare/v4.0.3...v4.0.4) (2018-04-10) 307 | 308 | 309 | ### Bug Fixes 310 | 311 | * comment out failing tests ([#283](https://github.com/zemirco/json2csv/issues/283)) ([5b25eaa](https://github.com/zemirco/json2csv/commit/5b25eaa)) 312 | * Support empty array with opts.fields ([#281](https://github.com/zemirco/json2csv/issues/281)) ([eccca89](https://github.com/zemirco/json2csv/commit/eccca89)) 313 | * **tests:** emit correct lines from transform ([#282](https://github.com/zemirco/json2csv/issues/282)) ([2322ddf](https://github.com/zemirco/json2csv/commit/2322ddf)) 314 | 315 | 316 | 317 | 318 | ## [4.0.3](https://github.com/zemirco/json2csv/compare/v4.0.2...v4.0.3) (2018-04-09) 319 | 320 | 321 | ### Bug Fixes 322 | 323 | * error when a field is null and flatten is used ([#274](https://github.com/zemirco/json2csv/issues/274)) ([1349a94](https://github.com/zemirco/json2csv/commit/1349a94)) 324 | * throw error for empty dataset only if fields not specified ([0d8534e](https://github.com/zemirco/json2csv/commit/0d8534e)) 325 | 326 | 327 | 328 | 329 | ## [4.0.2](https://github.com/zemirco/json2csv/compare/v4.0.1...v4.0.2) (2018-03-09) 330 | 331 | 332 | ### Bug Fixes 333 | 334 | * **parser:** RangeError ([#271](https://github.com/zemirco/json2csv/issues/271)) ([c8d5a87](https://github.com/zemirco/json2csv/commit/c8d5a87)) 335 | 336 | 337 | 338 | 339 | ## [4.0.1](https://github.com/zemirco/json2csv/compare/v4.0.0...v4.0.1) (2018-03-05) 340 | 341 | 342 | ### Bug Fixes 343 | 344 | * double quote escaping before new line ([#268](https://github.com/zemirco/json2csv/issues/268)) ([fa991cf](https://github.com/zemirco/json2csv/commit/fa991cf)) 345 | 346 | 347 | 348 | 349 | # [4.0.0](https://github.com/zemirco/json2csv/compare/v4.0.0-alpha.2...v4.0.0) (2018-02-27) 350 | 351 | 352 | ### Bug Fixes 353 | 354 | * Replace webpack with rollup packaging ([#266](https://github.com/zemirco/json2csv/issues/266)) ([a9f8020](https://github.com/zemirco/json2csv/commit/a9f8020)) 355 | 356 | 357 | ### Features 358 | 359 | * Pass transform options through ([#262](https://github.com/zemirco/json2csv/issues/262)) ([650913f](https://github.com/zemirco/json2csv/commit/650913f)) 360 | 361 | 362 | 363 | 364 | # [4.0.0-alpha.2](https://github.com/zemirco/json2csv/compare/v4.0.0-alpha.1...v4.0.0-alpha.2) (2018-02-25) 365 | 366 | 367 | ### Bug Fixes 368 | 369 | * flatten issue with toJSON ([#259](https://github.com/zemirco/json2csv/issues/259)) ([7006d2b](https://github.com/zemirco/json2csv/commit/7006d2b)) 370 | 371 | 372 | 373 | 374 | # [4.0.0-alpha.1](https://github.com/zemirco/json2csv/compare/v4.0.0-alpha.0...v4.0.0-alpha.1) (2018-02-21) 375 | 376 | 377 | ### Bug Fixes 378 | 379 | * Remove TypeScript definition ([#256](https://github.com/zemirco/json2csv/issues/256)) ([4f09694](https://github.com/zemirco/json2csv/commit/4f09694)) 380 | 381 | 382 | 383 | 384 | # [4.0.0-alpha.0](https://github.com/zemirco/json2csv/compare/v3.11.5...v4.0.0-alpha.0) (2018-02-21) 385 | 386 | 387 | ### Bug Fixes 388 | 389 | * Add CLI tests ([#247](https://github.com/zemirco/json2csv/issues/247)) ([bb8126f](https://github.com/zemirco/json2csv/commit/bb8126f)) 390 | * Add excel string to cli and standardize ([#231](https://github.com/zemirco/json2csv/issues/231)) ([421baad](https://github.com/zemirco/json2csv/commit/421baad)) 391 | * Allow passing ldjson input files ([#220](https://github.com/zemirco/json2csv/issues/220)) ([9c861ed](https://github.com/zemirco/json2csv/commit/9c861ed)) 392 | * Avoid throwing an error on elements that can't be stringified (like functions) ([#223](https://github.com/zemirco/json2csv/issues/223)) ([679c687](https://github.com/zemirco/json2csv/commit/679c687)) 393 | * backslash logic ([#222](https://github.com/zemirco/json2csv/issues/222)) ([29e9445](https://github.com/zemirco/json2csv/commit/29e9445)) 394 | * broken stdin input ([#241](https://github.com/zemirco/json2csv/issues/241)) ([6cb407c](https://github.com/zemirco/json2csv/commit/6cb407c)) 395 | * Combine EOL and newLine parameters ([#219](https://github.com/zemirco/json2csv/issues/219)) ([4668a8b](https://github.com/zemirco/json2csv/commit/4668a8b)) 396 | * header flag ([#221](https://github.com/zemirco/json2csv/issues/221)) ([7f7338f](https://github.com/zemirco/json2csv/commit/7f7338f)) 397 | * outdated jsdoc ([#243](https://github.com/zemirco/json2csv/issues/243)) ([efe9888](https://github.com/zemirco/json2csv/commit/efe9888)) 398 | * pretty print issues ([#242](https://github.com/zemirco/json2csv/issues/242)) ([3bd9655](https://github.com/zemirco/json2csv/commit/3bd9655)) 399 | * Process header cells as any other cell ([#244](https://github.com/zemirco/json2csv/issues/244)) ([1fcde13](https://github.com/zemirco/json2csv/commit/1fcde13)) 400 | * Remove callback support ([2096ade](https://github.com/zemirco/json2csv/commit/2096ade)) 401 | * Remove fieldNames ([#232](https://github.com/zemirco/json2csv/issues/232)) ([6cc74b2](https://github.com/zemirco/json2csv/commit/6cc74b2)) 402 | * Remove path-is-absolute dependency ([#225](https://github.com/zemirco/json2csv/issues/225)) ([f71a3df](https://github.com/zemirco/json2csv/commit/f71a3df)) 403 | * Rename hasCSVColumnTitle to noHeader ([#216](https://github.com/zemirco/json2csv/issues/216)) ([f053c8b](https://github.com/zemirco/json2csv/commit/f053c8b)) 404 | * Rename ld-json to ndjson ([#240](https://github.com/zemirco/json2csv/issues/240)) ([24a7893](https://github.com/zemirco/json2csv/commit/24a7893)) 405 | * Rename unwindPath to unwind ([#230](https://github.com/zemirco/json2csv/issues/230)) ([7143bc7](https://github.com/zemirco/json2csv/commit/7143bc7)) 406 | * Streamify pretty print ([#248](https://github.com/zemirco/json2csv/issues/248)) ([fb7ad53](https://github.com/zemirco/json2csv/commit/fb7ad53)) 407 | 408 | 409 | ### Chores 410 | 411 | * Refactor the entire library to ES6 ([#233](https://github.com/zemirco/json2csv/issues/233)) ([dce4d33](https://github.com/zemirco/json2csv/commit/dce4d33)) 412 | 413 | 414 | ### Features 415 | 416 | * add doubleQuote to cli, rename other options to line up with the cli ([5e402dc](https://github.com/zemirco/json2csv/commit/5e402dc)) 417 | * Add fields config option to CLI ([#245](https://github.com/zemirco/json2csv/issues/245)) ([74ef666](https://github.com/zemirco/json2csv/commit/74ef666)) 418 | * Add streaming API ([#235](https://github.com/zemirco/json2csv/issues/235)) ([01ca93e](https://github.com/zemirco/json2csv/commit/01ca93e)) 419 | * Split tests in multiple files ([#246](https://github.com/zemirco/json2csv/issues/246)) ([839de77](https://github.com/zemirco/json2csv/commit/839de77)) 420 | 421 | 422 | ### BREAKING CHANGES 423 | 424 | * Replaces field-list with field-config 425 | * Remove `preserveNewLinesInValues` option, preserve by default 426 | 427 | * Refactor the entire library to ES6 428 | 429 | * Fix PR issues 430 | 431 | * Add strict mode for node 4.X 432 | * Remove fieldNames 433 | 434 | * Increase coverage back to 100% 435 | * callback is no longer available, just return the csv from the json2csv. 436 | 437 | - updated tests 438 | - updated readme 439 | * * Rename unwindPath to unwind 440 | 441 | * Fix field-list in CLI 442 | * newLine removed, eol kept. 443 | * Rename del to delimiter to match the cli flag 444 | * Rename quotes to quote to match the cli flag 445 | 446 | * Remove unused double quotes comment 447 | 448 | * Fix noHeader in CLI 449 | 450 | * Revert "Remove unused double quotes comment" 451 | 452 | This reverts commit 250d3e6ddf3062cbdc1e0174493a37fa21197d8e. 453 | 454 | * Add doubleQuote to CLI 455 | * Rename hasCSVColumnTitle to noHeader to keep in line with the CLI 456 | 457 | 458 | 459 | 460 | ## [3.11.5](https://github.com/zemirco/json2csv/compare/v3.11.4...v3.11.5) (2017-10-23) 461 | 462 | 463 | ### Bug Fixes 464 | 465 | * backslash value not escaped properly ([#202](https://github.com/zemirco/json2csv/issues/202)) ([#204](https://github.com/zemirco/json2csv/issues/204)) ([2cf50f1](https://github.com/zemirco/json2csv/commit/2cf50f1)) 466 | 467 | 468 | 469 | 470 | ## [3.11.4](https://github.com/zemirco/json2csv/compare/v3.11.3...v3.11.4) (2017-10-09) 471 | 472 | 473 | ### Bug Fixes 474 | 475 | * **security:** Update debug to 3.1.0 for security reasons ([9c7cfaa](https://github.com/zemirco/json2csv/commit/9c7cfaa)) 476 | 477 | 478 | 479 | 480 | ## [3.11.3](https://github.com/zemirco/json2csv/compare/v3.11.2...v3.11.3) (2017-10-09) 481 | 482 | 483 | 484 | 485 | ## [3.11.2](https://github.com/zemirco/json2csv/compare/v3.11.1...v3.11.2) (2017-09-13) 486 | 487 | 488 | ### Bug Fixes 489 | 490 | * Remove extra space character in mode withBOM: true [#190](https://github.com/zemirco/json2csv/issues/190) ([#194](https://github.com/zemirco/json2csv/issues/194)) ([e8b6f6b](https://github.com/zemirco/json2csv/commit/e8b6f6b)) 491 | 492 | 493 | 494 | 495 | ## [3.11.1](https://github.com/zemirco/json2csv/compare/v3.11.0...v3.11.1) (2017-08-11) 496 | 497 | 498 | ### Bug Fixes 499 | 500 | * **cli:** pass BOM cli option to function ([#193](https://github.com/zemirco/json2csv/issues/193)) ([70cfdfe](https://github.com/zemirco/json2csv/commit/70cfdfe)) 501 | 502 | 503 | 504 | 505 | # [3.11.0](https://github.com/zemirco/json2csv/compare/v3.10.0...v3.11.0) (2017-08-02) 506 | 507 | 508 | ### Bug Fixes 509 | 510 | * Handle dates without double-escaping ([#189](https://github.com/zemirco/json2csv/issues/189)) ([ff514ba](https://github.com/zemirco/json2csv/commit/ff514ba)) 511 | * unwind parameter in command line mode ([#191](https://github.com/zemirco/json2csv/issues/191)) ([e706c25](https://github.com/zemirco/json2csv/commit/e706c25)) 512 | 513 | 514 | ### Features 515 | 516 | * Added flag to signal if resulting function value should be stringified or not ([#192](https://github.com/zemirco/json2csv/issues/192)) ([aaa6b05](https://github.com/zemirco/json2csv/commit/aaa6b05)) 517 | 518 | 519 | 520 | 521 | # [3.10.0](https://github.com/zemirco/json2csv/compare/v3.9.1...v3.10.0) (2017-07-24) 522 | 523 | 524 | ### Features 525 | 526 | * Add BOM character option ([#187](https://github.com/zemirco/json2csv/issues/187)) ([0c799ca](https://github.com/zemirco/json2csv/commit/0c799ca)) 527 | 528 | 529 | 530 | 531 | ## [3.9.1](https://github.com/zemirco/json2csv/compare/v3.9.0...v3.9.1) (2017-07-14) 532 | 533 | 534 | 535 | 536 | # [3.9.0](https://github.com/zemirco/json2csv/compare/v3.8.0...v3.9.0) (2017-07-11) 537 | 538 | 539 | ### Features 540 | 541 | * Parameter unwindPath for multiple fields ([#174](https://github.com/zemirco/json2csv/issues/174)) ([#183](https://github.com/zemirco/json2csv/issues/183)) ([fbcaa10](https://github.com/zemirco/json2csv/commit/fbcaa10)) 542 | 543 | 544 | 545 | 546 | # [3.8.0](https://github.com/zemirco/json2csv/compare/v3.7.3...v3.8.0) (2017-07-03) 547 | 548 | 549 | ### Bug Fixes 550 | 551 | * **docs:** Add a coma in the ReadMe example ([#181](https://github.com/zemirco/json2csv/issues/181)) ([abeb820](https://github.com/zemirco/json2csv/commit/abeb820)) 552 | 553 | 554 | ### Features 555 | 556 | * Preserve new lines in cells with option `preserveNewLinesInValues` ([#91](https://github.com/zemirco/json2csv/issues/91)) ([#171](https://github.com/zemirco/json2csv/issues/171)) ([187b701](https://github.com/zemirco/json2csv/commit/187b701)) 557 | 558 | 559 | 560 | 561 | ## [3.7.3](https://github.com/zemirco/json2csv/compare/v3.7.1...v3.7.3) (2016-12-08) 562 | 563 | 564 | ### Bug Fixes 565 | 566 | * **jsdoc:** JSDoc Editting ([#155](https://github.com/zemirco/json2csv/issues/155)) ([76075d6](https://github.com/zemirco/json2csv/commit/76075d6)) 567 | * **ts:** Fix type definition ([#154](https://github.com/zemirco/json2csv/issues/154)) ([fae53a1](https://github.com/zemirco/json2csv/commit/fae53a1)) 568 | 569 | 570 | 571 | ## 3.6.3 / 2016-08-17 572 | 573 | * Fix crashing on EPIPE error [#134](https://github.com/zemirco/json2csv/pull/134) 574 | * Add UMD build for browser usage [#136](https://github.com/zemirco/json2csv/pull/136) 575 | * Add docs during prepublish 576 | 577 | ## 3.6.2 / 2016-07-22 578 | 579 | * Remove debugger, see [#132](https://github.com/zemirco/json2csv/pull/132) 580 | * Fix changelog typo 581 | 582 | ## 3.6.1 / 2016-07-12 583 | 584 | * Fix auto-fields returning all available fields, even if not available on the first object, see #104 585 | 586 | ## 3.6.0 / 2016-07-07 587 | 588 | * Make callback optional 589 | * Make callback use `process.nextTick`, so it's not sync 590 | 591 | Thanks @STRML! 592 | 593 | ## 3.5.1 / 2016-06-29 594 | 595 | * Revert [#114](https://github.com/zemirco/json2csv/pull/114), due to more issues 596 | * Update npmignore 597 | * Add a changelog 598 | * Updatee readme 599 | 600 | ## 3.5.0 / 2016-06-21 601 | 602 | * `includeEmptyRows` options added, see [#122](https://github.com/zemirco/json2csv/pull/122) (Thanks @glutentag) 603 | * `-a` or `--include-empty-rows` added for the CLI. 604 | 605 | ## 2.2.1 / 2013-11-10 606 | 607 | * mainly for development e.g. adding code format, update readme.. 608 | 609 | ## 2.2.0 / 2013-11-08 610 | 611 | * not create CSV column title by passing hasCSVColumnTitle: false, into params. 612 | * if field is not exist in object then the field value in CSV will be empty. 613 | * fix data in object format - {...} 614 | 615 | ## 2.1.0 / 2013-06-11 616 | 617 | * quote titles in the first row 618 | 619 | ## 2.0.0 / 2013-03-04 620 | 621 | * err in callback function 622 | 623 | ## 1.3.1 / 2013-02-20 624 | 625 | * fix stdin encoding 626 | 627 | ## 1.3.0 / 2013-02-20 628 | 629 | * support reading from stdin [#9](https://github.com/zeMirco/json2csv/pull/9) 630 | 631 | ## 1.2.0 / 2013-02-20 632 | 633 | * support custom field names [#8](https://github.com/zeMirco/json2csv/pull/8) 634 | 635 | ## 1.1.0 / 2013-01-19 636 | 637 | * add optional custom delimiter 638 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 [Mirco Zeiss](mailto: mirco.zeiss@gmail.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 5 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 6 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 11 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 12 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 13 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 14 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :warning: WARNING: THIS PACKAGE IS ABANDONED 2 | 3 | The code has moved to a [new home](https://github.com/juanjoDiaz/json2csv). 4 | 5 | This repository stays as a the historic home of `json2csv` up until v5. 6 | From v6, the library has been broken into smaller libraries that are now published to NPM independently: 7 | 8 | * **[Plainjs](https://www.npmjs.com/package/@json2csv/plainjs):** Includes the `Parser` API and a new `StreamParser` API which doesn't the conversion in a streaming fashion in pure js. 9 | * **[Node](https://www.npmjs.com/package/@json2csv/node):** Includes the `Node Transform` and `Node Async Parser` APIs for Node users. 10 | * **[WHATWG](https://www.npmjs.com/package/@json2csv/whatwg):** Includes the `WHATWG Transform Stream` and `WHATWG Async Parser` APIs for users of WHATWG streams (browser, Node or Deno). 11 | * **[CLI](https://www.npmjs.com/package/@json2csv/cli):** Includes the `CLI` interface. 12 | * **[Transforms](https://www.npmjs.com/package/@json2csv/transforms):** Includes the built-in `transforms` for json2csv. 13 | * **[Formatters](https://www.npmjs.com/package/@json2csv/formatters):** Includes the built-in `formatters` for json2csv. Formatters are the new way to format data before adding it to the resulting CSV. 14 | 15 | Up-to-date documentation of the library can be found at https://juanjodiaz.github.io/json2csv -------------------------------------------------------------------------------- /bin/json2csv.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const { promisify } = require('util'); 6 | const { createReadStream, createWriteStream, readFile: readFileOrig, writeFile: writeFileOrig } = require('fs'); 7 | const os = require('os'); 8 | const { isAbsolute, join } = require('path'); 9 | const program = require('commander'); 10 | const pkg = require('../package'); 11 | const json2csv = require('../lib/json2csv'); 12 | const parseNdJson = require('./utils/parseNdjson'); 13 | const TablePrinter = require('./utils/TablePrinter'); 14 | 15 | const readFile = promisify(readFileOrig); 16 | const writeFile = promisify(writeFileOrig); 17 | 18 | const { unwind, flatten } = json2csv.transforms; 19 | const { string: stringFormatter, stringExcel: stringExcelFormatter } = json2csv.formatters; 20 | const JSON2CSVParser = json2csv.Parser; 21 | const Json2csvTransform = json2csv.Transform; 22 | 23 | program 24 | .version(pkg.version) 25 | .option('-i, --input ', 'Path and name of the incoming json file. Defaults to stdin.') 26 | .option('-o, --output ', 'Path and name of the resulting csv file. Defaults to stdout.') 27 | .option('-c, --config ', 'Specify a file with a valid JSON configuration.') 28 | .option('-n, --ndjson', 'Treat the input as NewLine-Delimited JSON.') 29 | .option('-s, --no-streaming', 'Process the whole JSON array in memory instead of doing it line by line.') 30 | .option('-f, --fields ', 'List of fields to process. Defaults to field auto-detection.') 31 | .option('-v, --default-value ', 'Default value to use for missing fields.') 32 | .option('-q, --quote ', 'Character(s) to use as quote mark. Defaults to \'"\'.') 33 | .option('-Q, --escaped-quote ', 'Character(s) to use as a escaped quote. Defaults to a double `quote`, \'""\'.') 34 | .option('-d, --delimiter ', 'Character(s) to use as delimiter. Defaults to \',\'.', ',') 35 | .option('-e, --eol ', 'Character(s) to use as End-of-Line for separating rows. Defaults to \'\\n\'.', os.EOL) 36 | .option('-E, --excel-strings','Wraps string data to force Excel to interpret it as string even if it contains a number.') 37 | .option('-H, --no-header', 'Disable the column name header.') 38 | .option('-a, --include-empty-rows', 'Includes empty rows in the resulting CSV output.') 39 | .option('-b, --with-bom', 'Includes BOM character at the beginning of the CSV.') 40 | .option('-p, --pretty', 'Print output as a pretty table. Use only when printing to console.') 41 | // Built-in transforms 42 | .option('--unwind [paths]', 'Creates multiple rows from a single JSON document similar to MongoDB unwind.') 43 | .option('--unwind-blank', 'When unwinding, blank out instead of repeating data. Defaults to false.', false) 44 | .option('--flatten-objects', 'Flatten nested objects. Defaults to false.', false) 45 | .option('--flatten-arrays', 'Flatten nested arrays. Defaults to false.', false) 46 | .option('--flatten-separator ', 'Flattened keys separator. Defaults to \'.\'.', '.') 47 | .parse(process.argv); 48 | 49 | function makePathAbsolute(filePath) { 50 | return (filePath && !isAbsolute(filePath)) 51 | ? join(process.cwd(), filePath) 52 | : filePath; 53 | } 54 | 55 | program.input = makePathAbsolute(program.input); 56 | program.output = makePathAbsolute(program.output); 57 | program.config = makePathAbsolute(program.config); 58 | 59 | // don't fail if piped to e.g. head 60 | /* istanbul ignore next */ 61 | process.stdout.on('error', (error) => { 62 | if (error.code === 'EPIPE') process.exit(1); 63 | }); 64 | 65 | function getInputStream(inputPath) { 66 | if (inputPath) return createReadStream(inputPath, { encoding: 'utf8' }); 67 | 68 | process.stdin.resume(); 69 | process.stdin.setEncoding('utf8'); 70 | return process.stdin; 71 | } 72 | 73 | function getOutputStream(outputPath, config) { 74 | if (outputPath) return createWriteStream(outputPath, { encoding: 'utf8' }); 75 | if (config.pretty) return new TablePrinter(config).writeStream(); 76 | return process.stdout; 77 | } 78 | 79 | async function getInput(inputPath, ndjson) { 80 | if (!inputPath) return getInputFromStdin(); 81 | if (ndjson) return parseNdJson(await readFile(inputPath, 'utf8')); 82 | return require(inputPath); 83 | } 84 | 85 | async function getInputFromStdin() { 86 | return new Promise((resolve, reject) => { 87 | process.stdin.resume(); 88 | process.stdin.setEncoding('utf8'); 89 | 90 | let inputData = ''; 91 | process.stdin.on('data', chunk => (inputData += chunk)); 92 | /* istanbul ignore next */ 93 | process.stdin.on('error', err => reject(new Error('Could not read from stdin', err))); 94 | process.stdin.on('end', () => { 95 | try { 96 | resolve(program.ndjson ? parseNdJson(inputData) : JSON.parse(inputData)); 97 | } catch (err) { 98 | reject(new Error('Invalid data received from stdin', err)); 99 | } 100 | }); 101 | }); 102 | } 103 | 104 | async function processOutput(outputPath, csv, config) { 105 | if (!outputPath) { 106 | // eslint-disable-next-line no-console 107 | config.pretty ? (new TablePrinter(config)).printCSV(csv) : console.log(csv); 108 | return; 109 | } 110 | 111 | await writeFile(outputPath, csv); 112 | } 113 | 114 | async function processInMemory(config, opts) { 115 | const input = await getInput(program.input, config.ndjson); 116 | const output = new JSON2CSVParser(opts).parse(input); 117 | await processOutput(program.output, output, config); 118 | } 119 | 120 | async function processStream(config, opts) { 121 | const input = getInputStream(program.input); 122 | const transform = new Json2csvTransform(opts); 123 | const output = getOutputStream(program.output, config); 124 | 125 | await new Promise((resolve, reject) => { 126 | input.pipe(transform).pipe(output); 127 | input.on('error', reject); 128 | transform.on('error', reject); 129 | output.on('error', reject) 130 | .on('finish', resolve); 131 | }); 132 | } 133 | 134 | (async (program) => { 135 | try { 136 | const config = Object.assign({}, program.config ? require(program.config) : {}, program); 137 | 138 | const transforms = []; 139 | if (config.unwind) { 140 | transforms.push(unwind({ 141 | paths: config.unwind === true ? undefined : config.unwind.split(','), 142 | blankOut: config.unwindBlank 143 | })); 144 | } 145 | 146 | if (config.flattenObjects || config.flattenArrays) { 147 | transforms.push(flatten({ 148 | objects: config.flattenObjects, 149 | arrays: config.flattenArrays, 150 | separator: config.flattenSeparator 151 | })); 152 | } 153 | 154 | const formatters = { 155 | string: config.excelStrings 156 | ? stringExcelFormatter 157 | : stringFormatter({ 158 | quote: config.quote, 159 | escapedQuote: config.escapedQuote, 160 | }) 161 | }; 162 | 163 | const opts = { 164 | transforms, 165 | formatters, 166 | fields: config.fields 167 | ? (Array.isArray(config.fields) ? config.fields : config.fields.split(',')) 168 | : config.fields, 169 | defaultValue: config.defaultValue, 170 | delimiter: config.delimiter, 171 | eol: config.eol, 172 | header: config.header, 173 | includeEmptyRows: config.includeEmptyRows, 174 | withBOM: config.withBom, 175 | ndjson: config.ndjson 176 | }; 177 | 178 | await (config.streaming ? processStream : processInMemory)(config, opts); 179 | } catch(err) { 180 | let processedError = err; 181 | if (program.input && err.message.includes(program.input)) { 182 | processedError = new Error(`Invalid input file. (${err.message})`); 183 | } else if (program.output && err.message.includes(program.output)) { 184 | processedError = new Error(`Invalid output file. (${err.message})`); 185 | } else if (program.config && err.message.includes(program.config)) { 186 | processedError = new Error(`Invalid config file. (${err.message})`); 187 | } 188 | // eslint-disable-next-line no-console 189 | console.error(processedError); 190 | process.exit(1); 191 | } 192 | })(program); 193 | -------------------------------------------------------------------------------- /bin/utils/TablePrinter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Writable } = require('stream'); 4 | 5 | const MIN_CELL_WIDTH = 15; 6 | 7 | class TablePrinter { 8 | constructor(opts) { 9 | this.opts = opts; 10 | this._hasWritten = false; 11 | } 12 | 13 | push(csv) { 14 | const lines = csv.split(this.opts.eol); 15 | 16 | if (!lines.length) return; 17 | 18 | if (!this._hasWritten) this.setColumnWidths(lines[0]); 19 | 20 | const top = this._hasWritten ? this.middleLine : this.topLine; 21 | this.print(top, lines); 22 | this._hasWritten = true; 23 | } 24 | 25 | end(csv) { 26 | let lines = csv.split(this.opts.eol); 27 | if (!this._hasWritten) this.setColumnWidths(lines[0]); 28 | const top = this._hasWritten ? this.middleLine : this.topLine; 29 | this.print(top, lines, this.bottomLine); 30 | } 31 | 32 | printCSV(csv) { 33 | this.end(csv); 34 | } 35 | 36 | setColumnWidths(line) { 37 | this.colWidths = line 38 | .split(this.opts.delimiter) 39 | .map(elem => Math.max(elem.length * 2, MIN_CELL_WIDTH)); 40 | 41 | this.topLine = `┌${this.colWidths.map(i => '─'.repeat(i)).join('┬')}┐`; 42 | this.middleLine = `├${this.colWidths.map(i => '─'.repeat(i)).join('┼')}┤`; 43 | this.bottomLine = `└${this.colWidths.map(i => '─'.repeat(i)).join('┴')}┘`; 44 | } 45 | 46 | print(top, lines, bottom) { 47 | const table = `${top}\n` 48 | + lines 49 | .map(row => this.formatRow(row)) 50 | .join(`\n${this.middleLine}\n`) 51 | + (bottom ? `\n${bottom}` : ''); 52 | 53 | // eslint-disable-next-line no-console 54 | console.log(table); 55 | } 56 | 57 | formatRow(row) { 58 | const wrappedRow = row 59 | .split(this.opts.delimiter) 60 | .map((cell, i) => cell.match(new RegExp(`(.{1,${this.colWidths[i] - 2}})`, 'g')) || []); 61 | 62 | const height = wrappedRow.reduce((acc, cell) => Math.max(acc, cell.length), 0); 63 | 64 | const processedCells = wrappedRow 65 | .map((cell, i) => this.formatCell(cell, height, this.colWidths[i])); 66 | 67 | return Array(height).fill('') 68 | .map((_, i) => `│${processedCells.map(cell => cell[i]).join('│')}│`) 69 | .join('\n'); 70 | } 71 | 72 | formatCell(content, heigth, width) { 73 | const paddedContent = this.padCellHorizontally(content, width); 74 | return this.padCellVertically(paddedContent, heigth, width); 75 | } 76 | 77 | padCellVertically(content, heigth, width) { 78 | const vertPad = heigth - content.length; 79 | const vertPadTop = Math.ceil(vertPad / 2); 80 | const vertPadBottom = vertPad - vertPadTop; 81 | const emptyLine = ' '.repeat(width); 82 | 83 | return [ 84 | ...Array(vertPadTop).fill(emptyLine), 85 | ...content, 86 | ...Array(vertPadBottom).fill(emptyLine) 87 | ]; 88 | } 89 | 90 | padCellHorizontally(content, width) { 91 | return content.map((line) => { 92 | const horPad = width - line.length - 2; 93 | return ` ${line}${' '.repeat(horPad)} `; 94 | }); 95 | } 96 | 97 | writeStream() { 98 | let csv = ''; 99 | const table = this; 100 | return new Writable({ 101 | write(chunk, encoding, callback) { 102 | csv += chunk.toString(); 103 | const index = csv.lastIndexOf(table.opts.eol); 104 | let lines = csv.substring(0, index); 105 | csv = csv.substring(index + 1); 106 | 107 | if (lines) table.push(lines); 108 | callback(); 109 | }, 110 | final() { 111 | table.end(csv); 112 | } 113 | }); 114 | } 115 | } 116 | 117 | module.exports = TablePrinter; 118 | -------------------------------------------------------------------------------- /bin/utils/parseNdjson.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function parseNdJson(input) { 4 | return input 5 | .split('\n') 6 | .map(line => line.trim()) 7 | .filter(line => line !== '') 8 | .map(line=> JSON.parse(line)); 9 | } 10 | 11 | module.exports = parseNdJson; 12 | -------------------------------------------------------------------------------- /docs/cli-examples.md: -------------------------------------------------------------------------------- 1 | # CLI Examples 2 | 3 | All examples use this example [input file](https://github.com/zemirco/json2csv/blob/master/test/fixtures/json/default.json). 4 | 5 | ## Input file and specify fields 6 | 7 | ```sh 8 | $ json2csv -i input.json -f carModel,price,color 9 | carModel,price,color 10 | "Audi",10000,"blue" 11 | "BMW",15000,"red" 12 | "Mercedes",20000,"yellow" 13 | "Porsche",30000,"green" 14 | ``` 15 | 16 | ## Input file, specify fields and use pretty logging 17 | 18 | ```sh 19 | $ json2csv -i input.json -f carModel,price,color -p 20 | ``` 21 | 22 | ![Screenshot](https://s3.amazonaws.com/zeMirco/github/json2csv/json2csv-pretty.png) 23 | 24 | ## Generating CSV containing only specific fields 25 | 26 | ```sh 27 | $ json2csv -i input.json -f carModel,price,color -o out.csv 28 | $ cat out.csv 29 | carModel,price,color 30 | "Audi",10000,"blue" 31 | "BMW",15000,"red" 32 | "Mercedes",20000,"yellow" 33 | "Porsche",30000,"green" 34 | ``` 35 | 36 | Same result will be obtained passing the fields config as a file. 37 | 38 | ```sh 39 | $ json2csv -i input.json -c fieldsConfig.json -o out.csv 40 | ``` 41 | 42 | where the file `fieldsConfig.json` contains 43 | 44 | ```json 45 | ["carModel", "price", "color"] 46 | ``` 47 | 48 | ## Read input from stdin 49 | 50 | ```sh 51 | $ json2csv -f price 52 | [{"price":1000},{"price":2000}] 53 | ``` 54 | 55 | Hit Enter and afterwards CTRL + D to end reading from stdin. The terminal should show 56 | 57 | ```sh 58 | price 59 | 1000 60 | 2000 61 | ``` 62 | 63 | ## Appending to existing CSV 64 | 65 | Sometimes you want to add some additional rows with the same columns. 66 | This is how you can do that. 67 | 68 | ```sh 69 | # Initial creation of csv with headings 70 | $ json2csv -i test.json -f name,version > test.csv 71 | # Append additional rows 72 | $ json2csv -i test.json -f name,version --no-header >> test.csv 73 | ``` 74 | -------------------------------------------------------------------------------- /docs/parser-examples.md: -------------------------------------------------------------------------------- 1 | # JavaScript Module Examples 2 | 3 | Most of the examples in this section use the same input data: 4 | 5 | ```js 6 | const myCars = [ 7 | { 8 | car: 'Audi', 9 | price: 40000, 10 | color: 'blue', 11 | }, 12 | { 13 | car: 'BMW', 14 | price: 35000, 15 | color: 'black', 16 | }, 17 | { 18 | car: 'Porsche', 19 | price: 60000, 20 | color: 'green', 21 | }, 22 | ]; 23 | ``` 24 | 25 | ## Example `fields` option 26 | 27 | ```js 28 | { 29 | fields: [ 30 | // Supports pathname -> pathvalue 31 | 'simplepath', // equivalent to {value:'simplepath'} 32 | 'path.to.value' // also equivalent to {value:'path.to.value'} 33 | 34 | // Supports label -> simple path 35 | { 36 | label: 'some label', // Optional, column will be labeled 'path.to.something' if not defined) 37 | value: 'path.to.something', // data.path.to.something 38 | default: 'NULL' // default if value is not found (Optional, overrides `defaultValue` for column) 39 | }, 40 | 41 | // Supports label -> derived value 42 | { 43 | label: 'some label', // Optional, column will be labeled with the function name or empty if the function is anonymous 44 | value: (row, field) => row[field.label].toLowerCase() ||field.default, 45 | default: 'NULL' // default if value function returns null or undefined 46 | }, 47 | 48 | // Supports label -> derived value 49 | { 50 | value: (row) => row.arrayField.join(',') 51 | }, 52 | 53 | // Supports label -> derived value 54 | { 55 | value: (row) => `"${row.arrayField.join(',')}"` 56 | }, 57 | ] 58 | } 59 | ``` 60 | 61 | ## Default parsing 62 | 63 | ```js 64 | const { Parser } = require('json2csv'); 65 | 66 | const json2csvParser = new Parser(); 67 | const csv = json2csvParser.parse(myCars); 68 | 69 | console.log(csv); 70 | ``` 71 | 72 | will output to console 73 | 74 | ``` 75 | "car","price","color" 76 | "Audi",40000,"blue" 77 | "BMW",35000,"black" 78 | "Porsche",60000,"green" 79 | ``` 80 | 81 | ## Specify fields to parse 82 | 83 | ```js 84 | const { Parser } = require('json2csv'); 85 | const fields = ['car', 'color']; 86 | 87 | const json2csvParser = new Parser({ fields }); 88 | const csv = json2csvParser.parse(myCars); 89 | 90 | console.log(csv); 91 | ``` 92 | 93 | will output to console 94 | 95 | ``` 96 | "car","color" 97 | "Audi","blue" 98 | "BMW","black" 99 | "Porsche","green" 100 | ``` 101 | 102 | ## Use custom headers 103 | 104 | ```js 105 | const { Parser } = require('json2csv'); 106 | 107 | const fields = [ 108 | { 109 | label: 'Car Name', 110 | value: 'car', 111 | }, 112 | { 113 | label: 'Price USD', 114 | value: 'price', 115 | }, 116 | ]; 117 | 118 | const json2csvParser = new Parser({ fields }); 119 | const csv = json2csvParser.parse(myCars); 120 | 121 | console.log(csv); 122 | ``` 123 | 124 | will output to console 125 | 126 | ``` 127 | "Car Name","Price USD" 128 | "Audi",40000 129 | "BMW",35000 130 | "Porsche",60000 131 | ``` 132 | 133 | ## Parse nested properties 134 | 135 | You can specify nested properties using dot notation. 136 | 137 | ```js 138 | const { Parser } = require('json2csv'); 139 | 140 | const myCars = [ 141 | { 142 | car: { make: 'Audi', model: 'A3' }, 143 | price: 40000, 144 | color: 'blue', 145 | }, 146 | { 147 | car: { make: 'BMW', model: 'F20' }, 148 | price: 35000, 149 | color: 'black', 150 | }, 151 | { 152 | car: { make: 'Porsche', model: '9PA AF1' }, 153 | price: 60000, 154 | color: 'green', 155 | }, 156 | ]; 157 | 158 | const fields = ['car.make', 'car.model', 'price', 'color']; 159 | 160 | const json2csvParser = new Parser({ fields }); 161 | const csv = json2csvParser.parse(myCars); 162 | 163 | console.log(csv); 164 | ``` 165 | 166 | will output to console 167 | 168 | ``` 169 | "car.make", "car.model", "price", "color" 170 | "Audi", "A3", 40000, "blue" 171 | "BMW", "F20", 35000, "black" 172 | "Porsche", "9PA AF1", 60000, "green" 173 | ``` 174 | 175 | ## Use a custom delimiter 176 | 177 | For example, to create tsv files 178 | 179 | ```js 180 | const { Parser } = require('json2csv'); 181 | 182 | const json2csvParser = new Parser({ delimiter: '\t' }); 183 | const tsv = json2csvParser.parse(myCars); 184 | 185 | console.log(tsv); 186 | ``` 187 | 188 | will output to console 189 | 190 | ``` 191 | "car" "price" "color" 192 | "Audi" 10000 "blue" 193 | "BMW" 15000 "red" 194 | "Mercedes" 20000 "yellow" 195 | "Porsche" 30000 "green" 196 | ``` 197 | 198 | If no delimiter is specified, the default `,` is used. 199 | 200 | ## Use custom formatting 201 | 202 | For example, you could use `*` as quotes and format numbers to always have 2 decimals and use `,` as separator. 203 | To avoid conflict between the number separator and the CSV delimiter, we can use a custom delimiter again. 204 | 205 | ```js 206 | const { 207 | Parser, 208 | formatters: { string: stringFormatter, number: numberFormatter }, 209 | } = require('json2csv'); 210 | 211 | const json2csvParser = new Parser({ 212 | delimiter: ';', 213 | formatters: { 214 | string: stringFormatter({ quote: '*' }), 215 | number: numberFormatter({ separator: ',', decimals: 2 }), 216 | }, 217 | }); 218 | const csv = json2csvParser.parse(myCars); 219 | 220 | console.log(csv); 221 | ``` 222 | 223 | will output to console 224 | 225 | ``` 226 | *car*;*price*;*color* 227 | *Audi*;40000,00;*blue* 228 | *BMW*;35000,00;*black* 229 | *Porsche*;60000,00;*green* 230 | ``` 231 | 232 | ## Format the headers differently 233 | 234 | For example, you can not quote the headers. 235 | 236 | ```js 237 | const { Parser } = require('json2csv'); 238 | 239 | const json2csvParser = new Parser({ 240 | formatters: { 241 | header: stringFormatter({ quote: '' }, 242 | }, 243 | }); 244 | const csv = json2csvParser.parse(myCars); 245 | 246 | console.log(csv); 247 | ``` 248 | 249 | will output to console 250 | 251 | ``` 252 | car, price, color 253 | "Audi",40000,"blue" 254 | "BMW",35000,"black" 255 | "Porsche",60000,"green" 256 | ``` 257 | 258 | ## Unwind arrays 259 | 260 | You can unwind arrays similar to MongoDB's \$unwind operation using the `unwind` transform. 261 | 262 | ```js 263 | const { 264 | Parser, 265 | transforms: { unwind }, 266 | } = require('json2csv'); 267 | 268 | const myCars = [ 269 | { 270 | carModel: 'Audi', 271 | price: 0, 272 | colors: ['blue', 'green', 'yellow'], 273 | }, 274 | { 275 | carModel: 'BMW', 276 | price: 15000, 277 | colors: ['red', 'blue'], 278 | }, 279 | { 280 | carModel: 'Mercedes', 281 | price: 20000, 282 | colors: 'yellow', 283 | }, 284 | { 285 | carModel: 'Porsche', 286 | price: 30000, 287 | colors: ['green', 'teal', 'aqua'], 288 | }, 289 | ]; 290 | 291 | const fields = ['carModel', 'price', 'colors']; 292 | const transforms = [unwind({ paths: ['colors'] })]; 293 | 294 | const json2csvParser = new Parser({ fields, transforms }); 295 | const csv = json2csvParser.parse(myCars); 296 | 297 | console.log(csv); 298 | ``` 299 | 300 | will output to console 301 | 302 | ``` 303 | "carModel","price","colors" 304 | "Audi",0,"blue" 305 | "Audi",0,"green" 306 | "Audi",0,"yellow" 307 | "BMW",15000,"red" 308 | "BMW",15000,"blue" 309 | "Mercedes",20000,"yellow" 310 | "Porsche",30000,"green" 311 | "Porsche",30000,"teal" 312 | "Porsche",30000,"aqua" 313 | ``` 314 | 315 | ## Unwind of nested arrays 316 | 317 | You can also unwind arrays multiple times or with nested objects. 318 | 319 | ```js 320 | const { 321 | Parser, 322 | transforms: { unwind }, 323 | } = require('json2csv'); 324 | 325 | const myCars = [ 326 | { 327 | carModel: 'BMW', 328 | price: 15000, 329 | items: [ 330 | { 331 | name: 'airbag', 332 | color: 'white', 333 | }, 334 | { 335 | name: 'dashboard', 336 | color: 'black', 337 | }, 338 | ], 339 | }, 340 | { 341 | carModel: 'Porsche', 342 | price: 30000, 343 | items: [ 344 | { 345 | name: 'airbag', 346 | items: [ 347 | { 348 | position: 'left', 349 | color: 'white', 350 | }, 351 | { 352 | position: 'right', 353 | color: 'gray', 354 | }, 355 | ], 356 | }, 357 | { 358 | name: 'dashboard', 359 | items: [ 360 | { 361 | position: 'left', 362 | color: 'gray', 363 | }, 364 | { 365 | position: 'right', 366 | color: 'black', 367 | }, 368 | ], 369 | }, 370 | ], 371 | }, 372 | ]; 373 | 374 | const fields = [ 375 | 'carModel', 376 | 'price', 377 | 'items.name', 378 | 'items.color', 379 | 'items.items.position', 380 | 'items.items.color', 381 | ]; 382 | const transforms = [unwind({ paths: ['items', 'items.items'] })]; 383 | const json2csvParser = new Parser({ fields, transforms }); 384 | const csv = json2csvParser.parse(myCars); 385 | 386 | console.log(csv); 387 | ``` 388 | 389 | will output to console 390 | 391 | ``` 392 | "carModel","price","items.name","items.color","items.items.position","items.items.color" 393 | "BMW",15000,"airbag","white",, 394 | "BMW",15000,"dashboard","black",, 395 | "Porsche",30000,"airbag",,"left","white" 396 | "Porsche",30000,"airbag",,"right","gray" 397 | "Porsche",30000,"dashboard",,"left","gray" 398 | "Porsche",30000,"dashboard",,"right","black" 399 | ``` 400 | 401 | ## Unwind array blanking the repeated fields 402 | 403 | You can also unwind arrays blanking the repeated fields. 404 | 405 | ```js 406 | const { 407 | Parser, 408 | transforms: { unwind }, 409 | } = require('json2csv'); 410 | 411 | const myCars = [ 412 | { 413 | carModel: 'BMW', 414 | price: 15000, 415 | items: [ 416 | { 417 | name: 'airbag', 418 | color: 'white', 419 | }, 420 | { 421 | name: 'dashboard', 422 | color: 'black', 423 | }, 424 | ], 425 | }, 426 | { 427 | carModel: 'Porsche', 428 | price: 30000, 429 | items: [ 430 | { 431 | name: 'airbag', 432 | items: [ 433 | { 434 | position: 'left', 435 | color: 'white', 436 | }, 437 | { 438 | position: 'right', 439 | color: 'gray', 440 | }, 441 | ], 442 | }, 443 | { 444 | name: 'dashboard', 445 | items: [ 446 | { 447 | position: 'left', 448 | color: 'gray', 449 | }, 450 | { 451 | position: 'right', 452 | color: 'black', 453 | }, 454 | ], 455 | }, 456 | ], 457 | }, 458 | ]; 459 | 460 | const fields = [ 461 | 'carModel', 462 | 'price', 463 | 'items.name', 464 | 'items.color', 465 | 'items.items.position', 466 | 'items.items.color', 467 | ]; 468 | const transforms = [ 469 | unwind({ paths: ['items', 'items.items'], blankOut: true }), 470 | ]; 471 | 472 | const json2csvParser = new Parser({ fields, transforms }); 473 | const csv = json2csvParser.parse(myCars); 474 | 475 | console.log(csv); 476 | ``` 477 | 478 | will output to console 479 | 480 | ``` 481 | "carModel","price","items.name","items.color","items.items.position","items.items.color" 482 | "BMW",15000,"airbag","white",, 483 | ,,"dashboard","black",, 484 | "Porsche",30000,"airbag",,"left","white" 485 | ,,,,"right","gray" 486 | ,,"dashboard",,"left","gray" 487 | ,,,,"right","black" 488 | ``` 489 | -------------------------------------------------------------------------------- /lib/JSON2CSVAsyncParser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Readable } = require('stream'); 4 | const JSON2CSVTransform = require('./JSON2CSVTransform'); 5 | 6 | class JSON2CSVAsyncParser { 7 | constructor(opts, transformOpts) { 8 | this.opts = opts; 9 | this.transformOpts = transformOpts; 10 | } 11 | 12 | /** 13 | * Main function that converts json to csv. 14 | * 15 | * @param {Stream|Array|Object} data Array of JSON objects to be converted to CSV 16 | * @returns {Stream} A stream producing the CSV formated data as a string 17 | */ 18 | parse(data) { 19 | if (typeof data === 'string' || ArrayBuffer.isView(data)) { 20 | data = Readable.from(data, { objectMode: false }); 21 | } else if (Array.isArray(data)) { 22 | data = Readable.from(data.filter(item => item !== null)); 23 | } else if (typeof data === 'object' && !(data instanceof Readable)) { 24 | data = Readable.from([data]); 25 | } 26 | 27 | if (!(data instanceof Readable)) { 28 | throw new Error('Data should be a JSON object, JSON array, typed array, string or stream'); 29 | } 30 | 31 | return data.pipe(new JSON2CSVTransform(this.opts, { objectMode: data.readableObjectMode, ...this.transformOpts })); 32 | } 33 | } 34 | 35 | module.exports = JSON2CSVAsyncParser; 36 | -------------------------------------------------------------------------------- /lib/JSON2CSVBase.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const os = require('os'); 4 | const lodashGet = require('lodash.get'); 5 | const { getProp } = require('./utils'); 6 | const defaultFormatter = require('./formatters/default'); 7 | const numberFormatterCtor = require('./formatters/number') 8 | const stringFormatterCtor = require('./formatters/string'); 9 | const symbolFormatterCtor = require('./formatters/symbol'); 10 | const objectFormatterCtor = require('./formatters/object'); 11 | 12 | class JSON2CSVBase { 13 | constructor(opts) { 14 | this.opts = this.preprocessOpts(opts); 15 | } 16 | 17 | /** 18 | * Check passing opts and set defaults. 19 | * 20 | * @param {Json2CsvOptions} opts Options object containing fields, 21 | * delimiter, default value, quote mark, header, etc. 22 | */ 23 | preprocessOpts(opts) { 24 | const processedOpts = Object.assign({}, opts); 25 | 26 | if (processedOpts.fields) { 27 | processedOpts.fields = this.preprocessFieldsInfo(processedOpts.fields, processedOpts.defaultValue); 28 | } 29 | 30 | processedOpts.transforms = processedOpts.transforms || []; 31 | 32 | const stringFormatter = (processedOpts.formatters && processedOpts.formatters['string']) || stringFormatterCtor(); 33 | const objectFormatter = objectFormatterCtor({ stringFormatter }); 34 | const defaultFormatters = { 35 | header: stringFormatter, 36 | undefined: defaultFormatter, 37 | boolean: defaultFormatter, 38 | number: numberFormatterCtor(), 39 | bigint: defaultFormatter, 40 | string: stringFormatter, 41 | symbol: symbolFormatterCtor({ stringFormatter }), 42 | function: objectFormatter, 43 | object: objectFormatter 44 | }; 45 | 46 | processedOpts.formatters = { 47 | ...defaultFormatters, 48 | ...processedOpts.formatters, 49 | }; 50 | 51 | processedOpts.delimiter = processedOpts.delimiter || ','; 52 | processedOpts.eol = processedOpts.eol || os.EOL; 53 | processedOpts.header = processedOpts.header !== false; 54 | processedOpts.includeEmptyRows = processedOpts.includeEmptyRows || false; 55 | processedOpts.withBOM = processedOpts.withBOM || false; 56 | 57 | return processedOpts; 58 | } 59 | 60 | /** 61 | * Check and normalize the fields configuration. 62 | * 63 | * @param {(string|object)[]} fields Fields configuration provided by the user 64 | * or inferred from the data 65 | * @returns {object[]} preprocessed FieldsInfo array 66 | */ 67 | preprocessFieldsInfo(fields, globalDefaultValue) { 68 | return fields.map((fieldInfo) => { 69 | if (typeof fieldInfo === 'string') { 70 | return { 71 | label: fieldInfo, 72 | value: (fieldInfo.includes('.') || fieldInfo.includes('[')) 73 | ? row => lodashGet(row, fieldInfo, globalDefaultValue) 74 | : row => getProp(row, fieldInfo, globalDefaultValue), 75 | }; 76 | } 77 | 78 | if (typeof fieldInfo === 'object') { 79 | const defaultValue = 'default' in fieldInfo 80 | ? fieldInfo.default 81 | : globalDefaultValue; 82 | 83 | if (typeof fieldInfo.value === 'string') { 84 | return { 85 | label: fieldInfo.label || fieldInfo.value, 86 | value: (fieldInfo.value.includes('.') || fieldInfo.value.includes('[')) 87 | ? row => lodashGet(row, fieldInfo.value, defaultValue) 88 | : row => getProp(row, fieldInfo.value, defaultValue), 89 | }; 90 | } 91 | 92 | if (typeof fieldInfo.value === 'function') { 93 | const label = fieldInfo.label || fieldInfo.value.name || ''; 94 | const field = { label, default: defaultValue }; 95 | return { 96 | label, 97 | value(row) { 98 | const value = fieldInfo.value(row, field); 99 | return (value === null || value === undefined) 100 | ? defaultValue 101 | : value; 102 | }, 103 | } 104 | } 105 | } 106 | 107 | throw new Error('Invalid field info option. ' + JSON.stringify(fieldInfo)); 108 | }); 109 | } 110 | 111 | /** 112 | * Create the title row with all the provided fields as column headings 113 | * 114 | * @returns {String} titles as a string 115 | */ 116 | getHeader(fields) { 117 | return fields 118 | .map(fieldInfo => this.opts.formatters.header(fieldInfo.label)) 119 | .join(this.opts.delimiter); 120 | } 121 | 122 | /** 123 | * Preprocess each object according to the given transforms (unwind, flatten, etc.). 124 | * @param {Object} row JSON object to be converted in a CSV row 125 | */ 126 | preprocessRow(row) { 127 | return this.opts.transforms.reduce((rows, transform) => 128 | rows.flatMap(row => transform(row)), 129 | [row] 130 | ); 131 | } 132 | 133 | /** 134 | * Create the content of a specific CSV row 135 | * 136 | * @param {Object} row JSON object to be converted in a CSV row 137 | * @returns {String} CSV string (row) 138 | */ 139 | processRow(row, fields) { 140 | if (!row) { 141 | return undefined; 142 | } 143 | 144 | const processedRow = fields.map(fieldInfo => this.processCell(row, fieldInfo)); 145 | 146 | if (!this.opts.includeEmptyRows && processedRow.every(field => field === '')) { 147 | return undefined; 148 | } 149 | 150 | return processedRow.join(this.opts.delimiter); 151 | } 152 | 153 | /** 154 | * Create the content of a specfic CSV row cell 155 | * 156 | * @param {Object} row JSON object representing the CSV row that the cell belongs to 157 | * @param {FieldInfo} fieldInfo Details of the field to process to be a CSV cell 158 | * @returns {String} CSV string (cell) 159 | */ 160 | processCell(row, fieldInfo) { 161 | return this.processValue(fieldInfo.value(row)); 162 | } 163 | 164 | /** 165 | * Create the content of a specfic CSV row cell 166 | * 167 | * @param {Any} value Value to be included in a CSV cell 168 | * @returns {String} Value stringified and processed 169 | */ 170 | processValue(value) { 171 | return this.opts.formatters[typeof value](value); 172 | } 173 | } 174 | 175 | module.exports = JSON2CSVBase; 176 | -------------------------------------------------------------------------------- /lib/JSON2CSVParser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const JSON2CSVBase = require('./JSON2CSVBase'); 4 | 5 | class JSON2CSVParser extends JSON2CSVBase { 6 | constructor(opts) { 7 | super(opts); 8 | } 9 | /** 10 | * Main function that converts json to csv. 11 | * 12 | * @param {Array|Object} data Array of JSON objects to be converted to CSV 13 | * @returns {String} The CSV formated data as a string 14 | */ 15 | parse(data) { 16 | const processedData = this.preprocessData(data, this.opts.fields); 17 | 18 | const fields = this.opts.fields || this.preprocessFieldsInfo(processedData 19 | .reduce((fields, item) => { 20 | Object.keys(item).forEach((field) => { 21 | if (!fields.includes(field)) { 22 | fields.push(field) 23 | } 24 | }); 25 | 26 | return fields 27 | }, [])); 28 | 29 | const header = this.opts.header ? this.getHeader(fields) : ''; 30 | const rows = this.processData(processedData, fields); 31 | const csv = (this.opts.withBOM ? '\ufeff' : '') 32 | + header 33 | + ((header && rows) ? this.opts.eol : '') 34 | + rows; 35 | 36 | return csv; 37 | } 38 | 39 | /** 40 | * Preprocess the data according to the give opts (unwind, flatten, etc.) 41 | and calculate the fields and field names if they are not provided. 42 | * 43 | * @param {Array|Object} data Array or object to be converted to CSV 44 | */ 45 | preprocessData(data, fields) { 46 | const processedData = Array.isArray(data) ? data : [data]; 47 | 48 | if (!fields && (processedData.length === 0 || typeof processedData[0] !== 'object')) { 49 | throw new Error('Data should not be empty or the "fields" option should be included'); 50 | } 51 | 52 | if (this.opts.transforms.length === 0) return processedData; 53 | 54 | return processedData 55 | .flatMap(row => this.preprocessRow(row)); 56 | } 57 | 58 | /** 59 | * Create the content row by row below the header 60 | * 61 | * @param {Array} data Array of JSON objects to be converted to CSV 62 | * @returns {String} CSV string (body) 63 | */ 64 | processData(data, fields) { 65 | return data 66 | .map(row => this.processRow(row, fields)) 67 | .filter(row => row) // Filter empty rows 68 | .join(this.opts.eol); 69 | } 70 | } 71 | 72 | module.exports = JSON2CSVParser; 73 | -------------------------------------------------------------------------------- /lib/JSON2CSVStreamParser.js: -------------------------------------------------------------------------------- 1 | const JSON2CSVBase = require("./JSON2CSVBase"); 2 | const { Tokenizer, TokenParser, TokenType } = require('@streamparser/json'); 3 | 4 | class JSON2CSVStreamParser extends JSON2CSVBase { 5 | constructor(opts, asyncOpts) { 6 | super(opts); 7 | this.opts = this.preprocessOpts(opts); 8 | this.initTokenizer(opts, asyncOpts); 9 | if (this.opts.fields) this.preprocessFieldsInfo(this.opts.fields); 10 | } 11 | 12 | initTokenizer(opts = {}, asyncOpts = {}) { 13 | if (asyncOpts.objectMode) { 14 | this.tokenizer = this.getObjectModeTokenizer(); 15 | return; 16 | } 17 | 18 | if (opts.ndjson) { 19 | this.tokenizer = this.getNdJsonTokenizer(asyncOpts); 20 | return; 21 | } 22 | 23 | this.tokenizer = this.getBinaryModeTokenizer(asyncOpts); 24 | return; 25 | } 26 | 27 | getObjectModeTokenizer() { 28 | return { 29 | write: (data) => this.pushLine(data), 30 | end: () => { 31 | this.pushHeaderIfNotWritten(); 32 | this.onEnd(); 33 | }, 34 | }; 35 | } 36 | 37 | configureCallbacks(tokenizer, tokenParser) { 38 | tokenizer.onToken = tokenParser.write.bind(this.tokenParser); 39 | tokenizer.onError = (err) => this.onError(err); 40 | tokenizer.onEnd = () => { 41 | if (!this.tokenParser.isEnded) this.tokenParser.end(); 42 | }; 43 | 44 | tokenParser.onValue = (value) => this.pushLine(value); 45 | tokenParser.onError = (err) => this.onError(err); 46 | tokenParser.onEnd = () => { 47 | this.pushHeaderIfNotWritten(); 48 | this.onEnd(); 49 | }; 50 | } 51 | 52 | getNdJsonTokenizer(asyncOpts) { 53 | const tokenizer = new Tokenizer({ ...asyncOpts, separator: '\n' }); 54 | this.tokenParser = new TokenParser({ paths: ['$'], keepStack: false, separator: '\n' }); 55 | this.configureCallbacks(tokenizer, this.tokenParser); 56 | return tokenizer; 57 | } 58 | 59 | getBinaryModeTokenizer(asyncOpts) { 60 | const tokenizer = new Tokenizer(asyncOpts); 61 | tokenizer.onToken = (token, value, offset) => { 62 | if (token === TokenType.LEFT_BRACKET) { 63 | this.tokenParser = new TokenParser({ paths: ['$.*'], keepStack: false }); 64 | } else if (token === TokenType.LEFT_BRACE) { 65 | this.tokenParser = new TokenParser({ paths: ['$'], keepStack: false }); 66 | } else { 67 | this.onError(new Error('Data should be a JSON object or array')); 68 | return; 69 | } 70 | 71 | this.configureCallbacks(tokenizer, this.tokenParser); 72 | 73 | this.tokenParser.write(token, value, offset); 74 | }; 75 | tokenizer.onError = () => this.onError(new Error('Data should be a JSON object or array')); 76 | tokenizer.onEnd = () => { 77 | this.onError(new Error('Data should not be empty or the "fields" option should be included')); 78 | this.onEnd(); 79 | }; 80 | 81 | return tokenizer; 82 | } 83 | 84 | write(data) { 85 | this.tokenizer.write(data); 86 | } 87 | 88 | end() { 89 | if (this.tokenizer && !this.tokenizer.isEnded) this.tokenizer.end(); 90 | } 91 | 92 | pushHeaderIfNotWritten() { 93 | if (this._hasWritten) return; 94 | if (!this.opts.fields) { 95 | this.onError(new Error('Data should not be empty or the "fields" option should be included')); 96 | return; 97 | } 98 | 99 | this.pushHeader(); 100 | } 101 | 102 | /** 103 | * Generate the csv header and pushes it downstream. 104 | */ 105 | pushHeader() { 106 | if (this.opts.withBOM) { 107 | this.onData('\ufeff'); 108 | } 109 | 110 | if (this.opts.header) { 111 | const header = this.getHeader(this.opts.fields); 112 | this.onHeader(header); 113 | this.onData(header); 114 | this._hasWritten = true; 115 | } 116 | } 117 | 118 | /** 119 | * Transforms an incoming json data to csv and pushes it downstream. 120 | * 121 | * @param {Object} data JSON object to be converted in a CSV row 122 | */ 123 | pushLine(data) { 124 | const processedData = this.preprocessRow(data); 125 | 126 | if (!this._hasWritten) { 127 | this.opts.fields = this.preprocessFieldsInfo(this.opts.fields || Object.keys(processedData[0])); 128 | this.pushHeader(this.opts.fields); 129 | } 130 | 131 | processedData.forEach(row => { 132 | const line = this.processRow(row, this.opts.fields); 133 | if (line === undefined) return; 134 | this.onLine(line); 135 | this.onData(this._hasWritten ? this.opts.eol + line : line); 136 | this._hasWritten = true; 137 | }); 138 | } 139 | 140 | // No idea why eslint doesn't detect the usage of these 141 | /* eslint-disable no-unused-vars */ 142 | onHeader(header) {} 143 | onLine(line) {} 144 | onData(data) {} 145 | onError() {} 146 | onEnd() {} 147 | /* eslint-enable no-unused-vars */ 148 | } 149 | 150 | module.exports = JSON2CSVStreamParser; 151 | -------------------------------------------------------------------------------- /lib/JSON2CSVTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Transform } = require('stream'); 4 | const JSON2CSVStreamParser = require('./JSON2CSVStreamParser'); 5 | const { fakeInherit } = require('./utils'); 6 | 7 | class JSON2CSVTransform extends Transform { 8 | constructor(opts, transformOpts = {}, asyncOptions = {}) { 9 | super(transformOpts); 10 | fakeInherit(this, JSON2CSVStreamParser); 11 | // To don't override the stream end method. 12 | this.endUnderlayingParser = JSON2CSVStreamParser.prototype.end; 13 | this.opts = this.preprocessOpts(opts); 14 | this.initTokenizer(opts, { ...asyncOptions, objectMode: transformOpts.objectMode || transformOpts.readableObjectMode }); 15 | if (this.opts.fields) this.preprocessFieldsInfo(this.opts.fields); 16 | } 17 | 18 | onHeader(header) { 19 | this.emit('header', header); 20 | } 21 | 22 | onLine(line) { 23 | this.emit('line', line); 24 | } 25 | 26 | onData(data) { 27 | this.push(data); 28 | } 29 | 30 | onError(err) { 31 | throw err; 32 | } 33 | 34 | onEnd() { 35 | if (!this.writableEnded) this.end(); 36 | } 37 | 38 | /** 39 | * Main function that send data to the parse to be processed. 40 | * 41 | * @param {Buffer} chunk Incoming data 42 | * @param {String} encoding Encoding of the incoming data. Defaults to 'utf8' 43 | * @param {Function} done Called when the proceesing of the supplied chunk is done 44 | */ 45 | _transform(chunk, encoding, done) { 46 | try { 47 | this.tokenizer.write(chunk); 48 | done(); 49 | } catch (err) { 50 | done(err); 51 | } 52 | } 53 | 54 | _final(done) { 55 | try { 56 | this.endUnderlayingParser(); 57 | done(); 58 | } catch (err) { 59 | done(err); 60 | } 61 | } 62 | 63 | promise() { 64 | return new Promise((resolve, reject) => { 65 | const csvBuffer = []; 66 | this 67 | .on('data', chunk => csvBuffer.push(chunk.toString())) 68 | .on('finish', () => resolve(csvBuffer.join(''))) 69 | .on('error', err => reject(err)); 70 | }); 71 | } 72 | } 73 | 74 | module.exports = JSON2CSVTransform; 75 | -------------------------------------------------------------------------------- /lib/formatters/default.js: -------------------------------------------------------------------------------- 1 | function defaultFormatter(value) { 2 | if (value === null || value === undefined) return ''; 3 | 4 | return `${value}`; 5 | } 6 | 7 | module.exports = defaultFormatter; 8 | -------------------------------------------------------------------------------- /lib/formatters/number.js: -------------------------------------------------------------------------------- 1 | function toFixedDecimals(value, decimals) { 2 | return value.toFixed(decimals); 3 | } 4 | 5 | function replaceSeparator(value, separator) { 6 | return value.replace('.', separator); 7 | } 8 | 9 | 10 | function numberFormatter(opts = {}) { 11 | if (opts.separator) { 12 | if (opts.decimals) { 13 | return (value) => replaceSeparator(toFixedDecimals(value, opts.decimals), opts.separator); 14 | } 15 | 16 | return (value) => replaceSeparator(value.toString(), opts.separator); 17 | } 18 | 19 | if (opts.decimals) { 20 | return (value) => toFixedDecimals(value, opts.decimals); 21 | } 22 | 23 | return (value) => value.toString(); 24 | } 25 | 26 | module.exports = numberFormatter; 27 | -------------------------------------------------------------------------------- /lib/formatters/object.js: -------------------------------------------------------------------------------- 1 | const defaulStringFormatter = require('./string'); 2 | 3 | function objectFormatter(opts = { stringFormatter: defaulStringFormatter() }) { 4 | return (value) => { 5 | if (value === null) return ''; 6 | 7 | value = JSON.stringify(value); 8 | 9 | if (value === undefined) return ''; 10 | 11 | if (value[0] === '"') value = value.replace(/^"(.+)"$/,'$1'); 12 | 13 | return opts.stringFormatter(value); 14 | } 15 | } 16 | 17 | module.exports = objectFormatter; 18 | -------------------------------------------------------------------------------- /lib/formatters/string.js: -------------------------------------------------------------------------------- 1 | function stringFormatter(opts = {}) { 2 | const quote = typeof opts.quote === 'string' ? opts.quote : '"'; 3 | const escapedQuote = typeof opts.escapedQuote === 'string' ? opts.escapedQuote : `${quote}${quote}`; 4 | 5 | if (!quote) { 6 | return (value) => value; 7 | } 8 | 9 | return (value) => { 10 | if(value.includes(quote)) { 11 | value = value.replace(new RegExp(quote, 'g'), escapedQuote); 12 | } 13 | 14 | return `${quote}${value}${quote}`; 15 | } 16 | } 17 | 18 | module.exports = stringFormatter; 19 | -------------------------------------------------------------------------------- /lib/formatters/stringExcel.js: -------------------------------------------------------------------------------- 1 | const quote = '"'; 2 | const escapedQuote = '""""'; 3 | 4 | function stringExcel(value) { 5 | return `"=""${value.replace(new RegExp(quote, 'g'), escapedQuote)}"""`; 6 | } 7 | 8 | module.exports = stringExcel; 9 | -------------------------------------------------------------------------------- /lib/formatters/stringQuoteOnlyIfNecessary.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const defaulStringFormatter = require('./string'); 3 | 4 | function stringQuoteOnlyIfNecessaryFormatter(opts = {}) { 5 | const quote = typeof opts.quote === 'string' ? opts.quote : '"'; 6 | const escapedQuote = typeof opts.escapedQuote === 'string' ? opts.escapedQuote : `${quote}${quote}`; 7 | const separator = typeof opts.separator === 'string' ? opts.separator : ','; 8 | const eol = typeof opts.eol === 'string' ? opts.escapedQeoluote : os.EOL; 9 | 10 | const stringFormatter = defaulStringFormatter({ quote, escapedQuote }); 11 | 12 | return (value) => { 13 | if([quote, separator, eol].some(char => value.includes(char))) { 14 | return stringFormatter(value); 15 | } 16 | 17 | return value; 18 | } 19 | } 20 | 21 | module.exports = stringQuoteOnlyIfNecessaryFormatter; 22 | -------------------------------------------------------------------------------- /lib/formatters/symbol.js: -------------------------------------------------------------------------------- 1 | const defaulStringFormatter = require('./string'); 2 | 3 | function symbolFormatter(opts = { stringFormatter: defaulStringFormatter() }) { 4 | return (value) => opts.stringFormatter((value.toString().slice(7,-1))); 5 | } 6 | 7 | module.exports = symbolFormatter; 8 | -------------------------------------------------------------------------------- /lib/json2csv.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const JSON2CSVParser = require('./JSON2CSVParser'); 4 | const JSON2CSVAsyncParser = require('./JSON2CSVAsyncParser'); 5 | const JSON2CSVStreamParser = require('./JSON2CSVStreamParser'); 6 | const JSON2CSVTransform = require('./JSON2CSVTransform'); 7 | 8 | // Transforms 9 | const flatten = require('./transforms/flatten'); 10 | const unwind = require('./transforms/unwind'); 11 | 12 | // Formatters 13 | const defaultFormatter = require('./formatters/default'); 14 | const number = require('./formatters/number'); 15 | const string = require('./formatters/string'); 16 | const stringQuoteOnlyIfNecessary = require('./formatters/stringQuoteOnlyIfNecessary'); 17 | const stringExcel = require('./formatters/stringExcel'); 18 | const symbol = require('./formatters/symbol'); 19 | const object = require('./formatters/object'); 20 | 21 | module.exports.Parser = JSON2CSVParser; 22 | module.exports.AsyncParser = JSON2CSVAsyncParser; 23 | module.exports.StreamParser = JSON2CSVStreamParser; 24 | module.exports.Transform = JSON2CSVTransform; 25 | 26 | // Convenience method to keep the API similar to version 3.X 27 | module.exports.parse = (data, opts) => new JSON2CSVParser(opts).parse(data); 28 | module.exports.parseAsync = (data, opts, transformOpts) => new JSON2CSVAsyncParser(opts, transformOpts).parse(data).promise(); 29 | 30 | module.exports.transforms = { 31 | flatten, 32 | unwind, 33 | }; 34 | 35 | module.exports.formatters = { 36 | default: defaultFormatter, 37 | number, 38 | string, 39 | stringQuoteOnlyIfNecessary, 40 | stringExcel, 41 | symbol, 42 | object, 43 | }; 44 | -------------------------------------------------------------------------------- /lib/transforms/flatten.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Performs the flattening of a data row recursively 3 | * 4 | * @param {String} separator Separator to be used as the flattened field name 5 | * @returns {Object => Object} Flattened object 6 | */ 7 | function flatten({ objects = true, arrays = false, separator = '.' } = {}) { 8 | function step (obj, flatDataRow, currentPath) { 9 | Object.keys(obj).forEach((key) => { 10 | const newPath = currentPath ? `${currentPath}${separator}${key}` : key; 11 | const value = obj[key]; 12 | 13 | if (objects 14 | && typeof value === 'object' 15 | && value !== null 16 | && !Array.isArray(value) 17 | && Object.prototype.toString.call(value.toJSON) !== '[object Function]' 18 | && Object.keys(value).length) { 19 | step(value, flatDataRow, newPath); 20 | return; 21 | } 22 | 23 | if (arrays && Array.isArray(value)) { 24 | step(value, flatDataRow, newPath); 25 | return; 26 | } 27 | 28 | flatDataRow[newPath] = value; 29 | }); 30 | 31 | return flatDataRow; 32 | } 33 | 34 | return dataRow => step(dataRow, {}); 35 | } 36 | 37 | module.exports = flatten; 38 | -------------------------------------------------------------------------------- /lib/transforms/unwind.js: -------------------------------------------------------------------------------- 1 | 2 | const lodashGet = require('lodash.get'); 3 | const { setProp, unsetProp } = require('../utils'); 4 | 5 | function getUnwindablePaths(obj, currentPath) { 6 | return Object.keys(obj).reduce((unwindablePaths, key) => { 7 | const newPath = currentPath ? `${currentPath}.${key}` : key; 8 | const value = obj[key]; 9 | 10 | if (typeof value === 'object' 11 | && value !== null 12 | && !Array.isArray(value) 13 | && Object.prototype.toString.call(value.toJSON) !== '[object Function]' 14 | && Object.keys(value).length) { 15 | unwindablePaths = unwindablePaths.concat(getUnwindablePaths(value, newPath)); 16 | } else if (Array.isArray(value)) { 17 | unwindablePaths.push(newPath); 18 | unwindablePaths = unwindablePaths.concat(value 19 | .flatMap(arrObj => getUnwindablePaths(arrObj, newPath)) 20 | .filter((item, index, arr) => arr.indexOf(item) !== index)); 21 | } 22 | 23 | return unwindablePaths; 24 | }, []); 25 | } 26 | 27 | /** 28 | * Performs the unwind recursively in specified sequence 29 | * 30 | * @param {String[]} unwindPaths The paths as strings to be used to deconstruct the array 31 | * @returns {Object => Array} Array of objects containing all rows after unwind of chosen paths 32 | */ 33 | function unwind({ paths = undefined, blankOut = false } = {}) { 34 | function unwindReducer(rows, unwindPath) { 35 | return rows 36 | .flatMap(row => { 37 | const unwindArray = lodashGet(row, unwindPath); 38 | 39 | if (!Array.isArray(unwindArray)) { 40 | return row; 41 | } 42 | 43 | if (!unwindArray.length) { 44 | return unsetProp(row, unwindPath); 45 | } 46 | 47 | const baseNewRow = blankOut ? {} : row; 48 | const [firstRow, ...restRows] = unwindArray; 49 | return [ 50 | setProp(row, unwindPath, firstRow), 51 | ...restRows.map(unwindRow => setProp(baseNewRow, unwindPath, unwindRow)) 52 | ]; 53 | }); 54 | } 55 | 56 | paths = Array.isArray(paths) ? paths : (paths ? [paths] : undefined); 57 | return dataRow => (paths || getUnwindablePaths(dataRow)).reduce(unwindReducer, [dataRow]); 58 | } 59 | 60 | module.exports = unwind; -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function getProp(obj, path, defaultValue) { 4 | return obj[path] === undefined ? defaultValue : obj[path]; 5 | } 6 | 7 | function setProp(obj, path, value) { 8 | const pathArray = Array.isArray(path) ? path : path.split('.'); 9 | const [key, ...restPath] = pathArray; 10 | return { 11 | ...obj, 12 | [key]: pathArray.length > 1 ? setProp(obj[key] || {}, restPath, value) : value 13 | }; 14 | } 15 | 16 | function unsetProp(obj, path) { 17 | const pathArray = Array.isArray(path) ? path : path.split('.'); 18 | const [key, ...restPath] = pathArray; 19 | 20 | // This will never be hit in the current code because unwind does the check before calling unsetProp 21 | /* istanbul ignore next */ 22 | if (typeof obj[key] !== 'object') { 23 | return obj; 24 | } 25 | 26 | if (pathArray.length === 1) { 27 | return Object.keys(obj) 28 | .filter(prop => prop !== key) 29 | .reduce((acc, prop) => ({ ...acc, [prop]: obj[prop] }), {}); 30 | } 31 | 32 | return Object.keys(obj) 33 | .reduce((acc, prop) => ({ 34 | ...acc, 35 | [prop]: prop !== key ? obj[prop] : unsetProp(obj[key], restPath), 36 | }), {}); 37 | } 38 | 39 | /** 40 | * Function to manually make a given object inherit all the properties and methods 41 | * from another object. 42 | * 43 | * @param {Buffer} chunk Incoming data 44 | * @param {String} encoding Encoding of the incoming data. Defaults to 'utf8' 45 | * @param {Function} done Called when the proceesing of the supplied chunk is done 46 | */ 47 | function fakeInherit(inheritingObj, parentObj) { 48 | let current = parentObj.prototype; 49 | do { 50 | Object.getOwnPropertyNames(current) 51 | .filter((prop) => ![ 52 | 'constructor', 53 | '__proto__', 54 | '__defineGetter__', 55 | '__defineSetter__', 56 | '__lookupGetter__', 57 | '__lookupSetter__', 58 | 'isPrototypeOf', 59 | 'hasOwnProperty', 60 | 'propertyIsEnumerable', 61 | 'valueOf', 62 | 'toString', 63 | 'toLocaleString' 64 | ].includes(prop) 65 | ) 66 | .forEach(prop => { 67 | if (!inheritingObj[prop]) { 68 | Object.defineProperty(inheritingObj, prop, Object.getOwnPropertyDescriptor(current, prop)); 69 | } 70 | }); 71 | // Bring back if we ever need to extend object with Symbol properties 72 | // Object.getOwnPropertySymbols(current).forEach(prop => { 73 | // if (!inheritingObj[prop]) { 74 | // Object.defineProperty(inheritingObj, prop, Object.getOwnPropertyDescriptor(current, prop)); 75 | // } 76 | // }); 77 | current = Object.getPrototypeOf(current); 78 | } while (current != null); 79 | } 80 | 81 | module.exports = { 82 | getProp, 83 | setProp, 84 | unsetProp, 85 | fakeInherit, 86 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json2csv", 3 | "version": "6.0.0-alpha.2", 4 | "description": "Convert JSON to CSV", 5 | "keywords": [ 6 | "json", 7 | "to", 8 | "csv", 9 | "export", 10 | "convert", 11 | "parse" 12 | ], 13 | "author": { 14 | "name": "Mirco Zeiss", 15 | "email": "mirco.zeiss@gmail.com", 16 | "twitter": "zeMirco" 17 | }, 18 | "license": "MIT", 19 | "bin": { 20 | "json2csv": "./bin/json2csv.js" 21 | }, 22 | "main": "lib/json2csv.js", 23 | "module": "dist/json2csv.esm.js", 24 | "browser": "dist/json2csv.umd.js", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/zemirco/json2csv" 28 | }, 29 | "homepage": "http://zemirco.github.io/json2csv", 30 | "scripts": { 31 | "build": "rollup -c", 32 | "dev": "rollup -c -w", 33 | "test": "node test | tap-spec", 34 | "lint": "eslint bin lib test", 35 | "test-with-coverage": "nyc --reporter=text node test | tap-spec", 36 | "coveralls": "nyc report --reporter=text-lcov | coveralls", 37 | "deploy:docs": "docpress b && gh-pages -d _docpress", 38 | "prepublish": "in-publish && npm run before:publish || not-in-publish", 39 | "before:publish": "npm test && npm run build && npm run deploy:docs", 40 | "release": "standard-version" 41 | }, 42 | "dependencies": { 43 | "@streamparser/json": "^0.0.6", 44 | "commander": "^6.2.0", 45 | "lodash.get": "^4.4.2" 46 | }, 47 | "devDependencies": { 48 | "@babel/core": "^7.13.15", 49 | "@babel/preset-env": "^7.13.15", 50 | "coveralls": "^3.1.0", 51 | "docpress": "^0.8.2", 52 | "eslint": "^7.24.0", 53 | "gh-pages": "^3.1.0", 54 | "in-publish": "^2.0.1", 55 | "jest": "^27.4.5", 56 | "nyc": "^15.1.0", 57 | "rollup": "^2.45.2", 58 | "rollup-plugin-babel": "^4.4.0", 59 | "rollup-plugin-commonjs": "^10.1.0", 60 | "rollup-plugin-node-polyfills": "^0.2.1", 61 | "rollup-plugin-node-resolve": "^5.2.0", 62 | "standard-version": "^9.2.0", 63 | "tap-spec": "^5.0.0", 64 | "tape": "^5.2.2" 65 | }, 66 | "engines": { 67 | "node": ">= 12", 68 | "npm": ">= 6.13.0" 69 | }, 70 | "volta": { 71 | "node": "12.20.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import nodePolyfills from 'rollup-plugin-node-polyfills'; 4 | import babel from 'rollup-plugin-babel'; 5 | import pkg from './package.json'; 6 | 7 | export default [ 8 | { 9 | input: 'lib/json2csv.js', 10 | output: { 11 | file: pkg.browser, 12 | format: 'umd', 13 | name: 'json2csv' 14 | }, 15 | plugins: [ 16 | resolve({ 17 | browser: true 18 | }), 19 | commonjs(), 20 | nodePolyfills(), 21 | babel({ 22 | exclude: ['node_modules/**'], 23 | babelrc: false, 24 | presets: [['@babel/env', { modules: false }]], 25 | }) 26 | ] 27 | }, 28 | { 29 | input: 'lib/json2csv.js', 30 | output: [ 31 | { file: pkg.module, format: 'es' } 32 | ], 33 | external: [ 'os', 'stream' ], 34 | plugins: [ 35 | resolve(), 36 | commonjs(), 37 | babel({ 38 | exclude: ['node_modules/**'], 39 | babelrc: false, 40 | presets: [['@babel/env', { modules: false }]], 41 | }) 42 | ] 43 | } 44 | ]; 45 | -------------------------------------------------------------------------------- /test/CLI.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { mkdir, rm, readFile } = require('fs').promises; 4 | const { join: joinPath } = require('path'); 5 | const { exec } = require('child_process'); 6 | const { promisify } = require('util'); 7 | 8 | const execAsync = promisify(exec); 9 | 10 | const cli = `node "${joinPath(process.cwd(), './bin/json2csv.js')}"`; 11 | 12 | const resultsPath = './test/fixtures/results'; 13 | const getFixturePath = fixture => joinPath('./test/fixtures', fixture); 14 | 15 | module.exports = (testRunner, jsonFixtures, csvFixtures) => { 16 | testRunner.addBefore(async () => { 17 | try { 18 | await mkdir(resultsPath); 19 | } catch(err) { 20 | if (err.code !== 'EEXIST') throw err; 21 | } 22 | }); 23 | 24 | testRunner.addAfter(async () => { 25 | rm(resultsPath, { recursive: true }); 26 | }); 27 | 28 | testRunner.add('should handle ndjson', async (t) => { 29 | const opts = '--fields carModel,price,color,manual --ndjson'; 30 | 31 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/ndjson.json')}" ${opts}`); 32 | 33 | t.equal(csv, csvFixtures.ndjson); 34 | }); 35 | 36 | testRunner.add('should error on invalid ndjson input path without streaming', async (t) => { 37 | const opts = '--fields carModel,price,color,manual --ndjson --no-streaming'; 38 | 39 | try { 40 | await execAsync(`${cli} -i "${getFixturePath('/json2/ndjsonInvalid.json')}" ${opts}`); 41 | 42 | t.fail('Exception expected.'); 43 | } catch (err) { 44 | t.ok(err.message.includes('Invalid input file.')); 45 | } 46 | }); 47 | 48 | testRunner.add('should error on invalid ndjson input data', async (t) => { 49 | const opts = '--fields carModel,price,color,manual --ndjson'; 50 | 51 | try { 52 | await execAsync(`${cli} -i "${getFixturePath('/json/ndjsonInvalid.json')}" ${opts}`); 53 | 54 | t.fail('Exception expected.'); 55 | } catch (err) { 56 | t.ok(err.message.includes('Unexpected SEPARATOR ("\\n") in state COMMA')); 57 | } 58 | }); 59 | 60 | testRunner.add('should handle ndjson without streaming', async (t) => { 61 | const opts = '--fields carModel,price,color,manual --ndjson --no-streaming'; 62 | 63 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/ndjson.json')}" ${opts}`); 64 | 65 | t.equal(csv, csvFixtures.ndjson + '\n'); // console.log append the new line 66 | }); 67 | 68 | testRunner.add('should error on invalid input file path', async (t) => { 69 | try { 70 | await execAsync(`${cli} -i "${getFixturePath('/json2/default.json')}"`); 71 | 72 | t.fail('Exception expected.'); 73 | } catch (err) { 74 | t.ok(err.message.includes('Invalid input file.')); 75 | } 76 | }); 77 | 78 | testRunner.add('should error on invalid input file path without streaming', async (t) => { 79 | const opts = '--no-streaming'; 80 | 81 | try { 82 | await execAsync(`${cli} -i "${getFixturePath('/json2/default.json')}" ${opts}`); 83 | 84 | t.fail('Exception expected.'); 85 | } catch (err) { 86 | t.ok(err.message.includes('Invalid input file.')); 87 | } 88 | }); 89 | 90 | testRunner.add('should error if input data is not an object', async (t) => { 91 | try { 92 | await execAsync(`${cli} -i "${getFixturePath('/json2/notAnObject.json')}"`); 93 | 94 | t.fail('Exception expected.'); 95 | } catch (err) { 96 | t.ok(err.message.includes('Invalid input file.')); 97 | } 98 | }); 99 | 100 | testRunner.add('should handle empty object', async (t) => { 101 | const opts = '--fields carModel,price,color'; 102 | 103 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/emptyObject.json')}" ${opts}`); 104 | 105 | t.equal(csv, csvFixtures.emptyObject); 106 | }); 107 | 108 | testRunner.add('should handle deep JSON objects', async (t) => { 109 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/deepJSON.json')}"`); 110 | 111 | t.equal(csv, csvFixtures.deepJSON); 112 | }); 113 | 114 | testRunner.add('should handle deep JSON objects without streaming', async (t) => { 115 | const opts = '--no-streaming'; 116 | 117 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/deepJSON.json')}" ${opts}`); 118 | 119 | t.equal(csv, csvFixtures.deepJSON + '\n'); // console.log append the new line 120 | }); 121 | 122 | testRunner.add('should parse json to csv and infer the fields automatically ', async (t) => { 123 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}"`); 124 | 125 | t.equal(csv, csvFixtures.defaultStream); 126 | }); 127 | 128 | testRunner.add('should error on invalid fields config file path', async (t) => { 129 | const opts = `--config "${getFixturePath('/fields2/fieldNames.json')}"`; 130 | 131 | try { 132 | await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 133 | 134 | t.fail('Exception expected.'); 135 | } catch (err) { 136 | t.ok(err.message.includes('Invalid config file.')); 137 | } 138 | }); 139 | 140 | testRunner.add('should parse json to csv using custom fields', async (t) => { 141 | const opts = '--fields carModel,price,color,manual'; 142 | 143 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 144 | 145 | t.equal(csv, csvFixtures.default); 146 | }); 147 | 148 | testRunner.add('should output only selected fields', async (t) => { 149 | const opts = '--fields carModel,price'; 150 | 151 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 152 | 153 | t.equal(csv, csvFixtures.selected); 154 | }); 155 | 156 | testRunner.add('should output fields in the order provided', async (t) => { 157 | const opts = '--fields price,carModel'; 158 | 159 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 160 | 161 | t.equal(csv, csvFixtures.reversed); 162 | }); 163 | 164 | testRunner.add('should output empty value for non-existing fields', async (t) => { 165 | const opts = '--fields "first not exist field",carModel,price,"not exist field",color'; 166 | 167 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 168 | 169 | t.equal(csv, csvFixtures.withNotExistField); 170 | }); 171 | 172 | testRunner.add('should name columns as specified in \'fields\' property', async (t) => { 173 | const opts = `--config "${getFixturePath('/fields/fieldNames.json')}"`; 174 | 175 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 176 | 177 | t.equal(csv, csvFixtures.fieldNames); 178 | }); 179 | 180 | testRunner.add('should support nested properties selectors', async (t) => { 181 | const opts = `--config "${getFixturePath('/fields/nested.json')}"`; 182 | 183 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/nested.json')}" ${opts}`); 184 | 185 | t.equal(csv, csvFixtures.nested); 186 | }); 187 | 188 | testRunner.add('field.value function should receive a valid field object', async (t) => { 189 | const opts = `--config "${getFixturePath('/fields/functionWithCheck.js')}"`; 190 | 191 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/functionStringifyByDefault.json')}" ${opts}`); 192 | 193 | t.equal(csv, csvFixtures.functionStringifyByDefault); 194 | }); 195 | 196 | testRunner.add('field.value function should stringify results by default', async (t) => { 197 | const opts = `--config "${getFixturePath('/fields/functionStringifyByDefault.js')}"`; 198 | 199 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/functionStringifyByDefault.json')}" ${opts}`); 200 | 201 | t.equal(csv, csvFixtures.functionStringifyByDefault); 202 | }); 203 | 204 | testRunner.add('should process different combinations in fields option', async (t) => { 205 | const opts = `--config "${getFixturePath('/fields/fancyfields.js')}" --default-value NULL`; 206 | 207 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/fancyfields.json')}" ${opts}`); 208 | 209 | t.equal(csv, csvFixtures.fancyfields); 210 | }); 211 | 212 | // Default value 213 | 214 | testRunner.add('should output the default value as set in \'defaultValue\'', async (t) => { 215 | const opts = '--fields carModel,price --default-value ""'; 216 | 217 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/defaultValueEmpty.json')}" ${opts}`); 218 | 219 | t.equal(csv, csvFixtures.defaultValueEmpty); 220 | }); 221 | 222 | testRunner.add('should override \'options.defaultValue\' with \'field.defaultValue\'', async (t) => { 223 | const opts = `--config "${getFixturePath('/fields/overriddenDefaultValue.json')}" --default-value ""`; 224 | 225 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/overriddenDefaultValue.json')}" ${opts}`); 226 | 227 | t.equal(csv, csvFixtures.overriddenDefaultValue); 228 | }); 229 | 230 | testRunner.add('should use \'options.defaultValue\' when no \'field.defaultValue\'', async (t) => { 231 | const opts = `--config "${getFixturePath('/fields/overriddenDefaultValue2.js')}" --default-value ""`; 232 | 233 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/overriddenDefaultValue.json')}" ${opts}`); 234 | 235 | t.equal(csv, csvFixtures.overriddenDefaultValue); 236 | }); 237 | 238 | // Delimiter 239 | 240 | testRunner.add('should use a custom delimiter when \'delimiter\' property is defined', async (t) => { 241 | const opts = '--fields carModel,price,color --delimiter "\t"'; 242 | 243 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 244 | 245 | t.equal(csv, csvFixtures.tsv); 246 | }); 247 | 248 | testRunner.add('should remove last delimiter |@|', async (t) => { 249 | const opts = '--delimiter "|@|"'; 250 | 251 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/delimiter.json')}" ${opts}`); 252 | 253 | t.equal(csv, csvFixtures.delimiter); 254 | }); 255 | 256 | // EOL 257 | 258 | testRunner.add('should use a custom eol character when \'eol\' property is present', async (t) => { 259 | const opts = '--fields carModel,price,color --eol "\r\n"'; 260 | 261 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 262 | 263 | t.equal(csv, csvFixtures.eol); 264 | }); 265 | 266 | // Header 267 | 268 | testRunner.add('should parse json to csv without column title', async (t) => { 269 | const opts = '--fields carModel,price,color,manual --no-header'; 270 | 271 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 272 | 273 | t.equal(csv, csvFixtures.withoutHeader); 274 | }); 275 | 276 | // Include empty rows 277 | 278 | testRunner.add('should not include empty rows when options.includeEmptyRows is not specified', async (t) => { 279 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/emptyRow.json')}"`); 280 | 281 | t.equal(csv, csvFixtures.emptyRowNotIncluded); 282 | }); 283 | 284 | testRunner.add('should include empty rows when options.includeEmptyRows is true', async (t) => { 285 | const opts = '--include-empty-rows'; 286 | 287 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/emptyRow.json')}" ${opts}`); 288 | 289 | t.equal(csv, csvFixtures.emptyRow); 290 | }); 291 | 292 | testRunner.add('should include empty rows when options.includeEmptyRows is true, with default values', async (t) => { 293 | const opts = `--config "${getFixturePath('/fields/emptyRowDefaultValues.json')}" --default-value NULL --include-empty-rows`; 294 | 295 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/emptyRow.json')}" ${opts}`); 296 | 297 | t.equal(csv, csvFixtures.emptyRowDefaultValues); 298 | }); 299 | 300 | testRunner.add('should parse data:[null] to csv with only column title, despite options.includeEmptyRows', async (t) => { 301 | const opts = '--fields carModel,price,color --include-empty-rows'; 302 | 303 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/arrayWithNull.json')}" ${opts}`); 304 | 305 | t.equal(csv, csvFixtures.emptyObject); 306 | }); 307 | 308 | // BOM 309 | 310 | testRunner.add('should add BOM character', async (t) => { 311 | const opts = '--fields carModel,price,color,manual --with-bom'; 312 | 313 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/specialCharacters.json')}" ${opts}`); 314 | 315 | // Compare csv length to check if the BOM character is present 316 | t.equal(csv[0], '\ufeff'); 317 | t.equal(csv.length, csvFixtures.default.length + 1); 318 | t.equal(csv.length, csvFixtures.withBOM.length); 319 | }); 320 | 321 | // Get input from stdin 322 | 323 | testRunner.add('should get input from stdin and process as stream', async (t) => { 324 | const execution = execAsync(cli); 325 | 326 | execution.child.stdin.write(JSON.stringify(jsonFixtures.default())); 327 | execution.child.stdin.end(); 328 | 329 | const { stdout: csv } = await execution; 330 | 331 | t.equal(csv, csvFixtures.defaultStream); 332 | }); 333 | 334 | testRunner.add('should error if stdin data is not valid', async (t) => { 335 | const execution = execAsync(cli); 336 | 337 | execution.child.stdin.write('{ "b": 1,'); 338 | execution.child.stdin.end(); 339 | 340 | try { 341 | await execution; 342 | 343 | t.fail('Exception expected.'); 344 | } catch (err) { 345 | t.ok(err.message.includes('Error: Parser ended in mid-parsing (state: KEY). Either not all the data was received or the data was invalid.')); 346 | } 347 | }); 348 | 349 | testRunner.add('should get input from stdin with -s flag', async (t) => { 350 | const execution = execAsync(`${cli} -s`); 351 | 352 | execution.child.stdin.write(JSON.stringify(jsonFixtures.default())); 353 | execution.child.stdin.end(); 354 | 355 | const { stdout: csv } = await execution; 356 | 357 | t.equal(csv, csvFixtures.default + '\n'); // console.log append the new line 358 | }); 359 | 360 | testRunner.add('should error if stdin data is not valid with -s flag', async (t) => { 361 | const execution = execAsync(`${cli} -s`); 362 | 363 | execution.child.stdin.write('{ "b": 1,'); 364 | execution.child.stdin.end(); 365 | 366 | try { 367 | await execution; 368 | 369 | t.fail('Exception expected.'); 370 | } catch (err) { 371 | t.ok(err.message.includes('Invalid data received from stdin')); 372 | } 373 | }); 374 | 375 | testRunner.add('should error if stdin fails', async (t) => { 376 | const execution = execAsync(cli); 377 | 378 | // TODO Figure out how to make the stdin to error 379 | execution.child.stdin._read = execution.child.stdin._write = () => {}; 380 | execution.child.stdin.on('error', () => {}); 381 | execution.child.stdin.destroy(new Error('Test error')); 382 | 383 | try { 384 | await execution; 385 | 386 | t.fail('Exception expected.'); 387 | } catch (err) { 388 | // TODO error message seems wrong 389 | t.ok(err.message.includes('Data should not be empty or the "fields" option should be included')); 390 | } 391 | }); 392 | 393 | // Put output to file 394 | 395 | testRunner.add('should output to file', async (t) => { 396 | const outputPath = getFixturePath('/results/default.csv'); 397 | const opts = `-o "${outputPath}" --fields carModel,price,color,manual`; 398 | 399 | await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 400 | 401 | const csv = await readFile(outputPath, 'utf-8'); 402 | t.equal(csv, csvFixtures.default); 403 | }); 404 | 405 | testRunner.add('should output to file without streaming', async (t) => { 406 | const outputPath = getFixturePath('/results/default.csv'); 407 | const opts = `-o ${outputPath} --fields carModel,price,color,manual --no-streaming`; 408 | 409 | await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 410 | 411 | const csv = await readFile(outputPath, 'utf-8'); 412 | t.equal(csv, csvFixtures.default); 413 | }); 414 | 415 | testRunner.add('should error on invalid output file path', async (t) => { 416 | const outputPath = getFixturePath('/results2/default.csv'); 417 | const opts = `-o "${outputPath}" --fields carModel,price,color,manual`; 418 | 419 | try{ 420 | await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 421 | } catch (err) { 422 | t.ok(err.message.includes('Invalid output file.')); 423 | } 424 | }); 425 | 426 | testRunner.add('should error on invalid output file path without streaming', async (t) => { 427 | const outputPath = getFixturePath('/results2/default.csv'); 428 | const opts = `-o "${outputPath}" --fields carModel,price,color,manual --no-streaming`; 429 | 430 | try { 431 | await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 432 | } catch (err) { 433 | t.ok(err.message.includes('Invalid output file.')); 434 | } 435 | }); 436 | 437 | // Pretty print 438 | 439 | testRunner.add('should print pretty table', async (t) => { 440 | const opts = '--pretty'; 441 | 442 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 443 | 444 | t.equal(csv, csvFixtures.prettyprint); 445 | }); 446 | 447 | testRunner.add('should print pretty table without header', async (t) => { 448 | const opts = '--no-header --pretty'; 449 | 450 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 451 | 452 | t.equal(csv, csvFixtures.prettyprintWithoutHeader); 453 | }); 454 | 455 | testRunner.add('should print pretty table without streaming', async (t) => { 456 | const opts = '--fields carModel,price,color --no-streaming --pretty '; 457 | 458 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 459 | 460 | t.equal(csv, csvFixtures.prettyprint); 461 | }); 462 | 463 | testRunner.add('should print pretty table without streaming and without header', async (t) => { 464 | const opts = '--fields carModel,price,color --no-streaming --no-header --pretty '; 465 | 466 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 467 | 468 | t.equal(csv, csvFixtures.prettyprintWithoutHeader); 469 | }); 470 | 471 | testRunner.add('should print pretty table without rows', async (t) => { 472 | const opts = '--fields fieldA,fieldB,fieldC --pretty'; 473 | 474 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 475 | 476 | t.equal(csv, csvFixtures.prettyprintWithoutRows); 477 | }); 478 | 479 | // Preprocessing 480 | 481 | testRunner.add('should unwind all unwindable fields using the unwind transform', async (t) => { 482 | const opts = '--fields carModel,price,extras.items.name,extras.items.color,extras.items.items.position,extras.items.items.color' 483 | + ' --unwind'; 484 | 485 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/unwind2.json')}" ${opts}`); 486 | 487 | t.equal(csv, csvFixtures.unwind2); 488 | }); 489 | 490 | testRunner.add('should support unwinding specific fields using the unwind transform', async (t) => { 491 | const opts = '--unwind colors'; 492 | 493 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/unwind.json')}" ${opts}`); 494 | 495 | t.equal(csv, csvFixtures.unwind); 496 | }); 497 | 498 | testRunner.add('should support multi-level unwind using the unwind transform', async (t) => { 499 | const opts = '--fields carModel,price,extras.items.name,extras.items.color,extras.items.items.position,extras.items.items.color' 500 | + ' --unwind extras.items,extras.items.items'; 501 | 502 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/unwind2.json')}" ${opts}`); 503 | 504 | t.equal(csv, csvFixtures.unwind2); 505 | }); 506 | 507 | testRunner.add('hould unwind and blank out repeated data', async (t) => { 508 | const opts = '--fields carModel,price,extras.items.name,extras.items.color,extras.items.items.position,extras.items.items.color' 509 | + ' --unwind extras.items,extras.items.items --unwind-blank'; 510 | 511 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/unwind2.json')}" ${opts}`); 512 | 513 | t.equal(csv, csvFixtures.unwind2Blank); 514 | }); 515 | 516 | testRunner.add('should support flattening deep JSON using the flatten transform', async (t) => { 517 | const opts = '--flatten-objects'; 518 | 519 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/deepJSON.json')}" ${opts}`); 520 | 521 | t.equal(csv, csvFixtures.flattenedDeepJSON); 522 | }); 523 | 524 | testRunner.add('should support flattening JSON with nested arrays using the flatten transform', async (t) => { 525 | const opts = '--flatten-objects --flatten-arrays'; 526 | 527 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/flattenArrays.json')}" ${opts}`); 528 | 529 | t.equal(csv, csvFixtures.flattenedArrays); 530 | }); 531 | 532 | testRunner.add('should support custom flatten separator using the flatten transform', async (t) => { 533 | const opts = '--flatten-objects --flatten-separator __'; 534 | 535 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/deepJSON.json')}" ${opts}`); 536 | 537 | t.equal(csv, csvFixtures.flattenedCustomSeparatorDeepJSON); 538 | }); 539 | 540 | testRunner.add('should support multiple transforms and honor the order in which they are declared', async (t) => { 541 | const opts = '--unwind items --flatten-objects'; 542 | 543 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/unwindAndFlatten.json')}" ${opts}`); 544 | 545 | t.equal(csv, csvFixtures.unwindAndFlatten); 546 | }); 547 | 548 | 549 | testRunner.add('should unwind complex objects using the unwind transform', async (t) => { 550 | const opts = '--fields carModel,price,extras.items.name,extras.items.items.position,extras.items.items.color,extras.items.color' 551 | + ' --unwind extras.items,extras.items.items --flatten-objects --flatten-arrays'; 552 | 553 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/unwindComplexObject.json')}" ${opts}`); 554 | 555 | t.equal(csv, csvFixtures.unwindComplexObject); 556 | }); 557 | 558 | // Formatters 559 | 560 | // String Quote 561 | 562 | testRunner.add('should use a custom quote when \'quote\' property is present', async (t) => { 563 | const opts = '--fields carModel,price --quote "\'"'; 564 | 565 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 566 | 567 | t.equal(csv, csvFixtures.withSimpleQuotes); 568 | }); 569 | 570 | testRunner.add('should be able to don\'t output quotes when setting \'quote\' to empty string', async (t) => { 571 | const opts = '--fields carModel,price --quote ""'; 572 | 573 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 574 | 575 | t.equal(csv, csvFixtures.withoutQuotes); 576 | }); 577 | 578 | testRunner.add('should escape quotes when setting \'quote\' property is present', async (t) => { 579 | const opts = '--fields carModel,color --quote "\'"'; 580 | 581 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/escapeCustomQuotes.json')}" ${opts}`); 582 | t.equal(csv, csvFixtures.escapeCustomQuotes); 583 | }); 584 | 585 | testRunner.add('should not escape \'"\' when setting \'quote\' set to something else', async (t) => { 586 | const opts = '--quote "\'"'; 587 | 588 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/escapedQuotes.json')}" ${opts}`); 589 | t.equal(csv, csvFixtures.escapedQuotesUnescaped); 590 | }); 591 | 592 | // String Escaped Quote 593 | 594 | testRunner.add('should escape quotes with double quotes', async (t) => { 595 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/quotes.json')}"`); 596 | 597 | t.equal(csv, csvFixtures.quotes); 598 | }); 599 | 600 | testRunner.add('should not escape quotes with double quotes, when there is a backslash in the end', async (t) => { 601 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/backslashAtEnd.json')}"`); 602 | 603 | t.equal(csv, csvFixtures.backslashAtEnd); 604 | }); 605 | 606 | testRunner.add('should not escape quotes with double quotes, when there is a backslash in the end, and its not the last column', async (t) => { 607 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/backslashAtEndInMiddleColumn.json')}"`); 608 | 609 | t.equal(csv, csvFixtures.backslashAtEndInMiddleColumn); 610 | }); 611 | 612 | testRunner.add('should escape quotes with value in \'escapedQuote\'', async (t) => { 613 | const opts = '--fields "a string" --escaped-quote "*"'; 614 | 615 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/escapedQuotes.json')}" ${opts}`); 616 | 617 | t.equal(csv, csvFixtures.escapedQuotes); 618 | }); 619 | 620 | // String Excel 621 | 622 | testRunner.add('should format strings to force excel to view the values as strings', async (t) => { 623 | const opts = '--fields carModel,price,color --excel-strings'; 624 | 625 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/default.json')}" ${opts}`); 626 | 627 | t.equal(csv, csvFixtures.excelStrings); 628 | }); 629 | 630 | testRunner.add('should format strings to force excel to view the values as strings with escaped quotes', async (t) => { 631 | const opts = '--excel-strings'; 632 | 633 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/quotes.json')}" ${opts}`); 634 | 635 | t.equal(csv, csvFixtures.excelStringsWithEscapedQuoted); 636 | }); 637 | 638 | // String Escaping and preserving values 639 | 640 | testRunner.add('should parse JSON values with trailing backslashes', async (t) => { 641 | const opts = '--fields carModel,price,color'; 642 | 643 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/trailingBackslash.json')}" ${opts}`); 644 | 645 | t.equal(csv, csvFixtures.trailingBackslash); 646 | }); 647 | 648 | testRunner.add('should escape " when preceeded by \\', async (t) => { 649 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/escapeDoubleBackslashedEscapedQuote.json')}"`); 650 | 651 | t.equal(csv, csvFixtures.escapeDoubleBackslashedEscapedQuote); 652 | }); 653 | 654 | testRunner.add('should preserve new lines in values', async (t) => { 655 | const opts = '--eol "\r\n"'; 656 | 657 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/escapeEOL.json')}" ${opts}`); 658 | 659 | t.equal(csv, [ 660 | '"a string"', 661 | '"with a \u2028description\\n and\na new line"', 662 | '"with a \u2029\u2028description and\r\nanother new line"' 663 | ].join('\r\n')); 664 | }); 665 | 666 | testRunner.add('should preserve tabs in values', async (t) => { 667 | const { stdout: csv } = await execAsync(`${cli} -i "${getFixturePath('/json/escapeTab.json')}"`); 668 | 669 | t.equal(csv, csvFixtures.escapeTab); 670 | }); 671 | }; 672 | -------------------------------------------------------------------------------- /test/JSON2CSVAsyncParserInMemory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { 4 | AsyncParser: Parser, 5 | transforms: { flatten, unwind }, 6 | formatters: { number: numberFormatter, string: stringFormatter, stringExcel: stringExcelFormatter, stringQuoteOnlyIfNecessary: stringQuoteOnlyIfNecessaryFormatter }, 7 | } = require('../lib/json2csv'); 8 | 9 | async function parseInput(parser, nodeStream) { 10 | return await parser.parse(nodeStream).promise(); 11 | } 12 | 13 | module.exports = (testRunner, jsonFixtures, csvFixtures) => { 14 | testRunner.add('should error if input is of an invalid format', async (t) => { 15 | try { 16 | const parser = new Parser(); 17 | await parseInput(parser, 123); 18 | 19 | t.fail('Exception expected'); 20 | } catch (err) { 21 | t.equal(err.message, 'Data should be a JSON object, JSON array, typed array, string or stream'); 22 | } 23 | }); 24 | 25 | testRunner.add('should handle object mode', async (t) => { 26 | const opts = { 27 | fields: ['carModel', 'price', 'color', 'manual'] 28 | }; 29 | const transformOpts = { objectMode: true }; 30 | 31 | const parser = new Parser(opts, transformOpts); 32 | const csv = await parseInput(parser, jsonFixtures.default({ objectMode: true })); 33 | 34 | t.equal(csv, csvFixtures.ndjson); 35 | }); 36 | 37 | testRunner.add('should not modify the opts passed', async (t) => { 38 | const opts = {}; 39 | const parser = new Parser(opts); 40 | const csv = await parseInput(parser, jsonFixtures.default()); 41 | 42 | t.equal(csv, csvFixtures.defaultStream); 43 | t.deepEqual(opts, {}); 44 | }); 45 | 46 | testRunner.add('should error if input data is empty and fields are not set', async (t) => { 47 | try { 48 | const parser = new Parser(); 49 | await parseInput(parser, jsonFixtures.empty()); 50 | 51 | t.fail('Exception expected'); 52 | } catch (err) { 53 | t.equal(err.message, 'Data should not be empty or the "fields" option should be included'); 54 | } 55 | }); 56 | 57 | testRunner.add('should error if input data is not an object', async (t) => { 58 | try { 59 | const parser = new Parser(); 60 | await parseInput(parser, `"${jsonFixtures.notAnObject()}"`); 61 | 62 | t.fail('Exception expected'); 63 | } catch (err) { 64 | t.equal(err.message, 'Data should be a JSON object or array'); 65 | } 66 | }); 67 | 68 | testRunner.add('should error if input data is not valid json', async (t) => { 69 | const opts = { 70 | fields: ['carModel', 'price', 'color', 'manual'] 71 | }; 72 | 73 | try { 74 | const parser = new Parser(opts); 75 | await parseInput(parser, jsonFixtures.defaultInvalid()); 76 | 77 | t.fail('Exception expected'); 78 | } catch (err) { 79 | t.equal(err.message, 'Unexpected LEFT_BRACE ("{") in state KEY'); 80 | } 81 | }); 82 | 83 | testRunner.add('should error if input data is not valid json and doesn\'t emit the first token', async (t) => { 84 | const opts = { 85 | fields: ['carModel', 'price', 'color', 'manual'] 86 | }; 87 | 88 | try { 89 | const parser = new Parser(opts); 90 | await parseInput(parser, jsonFixtures.invalidNoToken()); 91 | 92 | t.fail('Exception expected'); 93 | } catch (err) { 94 | t.equal(err.message, 'Data should be a JSON object or array'); 95 | } 96 | }); 97 | 98 | testRunner.add('should handle empty object', async (t) => { 99 | const opts = { 100 | fields: ['carModel', 'price', 'color'] 101 | }; 102 | 103 | const parser = new Parser(opts); 104 | const csv = await parseInput(parser, jsonFixtures.emptyObject()); 105 | 106 | t.equal(csv, csvFixtures.emptyObject); 107 | }); 108 | 109 | testRunner.add('should handle empty array', async (t) => { 110 | const opts = { 111 | fields: ['carModel', 'price', 'color'] 112 | }; 113 | 114 | const parser = new Parser(opts); 115 | const csv = await parseInput(parser, jsonFixtures.emptyArray()); 116 | 117 | t.equal(csv, csvFixtures.emptyObject); 118 | }); 119 | 120 | testRunner.add('should hanlde array with nulls', async (t) => { 121 | const opts = { 122 | fields: ['carModel', 'price', 'color'] 123 | }; 124 | 125 | const parser = new Parser(opts); 126 | const csv = await parseInput(parser, jsonFixtures.arrayWithNull()); 127 | 128 | t.equal(csv, csvFixtures.emptyObject); 129 | }); 130 | 131 | testRunner.add('should handle deep JSON objects', async (t) => { 132 | const parser = new Parser(); 133 | const csv = await parseInput(parser, jsonFixtures.deepJSON()); 134 | 135 | t.equal(csv, csvFixtures.deepJSON); 136 | }); 137 | 138 | testRunner.add('should parse json to csv and infer the fields automatically ', async (t) => { 139 | const parser = new Parser(); 140 | const csv = await parseInput(parser, jsonFixtures.default()); 141 | 142 | t.equal(csv, csvFixtures.defaultStream); 143 | }); 144 | 145 | testRunner.add('should parse json to csv using custom fields', async (t) => { 146 | const opts = { 147 | fields: ['carModel', 'price', 'color', 'manual'] 148 | }; 149 | 150 | const parser = new Parser(opts); 151 | const csv = await parseInput(parser, jsonFixtures.default()); 152 | 153 | t.equal(csv, csvFixtures.default); 154 | }); 155 | 156 | testRunner.add('should output only selected fields', async (t) => { 157 | const opts = { 158 | fields: ['carModel', 'price'] 159 | }; 160 | 161 | const parser = new Parser(opts); 162 | const csv = await parseInput(parser, jsonFixtures.default()); 163 | 164 | t.equal(csv, csvFixtures.selected); 165 | }); 166 | 167 | testRunner.add('should output fields in the order provided', async (t) => { 168 | const opts = { 169 | fields: ['price', 'carModel'] 170 | }; 171 | 172 | const parser = new Parser(opts); 173 | const csv = await parseInput(parser, jsonFixtures.default()); 174 | 175 | t.equal(csv, csvFixtures.reversed); 176 | }); 177 | 178 | testRunner.add('should output empty value for non-existing fields', async (t) => { 179 | const opts = { 180 | fields: ['first not exist field', 'carModel', 'price', 'not exist field', 'color'] 181 | }; 182 | 183 | const parser = new Parser(opts); 184 | const csv = await parseInput(parser, jsonFixtures.default()); 185 | 186 | t.equal(csv, csvFixtures.withNotExistField); 187 | }); 188 | 189 | testRunner.add('should name columns as specified in \'fields\' property', async (t) => { 190 | const opts = { 191 | fields: [{ 192 | label: 'Car Model', 193 | value: 'carModel' 194 | },{ 195 | label: 'Price USD', 196 | value: 'price' 197 | }] 198 | }; 199 | 200 | const parser = new Parser(opts); 201 | const csv = await parseInput(parser, jsonFixtures.default()); 202 | 203 | t.equal(csv, csvFixtures.fieldNames); 204 | }); 205 | 206 | testRunner.add('should error on invalid \'fields\' property', async (t) => { 207 | const opts = { 208 | fields: [ { value: 'price' }, () => {} ] 209 | }; 210 | 211 | try { 212 | const parser = new Parser(opts); 213 | await parseInput(parser, jsonFixtures.default()); 214 | 215 | t.fail('Exception expected'); 216 | } catch(error) { 217 | t.equal(error.message, `Invalid field info option. ${JSON.stringify(opts.fields[1])}`); 218 | } 219 | }); 220 | 221 | testRunner.add('should error on invalid \'fields.value\' property', async (t) => { 222 | const opts = { 223 | fields: [ 224 | { value: row => row.price }, 225 | { label: 'Price USD', value: [] } 226 | ] 227 | }; 228 | 229 | try { 230 | const parser = new Parser(opts); 231 | await parseInput(parser, jsonFixtures.default()); 232 | 233 | t.fail('Exception expected'); 234 | } catch(error) { 235 | t.equal(error.message, `Invalid field info option. ${JSON.stringify(opts.fields[1])}`); 236 | } 237 | }); 238 | 239 | testRunner.add('should support nested properties selectors', async (t) => { 240 | const opts = { 241 | fields: [{ 242 | label: 'Make', 243 | value: 'car.make' 244 | },{ 245 | label: 'Model', 246 | value: 'car.model' 247 | },{ 248 | label: 'Price', 249 | value: 'price' 250 | },{ 251 | label: 'Color', 252 | value: 'color' 253 | },{ 254 | label: 'Year', 255 | value: 'car.ye.ar' 256 | }] 257 | }; 258 | 259 | const parser = new Parser(opts); 260 | const csv = await parseInput(parser, jsonFixtures.nested()); 261 | 262 | t.equal(csv, csvFixtures.nested); 263 | }); 264 | 265 | testRunner.add('field.value function should receive a valid field object', async (t) => { 266 | const opts = { 267 | fields: [{ 268 | label: 'Value1', 269 | default: 'default value', 270 | value: (row, field) => { 271 | t.deepEqual(field, { label: 'Value1', default: 'default value' }); 272 | return row.value1.toLocaleString(); 273 | } 274 | }] 275 | }; 276 | 277 | const parser = new Parser(opts); 278 | const csv = await parseInput(parser, jsonFixtures.functionStringifyByDefault()); 279 | 280 | t.equal(csv, csvFixtures.functionStringifyByDefault); 281 | }); 282 | 283 | testRunner.add('field.value function should stringify results by default', async (t) => { 284 | const opts = { 285 | fields: [{ 286 | label: 'Value1', 287 | value: row => row.value1.toLocaleString() 288 | }] 289 | }; 290 | 291 | const parser = new Parser(opts); 292 | const csv = await parseInput(parser, jsonFixtures.functionStringifyByDefault()); 293 | 294 | t.equal(csv, csvFixtures.functionStringifyByDefault); 295 | }); 296 | 297 | testRunner.add('should process different combinations in fields option', async (t) => { 298 | const opts = { 299 | fields: [{ 300 | label: 'PATH1', 301 | value: 'path1' 302 | }, { 303 | label: 'PATH1+PATH2', 304 | value: row => row.path1+row.path2 305 | }, { 306 | label: 'NEST1', 307 | value: 'bird.nest1' 308 | }, 309 | 'bird.nest2', 310 | { 311 | label: 'nonexistent', 312 | value: 'fake.path', 313 | default: 'col specific default value' 314 | }], 315 | defaultValue: 'NULL' 316 | }; 317 | 318 | const parser = new Parser(opts); 319 | const csv = await parseInput(parser, jsonFixtures.fancyfields()); 320 | 321 | t.equal(csv, csvFixtures.fancyfields); 322 | }); 323 | 324 | // Default value 325 | 326 | testRunner.add('should output the default value as set in \'defaultValue\'', async (t) => { 327 | const opts = { 328 | fields: ['carModel', 'price'], 329 | defaultValue: '' 330 | }; 331 | 332 | const parser = new Parser(opts); 333 | const csv = await parseInput(parser, jsonFixtures.defaultValueEmpty()); 334 | 335 | t.equal(csv, csvFixtures.defaultValueEmpty); 336 | }); 337 | 338 | testRunner.add('should override \'options.defaultValue\' with \'field.defaultValue\'', async (t) => { 339 | const opts = { 340 | fields: [ 341 | { value: 'carModel' }, 342 | { value: 'price', default: 1 }, 343 | { value: 'color' } 344 | ], 345 | defaultValue: '' 346 | }; 347 | 348 | const parser = new Parser(opts); 349 | const csv = await parseInput(parser, jsonFixtures.overriddenDefaultValue()); 350 | 351 | t.equal(csv, csvFixtures.overriddenDefaultValue); 352 | }); 353 | 354 | testRunner.add('should use \'options.defaultValue\' when no \'field.defaultValue\'', async (t) => { 355 | const opts = { 356 | fields: [ 357 | { 358 | value: 'carModel' 359 | }, 360 | { 361 | label: 'price', 362 | value: row => row.price, 363 | default: 1 364 | }, 365 | { 366 | label: 'color', 367 | value: row => row.color 368 | } 369 | ], 370 | defaultValue: '' 371 | }; 372 | 373 | const parser = new Parser(opts); 374 | const csv = await parseInput(parser, jsonFixtures.overriddenDefaultValue()); 375 | 376 | t.equal(csv, csvFixtures.overriddenDefaultValue); 377 | }); 378 | 379 | // Delimiter 380 | 381 | testRunner.add('should use a custom delimiter when \'delimiter\' property is defined', async (t) => { 382 | const opts = { 383 | fields: ['carModel', 'price', 'color'], 384 | delimiter: '\t' 385 | }; 386 | 387 | const parser = new Parser(opts); 388 | const csv = await parseInput(parser, jsonFixtures.default()); 389 | 390 | t.equal(csv, csvFixtures.tsv); 391 | }); 392 | 393 | testRunner.add('should remove last delimiter |@|', async (t) => { 394 | const opts = { delimiter: '|@|' }; 395 | 396 | const parser = new Parser(opts); 397 | const csv = await parseInput(parser, jsonFixtures.delimiter()); 398 | 399 | t.equal(csv, csvFixtures.delimiter); 400 | }); 401 | 402 | // EOL 403 | 404 | testRunner.add('should use a custom eol character when \'eol\' property is present', async (t) => { 405 | const opts = { 406 | fields: ['carModel', 'price', 'color'], 407 | eol: '\r\n' 408 | }; 409 | 410 | const parser = new Parser(opts); 411 | const csv = await parseInput(parser, jsonFixtures.default()); 412 | 413 | t.equal(csv, csvFixtures.eol); 414 | }); 415 | 416 | // Header 417 | 418 | testRunner.add('should parse json to csv without column title', async (t) => { 419 | const opts = { 420 | header: false, 421 | fields: ['carModel', 'price', 'color', 'manual'] 422 | }; 423 | 424 | const parser = new Parser(opts); 425 | const csv = await parseInput(parser, jsonFixtures.default()); 426 | 427 | t.equal(csv, csvFixtures.withoutHeader); 428 | }); 429 | 430 | // Include empty rows 431 | 432 | testRunner.add('should not include empty rows when options.includeEmptyRows is not specified', async (t) => { 433 | const parser = new Parser(); 434 | const csv = await parseInput(parser, jsonFixtures.emptyRow()); 435 | 436 | t.equal(csv, csvFixtures.emptyRowNotIncluded); 437 | }); 438 | 439 | testRunner.add('should include empty rows when options.includeEmptyRows is true', async (t) => { 440 | const opts = { 441 | includeEmptyRows: true 442 | }; 443 | 444 | const parser = new Parser(opts); 445 | const csv = await parseInput(parser, jsonFixtures.emptyRow()); 446 | 447 | t.equal(csv, csvFixtures.emptyRow); 448 | }); 449 | 450 | testRunner.add('should not include empty rows when options.includeEmptyRows is false', async (t) => { 451 | const opts = { 452 | includeEmptyRows: false, 453 | }; 454 | 455 | const parser = new Parser(opts); 456 | const csv = await parseInput(parser, jsonFixtures.emptyRow()); 457 | 458 | t.equal(csv, csvFixtures.emptyRowNotIncluded); 459 | }); 460 | 461 | testRunner.add('should include empty rows when options.includeEmptyRows is true, with default values', async (t) => { 462 | const opts = { 463 | fields: [ 464 | { 465 | value: 'carModel' 466 | }, 467 | { 468 | value: 'price', 469 | default: 1 470 | }, 471 | { 472 | value: 'color' 473 | } 474 | ], 475 | defaultValue: 'NULL', 476 | includeEmptyRows: true, 477 | }; 478 | 479 | const parser = new Parser(opts); 480 | const csv = await parseInput(parser, jsonFixtures.emptyRow()); 481 | 482 | t.equal(csv, csvFixtures.emptyRowDefaultValues); 483 | }); 484 | 485 | testRunner.add('should parse data:[null] to csv with only column title, despite options.includeEmptyRows', async (t) => { 486 | const opts = { 487 | fields: ['carModel', 'price', 'color'], 488 | includeEmptyRows: true, 489 | }; 490 | 491 | const parser = new Parser(opts); 492 | const csv = await parseInput(parser, jsonFixtures.arrayWithNull()); 493 | 494 | t.equal(csv, csvFixtures.emptyObject); 495 | }); 496 | 497 | // BOM 498 | 499 | testRunner.add('should add BOM character', async (t) => { 500 | const opts = { 501 | withBOM: true, 502 | fields: ['carModel', 'price', 'color', 'manual'] 503 | }; 504 | 505 | const parser = new Parser(opts); 506 | const csv = await parseInput(parser, jsonFixtures.specialCharacters()); 507 | 508 | // Compare csv length to check if the BOM character is present 509 | t.equal(csv[0], '\ufeff'); 510 | t.equal(csv.length, csvFixtures.default.length + 1); 511 | t.equal(csv.length, csvFixtures.withBOM.length); 512 | }); 513 | 514 | // Transforms 515 | 516 | testRunner.add('should unwind all unwindable fields using the unwind transform', async (t) => { 517 | const opts = { 518 | fields: ['carModel', 'price', 'extras.items.name', 'extras.items.color', 'extras.items.items.position', 'extras.items.items.color'], 519 | transforms: [unwind()], 520 | }; 521 | 522 | const parser = new Parser(opts); 523 | const csv = await parseInput(parser, jsonFixtures.unwind2()); 524 | 525 | t.equal(csv, csvFixtures.unwind2); 526 | }); 527 | 528 | testRunner.add('should support unwinding specific fields using the unwind transform', async (t) => { 529 | const opts = { 530 | fields: ['carModel', 'price', 'colors'], 531 | transforms: [unwind({ paths: ['colors'] })], 532 | }; 533 | 534 | const parser = new Parser(opts); 535 | const csv = await parseInput(parser, jsonFixtures.unwind()); 536 | 537 | t.equal(csv, csvFixtures.unwind); 538 | }); 539 | 540 | testRunner.add('should support multi-level unwind using the unwind transform', async (t) => { 541 | const opts = { 542 | fields: ['carModel', 'price', 'extras.items.name', 'extras.items.color', 'extras.items.items.position', 'extras.items.items.color'], 543 | transforms: [unwind({ paths: ['extras.items', 'extras.items.items'] })], 544 | }; 545 | 546 | const parser = new Parser(opts); 547 | const csv = await parseInput(parser, jsonFixtures.unwind2()); 548 | 549 | t.equal(csv, csvFixtures.unwind2); 550 | }); 551 | 552 | testRunner.add('should support unwind and blank out repeated data using the unwind transform', async (t) => { 553 | const opts = { 554 | fields: ['carModel', 'price', 'extras.items.name', 'extras.items.color', 'extras.items.items.position', 'extras.items.items.color'], 555 | transforms: [unwind({ paths: ['extras.items', 'extras.items.items'], blankOut: true })], 556 | }; 557 | 558 | const parser = new Parser(opts); 559 | const csv = await parseInput(parser, jsonFixtures.unwind2()); 560 | 561 | t.equal(csv, csvFixtures.unwind2Blank); 562 | }); 563 | 564 | testRunner.add('should support flattening deep JSON using the flatten transform', async (t) => { 565 | const opts = { 566 | transforms: [flatten()], 567 | }; 568 | 569 | const parser = new Parser(opts); 570 | const csv = await parseInput(parser, jsonFixtures.deepJSON()); 571 | 572 | t.equal(csv, csvFixtures.flattenedDeepJSON); 573 | }); 574 | 575 | testRunner.add('should support flattening JSON with nested arrays using the flatten transform', async (t) => { 576 | const opts = { 577 | transforms: [flatten({ arrays: true })], 578 | }; 579 | 580 | const parser = new Parser(opts); 581 | const csv = await parseInput(parser, jsonFixtures.flattenArrays()); 582 | 583 | t.equal(csv, csvFixtures.flattenedArrays); 584 | }); 585 | 586 | testRunner.add('should support custom flatten separator using the flatten transform', async (t) => { 587 | const opts = { 588 | transforms: [flatten({ separator: '__' })], 589 | }; 590 | 591 | const parser = new Parser(opts); 592 | const csv = await parseInput(parser, jsonFixtures.deepJSON()); 593 | 594 | t.equal(csv, csvFixtures.flattenedCustomSeparatorDeepJSON); 595 | }); 596 | 597 | testRunner.add('should support multiple transforms and honor the order in which they are declared', async (t) => { 598 | const opts = { 599 | transforms: [unwind({ paths: ['items'] }), flatten()], 600 | }; 601 | 602 | const parser = new Parser(opts); 603 | const csv = await parseInput(parser, jsonFixtures.unwindAndFlatten()); 604 | 605 | t.equal(csv, csvFixtures.unwindAndFlatten); 606 | }); 607 | 608 | testRunner.add('should unwind complex objects using the unwind transform', async (t) => { 609 | const opts = { 610 | fields: ["carModel", "price", "extras.items.name", "extras.items.items.position", "extras.items.items.color", "extras.items.color"], 611 | transforms: [unwind({ paths: ['extras.items', 'extras.items.items'] }), flatten()], 612 | }; 613 | 614 | const parser = new Parser(opts); 615 | const csv = await parseInput(parser, jsonFixtures.unwindComplexObject()); 616 | 617 | t.equal(csv, csvFixtures.unwindComplexObject); 618 | }); 619 | 620 | testRunner.add('should support custom transforms', async (t) => { 621 | const opts = { 622 | transforms: [row => ({ 623 | model: row.carModel, 624 | price: row.price / 1000, 625 | color: row.color, 626 | manual: row.manual || 'automatic', 627 | })], 628 | }; 629 | 630 | const parser = new Parser(opts); 631 | const csv = await parseInput(parser, jsonFixtures.default()); 632 | 633 | t.equal(csv, csvFixtures.defaultCustomTransform); 634 | }); 635 | 636 | // Formatters 637 | 638 | // Number 639 | 640 | testRunner.add('should used a custom separator when \'decimals\' is passed to the number formatter', async (t) => { 641 | const opts = { 642 | formatters: { 643 | number: numberFormatter({ decimals: 2 }) 644 | } 645 | }; 646 | const parser = new Parser(opts); 647 | const csv = await parseInput(parser, jsonFixtures.numberFormatter()); 648 | 649 | t.equal(csv, csvFixtures.numberFixedDecimals); 650 | }); 651 | 652 | testRunner.add('should used a custom separator when \'separator\' is passed to the number formatter', async (t) => { 653 | const opts = { 654 | delimiter: ';', 655 | formatters: { 656 | number: numberFormatter({ separator: ',' }) 657 | } 658 | }; 659 | const parser = new Parser(opts); 660 | const csv = await parseInput(parser, jsonFixtures.numberFormatter()); 661 | 662 | t.equal(csv, csvFixtures.numberCustomSeparator); 663 | }); 664 | 665 | testRunner.add('should used a custom separator and fixed number of decimals when \'separator\' and \'decimals\' are passed to the number formatter', async (t) => { 666 | const opts = { 667 | delimiter: ';', 668 | formatters: { 669 | number: numberFormatter({ separator: ',', decimals: 2 }) 670 | } 671 | }; 672 | const parser = new Parser(opts); 673 | const csv = await parseInput(parser, jsonFixtures.numberFormatter()); 674 | 675 | t.equal(csv, csvFixtures.numberFixedDecimalsAndCustomSeparator); 676 | }); 677 | 678 | // Symbol 679 | 680 | testRunner.add('should format Symbol by its name', async (t) => { 681 | const transformOpts = { objectMode: true }; 682 | 683 | const parser = new Parser({}, transformOpts); 684 | const csv = await parseInput(parser, jsonFixtures.symbol({ objectMode: true })); 685 | 686 | t.equal(csv, csvFixtures.symbol); 687 | }); 688 | 689 | // String Quote 690 | 691 | testRunner.add('should use a custom quote when \'quote\' property is present', async (t) => { 692 | const opts = { 693 | fields: ['carModel', 'price'], 694 | formatters: { 695 | string: stringFormatter({ quote: '\'' }) 696 | } 697 | }; 698 | 699 | const parser = new Parser(opts); 700 | const csv = await parseInput(parser, jsonFixtures.default()); 701 | 702 | t.equal(csv, csvFixtures.withSimpleQuotes); 703 | }); 704 | 705 | testRunner.add('should be able to don\'t output quotes when setting \'quote\' to empty string', async (t) => { 706 | const opts = { 707 | fields: ['carModel', 'price'], 708 | formatters: { 709 | string: stringFormatter({ quote: '' }) 710 | } 711 | }; 712 | 713 | const parser = new Parser(opts); 714 | const csv = await parseInput(parser, jsonFixtures.default()); 715 | 716 | t.equal(csv, csvFixtures.withoutQuotes); 717 | }); 718 | 719 | testRunner.add('should escape quotes when setting \'quote\' property is present', async (t) => { 720 | const opts = { 721 | fields: ['carModel', 'color'], 722 | formatters: { 723 | string: stringFormatter({ quote: '\'' }) 724 | } 725 | }; 726 | 727 | const parser = new Parser(opts); 728 | const csv = await parseInput(parser, jsonFixtures.escapeCustomQuotes()); 729 | 730 | t.equal(csv, csvFixtures.escapeCustomQuotes); 731 | }); 732 | 733 | testRunner.add('should not escape \'"\' when setting \'quote\' set to something else', async (t) => { 734 | const opts = { 735 | formatters: { 736 | string: stringFormatter({ quote: '\'' }) 737 | } 738 | }; 739 | 740 | const parser = new Parser(opts); 741 | const csv = await parseInput(parser, jsonFixtures.escapedQuotes()); 742 | 743 | t.equal(csv, csvFixtures.escapedQuotesUnescaped); 744 | }); 745 | 746 | // String Escaped Quote 747 | 748 | testRunner.add('should escape quotes with double quotes', async (t) => { 749 | const parser = new Parser(); 750 | const csv = await parseInput(parser, jsonFixtures.quotes()); 751 | 752 | t.equal(csv, csvFixtures.quotes); 753 | }); 754 | 755 | testRunner.add('should not escape quotes with double quotes, when there is a backslash in the end', async (t) => { 756 | const parser = new Parser(); 757 | const csv = await parseInput(parser, jsonFixtures.backslashAtEnd()); 758 | 759 | t.equal(csv, csvFixtures.backslashAtEnd); 760 | }); 761 | 762 | testRunner.add('should not escape quotes with double quotes, when there is a backslash in the end, and its not the last column', async (t) => { 763 | const parser = new Parser(); 764 | const csv = await parseInput(parser, jsonFixtures.backslashAtEndInMiddleColumn()); 765 | 766 | t.equal(csv, csvFixtures.backslashAtEndInMiddleColumn); 767 | }); 768 | 769 | testRunner.add('should escape quotes with value in \'escapedQuote\'', async (t) => { 770 | const opts = { 771 | fields: ['a string'], 772 | formatters: { 773 | string: stringFormatter({ escapedQuote: '*' }) 774 | } 775 | }; 776 | 777 | const parser = new Parser(opts); 778 | const csv = await parseInput(parser, jsonFixtures.escapedQuotes()); 779 | 780 | t.equal(csv, csvFixtures.escapedQuotes); 781 | }); 782 | 783 | testRunner.add('should escape quotes before new line with value in \'escapedQuote\'', async (t) => { 784 | const opts = { 785 | fields: ['a string'] 786 | }; 787 | 788 | const parser = new Parser(opts); 789 | const csv = await parseInput(parser, jsonFixtures.backslashBeforeNewLine()); 790 | 791 | t.equal(csv, csvFixtures.backslashBeforeNewLine); 792 | }); 793 | 794 | // String Quote Only if Necessary 795 | 796 | testRunner.add('should quote only if necessary if using stringQuoteOnlyIfNecessary formatter', async (t) => { 797 | const opts = { 798 | formatters: { 799 | string: stringQuoteOnlyIfNecessaryFormatter() 800 | } 801 | }; 802 | 803 | const parser = new Parser(opts); 804 | const csv = await parseInput(parser, jsonFixtures.quoteOnlyIfNecessary()); 805 | 806 | t.equal(csv, csvFixtures.quoteOnlyIfNecessary); 807 | }); 808 | 809 | // String Excel 810 | 811 | testRunner.add('should format strings to force excel to view the values as strings', async (t) => { 812 | const opts = { 813 | fields: ['carModel', 'price', 'color'], 814 | formatters: { 815 | string: stringExcelFormatter 816 | } 817 | }; 818 | 819 | const parser = new Parser(opts); 820 | const csv = await parseInput(parser, jsonFixtures.default()); 821 | 822 | t.equal(csv, csvFixtures.excelStrings); 823 | }); 824 | 825 | testRunner.add('should format strings to force excel to view the values as strings with escaped quotes', async (t) => { 826 | const opts = { 827 | formatters: { 828 | string: stringExcelFormatter 829 | } 830 | }; 831 | 832 | const parser = new Parser(opts); 833 | const csv = await parseInput(parser, jsonFixtures.quotes()); 834 | 835 | t.equal(csv, csvFixtures.excelStringsWithEscapedQuoted); 836 | }); 837 | 838 | // String Escaping and preserving values 839 | 840 | testRunner.add('should parse JSON values with trailing backslashes', async (t) => { 841 | const opts = { 842 | fields: ['carModel', 'price', 'color'] 843 | }; 844 | 845 | const parser = new Parser(opts); 846 | const csv = await parseInput(parser, jsonFixtures.trailingBackslash()); 847 | 848 | t.equal(csv, csvFixtures.trailingBackslash); 849 | }); 850 | 851 | testRunner.add('should escape " when preceeded by \\', async (t) => { 852 | const parser = new Parser(); 853 | const csv = await parseInput(parser, jsonFixtures.escapeDoubleBackslashedEscapedQuote()); 854 | 855 | t.equal(csv, csvFixtures.escapeDoubleBackslashedEscapedQuote); 856 | }); 857 | 858 | testRunner.add('should preserve new lines in values', async (t) => { 859 | const opts = { 860 | eol: '\r\n' 861 | }; 862 | 863 | const parser = new Parser(opts); 864 | const csv = await parseInput(parser, jsonFixtures.escapeEOL()); 865 | 866 | 867 | t.equal(csv, [ 868 | '"a string"', 869 | '"with a \u2028description\\n and\na new line"', 870 | '"with a \u2029\u2028description and\r\nanother new line"' 871 | ].join('\r\n')); 872 | 873 | }); 874 | 875 | testRunner.add('should preserve tabs in values', async (t) => { 876 | const parser = new Parser(); 877 | const csv = await parseInput(parser, jsonFixtures.escapeTab()); 878 | 879 | t.equal(csv, csvFixtures.escapeTab); 880 | }); 881 | 882 | // Headers 883 | 884 | testRunner.add('should format headers based on the headers formatter', async (t) => { 885 | const opts = { 886 | fields: ['carModel', 'price', 'color', 'manual'], 887 | formatters: { 888 | header: stringFormatter({ quote: '' }) 889 | } 890 | }; 891 | 892 | const parser = new Parser(opts); 893 | const csv = await parseInput(parser, jsonFixtures.default()); 894 | 895 | t.equal(csv, csvFixtures.customHeaderQuotes); 896 | }); 897 | }; 898 | -------------------------------------------------------------------------------- /test/JSON2CSVParser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { 4 | Parser, 5 | transforms: { flatten, unwind }, 6 | formatters: { number: numberFormatter, string: stringFormatter, stringExcel: stringExcelFormatter, stringQuoteOnlyIfNecessary: stringQuoteOnlyIfNecessaryFormatter }, 7 | } = require('../lib/json2csv'); 8 | 9 | async function parseInput(parser, nodeStream) { 10 | return parser.parse(nodeStream); 11 | } 12 | 13 | module.exports = (testRunner, jsonFixtures, csvFixtures) => { 14 | testRunner.add('should not modify the JSON object passed passed', async (t) => { 15 | const opts = { 16 | fields: ["carModel","price","extras.items.name","extras.items.items.position","extras.items.items.color","extras.items.color"], 17 | transforms: [unwind({ paths: ['extras.items', 'extras.items.items'] }), flatten()], 18 | }; 19 | const originalJson = JSON.parse(JSON.stringify(jsonFixtures.unwindComplexObject())); 20 | 21 | const parser = new Parser(opts); 22 | const csv = await parseInput(parser, originalJson); 23 | 24 | t.equal(csv, csvFixtures.unwindComplexObject); 25 | t.deepEqual(jsonFixtures.unwindComplexObject(), originalJson); 26 | }); 27 | 28 | testRunner.add('should not modify the opts passed', async (t) => { 29 | const opts = {}; 30 | const parser = new Parser(opts); 31 | const csv = await parseInput(parser, jsonFixtures.default()); 32 | 33 | t.ok(typeof csv === 'string'); 34 | t.equal(csv, csvFixtures.default); 35 | t.deepEqual(opts, {}); 36 | }); 37 | 38 | testRunner.add('should error if input data is not an object', async (t) => { 39 | try { 40 | const parser = new Parser(); 41 | await parseInput(parser, jsonFixtures.notAnObject()); 42 | 43 | t.fail('Exception expected'); 44 | } catch (err) { 45 | t.equal(err.message, 'Data should not be empty or the "fields" option should be included'); 46 | } 47 | }); 48 | 49 | testRunner.add('should handle empty object', async (t) => { 50 | const opts = { 51 | fields: ['carModel', 'price', 'color'] 52 | }; 53 | 54 | const parser = new Parser(opts); 55 | const csv = await parseInput(parser, jsonFixtures.emptyObject()); 56 | 57 | t.equal(csv, csvFixtures.emptyObject); 58 | }); 59 | 60 | testRunner.add('should handle empty array', async (t) => { 61 | const opts = { 62 | fields: ['carModel', 'price', 'color'] 63 | }; 64 | 65 | const parser = new Parser(opts); 66 | const csv = await parseInput(parser, jsonFixtures.emptyArray()); 67 | 68 | t.equal(csv, csvFixtures.emptyObject); 69 | }); 70 | 71 | testRunner.add('should hanlde array with nulls', async (t) => { 72 | const opts = { 73 | fields: ['carModel', 'price', 'color'] 74 | }; 75 | 76 | const parser = new Parser(opts); 77 | const csv = await parseInput(parser, jsonFixtures.arrayWithNull()); 78 | 79 | t.equal(csv, csvFixtures.emptyObject); 80 | }); 81 | 82 | testRunner.add('should handle deep JSON objects', async (t) => { 83 | const parser = new Parser(); 84 | const csv = await parseInput(parser, jsonFixtures.deepJSON()); 85 | 86 | t.equal(csv, csvFixtures.deepJSON); 87 | }); 88 | 89 | testRunner.add('should parse json to csv and infer the fields automatically ', async (t) => { 90 | const parser = new Parser(); 91 | const csv = await parseInput(parser, jsonFixtures.default()); 92 | 93 | t.equal(csv, csvFixtures.default); 94 | }); 95 | 96 | testRunner.add('should parse json to csv using custom fields', async (t) => { 97 | const opts = { 98 | fields: ['carModel', 'price', 'color', 'manual'] 99 | }; 100 | 101 | const parser = new Parser(opts); 102 | const csv = await parseInput(parser, jsonFixtures.default()); 103 | 104 | t.equal(csv, csvFixtures.default); 105 | }); 106 | 107 | testRunner.add('should output only selected fields', async (t) => { 108 | const opts = { 109 | fields: ['carModel', 'price'] 110 | }; 111 | 112 | const parser = new Parser(opts); 113 | const csv = await parseInput(parser, jsonFixtures.default()); 114 | 115 | t.equal(csv, csvFixtures.selected); 116 | }); 117 | 118 | testRunner.add('should output fields in the order provided', async (t) => { 119 | const opts = { 120 | fields: ['price', 'carModel'] 121 | }; 122 | 123 | const parser = new Parser(opts); 124 | const csv = await parseInput(parser, jsonFixtures.default()); 125 | 126 | t.equal(csv, csvFixtures.reversed); 127 | }); 128 | 129 | testRunner.add('should output empty value for non-existing fields', async (t) => { 130 | const opts = { 131 | fields: ['first not exist field', 'carModel', 'price', 'not exist field', 'color'] 132 | }; 133 | 134 | const parser = new Parser(opts); 135 | const csv = await parseInput(parser, jsonFixtures.default()); 136 | 137 | t.equal(csv, csvFixtures.withNotExistField); 138 | }); 139 | 140 | testRunner.add('should name columns as specified in \'fields\' property', async (t) => { 141 | const opts = { 142 | fields: [{ 143 | label: 'Car Model', 144 | value: 'carModel' 145 | },{ 146 | label: 'Price USD', 147 | value: 'price' 148 | }] 149 | }; 150 | 151 | const parser = new Parser(opts); 152 | const csv = await parseInput(parser, jsonFixtures.default()); 153 | 154 | t.equal(csv, csvFixtures.fieldNames); 155 | }); 156 | 157 | testRunner.add('should error on invalid \'fields\' property', async (t) => { 158 | const opts = { 159 | fields: [ { value: 'price' }, () => {} ] 160 | }; 161 | 162 | try { 163 | const parser = new Parser(opts); 164 | await parseInput(parser, jsonFixtures.default()); 165 | 166 | t.fail('Exception expected'); 167 | } catch(error) { 168 | t.equal(error.message, `Invalid field info option. ${JSON.stringify(opts.fields[1])}`); 169 | } 170 | }); 171 | 172 | testRunner.add('should error on invalid \'fields.value\' property', async (t) => { 173 | const opts = { 174 | fields: [ 175 | { value: row => row.price }, 176 | { label: 'Price USD', value: [] } 177 | ] 178 | }; 179 | 180 | try { 181 | const parser = new Parser(opts); 182 | await parseInput(parser, jsonFixtures.default()); 183 | 184 | t.fail('Exception expected'); 185 | } catch(error) { 186 | t.equal(error.message, `Invalid field info option. ${JSON.stringify(opts.fields[1])}`); 187 | } 188 | }); 189 | 190 | testRunner.add('should support nested properties selectors', async (t) => { 191 | const opts = { 192 | fields: [{ 193 | label: 'Make', 194 | value: 'car.make' 195 | },{ 196 | label: 'Model', 197 | value: 'car.model' 198 | },{ 199 | label: 'Price', 200 | value: 'price' 201 | },{ 202 | label: 'Color', 203 | value: 'color' 204 | },{ 205 | label: 'Year', 206 | value: 'car.ye.ar' 207 | }] 208 | }; 209 | 210 | const parser = new Parser(opts); 211 | const csv = await parseInput(parser, jsonFixtures.nested()); 212 | 213 | t.equal(csv, csvFixtures.nested); 214 | }); 215 | 216 | testRunner.add('field.value function should receive a valid field object', async (t) => { 217 | const opts = { 218 | fields: [{ 219 | label: 'Value1', 220 | default: 'default value', 221 | value: (row, field) => { 222 | t.deepEqual(field, { label: 'Value1', default: 'default value' }); 223 | return row.value1.toLocaleString(); 224 | } 225 | }] 226 | }; 227 | 228 | const parser = new Parser(opts); 229 | const csv = await parseInput(parser, jsonFixtures.functionStringifyByDefault()); 230 | 231 | t.equal(csv, csvFixtures.functionStringifyByDefault); 232 | }); 233 | 234 | testRunner.add('field.value function should stringify results by default', async (t) => { 235 | const opts = { 236 | fields: [{ 237 | label: 'Value1', 238 | value: row => row.value1.toLocaleString() 239 | }] 240 | }; 241 | 242 | const parser = new Parser(opts); 243 | const csv = await parseInput(parser, jsonFixtures.functionStringifyByDefault()); 244 | 245 | t.equal(csv, csvFixtures.functionStringifyByDefault); 246 | }); 247 | 248 | testRunner.add('should process different combinations in fields option', async (t) => { 249 | const opts = { 250 | fields: [{ 251 | label: 'PATH1', 252 | value: 'path1' 253 | }, { 254 | label: 'PATH1+PATH2', 255 | value: row => row.path1+row.path2 256 | }, { 257 | label: 'NEST1', 258 | value: 'bird.nest1' 259 | }, 260 | 'bird.nest2', 261 | { 262 | label: 'nonexistent', 263 | value: 'fake.path', 264 | default: 'col specific default value' 265 | }], 266 | defaultValue: 'NULL' 267 | }; 268 | 269 | const parser = new Parser(opts); 270 | const csv = await parseInput(parser, jsonFixtures.fancyfields()); 271 | 272 | t.equal(csv, csvFixtures.fancyfields); 273 | }); 274 | 275 | // Default value 276 | 277 | testRunner.add('should output the default value as set in \'defaultValue\'', async (t) => { 278 | const opts = { 279 | fields: ['carModel', 'price'], 280 | defaultValue: '' 281 | }; 282 | 283 | const parser = new Parser(opts); 284 | const csv = await parseInput(parser, jsonFixtures.defaultValueEmpty()); 285 | 286 | t.equal(csv, csvFixtures.defaultValueEmpty); 287 | }); 288 | 289 | testRunner.add('should override \'options.defaultValue\' with \'field.defaultValue\'', async (t) => { 290 | const opts = { 291 | fields: [ 292 | { value: 'carModel' }, 293 | { value: 'price', default: 1 }, 294 | { value: 'color' } 295 | ], 296 | defaultValue: '' 297 | }; 298 | 299 | const parser = new Parser(opts); 300 | const csv = await parseInput(parser, jsonFixtures.overriddenDefaultValue()); 301 | 302 | t.equal(csv, csvFixtures.overriddenDefaultValue); 303 | }); 304 | 305 | testRunner.add('should use \'options.defaultValue\' when no \'field.defaultValue\'', async (t) => { 306 | const opts = { 307 | fields: [ 308 | { 309 | value: 'carModel' 310 | }, 311 | { 312 | label: 'price', 313 | value: row => row.price, 314 | default: 1 315 | }, 316 | { 317 | label: 'color', 318 | value: row => row.color 319 | } 320 | ], 321 | defaultValue: '' 322 | }; 323 | 324 | const parser = new Parser(opts); 325 | const csv = await parseInput(parser, jsonFixtures.overriddenDefaultValue()); 326 | 327 | t.equal(csv, csvFixtures.overriddenDefaultValue); 328 | }); 329 | 330 | // Delimiter 331 | 332 | testRunner.add('should use a custom delimiter when \'delimiter\' property is defined', async (t) => { 333 | const opts = { 334 | fields: ['carModel', 'price', 'color'], 335 | delimiter: '\t' 336 | }; 337 | 338 | const parser = new Parser(opts); 339 | const csv = await parseInput(parser, jsonFixtures.default()); 340 | 341 | t.equal(csv, csvFixtures.tsv); 342 | }); 343 | 344 | testRunner.add('should remove last delimiter |@|', async (t) => { 345 | const opts = { delimiter: '|@|' }; 346 | 347 | const parser = new Parser(opts); 348 | const csv = await parseInput(parser, jsonFixtures.delimiter()); 349 | 350 | t.equal(csv, csvFixtures.delimiter); 351 | }); 352 | 353 | // EOL 354 | 355 | testRunner.add('should use a custom eol character when \'eol\' property is present', async (t) => { 356 | const opts = { 357 | fields: ['carModel', 'price', 'color'], 358 | eol: '\r\n' 359 | }; 360 | 361 | const parser = new Parser(opts); 362 | const csv = await parseInput(parser, jsonFixtures.default()); 363 | 364 | t.equal(csv, csvFixtures.eol); 365 | }); 366 | 367 | // Header 368 | 369 | testRunner.add('should parse json to csv without column title', async (t) => { 370 | const opts = { 371 | header: false, 372 | fields: ['carModel', 'price', 'color', 'manual'] 373 | }; 374 | 375 | const parser = new Parser(opts); 376 | const csv = await parseInput(parser, jsonFixtures.default()); 377 | 378 | t.equal(csv, csvFixtures.withoutHeader); 379 | }); 380 | 381 | // Include empty rows 382 | 383 | testRunner.add('should not include empty rows when options.includeEmptyRows is not specified', async (t) => { 384 | const parser = new Parser(); 385 | const csv = await parseInput(parser, jsonFixtures.emptyRow()); 386 | 387 | t.equal(csv, csvFixtures.emptyRowNotIncluded); 388 | }); 389 | 390 | testRunner.add('should include empty rows when options.includeEmptyRows is true', async (t) => { 391 | const opts = { 392 | includeEmptyRows: true 393 | }; 394 | 395 | const parser = new Parser(opts); 396 | const csv = await parseInput(parser, jsonFixtures.emptyRow()); 397 | 398 | t.equal(csv, csvFixtures.emptyRow); 399 | }); 400 | 401 | testRunner.add('should not include empty rows when options.includeEmptyRows is false', async (t) => { 402 | const opts = { 403 | includeEmptyRows: false, 404 | }; 405 | 406 | const parser = new Parser(opts); 407 | const csv = await parseInput(parser, jsonFixtures.emptyRow()); 408 | 409 | t.equal(csv, csvFixtures.emptyRowNotIncluded); 410 | }); 411 | 412 | testRunner.add('should include empty rows when options.includeEmptyRows is true, with default values', async (t) => { 413 | const opts = { 414 | fields: [ 415 | { 416 | value: 'carModel' 417 | }, 418 | { 419 | value: 'price', 420 | default: 1 421 | }, 422 | { 423 | value: 'color' 424 | } 425 | ], 426 | defaultValue: 'NULL', 427 | includeEmptyRows: true, 428 | }; 429 | 430 | const parser = new Parser(opts); 431 | const csv = await parseInput(parser, jsonFixtures.emptyRow()); 432 | 433 | t.equal(csv, csvFixtures.emptyRowDefaultValues); 434 | }); 435 | 436 | testRunner.add('should parse data:[null] to csv with only column title, despite options.includeEmptyRows', async (t) => { 437 | const opts = { 438 | fields: ['carModel', 'price', 'color'], 439 | includeEmptyRows: true, 440 | }; 441 | 442 | const parser = new Parser(opts); 443 | const csv = await parseInput(parser, jsonFixtures.arrayWithNull()); 444 | 445 | t.equal(csv, csvFixtures.emptyObject); 446 | }); 447 | 448 | // BOM 449 | 450 | testRunner.add('should add BOM character', async (t) => { 451 | const opts = { 452 | withBOM: true, 453 | fields: ['carModel', 'price', 'color', 'manual'] 454 | }; 455 | 456 | const parser = new Parser(opts); 457 | const csv = await parseInput(parser, jsonFixtures.specialCharacters()); 458 | 459 | // Compare csv length to check if the BOM character is present 460 | t.equal(csv[0], '\ufeff'); 461 | t.equal(csv.length, csvFixtures.default.length + 1); 462 | t.equal(csv.length, csvFixtures.withBOM.length); 463 | }); 464 | 465 | // Transforms 466 | 467 | testRunner.add('should unwind all unwindable fields using the unwind transform', async (t) => { 468 | const opts = { 469 | fields: ['carModel', 'price', 'extras.items.name', 'extras.items.color', 'extras.items.items.position', 'extras.items.items.color'], 470 | transforms: [unwind()], 471 | }; 472 | 473 | const parser = new Parser(opts); 474 | const csv = await parseInput(parser, jsonFixtures.unwind2()); 475 | 476 | t.equal(csv, csvFixtures.unwind2); 477 | }); 478 | 479 | testRunner.add('should support unwinding specific fields using the unwind transform', async (t) => { 480 | const opts = { 481 | fields: ['carModel', 'price', 'colors'], 482 | transforms: [unwind({ paths: ['colors'] })], 483 | }; 484 | 485 | const parser = new Parser(opts); 486 | const csv = await parseInput(parser, jsonFixtures.unwind()); 487 | 488 | t.equal(csv, csvFixtures.unwind); 489 | }); 490 | 491 | testRunner.add('should support multi-level unwind using the unwind transform', async (t) => { 492 | const opts = { 493 | fields: ['carModel', 'price', 'extras.items.name', 'extras.items.color', 'extras.items.items.position', 'extras.items.items.color'], 494 | transforms: [unwind({ paths: ['extras.items', 'extras.items.items'] })], 495 | }; 496 | 497 | const parser = new Parser(opts); 498 | const csv = await parseInput(parser, jsonFixtures.unwind2()); 499 | 500 | t.equal(csv, csvFixtures.unwind2); 501 | }); 502 | 503 | testRunner.add('should support unwind and blank out repeated data using the unwind transform', async (t) => { 504 | const opts = { 505 | fields: ['carModel', 'price', 'extras.items.name', 'extras.items.color', 'extras.items.items.position', 'extras.items.items.color'], 506 | transforms: [unwind({ paths: ['extras.items', 'extras.items.items'], blankOut: true })], 507 | }; 508 | 509 | const parser = new Parser(opts); 510 | const csv = await parseInput(parser, jsonFixtures.unwind2()); 511 | 512 | t.equal(csv, csvFixtures.unwind2Blank); 513 | }); 514 | 515 | testRunner.add('should support flattening deep JSON using the flatten transform', async (t) => { 516 | const opts = { 517 | transforms: [flatten()], 518 | }; 519 | 520 | const parser = new Parser(opts); 521 | const csv = await parseInput(parser, jsonFixtures.deepJSON()); 522 | 523 | t.equal(csv, csvFixtures.flattenedDeepJSON); 524 | }); 525 | 526 | testRunner.add('should support flattening JSON with nested arrays using the flatten transform', async (t) => { 527 | const opts = { 528 | transforms: [flatten({ arrays: true })], 529 | }; 530 | 531 | const parser = new Parser(opts); 532 | const csv = await parseInput(parser, jsonFixtures.flattenArrays()); 533 | 534 | t.equal(csv, csvFixtures.flattenedArrays); 535 | }); 536 | 537 | testRunner.add('should support custom flatten separator using the flatten transform', async (t) => { 538 | const opts = { 539 | transforms: [flatten({ separator: '__' })], 540 | }; 541 | 542 | const parser = new Parser(opts); 543 | const csv = await parseInput(parser, jsonFixtures.deepJSON()); 544 | 545 | t.equal(csv, csvFixtures.flattenedCustomSeparatorDeepJSON); 546 | }); 547 | 548 | testRunner.add('should support multiple transforms and honor the order in which they are declared', async (t) => { 549 | const opts = { 550 | transforms: [unwind({ paths: ['items'] }), flatten()], 551 | }; 552 | 553 | const parser = new Parser(opts); 554 | const csv = await parseInput(parser, jsonFixtures.unwindAndFlatten()); 555 | 556 | t.equal(csv, csvFixtures.unwindAndFlatten); 557 | }); 558 | 559 | testRunner.add('should unwind complex objects using the unwind transform', async (t) => { 560 | const opts = { 561 | fields: ["carModel","price","extras.items.name","extras.items.items.position","extras.items.items.color","extras.items.color"], 562 | transforms: [unwind({ paths: ['extras.items', 'extras.items.items'] }), flatten()], 563 | }; 564 | 565 | const parser = new Parser(opts); 566 | const csv = await parseInput(parser, jsonFixtures.unwindComplexObject()); 567 | 568 | t.equal(csv, csvFixtures.unwindComplexObject); 569 | }); 570 | 571 | testRunner.add('should support custom transforms', async (t) => { 572 | const opts = { 573 | transforms: [row => ({ 574 | model: row.carModel, 575 | price: row.price / 1000, 576 | color: row.color, 577 | manual: row.manual || 'automatic', 578 | })], 579 | }; 580 | 581 | const parser = new Parser(opts); 582 | const csv = await parseInput(parser, jsonFixtures.default()); 583 | 584 | t.equal(csv, csvFixtures.defaultCustomTransform); 585 | }); 586 | 587 | // Formatters 588 | 589 | // Number 590 | 591 | testRunner.add('should used a custom separator when \'decimals\' is passed to the number formatter', async (t) => { 592 | const opts = { 593 | formatters: { 594 | number: numberFormatter({ decimals: 2 }) 595 | } 596 | }; 597 | const parser = new Parser(opts); 598 | const csv = await parseInput(parser, jsonFixtures.numberFormatter()); 599 | 600 | t.equal(csv, csvFixtures.numberFixedDecimals); 601 | }); 602 | 603 | testRunner.add('should used a custom separator when \'separator\' is passed to the number formatter', async (t) => { 604 | const opts = { 605 | delimiter: ';', 606 | formatters: { 607 | number: numberFormatter({ separator: ',' }) 608 | } 609 | }; 610 | const parser = new Parser(opts); 611 | const csv = await parseInput(parser, jsonFixtures.numberFormatter()); 612 | 613 | t.equal(csv, csvFixtures.numberCustomSeparator); 614 | }); 615 | 616 | testRunner.add('should used a custom separator and fixed number of decimals when \'separator\' and \'decimals\' are passed to the number formatter', async (t) => { 617 | const opts = { 618 | delimiter: ';', 619 | formatters: { 620 | number: numberFormatter({ separator: ',', decimals: 2 }) 621 | } 622 | }; 623 | const parser = new Parser(opts); 624 | const csv = await parseInput(parser, jsonFixtures.numberFormatter()); 625 | 626 | t.equal(csv, csvFixtures.numberFixedDecimalsAndCustomSeparator); 627 | }); 628 | 629 | // Symbol 630 | 631 | testRunner.add('should format Symbol by its name', async (t) => { 632 | const transformOpts = { objectMode: true }; 633 | 634 | const parser = new Parser({}, transformOpts); 635 | const csv = await parseInput(parser, jsonFixtures.symbol({ objectMode: true })); 636 | 637 | t.equal(csv, csvFixtures.symbol); 638 | }); 639 | 640 | // String Quote 641 | 642 | testRunner.add('should use a custom quote when \'quote\' property is present', async (t) => { 643 | const opts = { 644 | fields: ['carModel', 'price'], 645 | formatters: { 646 | string: stringFormatter({ quote: '\'' }) 647 | } 648 | }; 649 | 650 | const parser = new Parser(opts); 651 | const csv = await parseInput(parser, jsonFixtures.default()); 652 | 653 | t.equal(csv, csvFixtures.withSimpleQuotes); 654 | }); 655 | 656 | testRunner.add('should be able to don\'t output quotes when setting \'quote\' to empty string', async (t) => { 657 | const opts = { 658 | fields: ['carModel', 'price'], 659 | formatters: { 660 | string: stringFormatter({ quote: '' }) 661 | } 662 | }; 663 | 664 | const parser = new Parser(opts); 665 | const csv = await parseInput(parser, jsonFixtures.default()); 666 | 667 | t.equal(csv, csvFixtures.withoutQuotes); 668 | }); 669 | 670 | testRunner.add('should escape quotes when setting \'quote\' property is present', async (t) => { 671 | const opts = { 672 | fields: ['carModel', 'color'], 673 | formatters: { 674 | string: stringFormatter({ quote: '\'' }) 675 | } 676 | }; 677 | 678 | const parser = new Parser(opts); 679 | const csv = await parseInput(parser, jsonFixtures.escapeCustomQuotes()); 680 | 681 | t.equal(csv, csvFixtures.escapeCustomQuotes); 682 | }); 683 | 684 | testRunner.add('should not escape \'"\' when setting \'quote\' set to something else', async (t) => { 685 | const opts = { 686 | formatters: { 687 | string: stringFormatter({ quote: '\'' }) 688 | } 689 | }; 690 | 691 | const parser = new Parser(opts); 692 | const csv = await parseInput(parser, jsonFixtures.escapedQuotes()); 693 | 694 | t.equal(csv, csvFixtures.escapedQuotesUnescaped); 695 | }); 696 | 697 | // String Escaped Quote 698 | 699 | testRunner.add('should escape quotes with double quotes', async (t) => { 700 | const parser = new Parser(); 701 | const csv = await parseInput(parser, jsonFixtures.quotes()); 702 | 703 | t.equal(csv, csvFixtures.quotes); 704 | }); 705 | 706 | testRunner.add('should not escape quotes with double quotes, when there is a backslash in the end', async (t) => { 707 | const parser = new Parser(); 708 | const csv = await parseInput(parser, jsonFixtures.backslashAtEnd()); 709 | 710 | t.equal(csv, csvFixtures.backslashAtEnd); 711 | }); 712 | 713 | testRunner.add('should not escape quotes with double quotes, when there is a backslash in the end, and its not the last column', async (t) => { 714 | const parser = new Parser(); 715 | const csv = await parseInput(parser, jsonFixtures.backslashAtEndInMiddleColumn()); 716 | 717 | t.equal(csv, csvFixtures.backslashAtEndInMiddleColumn); 718 | }); 719 | 720 | testRunner.add('should escape quotes with value in \'escapedQuote\'', async (t) => { 721 | const opts = { 722 | fields: ['a string'], 723 | formatters: { 724 | string: stringFormatter({ escapedQuote: '*' }) 725 | } 726 | }; 727 | 728 | const parser = new Parser(opts); 729 | const csv = await parseInput(parser, jsonFixtures.escapedQuotes()); 730 | 731 | t.equal(csv, csvFixtures.escapedQuotes); 732 | }); 733 | 734 | testRunner.add('should escape quotes before new line with value in \'escapedQuote\'', async (t) => { 735 | const opts = { 736 | fields: ['a string'] 737 | }; 738 | 739 | const parser = new Parser(opts); 740 | const csv = await parseInput(parser, jsonFixtures.backslashBeforeNewLine()); 741 | 742 | t.equal(csv, csvFixtures.backslashBeforeNewLine); 743 | }); 744 | 745 | // String Quote Only if Necessary 746 | 747 | testRunner.add('should quote only if necessary if using stringQuoteOnlyIfNecessary formatter', async (t) => { 748 | const opts = { 749 | formatters: { 750 | string: stringQuoteOnlyIfNecessaryFormatter() 751 | } 752 | }; 753 | 754 | const parser = new Parser(opts); 755 | const csv = await parseInput(parser, jsonFixtures.quoteOnlyIfNecessary()); 756 | 757 | t.equal(csv, csvFixtures.quoteOnlyIfNecessary); 758 | }); 759 | 760 | // String Excel 761 | 762 | testRunner.add('should format strings to force excel to view the values as strings', async (t) => { 763 | const opts = { 764 | fields: ['carModel', 'price', 'color'], 765 | formatters: { 766 | string: stringExcelFormatter 767 | } 768 | }; 769 | 770 | const parser = new Parser(opts); 771 | const csv = await parseInput(parser, jsonFixtures.default()); 772 | 773 | t.equal(csv, csvFixtures.excelStrings); 774 | }); 775 | 776 | testRunner.add('should format strings to force excel to view the values as strings with escaped quotes', async (t) => { 777 | const opts = { 778 | formatters: { 779 | string: stringExcelFormatter 780 | } 781 | }; 782 | 783 | const parser = new Parser(opts); 784 | const csv = await parseInput(parser, jsonFixtures.quotes()); 785 | 786 | t.equal(csv, csvFixtures.excelStringsWithEscapedQuoted); 787 | }); 788 | 789 | // String Escaping and preserving values 790 | 791 | testRunner.add('should parse JSON values with trailing backslashes', async (t) => { 792 | const opts = { 793 | fields: ['carModel', 'price', 'color'] 794 | }; 795 | 796 | const parser = new Parser(opts); 797 | const csv = await parseInput(parser, jsonFixtures.trailingBackslash()); 798 | 799 | t.equal(csv, csvFixtures.trailingBackslash); 800 | }); 801 | 802 | testRunner.add('should escape " when preceeded by \\', async (t) => { 803 | const parser = new Parser(); 804 | const csv = await parseInput(parser, jsonFixtures.escapeDoubleBackslashedEscapedQuote()); 805 | 806 | t.equal(csv, csvFixtures.escapeDoubleBackslashedEscapedQuote); 807 | }); 808 | 809 | testRunner.add('should preserve new lines in values', async (t) => { 810 | const opts = { 811 | eol: '\r\n' 812 | }; 813 | 814 | const parser = new Parser(opts); 815 | const csv = await parseInput(parser, jsonFixtures.escapeEOL()); 816 | 817 | 818 | t.equal(csv, [ 819 | '"a string"', 820 | '"with a \u2028description\\n and\na new line"', 821 | '"with a \u2029\u2028description and\r\nanother new line"' 822 | ].join('\r\n')); 823 | 824 | }); 825 | 826 | testRunner.add('should preserve tabs in values', async (t) => { 827 | const parser = new Parser(); 828 | const csv = await parseInput(parser, jsonFixtures.escapeTab()); 829 | 830 | t.equal(csv, csvFixtures.escapeTab); 831 | }); 832 | 833 | // Headers 834 | 835 | testRunner.add('should format headers based on the headers formatter', async (t) => { 836 | const opts = { 837 | fields: ['carModel', 'price', 'color', 'manual'], 838 | formatters: { 839 | header: stringFormatter({ quote: '' }) 840 | } 841 | }; 842 | 843 | const parser = new Parser(opts); 844 | const csv = await parseInput(parser, jsonFixtures.default()); 845 | 846 | t.equal(csv, csvFixtures.customHeaderQuotes); 847 | }); 848 | }; 849 | -------------------------------------------------------------------------------- /test/fixtures/csv/backslashAtEnd.csv: -------------------------------------------------------------------------------- 1 | "a string" 2 | "with a description" 3 | "with a description and ""quotes and backslash\" 4 | "with a description and ""quotes and backslash\\" -------------------------------------------------------------------------------- /test/fixtures/csv/backslashAtEndInMiddleColumn.csv: -------------------------------------------------------------------------------- 1 | "uuid","title","id" 2 | "xxxx-yyyy","$25 something $50 \","someId" 3 | "xxxx-yyyy","$25 something $50 \\","someId" -------------------------------------------------------------------------------- /test/fixtures/csv/backslashBeforeNewLine.csv: -------------------------------------------------------------------------------- 1 | "a string" 2 | "with a description" 3 | "with a description and "" 4 | quotes and backslash\" 5 | "with a description and "" 6 | quotes and backslash\\" -------------------------------------------------------------------------------- /test/fixtures/csv/customHeaderQuotes.csv: -------------------------------------------------------------------------------- 1 | carModel,price,color,manual 2 | "Audi",0,"blue", 3 | "BMW",15000,"red",true 4 | "Mercedes",20000,"yellow", 5 | "Porsche",30000,"green", -------------------------------------------------------------------------------- /test/fixtures/csv/date.csv: -------------------------------------------------------------------------------- 1 | "date" 2 | "2017-01-01T00:00:00.000Z" -------------------------------------------------------------------------------- /test/fixtures/csv/deepJSON.csv: -------------------------------------------------------------------------------- 1 | "field1" 2 | "{""embeddedField1"":""embeddedValue1"",""embeddedField2"":""embeddedValue2""}" -------------------------------------------------------------------------------- /test/fixtures/csv/default.csv: -------------------------------------------------------------------------------- 1 | "carModel","price","color","manual" 2 | "Audi",0,"blue", 3 | "BMW",15000,"red",true 4 | "Mercedes",20000,"yellow", 5 | "Porsche",30000,"green", -------------------------------------------------------------------------------- /test/fixtures/csv/defaultCustomTransform.csv: -------------------------------------------------------------------------------- 1 | "model","price","color","manual" 2 | "Audi",0,"blue","automatic" 3 | "BMW",15,"red",true 4 | "Mercedes",20,"yellow","automatic" 5 | "Porsche",30,"green","automatic" -------------------------------------------------------------------------------- /test/fixtures/csv/defaultStream.csv: -------------------------------------------------------------------------------- 1 | "carModel","price","color" 2 | "Audi",0,"blue" 3 | "BMW",15000,"red" 4 | "Mercedes",20000,"yellow" 5 | "Porsche",30000,"green" -------------------------------------------------------------------------------- /test/fixtures/csv/defaultValue.csv: -------------------------------------------------------------------------------- 1 | "carModel","price" 2 | "Audi",0 3 | "BMW",15000 4 | "Mercedes","NULL" 5 | "Porsche",30000 -------------------------------------------------------------------------------- /test/fixtures/csv/defaultValueEmpty.csv: -------------------------------------------------------------------------------- 1 | "carModel","price" 2 | "Audi",0 3 | "BMW",15000 4 | "Mercedes", 5 | "Porsche",30000 -------------------------------------------------------------------------------- /test/fixtures/csv/delimiter.csv: -------------------------------------------------------------------------------- 1 | "firstname"|@|"lastname"|@|"email" 2 | "foo"|@|"bar"|@|"foo.bar@json2csv.com" 3 | "bar"|@|"foo"|@|"bar.foo@json2csv.com" -------------------------------------------------------------------------------- /test/fixtures/csv/embeddedjson.csv: -------------------------------------------------------------------------------- 1 | "field1" 2 | "{""embeddedField1"":""embeddedValue1"",""embeddedField2"":""embeddedValue2""}" -------------------------------------------------------------------------------- /test/fixtures/csv/emptyObject.csv: -------------------------------------------------------------------------------- 1 | "carModel","price","color" -------------------------------------------------------------------------------- /test/fixtures/csv/emptyRow.csv: -------------------------------------------------------------------------------- 1 | "carModel","price","color" 2 | "Audi",0,"blue" 3 | ,, 4 | "Mercedes",20000,"yellow" 5 | "Porsche",30000,"green" -------------------------------------------------------------------------------- /test/fixtures/csv/emptyRowDefaultValues.csv: -------------------------------------------------------------------------------- 1 | "carModel","price","color" 2 | "Audi",0,"blue" 3 | "NULL",1,"NULL" 4 | "Mercedes",20000,"yellow" 5 | "Porsche",30000,"green" -------------------------------------------------------------------------------- /test/fixtures/csv/emptyRowNotIncluded.csv: -------------------------------------------------------------------------------- 1 | "carModel","price","color" 2 | "Audi",0,"blue" 3 | "Mercedes",20000,"yellow" 4 | "Porsche",30000,"green" -------------------------------------------------------------------------------- /test/fixtures/csv/eol.csv: -------------------------------------------------------------------------------- 1 | "carModel","price","color" 2 | "Audi",0,"blue" 3 | "BMW",15000,"red" 4 | "Mercedes",20000,"yellow" 5 | "Porsche",30000,"green" -------------------------------------------------------------------------------- /test/fixtures/csv/escapeCustomQuotes.csv: -------------------------------------------------------------------------------- 1 | 'carModel','color' 2 | '''Audi''','''blue''' -------------------------------------------------------------------------------- /test/fixtures/csv/escapeDoubleBackslashedEscapedQuote.csv: -------------------------------------------------------------------------------- 1 | "field" 2 | "\\""" -------------------------------------------------------------------------------- /test/fixtures/csv/escapeEOL.csv: -------------------------------------------------------------------------------- 1 | "a string" 2 | "with a \ndescription\\n and\na new line" 3 | "with a \r\ndescription and\r\nanother new line" 4 | 5 | //TODO -------------------------------------------------------------------------------- /test/fixtures/csv/escapeTab.csv: -------------------------------------------------------------------------------- 1 | "Id","Name","CreatedById","CreatedDate","IsDeleted","LastModifiedById","LastModifiedDate","LastReferencedDate","LastViewedDate","OwnerId","SBQQ__CustomSource__c","SBQQ__FontFamily__c","SBQQ__FontSize__c","SBQQ__Markup__c","SBQQ__RawMarkup__c","SBQQ__TableStyle__c","SBQQ__TextColor__c","SBQQ__Type__c","SystemModstamp" 2 | "a13f4000001NrzWAAS","a13f4000001NrzW","005f4000002OzPRAA0","2018-06-20T15:49:06.000+0000","FALSE","005f4000002OzPRAA0","2018-06-20T15:51:36.000+0000","2018-06-20T15:51:36.000+0000","2018-06-20T15:51:36.000+0000","005f4000002OzPRAA0","","","",""," 3 | 4 | 5 | 6 | 7 | 14 | 15 | 22 | 23 | 24 |
{!companyLogo}  8 |

{!quote.Name__c}
9 | {!quote.Street__c}
10 | {!quote.City__c}, {!quote.State__c} {!quote.Postal_Code__c}
11 | Office: {!quote.Phone__c}
12 | Fax: {!quote.Fax__c}

13 |
   16 |

  Agreement #: {!quote.Name}-{!document.SBQQ__Version__c}

17 | 18 |

  Start Date: {!quote.SBQQ__StartDate__c}
19 |   Expiration Date: {!quote.SBQQ__ExpirationDate__c}
20 |   Sales Contact: {!salesRep.Name}

21 |
","Standard","","HTML","2018-06-20T15:51:36.000+0000" -------------------------------------------------------------------------------- /test/fixtures/csv/escapedQuotes.csv: -------------------------------------------------------------------------------- 1 | "a string" 2 | "with a description" 3 | "with a description and *quotes*" -------------------------------------------------------------------------------- /test/fixtures/csv/escapedQuotesUnescaped.csv: -------------------------------------------------------------------------------- 1 | 'a string' 2 | 'with a description' 3 | 'with a description and "quotes"' -------------------------------------------------------------------------------- /test/fixtures/csv/excelStrings.csv: -------------------------------------------------------------------------------- 1 | "=""carModel""","=""price""","=""color""" 2 | "=""Audi""",0,"=""blue""" 3 | "=""BMW""",15000,"=""red""" 4 | "=""Mercedes""",20000,"=""yellow""" 5 | "=""Porsche""",30000,"=""green""" -------------------------------------------------------------------------------- /test/fixtures/csv/excelStringsWithEscapedQuoted.csv: -------------------------------------------------------------------------------- 1 | "=""a string""" 2 | "=""with a description""" 3 | "=""with a description and """"quotes""""""" -------------------------------------------------------------------------------- /test/fixtures/csv/fancyfields.csv: -------------------------------------------------------------------------------- 1 | "PATH1","PATH1+PATH2","NEST1","bird.nest2","nonexistent" 2 | "hello ","hello world!","chirp","cheep","overrides default" 3 | "good ","good bye!","meep","meep","col specific default value" -------------------------------------------------------------------------------- /test/fixtures/csv/fieldNames.csv: -------------------------------------------------------------------------------- 1 | "Car Model","Price USD" 2 | "Audi",0 3 | "BMW",15000 4 | "Mercedes",20000 5 | "Porsche",30000 -------------------------------------------------------------------------------- /test/fixtures/csv/flattenToJSON.csv: -------------------------------------------------------------------------------- 1 | "hello.world","lorem.ipsum.dolor","lorem.ipsum.value" 2 | "good afternoon","good evening", -------------------------------------------------------------------------------- /test/fixtures/csv/flattenedArrays.csv: -------------------------------------------------------------------------------- 1 | "name","age","friends.0.name","friends.0.age","friends.1.name","friends.1.age" 2 | "Jack",39,"Oliver",40,"Harry",50 3 | "Thomas",40,"Harry",35,, -------------------------------------------------------------------------------- /test/fixtures/csv/flattenedCustomSeparatorDeepJSON.csv: -------------------------------------------------------------------------------- 1 | "field1__embeddedField1","field1__embeddedField2" 2 | "embeddedValue1","embeddedValue2" -------------------------------------------------------------------------------- /test/fixtures/csv/flattenedDeepJSON.csv: -------------------------------------------------------------------------------- 1 | "field1.embeddedField1","field1.embeddedField2" 2 | "embeddedValue1","embeddedValue2" -------------------------------------------------------------------------------- /test/fixtures/csv/flattenedEmbeddedJson.csv: -------------------------------------------------------------------------------- 1 | "field1.embeddedField1","field1.embeddedField2" 2 | "embeddedValue1","embeddedValue2" -------------------------------------------------------------------------------- /test/fixtures/csv/functionField.csv: -------------------------------------------------------------------------------- 1 | "a","funct" 2 | 1, -------------------------------------------------------------------------------- /test/fixtures/csv/functionNoStringify.csv: -------------------------------------------------------------------------------- 1 | "Value1" 2 | "abc" 3 | 1234 -------------------------------------------------------------------------------- /test/fixtures/csv/functionStringifyByDefault.csv: -------------------------------------------------------------------------------- 1 | "Value1" 2 | "abc" 3 | "1234" -------------------------------------------------------------------------------- /test/fixtures/csv/ndjson.csv: -------------------------------------------------------------------------------- 1 | "carModel","price","color","manual" 2 | "Audi",0,"blue", 3 | "BMW",15000,"red",true 4 | "Mercedes",20000,"yellow", 5 | "Porsche",30000,"green", -------------------------------------------------------------------------------- /test/fixtures/csv/nested.csv: -------------------------------------------------------------------------------- 1 | "Make","Model","Price","Color","Year" 2 | "Audi","A3",10000,"blue","2001" 3 | "BMW","F20",15000,"red","2002" 4 | "Mercedes","SLS",20000,"yellow","2003" 5 | "Porsche","9PA AF1",30000,"green","2004" -------------------------------------------------------------------------------- /test/fixtures/csv/numberCustomSeparator.csv: -------------------------------------------------------------------------------- 1 | "number1";"number2";"number3" 2 | 0,12341234;2,1;65 -------------------------------------------------------------------------------- /test/fixtures/csv/numberFixedDecimals.csv: -------------------------------------------------------------------------------- 1 | "number1","number2","number3" 2 | 0.12,2.10,65.00 -------------------------------------------------------------------------------- /test/fixtures/csv/numberFixedDecimalsAndCustomSeparator.csv: -------------------------------------------------------------------------------- 1 | "number1";"number2";"number3" 2 | 0,12;2,10;65,00 -------------------------------------------------------------------------------- /test/fixtures/csv/overriddenDefaultValue.csv: -------------------------------------------------------------------------------- 1 | "carModel","price","color" 2 | "Audi",0,"blue" 3 | "BMW",1,"red" 4 | "Mercedes",20000,"" 5 | "Porsche",30000,"green" -------------------------------------------------------------------------------- /test/fixtures/csv/prettyprint.txt: -------------------------------------------------------------------------------- 1 | ┌────────────────────┬───────────────┬───────────────┐ 2 | │ "carModel" │ "price" │ "color" │ 3 | ├────────────────────┼───────────────┼───────────────┤ 4 | │ "Audi" │ 0 │ "blue" │ 5 | ├────────────────────┼───────────────┼───────────────┤ 6 | │ "BMW" │ 15000 │ "red" │ 7 | ├────────────────────┼───────────────┼───────────────┤ 8 | │ "Mercedes" │ 20000 │ "yellow" │ 9 | ├────────────────────┼───────────────┼───────────────┤ 10 | │ "Porsche" │ 30000 │ "green" │ 11 | └────────────────────┴───────────────┴───────────────┘ 12 | -------------------------------------------------------------------------------- /test/fixtures/csv/prettyprintWithoutHeader.txt: -------------------------------------------------------------------------------- 1 | ┌───────────────┬───────────────┬───────────────┐ 2 | │ "Audi" │ 0 │ "blue" │ 3 | ├───────────────┼───────────────┼───────────────┤ 4 | │ "BMW" │ 15000 │ "red" │ 5 | ├───────────────┼───────────────┼───────────────┤ 6 | │ "Mercedes" │ 20000 │ "yellow" │ 7 | ├───────────────┼───────────────┼───────────────┤ 8 | │ "Porsche" │ 30000 │ "green" │ 9 | └───────────────┴───────────────┴───────────────┘ 10 | -------------------------------------------------------------------------------- /test/fixtures/csv/prettyprintWithoutRows.txt: -------------------------------------------------------------------------------- 1 | ┌────────────────┬────────────────┬────────────────┐ 2 | │ "fieldA" │ "fieldB" │ "fieldC" │ 3 | └────────────────┴────────────────┴────────────────┘ 4 | -------------------------------------------------------------------------------- /test/fixtures/csv/quoteOnlyIfNecessary.csv: -------------------------------------------------------------------------------- 1 | a string 2 | with a description 3 | "with a description and ""quotes""" 4 | "with a description, and a separator" 5 | "with a description 6 | and a new line" -------------------------------------------------------------------------------- /test/fixtures/csv/quotes.csv: -------------------------------------------------------------------------------- 1 | "a string" 2 | "with a description" 3 | "with a description and ""quotes""" -------------------------------------------------------------------------------- /test/fixtures/csv/reversed.csv: -------------------------------------------------------------------------------- 1 | "price","carModel" 2 | 0,"Audi" 3 | 15000,"BMW" 4 | 20000,"Mercedes" 5 | 30000,"Porsche" -------------------------------------------------------------------------------- /test/fixtures/csv/selected.csv: -------------------------------------------------------------------------------- 1 | "carModel","price" 2 | "Audi",0 3 | "BMW",15000 4 | "Mercedes",20000 5 | "Porsche",30000 -------------------------------------------------------------------------------- /test/fixtures/csv/symbol.csv: -------------------------------------------------------------------------------- 1 | "test" 2 | "test1" 3 | "test2" -------------------------------------------------------------------------------- /test/fixtures/csv/trailingBackslash.csv: -------------------------------------------------------------------------------- 1 | "carModel","price","color" 2 | "Audi\\",0,"blue" 3 | "BMW\\",15000,"red" 4 | "Mercedes\\",20000,"yellow" 5 | "Porsche\\",30000,"green" -------------------------------------------------------------------------------- /test/fixtures/csv/tsv.csv: -------------------------------------------------------------------------------- 1 | "carModel" "price" "color" 2 | "Audi" 0 "blue" 3 | "BMW" 15000 "red" 4 | "Mercedes" 20000 "yellow" 5 | "Porsche" 30000 "green" -------------------------------------------------------------------------------- /test/fixtures/csv/unwind.csv: -------------------------------------------------------------------------------- 1 | "carModel","price","colors" 2 | "Audi",0,"blue" 3 | "Audi",0,"green" 4 | "Audi",0,"yellow" 5 | "BMW",15000,"red" 6 | "BMW",15000,"blue" 7 | "Mercedes",20000,"yellow" 8 | "Porsche",30000,"green" 9 | "Porsche",30000,"teal" 10 | "Porsche",30000,"aqua" 11 | "Tesla",50000, -------------------------------------------------------------------------------- /test/fixtures/csv/unwind2.csv: -------------------------------------------------------------------------------- 1 | "carModel","price","extras.items.name","extras.items.color","extras.items.items.position","extras.items.items.color" 2 | "Porsche",30000,"airbag",,"left","white" 3 | "Porsche",30000,"airbag",,"right","gray" 4 | "Porsche",30000,"dashboard",,"left","gray" 5 | "Porsche",30000,"dashboard",,"right","black" 6 | "BMW",15000,"airbag","white",, 7 | "BMW",15000,"dashboard","black",, -------------------------------------------------------------------------------- /test/fixtures/csv/unwind2Blank.csv: -------------------------------------------------------------------------------- 1 | "carModel","price","extras.items.name","extras.items.color","extras.items.items.position","extras.items.items.color" 2 | "Porsche",30000,"airbag",,"left","white" 3 | ,,,,"right","gray" 4 | ,,"dashboard",,"left","gray" 5 | ,,,,"right","black" 6 | "BMW",15000,"airbag","white",, 7 | ,,"dashboard","black",, -------------------------------------------------------------------------------- /test/fixtures/csv/unwindAndFlatten.csv: -------------------------------------------------------------------------------- 1 | "carModel","price","items.name","items.color" 2 | "BMW",15000,"airbag","white" 3 | "BMW",15000,"dashboard","black" 4 | "Porsche",30000,"airbag","gray" 5 | "Porsche",30000,"dashboard","red" 6 | "Mercedes",20000,, -------------------------------------------------------------------------------- /test/fixtures/csv/unwindComplexObject.csv: -------------------------------------------------------------------------------- 1 | "carModel","price","extras.items.name","extras.items.items.position","extras.items.items.color","extras.items.color" 2 | "Porsche",30000,"airbag","left","white", 3 | "Porsche",30000,"airbag","right","gray", 4 | "Porsche",30000,"dashboard",,, 5 | "BMW",15000,"airbag",,,"white" 6 | "BMW",15000,"dashboard",,,"black" -------------------------------------------------------------------------------- /test/fixtures/csv/withBOM.csv: -------------------------------------------------------------------------------- 1 | "carModel","price","color","manual" 2 | "Audi",0,"blue", 3 | "BMW",15000,"red",true 4 | "Mercedes",20000,"yellow", 5 | "Citroën",30000,"green", -------------------------------------------------------------------------------- /test/fixtures/csv/withNotExistField.csv: -------------------------------------------------------------------------------- 1 | "first not exist field","carModel","price","not exist field","color" 2 | ,"Audi",0,,"blue" 3 | ,"BMW",15000,,"red" 4 | ,"Mercedes",20000,,"yellow" 5 | ,"Porsche",30000,,"green" -------------------------------------------------------------------------------- /test/fixtures/csv/withSimpleQuotes.csv: -------------------------------------------------------------------------------- 1 | 'carModel','price' 2 | 'Audi',0 3 | 'BMW',15000 4 | 'Mercedes',20000 5 | 'Porsche',30000 -------------------------------------------------------------------------------- /test/fixtures/csv/withoutHeader.csv: -------------------------------------------------------------------------------- 1 | "Audi",0,"blue", 2 | "BMW",15000,"red",true 3 | "Mercedes",20000,"yellow", 4 | "Porsche",30000,"green", -------------------------------------------------------------------------------- /test/fixtures/csv/withoutQuotes.csv: -------------------------------------------------------------------------------- 1 | carModel,price 2 | Audi,0 3 | BMW,15000 4 | Mercedes,20000 5 | Porsche,30000 -------------------------------------------------------------------------------- /test/fixtures/fields/emptyRowDefaultValues.json: -------------------------------------------------------------------------------- 1 | { 2 | "fields": [ 3 | { 4 | "value": "carModel" 5 | }, 6 | { 7 | "value": "price", 8 | "default": 1 9 | }, 10 | { 11 | "value": "color" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /test/fixtures/fields/fancyfields.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | fields: [{ 3 | label: 'PATH1', 4 | value: 'path1' 5 | }, { 6 | label: 'PATH1+PATH2', 7 | value: row => row.path1+row.path2 8 | }, { 9 | label: 'NEST1', 10 | value: 'bird.nest1' 11 | }, 12 | 'bird.nest2', 13 | { 14 | label: 'nonexistent', 15 | value: 'fake.path', 16 | default: 'col specific default value' 17 | }] 18 | }; -------------------------------------------------------------------------------- /test/fixtures/fields/fieldNames.json: -------------------------------------------------------------------------------- 1 | { 2 | "fields": [{ 3 | "label": "Car Model", 4 | "value": "carModel" 5 | },{ 6 | "label": "Price USD", 7 | "value": "price" 8 | }] 9 | } -------------------------------------------------------------------------------- /test/fixtures/fields/functionNoStringify.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | fields: [{ 3 | label: 'Value1', 4 | value: row => row.value1.toLocaleString() 5 | }] 6 | }; -------------------------------------------------------------------------------- /test/fixtures/fields/functionStringifyByDefault.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | fields: [{ 3 | label: 'Value1', 4 | value: row => row.value1.toLocaleString() 5 | }] 6 | }; -------------------------------------------------------------------------------- /test/fixtures/fields/functionWithCheck.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | fields: [{ 3 | label: 'Value1', 4 | value: (row, field) => { 5 | if(field.label !== 'Value1' && field.default !== 'default value') { 6 | throw new Error(`Expected ${JSON.stringify(field)} to equals ${JSON.stringify({ label: 'Value1', default: 'default value' })}.`); 7 | } 8 | return row.value1.toLocaleString(); 9 | } 10 | }] 11 | }; -------------------------------------------------------------------------------- /test/fixtures/fields/nested.json: -------------------------------------------------------------------------------- 1 | { 2 | "fields": [{ 3 | "label": "Make", 4 | "value": "car.make" 5 | },{ 6 | "label": "Model", 7 | "value": "car.model" 8 | },{ 9 | "label": "Price", 10 | "value": "price" 11 | },{ 12 | "label": "Color", 13 | "value": "color" 14 | },{ 15 | "label": "Year", 16 | "value": "car.ye.ar" 17 | }] 18 | } -------------------------------------------------------------------------------- /test/fixtures/fields/overriddenDefaultValue.json: -------------------------------------------------------------------------------- 1 | { 2 | "fields": [ 3 | { "value": "carModel" }, 4 | { "value": "price", "default": 1 }, 5 | { "value": "color" } 6 | ] 7 | } -------------------------------------------------------------------------------- /test/fixtures/fields/overriddenDefaultValue2.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | fields: [ 3 | { 4 | value: 'carModel' 5 | }, 6 | { 7 | label: 'price', 8 | value: row => row.price, 9 | default: 1 10 | }, 11 | { 12 | label: 'color', 13 | value: row => row.color 14 | } 15 | ] 16 | }; -------------------------------------------------------------------------------- /test/fixtures/json/arrayWithNull.json: -------------------------------------------------------------------------------- 1 | [null] -------------------------------------------------------------------------------- /test/fixtures/json/backslashAtEnd.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"a string": "with a description"}, 3 | {"a string": "with a description and \"quotes and backslash\\"}, 4 | {"a string": "with a description and \"quotes and backslash\\\\"} 5 | ] -------------------------------------------------------------------------------- /test/fixtures/json/backslashAtEndInMiddleColumn.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"uuid": "xxxx-yyyy", "title": "$25 something $50 \\", "id":"someId"}, 3 | {"uuid": "xxxx-yyyy", "title": "$25 something $50 \\\\", "id":"someId"} 4 | ] -------------------------------------------------------------------------------- /test/fixtures/json/backslashBeforeNewLine.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"a string": "with a description"}, 3 | {"a string": "with a description and \"\nquotes and backslash\\"}, 4 | {"a string": "with a description and \"\nquotes and backslash\\\\"} 5 | ] -------------------------------------------------------------------------------- /test/fixtures/json/date.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "date": new Date("2017-01-01T00:00:00.000Z") 3 | } -------------------------------------------------------------------------------- /test/fixtures/json/deepJSON.json: -------------------------------------------------------------------------------- 1 | { 2 | "field1": { 3 | "embeddedField1": "embeddedValue1", 4 | "embeddedField2": "embeddedValue2" 5 | } 6 | } -------------------------------------------------------------------------------- /test/fixtures/json/default.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "carModel": "Audi", "price": 0, "color": "blue" }, 3 | { "carModel": "BMW", "price": 15000, "color": "red", "manual": true }, 4 | { "carModel": "Mercedes", "price": 20000, "color": "yellow" }, 5 | { "carModel": "Porsche", "price": 30000, "color": "green" } 6 | ] 7 | -------------------------------------------------------------------------------- /test/fixtures/json/defaultInvalid.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "carModel": "Audi", "price": 0, "color": "blue" }, 3 | { "carModel": "BMW", "price": 15000, "color": "red", "manual": true }, 4 | { "carModel": "Mercedes", "price": 20000, "color": "yellow", 5 | { "carModel": "Porsche", "price": 30000, "color": "green" } 6 | ] 7 | -------------------------------------------------------------------------------- /test/fixtures/json/defaultValue.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "carModel": "Audi", "price": 0 }, 3 | { "carModel": "BMW", "price": 15000 }, 4 | { "carModel": "Mercedes" }, 5 | { "carModel": "Porsche", "price": 30000 } 6 | ] 7 | -------------------------------------------------------------------------------- /test/fixtures/json/defaultValueEmpty.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "carModel": "Audi", "price": 0 }, 3 | { "carModel": "BMW", "price": 15000 }, 4 | { "carModel": "Mercedes", "price": null }, 5 | { "carModel": "Porsche", "price": 30000 } 6 | ] 7 | -------------------------------------------------------------------------------- /test/fixtures/json/delimiter.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "firstname": "foo", "lastname": "bar", "email": "foo.bar@json2csv.com" }, 3 | { "firstname": "bar", "lastname": "foo", "email": "bar.foo@json2csv.com" } 4 | ] -------------------------------------------------------------------------------- /test/fixtures/json/empty.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zemirco/json2csv/5cd609bd1b444acd5d3ac55edc9edff172803495/test/fixtures/json/empty.json -------------------------------------------------------------------------------- /test/fixtures/json/emptyArray.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /test/fixtures/json/emptyObject.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/fixtures/json/emptyRow.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "carModel": "Audi", "price": 0, "color": "blue" }, 3 | {}, 4 | { "carModel": "Mercedes", "price": 20000, "color": "yellow" }, 5 | { "carModel": "Porsche", "price": 30000, "color": "green" } 6 | ] 7 | -------------------------------------------------------------------------------- /test/fixtures/json/eol.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"a string": "with a \u2028description\\n and\na new line"}, 3 | {"a string": "with a \u2029\u2028description and\r\nanother new line"} 4 | ] 5 | -------------------------------------------------------------------------------- /test/fixtures/json/escapeCustomQuotes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "carModel": "'Audi'", "price": 0, "color": "'blue'" } 3 | ] -------------------------------------------------------------------------------- /test/fixtures/json/escapeDoubleBackslashedEscapedQuote.json: -------------------------------------------------------------------------------- 1 | { 2 | "field": "\\\\\"" 3 | } -------------------------------------------------------------------------------- /test/fixtures/json/escapeEOL.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"a string": "with a \u2028description\\n and\na new line"}, 3 | {"a string": "with a \u2029\u2028description and\r\nanother new line"} 4 | ] 5 | -------------------------------------------------------------------------------- /test/fixtures/json/escapeTab.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Id": "a13f4000001NrzWAAS", 4 | "Name": "a13f4000001NrzW", 5 | "CreatedById": "005f4000002OzPRAA0", 6 | "CreatedDate": "2018-06-20T15:49:06.000+0000", 7 | "IsDeleted": "FALSE", 8 | "LastModifiedById": "005f4000002OzPRAA0", 9 | "LastModifiedDate": "2018-06-20T15:51:36.000+0000", 10 | "LastReferencedDate": "2018-06-20T15:51:36.000+0000", 11 | "LastViewedDate": "2018-06-20T15:51:36.000+0000", 12 | "OwnerId": "005f4000002OzPRAA0", 13 | "SBQQ__CustomSource__c": "", 14 | "SBQQ__FontFamily__c": "", 15 | "SBQQ__FontSize__c": "", 16 | "SBQQ__Markup__c": "", 17 | "SBQQ__RawMarkup__c": "\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n
{!companyLogo} \n\t\t\t

{!quote.Name__c}
\n\t\t\t{!quote.Street__c}
\n\t\t\t{!quote.City__c}, {!quote.State__c} {!quote.Postal_Code__c}
\n\t\t\tOffice: {!quote.Phone__c}
\n\t\t\tFax: {!quote.Fax__c}

\n\t\t\t
  \n\t\t\t

  Agreement #: {!quote.Name}-{!document.SBQQ__Version__c}

\n\n\t\t\t

  Start Date: {!quote.SBQQ__StartDate__c}
\n\t\t\t  Expiration Date: {!quote.SBQQ__ExpirationDate__c}
\n\t\t\t  Sales Contact: {!salesRep.Name}

\n\t\t\t
", 18 | "SBQQ__TableStyle__c": "Standard", 19 | "SBQQ__TextColor__c": "", 20 | "SBQQ__Type__c": "HTML", 21 | "SystemModstamp": "2018-06-20T15:51:36.000+0000" 22 | } 23 | ] -------------------------------------------------------------------------------- /test/fixtures/json/escapedQuotes.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"a string": "with a description"}, 3 | {"a string": "with a description and \"quotes\""} 4 | ] -------------------------------------------------------------------------------- /test/fixtures/json/fancyfields.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "path1": "hello ", 3 | "path2": "world!", 4 | "bird": { 5 | "nest1": "chirp", 6 | "nest2": "cheep" 7 | }, 8 | "fake": { 9 | "path": "overrides default" 10 | } 11 | }, { 12 | "path1": "good ", 13 | "path2": "bye!", 14 | "bird": { 15 | "nest1": "meep", 16 | "nest2": "meep" 17 | } 18 | }] -------------------------------------------------------------------------------- /test/fixtures/json/flattenArrays.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Jack", 4 | "age": 39, 5 | "friends": [{ "name": "Oliver", "age": 40 }, { "name": "Harry", "age": 50 }] 6 | }, 7 | { 8 | "name": "Thomas", 9 | "age": 40, 10 | "friends": [{ "name": "Harry", "age": 35 }] 11 | } 12 | ] -------------------------------------------------------------------------------- /test/fixtures/json/flattenToJSON.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | hello: { 3 | world: { 4 | again: 'good morning', 5 | toJSON: () =>'good afternoon' 6 | } 7 | }, 8 | lorem: { 9 | ipsum: { 10 | dolor: 'good evening', 11 | value: null 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /test/fixtures/json/functionField.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | a: 1, 3 | funct: (a) => a + 1 4 | }; -------------------------------------------------------------------------------- /test/fixtures/json/functionNoStringify.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "value1": "\"abc\"" 3 | }, { 4 | "value1": "1234" 5 | }] -------------------------------------------------------------------------------- /test/fixtures/json/functionStringifyByDefault.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "value1": "abc" }, 3 | { "value1": "1234" } 4 | ] -------------------------------------------------------------------------------- /test/fixtures/json/invalidNoToken.json: -------------------------------------------------------------------------------- 1 | nul -------------------------------------------------------------------------------- /test/fixtures/json/ndjson.json: -------------------------------------------------------------------------------- 1 | { "carModel": "Audi", "price": 0, "color": "blue" } 2 | { "carModel": "BMW", "price": 15000, "color": "red", "manual": true } 3 | { "carModel": "Mercedes", "price": 20000, "color": "yellow" } 4 | { "carModel": "Porsche", "price": 30000, "color": "green" } -------------------------------------------------------------------------------- /test/fixtures/json/ndjsonInvalid.json: -------------------------------------------------------------------------------- 1 | { "carModel": "Audi", "price": 0, "color": "blue" } 2 | { "carModel": "BMW", "price": 15000, "color": "red", "manual": true } 3 | { "carModel": "Mercedes", "price": 20000, "color": "yellow" 4 | { "carModel": "Porsche", "price": 30000, "color": "green" } -------------------------------------------------------------------------------- /test/fixtures/json/nested.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "car" : { "make" : "Audi", "model" : "A3", "ye": {"ar": "2001"}}, "price" : 10000, "color" : "blue" } 3 | , { "car" : { "make" : "BMW", "model" : "F20", "ye": {"ar": "2002"}}, "price" : 15000, "color" : "red" } 4 | , { "car" : { "make" : "Mercedes", "model" : "SLS", "ye": {"ar": "2003"}}, "price" : 20000, "color" : "yellow" } 5 | , { "car" : { "make" : "Porsche", "model" : "9PA AF1", "ye": {"ar": "2004"}}, "price" : 30000, "color" : "green" } 6 | ] 7 | -------------------------------------------------------------------------------- /test/fixtures/json/notAnObject.json: -------------------------------------------------------------------------------- 1 | "not an object" -------------------------------------------------------------------------------- /test/fixtures/json/numberFormatter.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "number1": 0.12341234, "number2": 2.1, "number3": 65} 3 | ] -------------------------------------------------------------------------------- /test/fixtures/json/overriddenDefaultValue.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "carModel": "Audi", "price": 0, "color": "blue" }, 3 | { "carModel": "BMW", "color": "red" }, 4 | { "carModel": "Mercedes", "price": 20000 }, 5 | { "carModel": "Porsche", "price": 30000, "color": "green" } 6 | ] 7 | -------------------------------------------------------------------------------- /test/fixtures/json/quoteOnlyIfNecessary.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"a string": "with a description"}, 3 | {"a string": "with a description and \"quotes\""}, 4 | {"a string": "with a description, and a separator"}, 5 | {"a string": "with a description\n and a new line"} 6 | ] -------------------------------------------------------------------------------- /test/fixtures/json/quotes.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"a string": "with a description"}, 3 | {"a string": "with a description and \"quotes\""} 4 | ] -------------------------------------------------------------------------------- /test/fixtures/json/specialCharacters.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "carModel": "Audi", 4 | "price": 0, 5 | "color": "blue" 6 | }, 7 | { 8 | "carModel": "BMW", 9 | "price": 15000, 10 | "color": "red", 11 | "manual": true 12 | }, 13 | { 14 | "carModel": "Mercedes", 15 | "price": 20000, 16 | "color": "yellow" 17 | }, 18 | { 19 | "carModel": "Citroën", 20 | "price": 30000, 21 | "color": "green" 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /test/fixtures/json/symbol.js: -------------------------------------------------------------------------------- 1 | module.exports = [{ test: Symbol('test1') }, { test: Symbol('test2') }]; -------------------------------------------------------------------------------- /test/fixtures/json/trailingBackslash.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "carModel": "Audi\\\\", "price": 0, "color": "blue" }, 3 | { "carModel": "BMW\\\\", "price": 15000, "color": "red" }, 4 | { "carModel": "Mercedes\\\\", "price": 20000, "color": "yellow" }, 5 | { "carModel": "Porsche\\\\", "price": 30000, "color": "green" } 6 | ] 7 | -------------------------------------------------------------------------------- /test/fixtures/json/unwind.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "carModel": "Audi", "price": 0, "colors": ["blue","green","yellow"] }, 3 | { "carModel": "BMW", "price": 15000, "colors": ["red","blue"] }, 4 | { "carModel": "Mercedes", "price": 20000, "colors": "yellow" }, 5 | { "carModel": "Porsche", "price": 30000, "colors": ["green","teal","aqua"] }, 6 | { "carModel": "Tesla", "price": 50000, "colors": []} 7 | ] 8 | -------------------------------------------------------------------------------- /test/fixtures/json/unwind2.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "carModel": "Porsche", 3 | "price": 30000, 4 | "extras": { 5 | "items": [ 6 | { 7 | "name": "airbag", 8 | "items": [ 9 | { 10 | "position": "left", 11 | "color": "white" 12 | }, { 13 | "position": "right", 14 | "color": "gray" 15 | } 16 | ] 17 | }, 18 | { 19 | "name": "dashboard", 20 | "items": [ 21 | { 22 | "position": "left", 23 | "color": "gray" 24 | }, { 25 | "position": "right", 26 | "color": "black" 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | }, { 33 | "carModel": "BMW", 34 | "price": 15000, 35 | "extras": { 36 | "items": [ 37 | { 38 | "name": "airbag", 39 | "color": "white" 40 | }, { 41 | "name": "dashboard", 42 | "color": "black" 43 | } 44 | ] 45 | } 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /test/fixtures/json/unwindAndFlatten.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "carModel": "BMW", 4 | "price": 15000, 5 | "items": [ 6 | { 7 | "name": "airbag", 8 | "color": "white" 9 | }, { 10 | "name": "dashboard", 11 | "color": "black" 12 | } 13 | ] 14 | }, { 15 | "carModel": "Porsche", 16 | "price": 30000, 17 | "items": [ 18 | { 19 | "name": "airbag", 20 | "color": "gray" 21 | }, 22 | { 23 | "name": "dashboard", 24 | "color": "red" 25 | } 26 | ] 27 | }, 28 | { 29 | "carModel": "Mercedes", 30 | "price": 20000, 31 | "items": [] 32 | } 33 | ] 34 | -------------------------------------------------------------------------------- /test/fixtures/json/unwindComplexObject.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "carModel": "Porsche", 4 | "price": 30000, 5 | "extras": { 6 | "items": [ 7 | { 8 | "name": "airbag", 9 | "items": [ 10 | { 11 | "position": "left", 12 | "color": "white" 13 | }, { 14 | "position": "right", 15 | "color": "gray" 16 | } 17 | ] 18 | }, 19 | { 20 | "name": "dashboard", 21 | "items": [] 22 | } 23 | ] 24 | } 25 | }, { 26 | "carModel": "BMW", 27 | "price": 15000, 28 | "extras": { 29 | "items": [ 30 | { 31 | "name": "airbag", 32 | "color": "white", 33 | "items": [] 34 | }, { 35 | "name": "dashboard", 36 | "color": "black" 37 | } 38 | ] 39 | } 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /test/helpers/loadFixtures.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { readdir, readFile, createReadStream } = require('fs'); 4 | const path = require('path'); 5 | const { Readable } = require('stream'); 6 | const { promisify } = require('util'); 7 | const csvDirectory = path.join(__dirname, '../fixtures/csv'); 8 | const jsonDirectory = path.join(__dirname, '../fixtures/json'); 9 | 10 | const readdirAsync = promisify(readdir); 11 | const readFileAsync = promisify(readFile); 12 | 13 | 14 | function parseToJson(fixtures) { 15 | return fixtures.reduce((data, fixture) => { 16 | data[fixture.name] = fixture.content; 17 | return data; 18 | } ,{}); 19 | } 20 | 21 | module.exports.loadJSON = async function () { 22 | const filenames = await readdirAsync(jsonDirectory); 23 | const fixtures = await Promise.all(filenames 24 | .filter(filename => !filename.startsWith('.')) 25 | .map(async (filename) => { 26 | const name = path.parse(filename).name; 27 | const filePath = path.join(jsonDirectory, filename); 28 | let content; 29 | try { 30 | content = require(filePath); 31 | } catch (e) { 32 | content = await readFileAsync(filePath, 'utf-8'); 33 | } 34 | 35 | return { 36 | name, 37 | content: () => content, 38 | }; 39 | })); 40 | 41 | return parseToJson(fixtures); 42 | }; 43 | 44 | module.exports.loadJSONStreams = async function () { 45 | const filenames = await readdirAsync(jsonDirectory); 46 | const fixtures = filenames 47 | .filter(filename => !filename.startsWith('.')) 48 | .map(filename => { 49 | return { 50 | name: path.parse(filename).name, 51 | content: ({ objectMode } = { objectMode: false }) => { 52 | if (objectMode) { 53 | return Readable.from(require(path.join(jsonDirectory, filename))); 54 | } 55 | return createReadStream(path.join(jsonDirectory, filename), { highWaterMark: 175 }); 56 | }, 57 | } 58 | }); 59 | 60 | return parseToJson(fixtures); 61 | }; 62 | 63 | module.exports.loadCSV = async function () { 64 | const filenames = await readdirAsync(csvDirectory); 65 | const fixtures = await Promise.all( 66 | filenames 67 | .filter(filename => !filename.startsWith('.')) 68 | .map(async (filename) => ({ 69 | name: path.parse(filename).name, 70 | content: await readFileAsync(path.join(csvDirectory, filename), 'utf-8'), 71 | })) 72 | ); 73 | 74 | return parseToJson(fixtures); 75 | }; 76 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tape = require('tape'); 4 | const loadFixtures = require('./helpers/loadFixtures'); 5 | const CLI = require('./CLI'); 6 | const JSON2CSVParser = require('./JSON2CSVParser'); 7 | const JSON2CSVAsyncParser = require('./JSON2CSVAsyncParser'); 8 | const JSON2CSVAsyncParserInMemory = require('./JSON2CSVAsyncParserInMemory'); 9 | const JSON2CSVStreamParser = require('./JSON2CSVStreamParser'); 10 | const JSON2CSVTransform = require('./JSON2CSVTransform'); 11 | const parseNdjson = require('./parseNdjson'); 12 | 13 | const testRunner = { 14 | tests: [], 15 | before: [], 16 | after: [], 17 | add(name, test) { 18 | this.tests.push({ name, test }); 19 | }, 20 | addBefore(func) { 21 | this.before.push(func); 22 | }, 23 | addAfter(func) { 24 | this.after.push(func); 25 | }, 26 | async run() { 27 | try { 28 | await Promise.all(testRunner.before.map(before => before())); 29 | this.tests.forEach(({ name, test }) => tape(name, async (t) => { 30 | try { 31 | await test(t); 32 | } catch (err) { 33 | t.fail(err); 34 | } 35 | })); 36 | this.after.forEach(after => tape.onFinish(after)); 37 | } catch (err) { 38 | // eslint-disable-next-line no-console 39 | console.error(err); 40 | } 41 | } 42 | }; 43 | 44 | async function loadAllFixtures() { 45 | return Promise.all([ 46 | loadFixtures.loadJSON(), 47 | loadFixtures.loadJSONStreams(), 48 | loadFixtures.loadCSV() 49 | ]); 50 | } 51 | 52 | async function setupTests([jsonFixtures, jsonFixturesStreams, csvFixtures]) { 53 | CLI(testRunner, jsonFixtures, csvFixtures); 54 | JSON2CSVParser(testRunner, jsonFixtures, csvFixtures); 55 | JSON2CSVAsyncParser(testRunner, jsonFixturesStreams, csvFixtures); 56 | JSON2CSVAsyncParserInMemory(testRunner, jsonFixtures, csvFixtures); 57 | JSON2CSVStreamParser(testRunner, jsonFixturesStreams, csvFixtures); 58 | JSON2CSVTransform(testRunner, jsonFixturesStreams, csvFixtures); 59 | parseNdjson(testRunner, jsonFixtures); 60 | } 61 | 62 | loadAllFixtures() 63 | .then(setupTests) 64 | .then(() => testRunner.run()); -------------------------------------------------------------------------------- /test/parseNdjson.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const parsendjson = require('../bin/utils/parseNdjson'); 4 | 5 | module.exports = (testRunner, jsonFixtures) => { 6 | testRunner.add('should parse line-delimited JSON', (t) => { 7 | const parsed = parsendjson(jsonFixtures.ndjson()); 8 | 9 | t.equal(parsed.length, 4, 'parsed input has correct length'); 10 | }); 11 | }; --------------------------------------------------------------------------------