From d02443a288b268ba5311899fb8ae8786b22f6413 Mon Sep 17 00:00:00 2001 From: sharevb Date: Sun, 1 Sep 2024 18:48:32 +0200 Subject: [PATCH] fix(json-to-csv): handle single object and flatten Fix #1203 --- package.json | 3 +- pnpm-lock.yaml | 28 ++++++++++++++ src/tools/json-to-csv/flatten-anything.d.ts | 7 ++++ .../json-to-csv/json-to-csv.service.test.ts | 38 ++++++++++++++++--- src/tools/json-to-csv/json-to-csv.service.ts | 10 +++-- src/tools/json-to-csv/json-to-csv.vue | 2 +- 6 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 src/tools/json-to-csv/flatten-anything.d.ts diff --git a/package.json b/package.json index 63e5856a..7e033355 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,8 @@ "@tiptap/pm": "2.1.6", "@tiptap/starter-kit": "2.1.6", "@tiptap/vue-3": "2.0.3", - "@types/markdown-it": "^13.0.7", "@types/figlet": "^1.5.8", + "@types/markdown-it": "^13.0.7", "@vicons/material": "^0.12.0", "@vicons/tabler": "^0.12.0", "@vueuse/core": "^10.3.0", @@ -62,6 +62,7 @@ "emojilib": "^3.0.10", "figlet": "^1.7.0", "figue": "^1.2.0", + "flatten-anything": "^4.0.1", "fuse.js": "^6.6.2", "highlight.js": "^11.7.0", "iarna-toml-esm": "^3.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2311f3af..43636acd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,9 @@ dependencies: figue: specifier: ^1.2.0 version: 1.2.0 + flatten-anything: + specifier: ^4.0.1 + version: 4.0.1 fuse.js: specifier: ^6.6.2 version: 6.6.2 @@ -5651,6 +5654,14 @@ packages: dependencies: to-regex-range: 5.0.1 + /filter-anything@4.0.2: + resolution: {integrity: sha512-tNBpTytI180+uGpKu4dFupbkA7rmEl+ZFo27mSeR8Ov80rpMDBbmLORGTsCkCN8s50aOS79P0sg3YUKz3Tr/4Q==} + engines: {node: '>=18'} + dependencies: + is-what: 5.0.2 + ts-toolbelt: 9.6.0 + dev: false + /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -5678,6 +5689,14 @@ packages: resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} dev: true + /flatten-anything@4.0.1: + resolution: {integrity: sha512-Wz5tXB7Sgm54LHOqgbqvFlhKxzBZxA+TAv8AE28wfGpqk6zYkNauIS/ierU4VmNRokgzG2KnxhVYaUCXP+oPCQ==} + engines: {node: '>=18'} + dependencies: + filter-anything: 4.0.2 + is-what: 5.0.2 + dev: false + /flex-js@1.0.5: resolution: {integrity: sha512-Z5uoLzOGtTB/nzaTVVBbwmxOHBzHovAGJHLXE1TUKsQuN1RRWMOWeA08J9RRKtAl9TH9tkaH6fpjA4sLf0DzQw==} dev: false @@ -6448,6 +6467,11 @@ packages: resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==} dev: true + /is-what@5.0.2: + resolution: {integrity: sha512-vI7Ui0qzNQ2ClDZd0bC7uqRk3T1imbX5cZODmVlqqdqiwmSIUX3CNSiRgFjFMJ987sVCMSa7xZeEDtpJduPg4A==} + engines: {node: '>=18'} + dev: false + /is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -8548,6 +8572,10 @@ packages: typescript: 5.2.2 dev: true + /ts-toolbelt@9.6.0: + resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} + dev: false + /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} dev: true diff --git a/src/tools/json-to-csv/flatten-anything.d.ts b/src/tools/json-to-csv/flatten-anything.d.ts new file mode 100644 index 00000000..e361ccc0 --- /dev/null +++ b/src/tools/json-to-csv/flatten-anything.d.ts @@ -0,0 +1,7 @@ +declare module "flatten-anything" { + export function flatten(objectOrArray: { + [key in string]: any; + }, untilDepth?: number): { + [key in string]: any; + }; +} \ No newline at end of file diff --git a/src/tools/json-to-csv/json-to-csv.service.test.ts b/src/tools/json-to-csv/json-to-csv.service.test.ts index c27bf9b6..f49b6c8a 100644 --- a/src/tools/json-to-csv/json-to-csv.service.test.ts +++ b/src/tools/json-to-csv/json-to-csv.service.test.ts @@ -3,6 +3,10 @@ import { convertArrayToCsv, getHeaders } from './json-to-csv.service'; describe('json-to-csv service', () => { describe('getHeaders', () => { + it('extracts all the keys from the array of nested objects', () => { + expect(getHeaders({ array: [{ a: { c: 1, d: 1 }, b: 2 }, { a: 3, c: 4 }] })).toEqual(['a.c', 'a.d', 'b', 'a', 'c']); + }); + it('extracts all the keys from the array of objects', () => { expect(getHeaders({ array: [{ a: 1, b: 2 }, { a: 3, c: 4 }] })).toEqual(['a', 'b', 'c']); }); @@ -20,7 +24,7 @@ describe('json-to-csv service', () => { ]; - expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` + expect(convertArrayToCsv({ arrayOrObject: array })).toMatchInlineSnapshot(` "a,b 1,2 3,4" @@ -33,7 +37,7 @@ describe('json-to-csv service', () => { { a: 3, c: 4 }, ]; - expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` + expect(convertArrayToCsv({ arrayOrObject: array })).toMatchInlineSnapshot(` "a,b,c 1,2, 3,,4" @@ -45,7 +49,7 @@ describe('json-to-csv service', () => { { a: null, b: 2 }, ]; - expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` + expect(convertArrayToCsv({ arrayOrObject: array })).toMatchInlineSnapshot(` "a,b null,2" `); @@ -57,7 +61,7 @@ describe('json-to-csv service', () => { { b: 3 }, ]; - expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` + expect(convertArrayToCsv({ arrayOrObject: array })).toMatchInlineSnapshot(` "a,b ,2 ,3" @@ -69,7 +73,7 @@ describe('json-to-csv service', () => { { a: 'hello, world', b: 2 }, ]; - expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` + expect(convertArrayToCsv({ arrayOrObject: array })).toMatchInlineSnapshot(` "a,b \\"hello, world\\",2" `); @@ -80,10 +84,32 @@ describe('json-to-csv service', () => { { a: 'hello "world"', b: 2 }, ]; - expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` + expect(convertArrayToCsv({ arrayOrObject: array })).toMatchInlineSnapshot(` "a,b hello \\\\\\"world\\\\\\",2" `); }); + + it('converts an array of nested objects to a CSV string', () => { + const array = [ + { a: { c: 1, d: 1 }, b: 2 }, + { a: 3, c: 4 }, + ]; + + expect(convertArrayToCsv({ arrayOrObject: array })).toMatchInlineSnapshot(` + "a.c,a.d,b,a,c + 1,1,2,, + ,,,3,4" + `); + }); + + it('converts an object to a CSV string', () => { + const obj = { a: { c: 1, d: 1 }, b: 2 }; + + expect(convertArrayToCsv({ arrayOrObject: obj })).toMatchInlineSnapshot(` + "a.c,a.d,b + 1,1,2" + `); + }); }); }); diff --git a/src/tools/json-to-csv/json-to-csv.service.ts b/src/tools/json-to-csv/json-to-csv.service.ts index ab3c04e7..541ed333 100644 --- a/src/tools/json-to-csv/json-to-csv.service.ts +++ b/src/tools/json-to-csv/json-to-csv.service.ts @@ -1,9 +1,11 @@ +import { flatten } from 'flatten-anything'; + export { getHeaders, convertArrayToCsv }; function getHeaders({ array }: { array: Record[] }): string[] { const headers = new Set(); - array.forEach(item => Object.keys(item).forEach(key => headers.add(key))); + array.forEach(item => Object.keys(flatten(item)).forEach(key => headers.add(key))); return Array.from(headers); } @@ -26,10 +28,12 @@ function serializeValue(value: unknown): string { return valueAsString; } -function convertArrayToCsv({ array }: { array: Record[] }): string { +function convertArrayToCsv({ arrayOrObject }: { arrayOrObject: Record[] | Record }): string { + const array = !Array.isArray(arrayOrObject) ? [arrayOrObject] : arrayOrObject; + const headers = getHeaders({ array }); - const rows = array.map(item => headers.map(header => serializeValue(item[header]))); + const rows = array.map(item => headers.map(header => serializeValue(flatten(item)[header]))); return [headers.join(','), ...rows].join('\n'); } diff --git a/src/tools/json-to-csv/json-to-csv.vue b/src/tools/json-to-csv/json-to-csv.vue index e2f5ddb6..75776182 100644 --- a/src/tools/json-to-csv/json-to-csv.vue +++ b/src/tools/json-to-csv/json-to-csv.vue @@ -9,7 +9,7 @@ function transformer(value: string) { if (value === '') { return ''; } - return convertArrayToCsv({ array: JSON5.parse(value) }); + return convertArrayToCsv({ arrayOrObject: JSON5.parse(value) }); }, ''); }