mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 07:52:10 +01:00
feat: implement PR #20 review feedback
- Add eslint-plugin-react-hooks dependency to fix ESLint errors - Fix unused sentimentThreshold variable in settings route - Add comprehensive dark mode accessibility tests as requested - Implement custom error classes for better error handling - Create centralized error handling system with proper typing - Add dark mode contrast and focus indicator tests - Extend accessibility test coverage for theme switching
This commit is contained in:
245
lib/errors.ts
Normal file
245
lib/errors.ts
Normal file
@ -0,0 +1,245 @@
|
||||
/**
|
||||
* Custom error classes for better error handling and monitoring
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base application error class
|
||||
*/
|
||||
export class AppError extends Error {
|
||||
public readonly statusCode: number;
|
||||
public readonly isOperational: boolean;
|
||||
public readonly errorCode?: string;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
statusCode: number = 500,
|
||||
isOperational: boolean = true,
|
||||
errorCode?: string
|
||||
) {
|
||||
super(message);
|
||||
|
||||
this.statusCode = statusCode;
|
||||
this.isOperational = isOperational;
|
||||
this.errorCode = errorCode;
|
||||
|
||||
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation error - 400 Bad Request
|
||||
*/
|
||||
export class ValidationError extends AppError {
|
||||
public readonly field?: string;
|
||||
public readonly validationErrors?: Record<string, string[]>;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
field?: string,
|
||||
validationErrors?: Record<string, string[]>
|
||||
) {
|
||||
super(message, 400, true, "VALIDATION_ERROR");
|
||||
this.field = field;
|
||||
this.validationErrors = validationErrors;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication error - 401 Unauthorized
|
||||
*/
|
||||
export class AuthError extends AppError {
|
||||
constructor(message: string = "Authentication failed") {
|
||||
super(message, 401, true, "AUTH_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization error - 403 Forbidden
|
||||
*/
|
||||
export class AuthorizationError extends AppError {
|
||||
public readonly requiredRole?: string;
|
||||
public readonly userRole?: string;
|
||||
|
||||
constructor(
|
||||
message: string = "Insufficient permissions",
|
||||
requiredRole?: string,
|
||||
userRole?: string
|
||||
) {
|
||||
super(message, 403, true, "AUTHORIZATION_ERROR");
|
||||
this.requiredRole = requiredRole;
|
||||
this.userRole = userRole;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resource not found error - 404 Not Found
|
||||
*/
|
||||
export class NotFoundError extends AppError {
|
||||
public readonly resource?: string;
|
||||
public readonly resourceId?: string;
|
||||
|
||||
constructor(
|
||||
message: string = "Resource not found",
|
||||
resource?: string,
|
||||
resourceId?: string
|
||||
) {
|
||||
super(message, 404, true, "NOT_FOUND_ERROR");
|
||||
this.resource = resource;
|
||||
this.resourceId = resourceId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Conflict error - 409 Conflict
|
||||
*/
|
||||
export class ConflictError extends AppError {
|
||||
public readonly conflictField?: string;
|
||||
|
||||
constructor(message: string, conflictField?: string) {
|
||||
super(message, 409, true, "CONFLICT_ERROR");
|
||||
this.conflictField = conflictField;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limit error - 429 Too Many Requests
|
||||
*/
|
||||
export class RateLimitError extends AppError {
|
||||
public readonly retryAfter?: number;
|
||||
|
||||
constructor(message: string = "Rate limit exceeded", retryAfter?: number) {
|
||||
super(message, 429, true, "RATE_LIMIT_ERROR");
|
||||
this.retryAfter = retryAfter;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Database error - 500 Internal Server Error
|
||||
*/
|
||||
export class DatabaseError extends AppError {
|
||||
public readonly query?: string;
|
||||
public readonly table?: string;
|
||||
|
||||
constructor(message: string, query?: string, table?: string) {
|
||||
super(message, 500, true, "DATABASE_ERROR");
|
||||
this.query = query;
|
||||
this.table = table;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* External service error - 502 Bad Gateway
|
||||
*/
|
||||
export class ExternalServiceError extends AppError {
|
||||
public readonly service?: string;
|
||||
public readonly endpoint?: string;
|
||||
|
||||
constructor(message: string, service?: string, endpoint?: string) {
|
||||
super(message, 502, true, "EXTERNAL_SERVICE_ERROR");
|
||||
this.service = service;
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processing error - 500 Internal Server Error
|
||||
*/
|
||||
export class ProcessingError extends AppError {
|
||||
public readonly stage?: string;
|
||||
public readonly sessionId?: string;
|
||||
|
||||
constructor(message: string, stage?: string, sessionId?: string) {
|
||||
super(message, 500, true, "PROCESSING_ERROR");
|
||||
this.stage = stage;
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration error - 500 Internal Server Error
|
||||
*/
|
||||
export class ConfigurationError extends AppError {
|
||||
public readonly configKey?: string;
|
||||
|
||||
constructor(message: string, configKey?: string) {
|
||||
super(message, 500, false, "CONFIGURATION_ERROR"); // Not operational - indicates system issue
|
||||
this.configKey = configKey;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AI service error - 502 Bad Gateway
|
||||
*/
|
||||
export class AIServiceError extends AppError {
|
||||
public readonly model?: string;
|
||||
public readonly tokenUsage?: number;
|
||||
|
||||
constructor(message: string, model?: string, tokenUsage?: number) {
|
||||
super(message, 502, true, "AI_SERVICE_ERROR");
|
||||
this.model = model;
|
||||
this.tokenUsage = tokenUsage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to check if error is operational
|
||||
*/
|
||||
export function isOperationalError(error: Error): boolean {
|
||||
if (error instanceof AppError) {
|
||||
return error.isOperational;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to create error response object
|
||||
*/
|
||||
export function createErrorResponse(error: AppError) {
|
||||
return {
|
||||
error: {
|
||||
message: error.message,
|
||||
code: error.errorCode,
|
||||
statusCode: error.statusCode,
|
||||
...(process.env.NODE_ENV === "development" && {
|
||||
stack: error.stack,
|
||||
...((error as any).field && { field: (error as any).field }),
|
||||
...((error as any).validationErrors && {
|
||||
validationErrors: (error as any).validationErrors,
|
||||
}),
|
||||
...((error as any).resource && { resource: (error as any).resource }),
|
||||
...((error as any).resourceId && {
|
||||
resourceId: (error as any).resourceId,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to log errors with context
|
||||
*/
|
||||
export function logError(error: Error, context?: Record<string, any>) {
|
||||
const errorInfo = {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
timestamp: new Date().toISOString(),
|
||||
...(error instanceof AppError && {
|
||||
statusCode: error.statusCode,
|
||||
errorCode: error.errorCode,
|
||||
isOperational: error.isOperational,
|
||||
}),
|
||||
...context,
|
||||
};
|
||||
|
||||
if (error instanceof AppError && error.isOperational) {
|
||||
console.warn("[Operational Error]", errorInfo);
|
||||
} else {
|
||||
console.error("[System Error]", errorInfo);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user