From bed3e6abfe11ecf0f16daf4c98c1006dc5b0321c Mon Sep 17 00:00:00 2001 From: jlenon7 Date: Mon, 5 Feb 2024 22:47:17 +0000 Subject: [PATCH] feat(parser): add csv parser --- package-lock.json | 20 ++++++++-- package.json | 3 +- src/helpers/Parser.ts | 63 +++++++++++++++++++++++++++++-- tests/fixtures/resources/data.csv | 3 ++ tests/unit/ParserTest.ts | 45 +++++++++++++++++++++- 5 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 tests/fixtures/resources/data.csv diff --git a/package-lock.json b/package-lock.json index a2d0ca0..3a07346 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athenna/common", - "version": "4.30.0", + "version": "4.31.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@athenna/common", - "version": "4.30.0", + "version": "4.31.0", "license": "MIT", "dependencies": { "@fastify/formbody": "^7.4.0", @@ -15,6 +15,7 @@ "chalk": "^5.3.0", "change-case": "^4.1.2", "collect.js": "^4.36.1", + "csv-parser": "^3.0.0", "execa": "^8.0.1", "fastify": "^4.25.2", "got": "^12.6.1", @@ -3240,6 +3241,20 @@ "node": "*" } }, + "node_modules/csv-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.0.0.tgz", + "integrity": "sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ==", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "csv-parser": "bin/csv-parser" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/cz-conventional-changelog": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.3.0.tgz", @@ -7243,7 +7258,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } diff --git a/package.json b/package.json index 363cf8d..f4b0dcc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athenna/common", - "version": "4.30.0", + "version": "4.31.0", "description": "The Athenna common helpers to use in any Node.js ESM project.", "license": "MIT", "author": "João Lenon ", @@ -60,6 +60,7 @@ "chalk": "^5.3.0", "change-case": "^4.1.2", "collect.js": "^4.36.1", + "csv-parser": "^3.0.0", "execa": "^8.0.1", "fastify": "^4.25.2", "got": "^12.6.1", diff --git a/src/helpers/Parser.ts b/src/helpers/Parser.ts index 00ada4d..4495925 100644 --- a/src/helpers/Parser.ts +++ b/src/helpers/Parser.ts @@ -10,6 +10,7 @@ import ms from 'ms' import bytes from 'bytes' import yaml from 'js-yaml' +import csvParser from 'csv-parser' import { Is } from '#src/helpers/Is' import { String } from '#src/helpers/String' @@ -20,6 +21,21 @@ import { getReasonPhrase, getStatusCode } from 'http-status-codes' import { InvalidNumberException } from '#src/exceptions/InvalidNumberException' export class Parser { + /** + * Parse using Node.js streams, useful for + * parsing multiple values in files. + */ + public static stream() { + return { + /** + * Parse a csv chunk to json object. + */ + csvToJson: (options?: csvParser.Options | string[]) => { + return csvParser(options) + } + } + } + /** * Parse a string to array. */ @@ -50,14 +66,14 @@ export class Parser { return `${values[0]}${options?.pairSeparator || ' and '}${values[1]}` } - const normalized = Options.create(options, { + options = Options.create(options, { separator: ', ', lastSeparator: ' and ' }) return ( - values.slice(0, -1).join(normalized.separator) + - normalized.lastSeparator + + values.slice(0, -1).join(options.separator) + + options.lastSeparator + values[values.length - 1] ) } @@ -162,6 +178,47 @@ export class Parser { return ms(value, { long }) } + /** + * Parses a json to a csv string. + */ + public static jsonToCsv( + value: Record, + options: { headers?: string[] } = {} + ) { + options = Options.create(options, { + headers: Object.keys(value) + }) + + let csv = '' + + if (!Is.Empty(options.headers)) { + csv += + Parser.arrayToString(options.headers, { + separator: ',', + pairSeparator: ',', + lastSeparator: ',' + }) + '\n' + } + + const values = [] + + Object.keys(value).forEach(key => { + if (!Is.Empty(options.headers) && !options.headers.includes(key)) { + return + } + + values.push(value[key]) + }) + + csv += Parser.arrayToString(values, { + separator: ',', + pairSeparator: ',', + lastSeparator: ',' + }) + + return csv + } + /** * Parses the status code number to it reason in string. */ diff --git a/tests/fixtures/resources/data.csv b/tests/fixtures/resources/data.csv new file mode 100644 index 0000000..e460d61 --- /dev/null +++ b/tests/fixtures/resources/data.csv @@ -0,0 +1,3 @@ +id,name +1,lenon +2,victor diff --git a/tests/unit/ParserTest.ts b/tests/unit/ParserTest.ts index c0b88c9..1e86a53 100644 --- a/tests/unit/ParserTest.ts +++ b/tests/unit/ParserTest.ts @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import { Parser } from '#src' +import { File, Parser } from '#src' import { Test, type Context } from '@athenna/test' import { InvalidNumberException } from '#src/exceptions/InvalidNumberException' @@ -279,4 +279,47 @@ export default class ParserTest { assert.deepEqual(object, { version: 3 }) } + + @Test() + public async shouldBeAbleToParseObjectToCsv({ assert }: Context) { + const csv = Parser.jsonToCsv({ id: 1, name: 'lenon' }) + + assert.deepEqual(csv, 'id,name\n1,lenon') + } + + @Test() + public async shouldBeAbleToParseObjectToCsvWithoutHeaders({ assert }: Context) { + const csv = Parser.jsonToCsv({ id: 1, name: 'lenon' }, { headers: [] }) + + assert.deepEqual(csv, '1,lenon') + } + + @Test() + public async shouldBeAbleToParseObjectToCsvOnlyWithDefinedHeaders({ assert }: Context) { + const csv = Parser.jsonToCsv({ id: 1, name: 'lenon' }, { headers: ['id'] }) + + assert.deepEqual(csv, 'id\n1') + } + + @Test() + public async shouldBeAbleToParseACsvFileToJson({ assert }: Context) { + const getCsvData = async () => { + return new Promise(resolve => { + const data = [] + + new File(Path.fixtures('resources/data.csv')) + .createReadStream() + .pipe(Parser.stream().csvToJson()) + .on('data', user => data.push(user)) + .on('end', () => resolve(data)) + }) + } + + const data = await getCsvData() + + assert.deepEqual(data, [ + { id: '1', name: 'lenon' }, + { id: '2', name: 'victor' } + ]) + } }