diff --git a/package-lock.json b/package-lock.json index d6c7a45..7aa1dcf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athenna/common", - "version": "5.31.0", + "version": "5.32.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@athenna/common", - "version": "5.31.0", + "version": "5.32.0", "license": "MIT", "dependencies": { "@fastify/formbody": "^8.0.2", diff --git a/package.json b/package.json index 27724cc..7c63d27 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athenna/common", - "version": "5.31.0", + "version": "5.32.0", "description": "The Athenna common helpers to use in any Node.js ESM project.", "license": "MIT", "author": "João Lenon ", diff --git a/src/handlers/ExceptionHandler.ts b/src/handlers/ExceptionHandler.ts index 34f3b80..4008101 100644 --- a/src/handlers/ExceptionHandler.ts +++ b/src/handlers/ExceptionHandler.ts @@ -16,7 +16,7 @@ export type ExceptionHandlerContext = { export class ExceptionHandler extends Macroable { public async handle(_: ExceptionHandlerContext): Promise { /** - * This method is meant to be overridden by the user + * This method is meant to be overridden by the user * using the `macro()` method */ } diff --git a/src/helpers/Enum.ts b/src/helpers/Enum.ts index 2b5a49a..24f4a42 100644 --- a/src/helpers/Enum.ts +++ b/src/helpers/Enum.ts @@ -72,8 +72,8 @@ export class Enum extends Macroable { * public static PENDING = 'pending' as const * public static APPROVED = 'approved' as const * public static BLOCKED = 'blocked' as const - * } - * + * } + * * const randomKey = StatusEnum.randomKey() // 'PENDING' * ``` */ @@ -90,8 +90,8 @@ export class Enum extends Macroable { * public static PENDING = 'pending' as const * public static APPROVED = 'approved' as const * public static BLOCKED = 'blocked' as const - * } - * + * } + * * const randomValue = StatusEnum.randomValue() // 'pending' * ``` */ diff --git a/src/helpers/Parser.ts b/src/helpers/Parser.ts index 7ae68a1..5cb18c4 100644 --- a/src/helpers/Parser.ts +++ b/src/helpers/Parser.ts @@ -12,7 +12,16 @@ import bytes from 'bytes' import yaml from 'js-yaml' import csvParser from 'csv-parser' +import { + json2csv, + csv2json, + type Json2CsvOptions, + type Csv2JsonOptions +} from 'json-2-csv' +import type { HTMLJson } from '#src/types/json/HTMLJson' + import { Is } from '#src/helpers/Is' +import { Clean } from '#src/helpers/Clean' import { String } from '#src/helpers/String' import { Options } from '#src/helpers/Options' import { Macroable } from '#src/helpers/Macroable' @@ -21,14 +30,6 @@ import type { ObjectBuilderOptions } from '#src/types' import { getReasonPhrase, getStatusCode } from 'http-status-codes' import { InvalidNumberException } from '#src/exceptions/InvalidNumberException' -import { - json2csv, - csv2json, - type Json2CsvOptions, - type Csv2JsonOptions -} from 'json-2-csv' -import type { HTMLJson } from '#src/types/json/HTMLJson' - export class Parser extends Macroable { /** * Parse using Node.js streams, useful for @@ -216,6 +217,68 @@ export class Parser extends Macroable { return bytes.parse(byte) } + /** + * Parses an object to a base64 string. + */ + public static objectToBase64( + object: Record, + options?: { nullOnEmpty?: boolean; removeEmpty?: boolean } + ) { + options = Options.create(options, { + nullOnEmpty: false, + removeEmpty: false + }) + + if (options.removeEmpty) { + object = Clean.cleanObject(object, { removeEmpty: true, recursive: true }) + } + + if (options.nullOnEmpty && Is.Empty(object)) { + return null + } + + return Buffer.from(JSON.stringify(object)).toString('base64') + } + + /** + * Parses a base64 string to an object. + */ + public static base64ToObject>(base64: string): T { + return JSON.parse(Buffer.from(base64, 'base64').toString('utf-8')) + } + + /** + * Parses an object to a base64url string. + */ + public static objectToBase64Url( + object: Record, + options?: { nullOnEmpty?: boolean; removeEmpty?: boolean } + ) { + options = Options.create(options, { + nullOnEmpty: false, + removeEmpty: false + }) + + if (options.removeEmpty) { + object = Clean.cleanObject(object, { removeEmpty: true, recursive: true }) + } + + if (options.nullOnEmpty && Is.Empty(object)) { + return null + } + + return Buffer.from(JSON.stringify(object)).toString('base64url') + } + + /** + * Parses a base64url string to an object. + */ + public static base64UrlToObject>( + base64Url: string + ): T { + return JSON.parse(Buffer.from(base64Url, 'base64url').toString('utf-8')) + } + /** * Parses a string to MS format. */ @@ -230,6 +293,20 @@ export class Parser extends Macroable { return ms(value, { long }) } + /** + * Parses a time string to seconds format. + */ + public static timeToSeconds(value: string): number { + return ms(value) / 1000 + } + + /** + * Parses a seconds number to time format. + */ + public static secondsToTime(value: number, long = false): string { + return ms(value * 1000, { long }) + } + /** * Parses a json to a csv string. */ diff --git a/tests/unit/helpers/ParserTest.ts b/tests/unit/helpers/ParserTest.ts index 3da6612..ff21338 100644 --- a/tests/unit/helpers/ParserTest.ts +++ b/tests/unit/helpers/ParserTest.ts @@ -119,6 +119,25 @@ export default class ParserTest { assert.equal(Parser.msToTime(-31557600000, true), '-365 days') } + @Test() + public async shouldParseTheStringToMsFormatAndSecondsFormatToString({ assert }: Context) { + // time to seconds + assert.equal(Parser.timeToSeconds('2 days'), 172800) + assert.equal(Parser.timeToSeconds('1d'), 86400) + assert.equal(Parser.timeToSeconds('10h'), 36000) + assert.equal(Parser.timeToSeconds('-10h'), -36000) + assert.equal(Parser.timeToSeconds('1 year'), 31557600) + assert.equal(Parser.timeToSeconds('-1 year'), -31557600) + + // ms to time + assert.equal(Parser.secondsToTime(172800, true), '2 days') + assert.equal(Parser.secondsToTime(86400), '1d') + assert.equal(Parser.secondsToTime(36000), '10h') + assert.equal(Parser.secondsToTime(-36000), '-10h') + assert.equal(Parser.secondsToTime(31557600, true), '365 days') + assert.equal(Parser.secondsToTime(-31557600, true), '-365 days') + } + @Test() public async shouldParseTheStatusCodeToReasonAndReasonToStatusCode({ assert }: Context) { // status code to reason @@ -280,6 +299,82 @@ export default class ParserTest { assert.deepEqual(object, { version: 3 }) } + @Test() + public async shouldBeAbleToParseAnObjectToBase64String({ assert }: Context) { + const string1 = Parser.objectToBase64({ hello: 'world' }) + const string2 = Parser.objectToBase64( + { + hello: 'world', + foo: null, + bar: undefined, + baz: {}, + qux: [], + quux: '' + }, + { removeEmpty: true } + ) + const string3 = Parser.objectToBase64( + { + foo: null, + bar: undefined, + baz: {}, + qux: [] + }, + { nullOnEmpty: true, removeEmpty: true } + ) + + assert.deepEqual(string1, 'eyJoZWxsbyI6IndvcmxkIn0=') + assert.deepEqual(string2, 'eyJoZWxsbyI6IndvcmxkIn0=') + assert.deepEqual(string3, null) + } + + @Test() + public async shouldBeAbleToParseBase64StringToObject({ assert }: Context) { + const string = 'eyJoZWxsbyI6IndvcmxkIn0=' + + const object = Parser.base64ToObject(string) + + assert.deepEqual(object, { hello: 'world' }) + } + + @Test() + public async shouldBeAbleToParseAnObjectToBase64UrlString({ assert }: Context) { + const string1 = Parser.objectToBase64Url({ hello: 'world' }) + const string2 = Parser.objectToBase64Url( + { + hello: 'world', + foo: null, + bar: undefined, + baz: {}, + qux: [], + quux: '' + }, + { removeEmpty: true } + ) + const string3 = Parser.objectToBase64Url( + { + foo: null, + bar: undefined, + baz: {}, + qux: [] + }, + { nullOnEmpty: true, removeEmpty: true } + ) + + assert.deepEqual(string1, 'eyJoZWxsbyI6IndvcmxkIn0') + assert.deepEqual(string2, 'eyJoZWxsbyI6IndvcmxkIn0') + assert.deepEqual(string3, null) + } + + @Test() + public async shouldBeAbleToParseBase64UrlStringToObject({ assert }: Context) { + const string = 'eyJoZWxsbyI6IndvcmxkIn0' + + const object = Parser.base64UrlToObject(string) + + assert.deepEqual(object, { hello: 'world' }) + } + @Test() public async shouldBeAbleToParseArrayToCsv({ assert }: Context) { const csv = Parser.arrayToCsv([{ id: 1, name: 'lenon' }])