@teardown/errors 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +44 -0
- package/src/index.test.ts +130 -0
- package/src/index.ts +293 -0
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@teardown/errors",
|
|
3
|
+
"version": "0.1.29",
|
|
4
|
+
"private": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"files": [
|
|
10
|
+
"src/**/*"
|
|
11
|
+
],
|
|
12
|
+
"main": "./src/index.ts",
|
|
13
|
+
"module": "./src/index.ts",
|
|
14
|
+
"types": "./src/index.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./src/index.ts",
|
|
18
|
+
"import": "./src/index.ts",
|
|
19
|
+
"default": "./src/index.ts"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc --project ./tsconfig.lib.json",
|
|
25
|
+
"dev": "tsc --watch --project ./tsconfig.lib.json",
|
|
26
|
+
"typecheck": "tsc --noEmit --project ./tsconfig.lib.json",
|
|
27
|
+
"fmt": "bun x biome format --write ./src",
|
|
28
|
+
"lint": "bun x biome lint --write ./src",
|
|
29
|
+
"check": "bun x biome check ./src",
|
|
30
|
+
"prepublishOnly": "bun x turbo run build"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@biomejs/biome": "2.3.7",
|
|
34
|
+
"@teardown/tsconfig": "1.0.0",
|
|
35
|
+
"@types/bun": "1.3.3"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"typescript": "^5.9.3",
|
|
39
|
+
"zod": "^4"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"elysia": "1.4.16"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
BadRequestError,
|
|
4
|
+
ErrorCodes,
|
|
5
|
+
ForbiddenError,
|
|
6
|
+
InternalServerError,
|
|
7
|
+
NotFoundError,
|
|
8
|
+
TeardownError,
|
|
9
|
+
UnauthorizedError,
|
|
10
|
+
} from "./index";
|
|
11
|
+
|
|
12
|
+
describe("Errors", () => {
|
|
13
|
+
describe("TeardownError", () => {
|
|
14
|
+
it("should create error with code and message", () => {
|
|
15
|
+
const error = new TeardownError(ErrorCodes.BadRequest, "Test error");
|
|
16
|
+
|
|
17
|
+
expect(error.code).toBe(ErrorCodes.BadRequest);
|
|
18
|
+
expect(error.message).toBe("Test error");
|
|
19
|
+
expect(error.status).toBe(ErrorCodes.BadRequest);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should have status property matching code", () => {
|
|
23
|
+
const error = new TeardownError(ErrorCodes.InternalServerError);
|
|
24
|
+
|
|
25
|
+
expect(error.status).toBe(error.code);
|
|
26
|
+
expect(error.status).toBe(500);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("ForbiddenError", () => {
|
|
31
|
+
it("should create error with default message", () => {
|
|
32
|
+
const error = new ForbiddenError();
|
|
33
|
+
|
|
34
|
+
expect(error.code).toBe(ErrorCodes.Forbidden);
|
|
35
|
+
expect(error.message).toBe("Forbidden");
|
|
36
|
+
expect(error.status).toBe(403);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should create error with custom message", () => {
|
|
40
|
+
const error = new ForbiddenError("Custom forbidden message");
|
|
41
|
+
|
|
42
|
+
expect(error.code).toBe(ErrorCodes.Forbidden);
|
|
43
|
+
expect(error.message).toBe("Custom forbidden message");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("UnauthorizedError", () => {
|
|
48
|
+
it("should create error with default message", () => {
|
|
49
|
+
const error = new UnauthorizedError();
|
|
50
|
+
|
|
51
|
+
expect(error.code).toBe(ErrorCodes.Unauthorized);
|
|
52
|
+
expect(error.message).toBe("Authorization required");
|
|
53
|
+
expect(error.status).toBe(401);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should create error with custom message", () => {
|
|
57
|
+
const error = new UnauthorizedError("Invalid credentials");
|
|
58
|
+
|
|
59
|
+
expect(error.code).toBe(ErrorCodes.Unauthorized);
|
|
60
|
+
expect(error.message).toBe("Invalid credentials");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("BadRequestError", () => {
|
|
65
|
+
it("should create error with default message", () => {
|
|
66
|
+
const error = new BadRequestError();
|
|
67
|
+
|
|
68
|
+
expect(error.code).toBe(ErrorCodes.BadRequest);
|
|
69
|
+
expect(error.message).toBe("Bad request");
|
|
70
|
+
expect(error.status).toBe(400);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should create error with custom message", () => {
|
|
74
|
+
const error = new BadRequestError("Invalid input data");
|
|
75
|
+
|
|
76
|
+
expect(error.code).toBe(ErrorCodes.BadRequest);
|
|
77
|
+
expect(error.message).toBe("Invalid input data");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("NotFoundError", () => {
|
|
82
|
+
it("should create error with default message", () => {
|
|
83
|
+
const error = new NotFoundError();
|
|
84
|
+
|
|
85
|
+
expect(error.code).toBe(ErrorCodes.NotFound);
|
|
86
|
+
expect(error.message).toBe("Not found");
|
|
87
|
+
expect(error.status).toBe(404);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should create error with custom message", () => {
|
|
91
|
+
const error = new NotFoundError("Resource not found");
|
|
92
|
+
|
|
93
|
+
expect(error.code).toBe(ErrorCodes.NotFound);
|
|
94
|
+
expect(error.message).toBe("Resource not found");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("InternalServerError", () => {
|
|
99
|
+
it("should create error with default message", () => {
|
|
100
|
+
const error = new InternalServerError();
|
|
101
|
+
|
|
102
|
+
expect(error.code).toBe(ErrorCodes.InternalServerError);
|
|
103
|
+
expect(error.message).toBe("Internal server error");
|
|
104
|
+
expect(error.status).toBe(500);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should create error with custom message", () => {
|
|
108
|
+
const error = new InternalServerError("Database connection failed");
|
|
109
|
+
|
|
110
|
+
expect(error.code).toBe(ErrorCodes.InternalServerError);
|
|
111
|
+
expect(error.message).toBe("Database connection failed");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("ErrorCodes enum", () => {
|
|
116
|
+
it("should have correct status codes", () => {
|
|
117
|
+
expect(ErrorCodes.OK).toBe(200);
|
|
118
|
+
expect(ErrorCodes.Created).toBe(201);
|
|
119
|
+
expect(ErrorCodes.Accepted).toBe(202);
|
|
120
|
+
expect(ErrorCodes.BadRequest).toBe(400);
|
|
121
|
+
expect(ErrorCodes.Unauthorized).toBe(401);
|
|
122
|
+
expect(ErrorCodes.Forbidden).toBe(403);
|
|
123
|
+
expect(ErrorCodes.NotFound).toBe(404);
|
|
124
|
+
expect(ErrorCodes.UnprocessableContent).toBe(422);
|
|
125
|
+
expect(ErrorCodes.TooManyRequests).toBe(429);
|
|
126
|
+
expect(ErrorCodes.InternalServerError).toBe(500);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { Elysia, type ValidationError } from "elysia";
|
|
2
|
+
import { ZodError } from "zod";
|
|
3
|
+
|
|
4
|
+
export enum ErrorCodes {
|
|
5
|
+
Continue = 100,
|
|
6
|
+
SwitchingProtocols = 101,
|
|
7
|
+
Processing = 102,
|
|
8
|
+
EarlyHints = 103,
|
|
9
|
+
OK = 200,
|
|
10
|
+
Created = 201,
|
|
11
|
+
Accepted = 202,
|
|
12
|
+
NonAuthoritativeInformation = 203,
|
|
13
|
+
NoContent = 204,
|
|
14
|
+
ResetContent = 205,
|
|
15
|
+
PartialContent = 206,
|
|
16
|
+
MultiStatus = 207,
|
|
17
|
+
AlreadyReported = 208,
|
|
18
|
+
MultipleChoices = 300,
|
|
19
|
+
MovedPermanently = 301,
|
|
20
|
+
Found = 302,
|
|
21
|
+
SeeOther = 303,
|
|
22
|
+
NotModified = 304,
|
|
23
|
+
TemporaryRedirect = 307,
|
|
24
|
+
PermanentRedirect = 308,
|
|
25
|
+
BadRequest = 400,
|
|
26
|
+
Unauthorized = 401,
|
|
27
|
+
PaymentRequired = 402,
|
|
28
|
+
Forbidden = 403,
|
|
29
|
+
NotFound = 404,
|
|
30
|
+
MethodNotAllowed = 405,
|
|
31
|
+
NotAcceptable = 406,
|
|
32
|
+
ProxyAuthenticationRequired = 407,
|
|
33
|
+
RequestTimeout = 408,
|
|
34
|
+
Conflict = 409,
|
|
35
|
+
Gone = 410,
|
|
36
|
+
LengthRequired = 411,
|
|
37
|
+
PreconditionFailed = 412,
|
|
38
|
+
PayloadTooLarge = 413,
|
|
39
|
+
UriTooLong = 414,
|
|
40
|
+
UnsupportedMediaType = 415,
|
|
41
|
+
RangeNotSatisfiable = 416,
|
|
42
|
+
ExpectationFailed = 417,
|
|
43
|
+
ImATeapot = 418,
|
|
44
|
+
MisdirectedRequest = 421,
|
|
45
|
+
UnprocessableContent = 422,
|
|
46
|
+
Locked = 423,
|
|
47
|
+
FailedDependency = 424,
|
|
48
|
+
TooEarly = 425,
|
|
49
|
+
UpgradeRequired = 426,
|
|
50
|
+
PreconditionRequired = 428,
|
|
51
|
+
TooManyRequests = 429,
|
|
52
|
+
RequestHeaderFieldsTooLarge = 431,
|
|
53
|
+
UnavailableForLegalReasons = 451,
|
|
54
|
+
InternalServerError = 500,
|
|
55
|
+
NotImplemented = 501,
|
|
56
|
+
BadGateway = 502,
|
|
57
|
+
ServiceUnavailable = 503,
|
|
58
|
+
GatewayTimeout = 504,
|
|
59
|
+
HttpVersionNotSupported = 505,
|
|
60
|
+
VariantAlsoNegotiates = 506,
|
|
61
|
+
InsufficientStorage = 507,
|
|
62
|
+
LoopDetected = 508,
|
|
63
|
+
NotExtended = 510,
|
|
64
|
+
NetworkAuthenticationRequired = 511,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class TeardownError extends Error {
|
|
68
|
+
code: ErrorCodes;
|
|
69
|
+
|
|
70
|
+
constructor(code: ErrorCodes, message?: string) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.code = code;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get status() {
|
|
76
|
+
return this.code;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class ForbiddenError extends TeardownError {
|
|
81
|
+
constructor(message = "Forbidden") {
|
|
82
|
+
super(ErrorCodes.Forbidden, message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class UnauthorizedError extends TeardownError {
|
|
87
|
+
constructor(message = "Authorization required") {
|
|
88
|
+
super(ErrorCodes.Unauthorized, message);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export class BadRequestError extends TeardownError {
|
|
93
|
+
constructor(message = "Bad request") {
|
|
94
|
+
super(ErrorCodes.BadRequest, message);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class FailedRequestError extends TeardownError {
|
|
99
|
+
constructor(message = "Failed request") {
|
|
100
|
+
super(ErrorCodes.InternalServerError, message);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class NotFoundError extends TeardownError {
|
|
105
|
+
constructor(message = "Not found") {
|
|
106
|
+
super(ErrorCodes.NotFound, message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class UnprocessableContentError extends TeardownError {
|
|
111
|
+
constructor(message = "Unprocessable content") {
|
|
112
|
+
super(ErrorCodes.UnprocessableContent, message);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class InternalServerError extends TeardownError {
|
|
117
|
+
constructor(message = "Internal server error") {
|
|
118
|
+
super(ErrorCodes.InternalServerError, message);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class ConflictError extends TeardownError {
|
|
123
|
+
constructor(message = "Conflict") {
|
|
124
|
+
super(ErrorCodes.Conflict, message);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export class RateLimitError extends Error {
|
|
129
|
+
constructor(
|
|
130
|
+
public readonly resetTime: Date | null,
|
|
131
|
+
public readonly remaining: number | null,
|
|
132
|
+
public readonly limit: number | null,
|
|
133
|
+
message = "Rate limit exceeded"
|
|
134
|
+
) {
|
|
135
|
+
super(message);
|
|
136
|
+
this.name = "RateLimitError";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export class ConfigurationError extends TeardownError {
|
|
141
|
+
constructor(message = "Configuration not setup or is invalid") {
|
|
142
|
+
super(ErrorCodes.InternalServerError, message);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface ValidationErrorResponse {
|
|
147
|
+
error: string;
|
|
148
|
+
message: string;
|
|
149
|
+
path?: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface ErrorResponse {
|
|
153
|
+
success: false;
|
|
154
|
+
error: string;
|
|
155
|
+
message: string;
|
|
156
|
+
path?: string;
|
|
157
|
+
[key: string]: unknown;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface ErrorHandlerResult {
|
|
161
|
+
status: number;
|
|
162
|
+
body: ErrorResponse;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const handleValidationError = (error: ValidationError): ErrorHandlerResult => {
|
|
166
|
+
const message = error instanceof Error ? error.message : "Validation failed";
|
|
167
|
+
return {
|
|
168
|
+
status: 422,
|
|
169
|
+
body: {
|
|
170
|
+
success: false,
|
|
171
|
+
error: "VALIDATION",
|
|
172
|
+
message: message || "Validation failed",
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const handleValidCode = (parsedCode: number): ErrorHandlerResult => {
|
|
178
|
+
return {
|
|
179
|
+
status: parsedCode,
|
|
180
|
+
body: {
|
|
181
|
+
success: false,
|
|
182
|
+
error: "Unknown error",
|
|
183
|
+
message: "An unknown error occurred",
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const handleZodError = (error: ZodError): ErrorHandlerResult => {
|
|
189
|
+
const message = error instanceof Error ? error.message : "Validation failed";
|
|
190
|
+
return {
|
|
191
|
+
status: 400,
|
|
192
|
+
body: {
|
|
193
|
+
success: false,
|
|
194
|
+
error: "Validation failed",
|
|
195
|
+
message,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export function handleError(error: unknown, code: number | string = 500): ErrorHandlerResult {
|
|
201
|
+
// Handle Elysia ValidationError (duck-typed check)
|
|
202
|
+
if (code === "VALIDATION" || (error && typeof error === "object" && "type" in error && error.type === "validation")) {
|
|
203
|
+
return handleValidationError(error as ValidationError);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Handle ZodError (duck-typed check for issues array)
|
|
207
|
+
if (
|
|
208
|
+
error instanceof ZodError &&
|
|
209
|
+
error &&
|
|
210
|
+
typeof error === "object" &&
|
|
211
|
+
"issues" in error &&
|
|
212
|
+
Array.isArray(error.issues)
|
|
213
|
+
) {
|
|
214
|
+
return handleZodError(error as ZodError);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (error instanceof TeardownError) {
|
|
218
|
+
return {
|
|
219
|
+
status: error.status,
|
|
220
|
+
body: {
|
|
221
|
+
success: false,
|
|
222
|
+
error: error.constructor.name,
|
|
223
|
+
message: error.message,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Handle JSON-encoded validation errors
|
|
229
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
230
|
+
const errorWithMessage = error as { message: unknown };
|
|
231
|
+
if (typeof errorWithMessage.message === "string") {
|
|
232
|
+
try {
|
|
233
|
+
const validationError = JSON.parse(errorWithMessage.message);
|
|
234
|
+
if (validationError && Array.isArray(validationError) && validationError.length > 0) {
|
|
235
|
+
const firstError = validationError[0];
|
|
236
|
+
const path = firstError?.path?.join(".");
|
|
237
|
+
return {
|
|
238
|
+
status: 400,
|
|
239
|
+
body: {
|
|
240
|
+
success: false,
|
|
241
|
+
error: "Validation failed",
|
|
242
|
+
message: firstError?.message ?? "Invalid input",
|
|
243
|
+
path: path && path.length > 0 ? path : undefined,
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
// Not a JSON validation error, continue
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const parsedCode = Number(code);
|
|
254
|
+
|
|
255
|
+
if (!Number.isNaN(parsedCode)) {
|
|
256
|
+
return handleValidCode(parsedCode);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
status: 500,
|
|
261
|
+
body: {
|
|
262
|
+
success: false,
|
|
263
|
+
error: "Internal server error",
|
|
264
|
+
message: "An unexpected error occurred",
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export const ElysiaErrors = new Elysia()
|
|
270
|
+
.error({
|
|
271
|
+
FORBIDDEN: ForbiddenError,
|
|
272
|
+
NOT_FOUND: NotFoundError,
|
|
273
|
+
INTERNAL_SERVER_ERROR: InternalServerError,
|
|
274
|
+
BAD_REQUEST: BadRequestError,
|
|
275
|
+
UNAUTHORIZED: UnauthorizedError,
|
|
276
|
+
UNPROCESSABLE_ENTITY: UnprocessableContentError,
|
|
277
|
+
TOO_MANY_REQUESTS: RateLimitError,
|
|
278
|
+
SERVICE_UNAVAILABLE: InternalServerError,
|
|
279
|
+
})
|
|
280
|
+
.onError(({ error, code, set }) => {
|
|
281
|
+
console.error("[ErrorHandler] Unhandled error:", {
|
|
282
|
+
code,
|
|
283
|
+
error,
|
|
284
|
+
errorType: typeof error,
|
|
285
|
+
errorConstructor: error?.constructor?.name,
|
|
286
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
287
|
+
errorStack: error instanceof Error ? error.stack : undefined,
|
|
288
|
+
});
|
|
289
|
+
const result = handleError(error, code);
|
|
290
|
+
set.status = result.status;
|
|
291
|
+
return result.body;
|
|
292
|
+
})
|
|
293
|
+
.as("global");
|