@nebutra/errors 0.1.0 → 0.1.2

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/src/index.ts DELETED
@@ -1,392 +0,0 @@
1
- /**
2
- * Errors - Unified error handling for Nebutra services
3
- *
4
- * Provides:
5
- * - Typed error classes
6
- * - Consistent API error responses
7
- * - Error serialization for logging
8
- */
9
-
10
- // ============================================
11
- // Error Codes
12
- // ============================================
13
-
14
- export const ERROR_CODES = {
15
- // Client errors (4xx)
16
- BAD_REQUEST: "BAD_REQUEST",
17
- UNAUTHORIZED: "UNAUTHORIZED",
18
- FORBIDDEN: "FORBIDDEN",
19
- NOT_FOUND: "NOT_FOUND",
20
- CONFLICT: "CONFLICT",
21
- VALIDATION_ERROR: "VALIDATION_ERROR",
22
- RATE_LIMITED: "RATE_LIMITED",
23
- QUOTA_EXCEEDED: "QUOTA_EXCEEDED",
24
-
25
- // Server errors (5xx)
26
- INTERNAL_ERROR: "INTERNAL_ERROR",
27
- SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
28
- EXTERNAL_SERVICE_ERROR: "EXTERNAL_SERVICE_ERROR",
29
- DATABASE_ERROR: "DATABASE_ERROR",
30
- TIMEOUT: "TIMEOUT",
31
-
32
- // Business errors
33
- PAYMENT_REQUIRED: "PAYMENT_REQUIRED",
34
- SUBSCRIPTION_EXPIRED: "SUBSCRIPTION_EXPIRED",
35
- FEATURE_DISABLED: "FEATURE_DISABLED",
36
- TENANT_SUSPENDED: "TENANT_SUSPENDED",
37
- } as const;
38
-
39
- export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
40
-
41
- // ============================================
42
- // Base Error Class
43
- // ============================================
44
-
45
- export interface AppErrorOptions {
46
- code: ErrorCode;
47
- message: string;
48
- statusCode?: number;
49
- cause?: Error;
50
- metadata?: Record<string, unknown>;
51
- suggestion?: string;
52
- isOperational?: boolean;
53
- }
54
-
55
- export class AppError extends Error {
56
- public readonly code: ErrorCode;
57
- public readonly statusCode: number;
58
- public readonly isOperational: boolean;
59
- public readonly metadata?: Record<string, unknown>;
60
- public readonly suggestion?: string;
61
- public readonly timestamp: string;
62
-
63
- constructor(options: AppErrorOptions) {
64
- super(options.message);
65
- this.name = "AppError";
66
- this.code = options.code;
67
- this.statusCode = options.statusCode || getDefaultStatusCode(options.code);
68
- this.isOperational = options.isOperational ?? true;
69
- if (options.metadata !== undefined) {
70
- this.metadata = options.metadata;
71
- }
72
- if (options.suggestion !== undefined) {
73
- this.suggestion = options.suggestion;
74
- }
75
- this.timestamp = new Date().toISOString();
76
-
77
- if (options.cause) {
78
- this.cause = options.cause;
79
- }
80
-
81
- Error.captureStackTrace(this, this.constructor);
82
- }
83
-
84
- toJSON(): Record<string, unknown> {
85
- return {
86
- name: this.name,
87
- code: this.code,
88
- message: this.message,
89
- statusCode: this.statusCode,
90
- timestamp: this.timestamp,
91
- suggestion: this.suggestion,
92
- metadata: this.metadata,
93
- };
94
- }
95
- }
96
-
97
- export interface CapabilityErrorOptions {
98
- statusCode?: number;
99
- cause?: Error;
100
- metadata?: Record<string, unknown>;
101
- suggestion: string;
102
- code?: ErrorCode;
103
- }
104
-
105
- export class CapabilityError extends AppError {
106
- public readonly capability: string;
107
-
108
- constructor(capability: string, message: string, options: CapabilityErrorOptions) {
109
- super({
110
- code: options.code ?? ERROR_CODES.EXTERNAL_SERVICE_ERROR,
111
- message,
112
- statusCode: options.statusCode ?? 502,
113
- ...(options.cause !== undefined ? { cause: options.cause } : {}),
114
- suggestion: options.suggestion,
115
- metadata: {
116
- capability,
117
- ...(options.metadata ?? {}),
118
- },
119
- });
120
- this.name = "CapabilityError";
121
- this.capability = capability;
122
- }
123
- }
124
-
125
- // ============================================
126
- // Specific Error Classes
127
- // ============================================
128
-
129
- export class ValidationError extends AppError {
130
- public readonly fields?: Record<string, string[]>;
131
-
132
- constructor(message: string, fields?: Record<string, string[]>) {
133
- super({
134
- code: ERROR_CODES.VALIDATION_ERROR,
135
- message,
136
- statusCode: 400,
137
- ...(fields !== undefined && { metadata: { fields } }),
138
- });
139
- this.name = "ValidationError";
140
- if (fields !== undefined) {
141
- this.fields = fields;
142
- }
143
- }
144
- }
145
-
146
- export class UnauthorizedError extends AppError {
147
- constructor(message = "Unauthorized") {
148
- super({ code: ERROR_CODES.UNAUTHORIZED, message, statusCode: 401 });
149
- this.name = "UnauthorizedError";
150
- }
151
- }
152
-
153
- export class ForbiddenError extends AppError {
154
- constructor(message = "Forbidden") {
155
- super({ code: ERROR_CODES.FORBIDDEN, message, statusCode: 403 });
156
- this.name = "ForbiddenError";
157
- }
158
- }
159
-
160
- export class NotFoundError extends AppError {
161
- constructor(resource = "Resource", id?: string) {
162
- const message = id ? `${resource} with id '${id}' not found` : `${resource} not found`;
163
- super({ code: ERROR_CODES.NOT_FOUND, message, statusCode: 404 });
164
- this.name = "NotFoundError";
165
- }
166
- }
167
-
168
- export class ConflictError extends AppError {
169
- constructor(message = "Resource already exists") {
170
- super({ code: ERROR_CODES.CONFLICT, message, statusCode: 409 });
171
- this.name = "ConflictError";
172
- }
173
- }
174
-
175
- export class RateLimitError extends AppError {
176
- public readonly retryAfter?: number;
177
-
178
- constructor(retryAfter?: number) {
179
- super({
180
- code: ERROR_CODES.RATE_LIMITED,
181
- message: "Too many requests",
182
- statusCode: 429,
183
- ...(retryAfter !== undefined && { metadata: { retryAfter } }),
184
- });
185
- this.name = "RateLimitError";
186
- if (retryAfter !== undefined) {
187
- this.retryAfter = retryAfter;
188
- }
189
- }
190
- }
191
-
192
- export class QuotaExceededError extends AppError {
193
- constructor(quota: string, limit: number, current: number) {
194
- super({
195
- code: ERROR_CODES.QUOTA_EXCEEDED,
196
- message: `${quota} quota exceeded (${current}/${limit})`,
197
- statusCode: 429,
198
- metadata: { quota, limit, current },
199
- });
200
- this.name = "QuotaExceededError";
201
- }
202
- }
203
-
204
- export class ExternalServiceError extends AppError {
205
- constructor(service: string, cause?: Error) {
206
- super({
207
- code: ERROR_CODES.EXTERNAL_SERVICE_ERROR,
208
- message: `External service '${service}' failed`,
209
- statusCode: 502,
210
- ...(cause !== undefined && { cause }),
211
- metadata: { service },
212
- });
213
- this.name = "ExternalServiceError";
214
- }
215
- }
216
-
217
- export class DatabaseError extends AppError {
218
- constructor(operation: string, cause?: Error) {
219
- super({
220
- code: ERROR_CODES.DATABASE_ERROR,
221
- message: `Database operation '${operation}' failed`,
222
- statusCode: 500,
223
- ...(cause !== undefined && { cause }),
224
- isOperational: false,
225
- metadata: { operation },
226
- });
227
- this.name = "DatabaseError";
228
- }
229
- }
230
-
231
- // ============================================
232
- // API Response Helpers
233
- // ============================================
234
-
235
- export interface ApiErrorResponse {
236
- error: {
237
- code: ErrorCode;
238
- message: string;
239
- details?: Record<string, unknown>;
240
- };
241
- requestId?: string;
242
- }
243
-
244
- export function toApiError(error: unknown, requestId?: string): ApiErrorResponse {
245
- if (error instanceof AppError) {
246
- return {
247
- error: {
248
- code: error.code,
249
- message: error.message,
250
- ...((error.metadata !== undefined || error.suggestion !== undefined) && {
251
- details: {
252
- ...(error.metadata ?? {}),
253
- ...(error.suggestion !== undefined && { suggestion: error.suggestion }),
254
- },
255
- }),
256
- },
257
- ...(requestId !== undefined && { requestId }),
258
- };
259
- }
260
-
261
- // Unknown error - don't leak details
262
- return {
263
- error: {
264
- code: ERROR_CODES.INTERNAL_ERROR,
265
- message: "An unexpected error occurred",
266
- },
267
- ...(requestId !== undefined && { requestId }),
268
- };
269
- }
270
-
271
- export function getStatusCode(error: unknown): number {
272
- if (error instanceof AppError) {
273
- return error.statusCode;
274
- }
275
- return 500;
276
- }
277
-
278
- // ============================================
279
- // Error Middleware for Hono
280
- // ============================================
281
-
282
- interface HonoContext {
283
- req: { header: (name: string) => string | undefined };
284
- json: (data: unknown, status?: number) => unknown;
285
- }
286
-
287
- export interface ErrorHandlerOptions {
288
- /**
289
- * Called for every caught error so callers can route it to their structured
290
- * logger (e.g. @nebutra/logger). Defaults to a no-op — DO NOT rely on the
291
- * previous process.stderr.write behaviour; pass an onError callback instead.
292
- */
293
- onError?: (error: unknown, meta: { requestId?: string; statusCode: number }) => void;
294
- }
295
-
296
- export function errorHandler(options: ErrorHandlerOptions = {}) {
297
- return async (c: HonoContext, next: () => Promise<void>) => {
298
- try {
299
- await next();
300
- } catch (error) {
301
- const requestId = c.req.header("x-request-id");
302
- const statusCode = getStatusCode(error);
303
- const response = toApiError(error, requestId);
304
-
305
- options.onError?.(
306
- error,
307
- requestId !== undefined ? { requestId, statusCode } : { statusCode },
308
- );
309
-
310
- return c.json(response, statusCode);
311
- }
312
- };
313
- }
314
-
315
- // ============================================
316
- // Utility Functions
317
- // ============================================
318
-
319
- function getDefaultStatusCode(code: ErrorCode): number {
320
- switch (code) {
321
- case ERROR_CODES.BAD_REQUEST:
322
- case ERROR_CODES.VALIDATION_ERROR:
323
- return 400;
324
- case ERROR_CODES.UNAUTHORIZED:
325
- return 401;
326
- case ERROR_CODES.PAYMENT_REQUIRED:
327
- case ERROR_CODES.SUBSCRIPTION_EXPIRED:
328
- return 402;
329
- case ERROR_CODES.FORBIDDEN:
330
- case ERROR_CODES.FEATURE_DISABLED:
331
- case ERROR_CODES.TENANT_SUSPENDED:
332
- return 403;
333
- case ERROR_CODES.NOT_FOUND:
334
- return 404;
335
- case ERROR_CODES.CONFLICT:
336
- return 409;
337
- case ERROR_CODES.RATE_LIMITED:
338
- case ERROR_CODES.QUOTA_EXCEEDED:
339
- return 429;
340
- case ERROR_CODES.INTERNAL_ERROR:
341
- case ERROR_CODES.DATABASE_ERROR:
342
- return 500;
343
- case ERROR_CODES.EXTERNAL_SERVICE_ERROR:
344
- return 502;
345
- case ERROR_CODES.SERVICE_UNAVAILABLE:
346
- return 503;
347
- case ERROR_CODES.TIMEOUT:
348
- return 504;
349
- default:
350
- return 500;
351
- }
352
- }
353
-
354
- /**
355
- * Wrap async function with error handling
356
- */
357
- export function tryCatch<T>(
358
- fn: () => Promise<T>,
359
- errorHandler?: (error: unknown) => T | Promise<T>,
360
- ): Promise<T> {
361
- return fn().catch((error) => {
362
- if (errorHandler) {
363
- return errorHandler(error);
364
- }
365
- throw error;
366
- });
367
- }
368
-
369
- /**
370
- * Assert condition or throw error
371
- */
372
- export function assert(condition: unknown, message: string): asserts condition {
373
- if (!condition) {
374
- throw new AppError({
375
- code: ERROR_CODES.BAD_REQUEST,
376
- message,
377
- });
378
- }
379
- }
380
-
381
- /**
382
- * Assert not null/undefined or throw NotFoundError
383
- */
384
- export function assertFound<T>(
385
- value: T | null | undefined,
386
- resource: string,
387
- id?: string,
388
- ): asserts value is T {
389
- if (value === null || value === undefined) {
390
- throw new NotFoundError(resource, id);
391
- }
392
- }
package/tsconfig.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "extends": "../../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src",
6
- "composite": false
7
- },
8
- "include": ["src/**/*"],
9
- "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
10
- }