fix(json-to-csv): handle single object and flatten

Fix #1203
This commit is contained in:
sharevb 2024-09-01 18:48:32 +02:00 committed by ShareVB
parent 87984e2081
commit d02443a288
6 changed files with 77 additions and 11 deletions

View file

@ -41,8 +41,8 @@
"@tiptap/pm": "2.1.6", "@tiptap/pm": "2.1.6",
"@tiptap/starter-kit": "2.1.6", "@tiptap/starter-kit": "2.1.6",
"@tiptap/vue-3": "2.0.3", "@tiptap/vue-3": "2.0.3",
"@types/markdown-it": "^13.0.7",
"@types/figlet": "^1.5.8", "@types/figlet": "^1.5.8",
"@types/markdown-it": "^13.0.7",
"@vicons/material": "^0.12.0", "@vicons/material": "^0.12.0",
"@vicons/tabler": "^0.12.0", "@vicons/tabler": "^0.12.0",
"@vueuse/core": "^10.3.0", "@vueuse/core": "^10.3.0",
@ -62,6 +62,7 @@
"emojilib": "^3.0.10", "emojilib": "^3.0.10",
"figlet": "^1.7.0", "figlet": "^1.7.0",
"figue": "^1.2.0", "figue": "^1.2.0",
"flatten-anything": "^4.0.1",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"highlight.js": "^11.7.0", "highlight.js": "^11.7.0",
"iarna-toml-esm": "^3.0.5", "iarna-toml-esm": "^3.0.5",

28
pnpm-lock.yaml generated
View file

@ -86,6 +86,9 @@ dependencies:
figue: figue:
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.0 version: 1.2.0
flatten-anything:
specifier: ^4.0.1
version: 4.0.1
fuse.js: fuse.js:
specifier: ^6.6.2 specifier: ^6.6.2
version: 6.6.2 version: 6.6.2
@ -5651,6 +5654,14 @@ packages:
dependencies: dependencies:
to-regex-range: 5.0.1 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: /find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -5678,6 +5689,14 @@ packages:
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
dev: true 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: /flex-js@1.0.5:
resolution: {integrity: sha512-Z5uoLzOGtTB/nzaTVVBbwmxOHBzHovAGJHLXE1TUKsQuN1RRWMOWeA08J9RRKtAl9TH9tkaH6fpjA4sLf0DzQw==} resolution: {integrity: sha512-Z5uoLzOGtTB/nzaTVVBbwmxOHBzHovAGJHLXE1TUKsQuN1RRWMOWeA08J9RRKtAl9TH9tkaH6fpjA4sLf0DzQw==}
dev: false dev: false
@ -6448,6 +6467,11 @@ packages:
resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==} resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==}
dev: true dev: true
/is-what@5.0.2:
resolution: {integrity: sha512-vI7Ui0qzNQ2ClDZd0bC7uqRk3T1imbX5cZODmVlqqdqiwmSIUX3CNSiRgFjFMJ987sVCMSa7xZeEDtpJduPg4A==}
engines: {node: '>=18'}
dev: false
/is-wsl@2.2.0: /is-wsl@2.2.0:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -8548,6 +8572,10 @@ packages:
typescript: 5.2.2 typescript: 5.2.2
dev: true dev: true
/ts-toolbelt@9.6.0:
resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==}
dev: false
/tslib@1.14.1: /tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
dev: true dev: true

View file

@ -0,0 +1,7 @@
declare module "flatten-anything" {
export function flatten(objectOrArray: {
[key in string]: any;
}, untilDepth?: number): {
[key in string]: any;
};
}

View file

@ -3,6 +3,10 @@ import { convertArrayToCsv, getHeaders } from './json-to-csv.service';
describe('json-to-csv service', () => { describe('json-to-csv service', () => {
describe('getHeaders', () => { 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', () => { 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']); 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 "a,b
1,2 1,2
3,4" 3,4"
@ -33,7 +37,7 @@ describe('json-to-csv service', () => {
{ a: 3, c: 4 }, { a: 3, c: 4 },
]; ];
expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` expect(convertArrayToCsv({ arrayOrObject: array })).toMatchInlineSnapshot(`
"a,b,c "a,b,c
1,2, 1,2,
3,,4" 3,,4"
@ -45,7 +49,7 @@ describe('json-to-csv service', () => {
{ a: null, b: 2 }, { a: null, b: 2 },
]; ];
expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` expect(convertArrayToCsv({ arrayOrObject: array })).toMatchInlineSnapshot(`
"a,b "a,b
null,2" null,2"
`); `);
@ -57,7 +61,7 @@ describe('json-to-csv service', () => {
{ b: 3 }, { b: 3 },
]; ];
expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` expect(convertArrayToCsv({ arrayOrObject: array })).toMatchInlineSnapshot(`
"a,b "a,b
,2 ,2
,3" ,3"
@ -69,7 +73,7 @@ describe('json-to-csv service', () => {
{ a: 'hello, world', b: 2 }, { a: 'hello, world', b: 2 },
]; ];
expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` expect(convertArrayToCsv({ arrayOrObject: array })).toMatchInlineSnapshot(`
"a,b "a,b
\\"hello, world\\",2" \\"hello, world\\",2"
`); `);
@ -80,10 +84,32 @@ describe('json-to-csv service', () => {
{ a: 'hello "world"', b: 2 }, { a: 'hello "world"', b: 2 },
]; ];
expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` expect(convertArrayToCsv({ arrayOrObject: array })).toMatchInlineSnapshot(`
"a,b "a,b
hello \\\\\\"world\\\\\\",2" 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"
`);
});
}); });
}); });

View file

@ -1,9 +1,11 @@
import { flatten } from 'flatten-anything';
export { getHeaders, convertArrayToCsv }; export { getHeaders, convertArrayToCsv };
function getHeaders({ array }: { array: Record<string, unknown>[] }): string[] { function getHeaders({ array }: { array: Record<string, unknown>[] }): string[] {
const headers = new Set<string>(); const headers = new Set<string>();
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); return Array.from(headers);
} }
@ -26,10 +28,12 @@ function serializeValue(value: unknown): string {
return valueAsString; return valueAsString;
} }
function convertArrayToCsv({ array }: { array: Record<string, unknown>[] }): string { function convertArrayToCsv({ arrayOrObject }: { arrayOrObject: Record<string, unknown>[] | Record<string, unknown> }): string {
const array = !Array.isArray(arrayOrObject) ? [arrayOrObject] : arrayOrObject;
const headers = getHeaders({ array }); 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'); return [headers.join(','), ...rows].join('\n');
} }

View file

@ -9,7 +9,7 @@ function transformer(value: string) {
if (value === '') { if (value === '') {
return ''; return '';
} }
return convertArrayToCsv({ array: JSON5.parse(value) }); return convertArrayToCsv({ arrayOrObject: JSON5.parse(value) });
}, ''); }, '');
} }