From 18ef3c0e4fedaf2627e4d156dfa2acc0cd3134f1 Mon Sep 17 00:00:00 2001 From: Brent Van Geertruy Date: Mon, 20 Jan 2020 15:45:31 -0500 Subject: [PATCH] Add extra proprty to check for instanceof ValidationError across versions --- README.md | 22 ++++++++++++++++++---- package-lock.json | 20 ++++++++++---------- package.json | 6 +++--- src/lib/errors.ts | 2 ++ src/lib/parser.ts | 9 ++++++--- tests/parser.test.ts | 39 ++++++++++++++++++++++++++++++--------- 6 files changed, 69 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index d08b0b3..9e87177 100644 --- a/README.md +++ b/README.md @@ -131,20 +131,34 @@ throw new ApiError(400, errors.BAD_REQUEST); ## Error parsing -### isApiError(object) +### isApiError(apiError) -Will return boolean indicating whether object has all required properties to be an `ApiError`. +Will return boolean indicating whether error is instance of `ApiError`. ```javascript // Will return true - isApiError({ status: 200, code: 'MY_CODE', title: 'MY_ERROR', detail: {} }) + isApiError(new BadRequestError()) // Will return false - isApiError({ status: 200, code: 'MY_CODE' }) + isApiError(new Error('Something')) ``` > Will automatically cast to ApiError if succeeds and using Typescript +### isJsonApiError(object) + +Will return boolean indicating whether object has all required properties to be a parsed `ApiError`. + +```javascript + // Will return true + isJsonApiError({ status: 200, code: 'MY_CODE', title: 'MY_ERROR', detail: {} }) + + // Will return false + isJsonApiError({ status: 200, code: 'MY_CODE' }) +``` + +> Will automatically cast to ParsedError if succeeds and using Typescript + ### parseErrors(error, i18nOptions (optional)) Parse any data into an error object with all properties needed for jsonade parser. Also parses [`express-validation`](https://github.com/andrewkeig/express-validation) and [`celebrate`](https://github.com/arb/celebrate) errors. diff --git a/package-lock.json b/package-lock.json index f0de11b..6fd03a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -563,9 +563,9 @@ } }, "@types/jest": { - "version": "24.0.25", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.25.tgz", - "integrity": "sha512-hnP1WpjN4KbGEK4dLayul6lgtys6FPz0UfxMeMQCv0M+sTnzN3ConfiO72jHgLxl119guHgI8gLqDOrRLsyp2g==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.9.0.tgz", + "integrity": "sha512-dXvuABY9nM1xgsXlOtLQXJKdacxZJd7AtvLsKZ/0b57ruMXDKCOXAC/M75GbllQX6o1pcZ5hAG4JzYy7Z/wM2w==", "dev": true, "requires": { "jest-diff": "^24.3.0" @@ -5778,7 +5778,7 @@ "dependencies": { "minimist": { "version": "0.0.10", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", "dev": true } @@ -7510,9 +7510,9 @@ "dev": true }, "typescript": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.4.tgz", - "integrity": "sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw==", + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz", + "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==", "dev": true }, "uglify-js": { @@ -7703,9 +7703,9 @@ } }, "uuid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, "validate-npm-package-license": { "version": "3.0.4", diff --git a/package.json b/package.json index 9c2ad76..4be7d40 100644 --- a/package.json +++ b/package.json @@ -26,14 +26,14 @@ "http-status": "~1.4.2", "i18n": "~0.8.4", "joi": "~14.3.1", - "safe-json-stringify": "1.2.0", + "safe-json-stringify": "~1.2.0", "tree-house-translations": "~1.2.2", - "uuid": "~3.3.3" + "uuid": "~3.4.0" }, "devDependencies": { "@types/http-status": "~1.1.2", "@types/i18n": "~0.8.6", - "@types/jest": "~24.0.25", + "@types/jest": "24.9.0", "@types/uuid": "~3.4.6", "coveralls": "~3.0.9", "jest": "~24.9.0", diff --git a/src/lib/errors.ts b/src/lib/errors.ts index b437bd4..e54e4af 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -9,11 +9,13 @@ export class ApiError extends Error { i18n?: string; id?: string; detail?: any; + isApiError: boolean; constructor(status: number, error: ErrorType, args: { message?: string, detail?: any, stack?: any } = {}) { const { message, detail, stack } = args; super(message || error.message); this.name = 'ApiError'; + this.isApiError = true; this.id = uuid.v1(); this.code = error.code; this.i18n = error.i18n; diff --git a/src/lib/parser.ts b/src/lib/parser.ts index f3f6418..23b48d0 100644 --- a/src/lib/parser.ts +++ b/src/lib/parser.ts @@ -11,12 +11,15 @@ import { getTranslator } from './translator'; * Check if object has all required properties to be an ApiError * @param {Object} obj */ -export const isApiError = (obj: ParsedError | any = {}): obj is ParsedError => +export const isJsonApiError = (obj: ParsedError | any = {}): obj is ParsedError => _.has(obj, 'status') && _.has(obj, 'code') && _.has(obj, 'title') && _.has(obj, 'detail'); +export const isApiError = (err: ApiError | any = {}): err is ApiError => + (err || {}).hasOwnProperty('isApiError') && err.isApiError === true; + /** * Parse errors * @param {String} error @@ -54,7 +57,7 @@ export function parseErrors(error: any = {}, translatorOptions?: TranslatorOptio } // Own thrown ApiErrors - if (error instanceof ApiError) { + if (isApiError(error)) { let translatedMessage = error.message; if (translatorOptions) { @@ -94,7 +97,7 @@ export function parseErrors(error: any = {}, translatorOptions?: TranslatorOptio export function parseJsonErrors(response: any): ApiError[] { if ((response || {}).hasOwnProperty('errors') && Array.isArray(response.errors)) { return response.errors.reduce((acc: ApiError[], error: any) => { - if (isApiError(error)) { + if (isJsonApiError(error)) { const { status, code, title, detail, meta = {} } = error; return [...acc, new ApiError(status, { code, message: title }, { detail, stack: (meta || {}).stack })]; } diff --git a/tests/parser.test.ts b/tests/parser.test.ts index 8f58df9..cba00ed 100644 --- a/tests/parser.test.ts +++ b/tests/parser.test.ts @@ -2,7 +2,10 @@ import * as httpStatus from 'http-status'; import { ValidationError } from 'express-validation'; import * as translator from '../src/lib/translator'; -import { ApiError, errors, parseErrors, parseJsonErrors, isApiError, InternalServerError } from '../src'; +import { + ApiError, errors, parseErrors, parseJsonErrors, isJsonApiError, InternalServerError, BadRequestError, isApiError, UnauthorizedError, + ForbiddenError, +} from '../src'; import { errorDefaults } from '../src/config/defaults.config'; describe('errorParser', () => { @@ -111,7 +114,7 @@ describe('errorParser', () => { it('Should succesfully parse default ApiError with default message when language is not available', () => { expect.assertions(1); try { - throw new ApiError(httpStatus.BAD_REQUEST, errors.INVALID_INPUT); + throw new BadRequestError(errors.INVALID_INPUT); } catch (err) { const parsedError = parseErrors(err, { path: '', language: 'du' }); expect(parsedError).toMatchObject({ @@ -182,7 +185,7 @@ describe('errorParser', () => { it('Should parse celebrate joi error without ValidationError', () => { const celebrateError = { - joi: {}, + joi: null, }; const parsedError = parseErrors(celebrateError); @@ -197,9 +200,9 @@ describe('errorParser', () => { }); }); - describe('isApiError', () => { + describe('isJsonApiError', () => { it('Should return true when all properties are available', () => { - const output = isApiError({ + const output = isJsonApiError({ status: httpStatus.BAD_REQUEST, code: 'RANDOM_CODE', title: 'My title', @@ -212,11 +215,29 @@ describe('errorParser', () => { }); it('Should return false when some properties are missing', () => { + expect(isJsonApiError()).toEqual(false); + expect(isJsonApiError({})).toEqual(false); + expect(isJsonApiError({ status: httpStatus.NOT_ACCEPTABLE, code: '120', title: 'any Title' })).toEqual(false); + expect(isJsonApiError({ status: httpStatus.INTERNAL_SERVER_ERROR, code: '120', detail: 'any Title' })).toEqual(false); + expect(isJsonApiError({ code: '120', detail: 'any Title' })).toEqual(false); + expect(isJsonApiError({ status: httpStatus.BAD_REQUEST, detail: 'any Title' })).toEqual(false); + }); + }); + + describe('isApiError', () => { + it('Should return true when all properties are available', () => { + expect(isApiError(new BadRequestError())).toEqual(true); + expect(isApiError(new ApiError(401, errors.AUTHENTICATION_FAILED))).toEqual(true); + expect(isApiError(new InternalServerError())).toEqual(true); + expect(isApiError(new UnauthorizedError())).toEqual(true); + expect(isApiError(new ForbiddenError())).toEqual(true); + }); + + it('Should return false when some properties are missing', () => { + expect(isApiError()).toEqual(false); expect(isApiError({})).toEqual(false); - expect(isApiError({ status: httpStatus.NOT_ACCEPTABLE, code: '120', title: 'any Title' })).toEqual(false); - expect(isApiError({ status: httpStatus.INTERNAL_SERVER_ERROR, code: '120', detail: 'any Title' })).toEqual(false); - expect(isApiError({ code: '120', detail: 'any Title' })).toEqual(false); - expect(isApiError({ status: httpStatus.BAD_REQUEST, detail: 'any Title' })).toEqual(false); + expect(isApiError(null)).toEqual(false); + expect(isApiError(new Error('SOMETHING WRONG'))).toEqual(false); }); });