feat(errors): add @bytelyst/errors package

- ServiceError base class with statusCode, message, details
- HTTP errors: BadRequest, Unauthorized, Forbidden, NotFound, Conflict, TooManyRequests
- 10 tests passing (vitest)
- Superset of all 4 service error files in LysnrAI
This commit is contained in:
saravanakumardb1 2026-02-12 11:19:35 -08:00
parent d875df09a3
commit 9c0ab36171
6 changed files with 143 additions and 0 deletions

View File

@ -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"
}
}

View File

@ -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 });
});
});

View File

@ -0,0 +1,37 @@
import { ServiceError } from "./service-error.js";
export class BadRequestError extends ServiceError {
constructor(message = "Bad request", details?: Record<string, unknown>) {
super(400, message, details);
}
}
export class UnauthorizedError extends ServiceError {
constructor(message = "Unauthorized", details?: Record<string, unknown>) {
super(401, message, details);
}
}
export class ForbiddenError extends ServiceError {
constructor(message = "Forbidden", details?: Record<string, unknown>) {
super(403, message, details);
}
}
export class NotFoundError extends ServiceError {
constructor(message = "Not found", details?: Record<string, unknown>) {
super(404, message, details);
}
}
export class ConflictError extends ServiceError {
constructor(message = "Conflict", details?: Record<string, unknown>) {
super(409, message, details);
}
}
export class TooManyRequestsError extends ServiceError {
constructor(message = "Too many requests", details?: Record<string, unknown>) {
super(429, message, details);
}
}

View File

@ -0,0 +1,9 @@
export { ServiceError } from "./service-error.js";
export {
BadRequestError,
UnauthorizedError,
ForbiddenError,
NotFoundError,
ConflictError,
TooManyRequestsError,
} from "./http-errors.js";

View File

@ -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<string, unknown>,
) {
super(message);
this.name = "ServiceError";
}
}

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["src/**/*.test.ts"]
}