Skip to content

Commit

Permalink
Merge pull request #3 from dancastillo/v0.0.2-esm-dc-error
Browse files Browse the repository at this point in the history
feat: v0.0.2 esm and dc error
  • Loading branch information
dancastillo authored Nov 21, 2024
2 parents 6f74d25 + 1be2695 commit 54b9b09
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 121 deletions.
5 changes: 0 additions & 5 deletions .changeset/light-planets-agree.md

This file was deleted.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,6 @@ package-lock.json

# yarn dependencies
yarn.lock

# notes
notes.txt
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# @dancastillo/error

## 0.0.2

### Patch Changes

- Update to esm and update to return dc error object
- 857869e: Initial Release
9 changes: 4 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"name": "@dancastillo/error",
"version": "0.0.1",
"version": "0.0.2",
"description": "A small utility for generating consistent errors",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
Expand Down Expand Up @@ -31,15 +32,13 @@
"scripts": {
"test": "tsc --project tsconfig.test.json && borp --coverage --check-coverage --lines 100",
"format": "prettier --check .",
"format:fix": "prettier --write .",
"format:check": "prettier --write .",
"lint": "eslint ./src",
"prepare": "husky",
"build": "tsup",
"check-exports": "attw --pack .",
"prepublish": "pnpm build && pnpm check-exports && pnpm lint && pnpm format && pnpm test"
"prepublish": "npm run build && npm run lint && npm run format:check && npm run test"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.17.0",
"@changesets/cli": "^2.27.7",
"@eslint/js": "^9.9.1",
"@types/node": "^22.5.0",
Expand Down
89 changes: 20 additions & 69 deletions src/create-error.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,27 @@
import { format } from 'node:util'
import { transformCodeToErrorCode, transfromErrorCodeToErrorName, transfromErrorCodeToHttpStatusCode } from './helper'
import { ErrorCode } from './types'
import { DCError } from './types/dc-error.js'

/**
* Create error
*
* @param {ErrorCode | string} code - The error code of commonly thrown errors
* @param {string} message - The error message
* @param {string[]} details - Additional details
* @param {number} statusCode - HTTP Status Code
* @returns {Error} - The error object
* Create Custom Error
*/
export function createError(code: ErrorCode | string, message: string, details: string[] = [], statusCode?: number) {
export function createError<T = unknown>(
code: number | string,
message: string,
details: string[] = [],
meta?: T
): DCError<T> {
if (!code) throw new Error('Error code must not be empty')
if (!message?.trim()) throw new Error('Error message must not be empty')

const errCode = transformCodeToErrorCode(code.toUpperCase())
const errName = transfromErrorCodeToErrorName(errCode)
const errStatusCode = statusCode ?? transfromErrorCodeToHttpStatusCode(errCode)
const errMessage = message.trim()

/**
* Return error as string
*
* @param {string} formattedErrMessage - The formatted error message
* @returns {string}
*/
function toString(formattedErrMessage: string): string {
return `${errName} [${errCode}]: ${formattedErrMessage}`
}

/**
* Base Error Class
*/
class BaseError extends Error {
name: string
message: string
details: string[]
code: string
statusCode: number

/**
* Constructor
*
* @param {string} args.name - error name
* @param {string} args.message - error message
* @param {string[]} args.details - additional details
* @param {number} args.statusCode - optional parameter HTTP Status Code
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(...args: any[]) {
super(...args)
this.name = errName
this.message = format(errMessage, ...args)
this.details = details
this.code = errCode
this.statusCode = errStatusCode

// Ensuring stack trace is captured
if (Error.captureStackTrace) {
Error.captureStackTrace(this, BaseError)
}
}

/**
* Return error as string
* @returns {string}
*/
toString(): string {
return toString(this.message)
}
if (details && !Array.isArray(details)) throw new Error('Error details must be an array')

details.forEach((detail) => {
if (typeof detail !== 'string') throw new Error('Error details must contain string values')
if (!detail?.trim()) throw new Error('Error details must not contain empty string values')
})

return {
message: message,
details: details,
code: code,
meta: meta,
}

return BaseError
}
19 changes: 11 additions & 8 deletions src/helper/index.ts → src/helper/http.helper.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ErrorCode } from '../types/index'
import { ErrorCode } from '../types/http.types.js'

/**
* Map error code string to ErrorCode
*/
const CODE_STRING_TO_ERROR_CODE_MAP: Record<string, ErrorCode> = {
export const CODE_STRING_TO_ERROR_CODE_MAP: Record<string, ErrorCode> = {
INTERNAL_ERROR: ErrorCode.INTERNAL_ERROR,
INVALID_ARGUMENT: ErrorCode.INVALID_ARGUMENT,
INVALID_OPERATION: ErrorCode.INVALID_OPERATION,
Expand Down Expand Up @@ -33,7 +33,7 @@ export function transformCodeToErrorCode(code: string | ErrorCode): ErrorCode {
/**
* Map ErrorCode to error name
*/
const ERROR_CODE_TO_ERROR_NAME_MAP: Record<ErrorCode, string> = {
export const ERROR_CODE_TO_ERROR_NAME_MAP: Record<ErrorCode, string> = {
[ErrorCode.INTERNAL_ERROR]: 'InternalError',
[ErrorCode.INVALID_ARGUMENT]: 'InvalidArgumentError',
[ErrorCode.INVALID_OPERATION]: 'InvalidOperationError',
Expand All @@ -56,14 +56,14 @@ const ERROR_CODE_TO_ERROR_NAME_MAP: Record<ErrorCode, string> = {
* @param {ErrorCode} code - The error code of commonly thrown errors
* @returns {string} - The error name of commonly thrown errors
*/
export function transfromErrorCodeToErrorName(code: ErrorCode): string {
return ERROR_CODE_TO_ERROR_NAME_MAP[code] ?? ERROR_CODE_TO_ERROR_NAME_MAP[ErrorCode.UNKNOWN]
export function transfromErrorCodeToErrorName(code: ErrorCode | string): string {
return ERROR_CODE_TO_ERROR_NAME_MAP[code as keyof typeof ErrorCode] ?? ERROR_CODE_TO_ERROR_NAME_MAP[ErrorCode.UNKNOWN]
}

/**
* Map ErrorCode to HTTP status code
*/
const ERROR_CODE_TO_HTTP_STATUS_CODE_MAP: Record<ErrorCode, number> = {
export const ERROR_CODE_TO_HTTP_STATUS_CODE_MAP: Record<ErrorCode, number> = {
[ErrorCode.INTERNAL_ERROR]: 500,
[ErrorCode.INVALID_ARGUMENT]: 400,
[ErrorCode.INVALID_OPERATION]: 400,
Expand All @@ -86,6 +86,9 @@ const ERROR_CODE_TO_HTTP_STATUS_CODE_MAP: Record<ErrorCode, number> = {
* @param {ErrorCode} code - The error code of commonly thrown errors
* @returns {number} - The HTTP status code of commonly thrown errors
*/
export function transfromErrorCodeToHttpStatusCode(code: ErrorCode): number {
return ERROR_CODE_TO_HTTP_STATUS_CODE_MAP[code] ?? ERROR_CODE_TO_HTTP_STATUS_CODE_MAP[ErrorCode.UNKNOWN]
export function transfromErrorCodeToHttpStatusCode(code: ErrorCode | string): number {
return (
ERROR_CODE_TO_HTTP_STATUS_CODE_MAP[code as keyof typeof ErrorCode] ??
ERROR_CODE_TO_HTTP_STATUS_CODE_MAP[ErrorCode.UNKNOWN]
)
}
13 changes: 11 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
export { createError } from './create-error'
export { ErrorCode } from './types/index'
export { createError } from './create-error.js'
export { type DCError } from './types/dc-error.js'
export { ErrorCode } from './types/http.types.js'
export {
CODE_STRING_TO_ERROR_CODE_MAP,
transformCodeToErrorCode,
ERROR_CODE_TO_ERROR_NAME_MAP,
transfromErrorCodeToErrorName,
ERROR_CODE_TO_HTTP_STATUS_CODE_MAP,
transfromErrorCodeToHttpStatusCode,
} from './helper/http.helper.js'
21 changes: 21 additions & 0 deletions src/types/dc-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export type DCError<T> = {
/**
* The error message.
*/
message: string

/**
* Additional details about the error.
*/
details: string[]

/**
* The error code, typically uppercase and standardized.
*/
code: number | string

/**
* Additional metadata about the error.
*/
meta?: T
}
File renamed without changes.
107 changes: 75 additions & 32 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,49 @@
import test from 'node:test'
import assert from 'node:assert'
import { createError, ErrorCode } from '../src/index'
import { transfromErrorCodeToErrorName, transfromErrorCodeToHttpStatusCode } from '../src/helper'

test('Create error', (t) => {
const createdError = createError('INTERNAL_ERROR', 'Not available')
const err = new createdError()
assert.ok(err instanceof Error)
assert.strictEqual(err.name, 'InternalError')
assert.strictEqual(err.message, 'Not available')
assert.strictEqual(err.code, 'INTERNAL_ERROR')
assert.strictEqual(err.statusCode, 500)
assert.strictEqual(err.toString(), 'InternalError [INTERNAL_ERROR]: Not available')
})
import { ErrorCode } from '../src/index.js'
import {
CODE_STRING_TO_ERROR_CODE_MAP,
transformCodeToErrorCode,
transfromErrorCodeToErrorName,
transfromErrorCodeToHttpStatusCode,
} from '../src/helper/http.helper.js'
import { createError } from '../src/create-error.js'

// Loop over all Error codes
for (const code of Object.keys(ErrorCode)) {
const name = transfromErrorCodeToErrorName(code as ErrorCode)
const message = transfromErrorCodeToErrorName(code as ErrorCode)
const statusCode = transfromErrorCodeToHttpStatusCode(code as ErrorCode)

test(`Create error with ${code}, ${message}, and details`, (t) => {
test(`Create Http error with ${code}, ${message}, and details`, (t) => {
const createdError = createError(code, message, [name, message])
const err = new createdError()
assert.ok(err instanceof Error)
assert.strictEqual(err.name, name)
assert.strictEqual(err.message, message)
assert.deepStrictEqual(err.details, [name, message])
assert.strictEqual(err.code, code)
assert.strictEqual(err.statusCode, statusCode)
assert.strictEqual(err.toString(), `${name} [${code}]: ${message}`)
// assert.ok(createdError instanceof DCError)
assert.strictEqual(createdError.message, message)
assert.deepStrictEqual(createdError.details, [name, message])
assert.strictEqual(createdError.code, code)
})
}

test('Create error :: non error code returns as UnknownError', (t) => {
const createdError = createError('NO_ERROR_CODE', 'Not available %s')
const err = new createdError('testing 123')
assert.strictEqual(err.name, 'UnknownError')
assert.strictEqual(err.message, 'Not available testing 123')
assert.strictEqual(err.code, 'UNKNOWN')
assert.strictEqual(err.statusCode, 500)
assert.strictEqual(err.toString(), 'UnknownError [UNKNOWN]: Not available testing 123')
const createdError = createError(5000, 'Not available', [], { statusCode: 500 })
// const err = createdError('testing 123')
assert.strictEqual(createdError.message, 'Not available')
assert.strictEqual(createdError.code, 5000)
assert.deepStrictEqual(createdError.details, [])
assert.deepStrictEqual(createdError.meta, { statusCode: 500 })
})

test('Fail when details is not an array', (t) => {
// @ts-expect-error - details is not a string array
assert.throws(() => createError(4000, 'fail', true), new Error('Error details must be an array'))
})

test('When details is not a string array', (t) => {
// @ts-expect-error - details is not a string array
assert.throws(() => createError(4000, 'fail', [true, false]), new Error('Error details must contain string values'))
})

test('When empty string is passed as details', (t) => {
assert.throws(() => createError(4000, 'fail', ['']), new Error('Error details must not contain empty string values'))
})

test('Missing error required arguements', (t) => {
Expand All @@ -59,13 +62,53 @@ test('Missing error message', (t) => {
})

test('Return UnknownError when error code is not found', (t) => {
// @ts-expect-error - testing invalid error code
const errorName = transfromErrorCodeToErrorName('INVALID_ERROR_CODE_HERE_TEST')
assert.strictEqual('UnknownError', errorName)
})

test('Return 500 when error code is not found', (t) => {
// @ts-expect-error - testing invalid error code
const errorHttpCode = transfromErrorCodeToHttpStatusCode('INVALID_ERROR_CODE_HERE_TEST')
assert.strictEqual(500, errorHttpCode)
})

test('transformCodeToErrorCode: valid code string', () => {
const result = transformCodeToErrorCode('INTERNAL_ERROR')
assert.strictEqual(result, ErrorCode.INTERNAL_ERROR)
})

test('transformCodeToErrorCode: invalid code string defaults to UNKNOWN', () => {
const result = transformCodeToErrorCode('INVALID_CODE')
assert.strictEqual(result, ErrorCode.UNKNOWN)
})

test('transformCodeToErrorCode: valid ErrorCode', () => {
const result = transformCodeToErrorCode(ErrorCode.NOT_FOUND)
assert.strictEqual(result, ErrorCode.NOT_FOUND)
})

test('transfromErrorCodeToErrorName: valid ErrorCode', () => {
const result = transfromErrorCodeToErrorName(ErrorCode.NOT_FOUND)
assert.strictEqual(result, 'NotFoundError')
})

test('transfromErrorCodeToErrorName: invalid ErrorCode defaults to UnknownError', () => {
const result = transfromErrorCodeToErrorName('INVALID_CODE')
assert.strictEqual(result, 'UnknownError')
})

test('transfromErrorCodeToHttpStatusCode: valid ErrorCode', () => {
const result = transfromErrorCodeToHttpStatusCode(ErrorCode.NOT_FOUND)
assert.strictEqual(result, 404)
})

test('transfromErrorCodeToHttpStatusCode: invalid ErrorCode defaults to 500', () => {
const result = transfromErrorCodeToHttpStatusCode('INVALID_CODE')
assert.strictEqual(result, 500)
})

test('CODE_STRING_TO_ERROR_CODE_MAP: all keys map correctly', () => {
for (const [key, value] of Object.entries(CODE_STRING_TO_ERROR_CODE_MAP)) {
const transformed = transformCodeToErrorCode(key)
assert.strictEqual(transformed, value)
}
})

0 comments on commit 54b9b09

Please sign in to comment.