diff --git a/packages/errors/package.json b/packages/errors/package.json new file mode 100644 index 00000000..999bbc98 --- /dev/null +++ b/packages/errors/package.json @@ -0,0 +1,18 @@ +{ + "name": "@bytelyst/errors", + "version": "0.1.0", + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": ["dist"], + "scripts": { + "build": "tsc", + "test": "vitest run" + } +} diff --git a/packages/errors/src/__tests__/errors.test.ts b/packages/errors/src/__tests__/errors.test.ts new file mode 100644 index 00000000..ee5ee235 --- /dev/null +++ b/packages/errors/src/__tests__/errors.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from "vitest"; +import { + ServiceError, + BadRequestError, + UnauthorizedError, + ForbiddenError, + NotFoundError, + ConflictError, + TooManyRequestsError, +} from "../index.js"; + +describe("ServiceError", () => { + it("sets statusCode and message", () => { + const err = new ServiceError(500, "boom"); + expect(err.statusCode).toBe(500); + expect(err.message).toBe("boom"); + expect(err).toBeInstanceOf(Error); + expect(err).toBeInstanceOf(ServiceError); + }); + + it("supports optional details", () => { + const err = new ServiceError(500, "boom", { field: "name" }); + expect(err.details).toEqual({ field: "name" }); + }); +}); + +describe("HTTP error classes", () => { + const cases: [string, new () => ServiceError, number, string][] = [ + ["BadRequestError", BadRequestError, 400, "Bad request"], + ["UnauthorizedError", UnauthorizedError, 401, "Unauthorized"], + ["ForbiddenError", ForbiddenError, 403, "Forbidden"], + ["NotFoundError", NotFoundError, 404, "Not found"], + ["ConflictError", ConflictError, 409, "Conflict"], + ["TooManyRequestsError", TooManyRequestsError, 429, "Too many requests"], + ]; + + for (const [name, Ctor, expectedStatus, expectedMessage] of cases) { + it(`${name} has status ${expectedStatus}`, () => { + const err = new Ctor(); + expect(err.statusCode).toBe(expectedStatus); + expect(err.message).toBe(expectedMessage); + expect(err).toBeInstanceOf(ServiceError); + }); + } + + it("accepts custom message", () => { + const err = new NotFoundError("User not found"); + expect(err.message).toBe("User not found"); + expect(err.statusCode).toBe(404); + }); + + it("accepts details", () => { + const err = new TooManyRequestsError("Rate limited", { retryAfter: 60 }); + expect(err.details).toEqual({ retryAfter: 60 }); + }); +}); diff --git a/packages/errors/src/http-errors.ts b/packages/errors/src/http-errors.ts new file mode 100644 index 00000000..7cae6b6a --- /dev/null +++ b/packages/errors/src/http-errors.ts @@ -0,0 +1,37 @@ +import { ServiceError } from "./service-error.js"; + +export class BadRequestError extends ServiceError { + constructor(message = "Bad request", details?: Record) { + super(400, message, details); + } +} + +export class UnauthorizedError extends ServiceError { + constructor(message = "Unauthorized", details?: Record) { + super(401, message, details); + } +} + +export class ForbiddenError extends ServiceError { + constructor(message = "Forbidden", details?: Record) { + super(403, message, details); + } +} + +export class NotFoundError extends ServiceError { + constructor(message = "Not found", details?: Record) { + super(404, message, details); + } +} + +export class ConflictError extends ServiceError { + constructor(message = "Conflict", details?: Record) { + super(409, message, details); + } +} + +export class TooManyRequestsError extends ServiceError { + constructor(message = "Too many requests", details?: Record) { + super(429, message, details); + } +} diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts new file mode 100644 index 00000000..bb31685e --- /dev/null +++ b/packages/errors/src/index.ts @@ -0,0 +1,9 @@ +export { ServiceError } from "./service-error.js"; +export { + BadRequestError, + UnauthorizedError, + ForbiddenError, + NotFoundError, + ConflictError, + TooManyRequestsError, +} from "./http-errors.js"; diff --git a/packages/errors/src/service-error.ts b/packages/errors/src/service-error.ts new file mode 100644 index 00000000..1d7b4bb7 --- /dev/null +++ b/packages/errors/src/service-error.ts @@ -0,0 +1,14 @@ +/** + * Base error class for typed HTTP service errors. + * All specific error types extend this class. + */ +export class ServiceError extends Error { + constructor( + public statusCode: number, + message: string, + public details?: Record, + ) { + super(message); + this.name = "ServiceError"; + } +} diff --git a/packages/errors/tsconfig.json b/packages/errors/tsconfig.json new file mode 100644 index 00000000..5edad813 --- /dev/null +++ b/packages/errors/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["src/**/*.test.ts"] +}