import { PostgrestError } from '@supabase/supabase-js';
import { logger } from './logger.js';
export enum ErrorCode {
// Database errors
TRANSACTION_FAILED = 'TRANSACTION_FAILED',
CONSTRAINT_VIOLATION = 'CONSTRAINT_VIOLATION',
FOREIGN_KEY_VIOLATION = 'FOREIGN_KEY_VIOLATION',
UNIQUE_VIOLATION = 'UNIQUE_VIOLATION',
// Validation errors
VALIDATION_FAILED = 'VALIDATION_FAILED',
INVALID_INPUT = 'INVALID_INPUT',
MISSING_REQUIRED_FIELD = 'MISSING_REQUIRED_FIELD',
// Business logic errors
PROJECT_NOT_FOUND = 'PROJECT_NOT_FOUND',
INSUFFICIENT_DATA = 'INSUFFICIENT_DATA',
CALCULATION_ERROR = 'CALCULATION_ERROR',
// System errors
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
TIMEOUT_ERROR = 'TIMEOUT_ERROR',
NETWORK_ERROR = 'NETWORK_ERROR'
}
export class ROIError extends Error {
constructor(
public code: ErrorCode,
message: string,
public details?: any,
public originalError?: Error
) {
super(message);
this.name = 'ROIError';
}
toJSON() {
return {
code: this.code,
message: this.message,
details: this.details,
timestamp: new Date().toISOString()
};
}
}
export class TransactionError extends ROIError {
constructor(
message: string,
public rollbackRequired: boolean = true,
details?: any,
originalError?: Error
) {
super(ErrorCode.TRANSACTION_FAILED, message, details, originalError);
this.name = 'TransactionError';
}
}
export class ValidationError extends ROIError {
constructor(
message: string,
public fields: Array<{ field: string; message: string }>,
details?: any
) {
super(ErrorCode.VALIDATION_FAILED, message, { fields, ...details });
this.name = 'ValidationError';
}
}
/**
* Converts Supabase/PostgreSQL errors to ROIError
*/
export function handleDatabaseError(error: PostgrestError | Error): ROIError {
if ('code' in error) {
const pgError = error as PostgrestError;
switch (pgError.code) {
case '23505': // unique_violation
return new ROIError(
ErrorCode.UNIQUE_VIOLATION,
'A record with this value already exists',
{ originalCode: pgError.code, hint: pgError.hint }
);
case '23503': // foreign_key_violation
return new ROIError(
ErrorCode.FOREIGN_KEY_VIOLATION,
'Referenced record does not exist',
{ originalCode: pgError.code, hint: pgError.hint }
);
case '23502': // not_null_violation
return new ROIError(
ErrorCode.MISSING_REQUIRED_FIELD,
'Required field is missing',
{ originalCode: pgError.code, hint: pgError.hint }
);
case '23514': // check_violation
return new ROIError(
ErrorCode.CONSTRAINT_VIOLATION,
'Data validation constraint failed',
{ originalCode: pgError.code, hint: pgError.hint }
);
default:
return new ROIError(
ErrorCode.UNKNOWN_ERROR,
pgError.message || 'Database operation failed',
{ originalCode: pgError.code, details: pgError.details }
);
}
}
// Handle generic errors
return new ROIError(
ErrorCode.UNKNOWN_ERROR,
error.message || 'An unexpected error occurred',
undefined,
error
);
}
/**
* Error recovery strategies
*/
export interface RecoveryStrategy {
canRecover: (error: ROIError) => boolean;
recover: (error: ROIError, context?: any) => Promise<any>;
}
export class ErrorRecovery {
private strategies: Map<ErrorCode, RecoveryStrategy> = new Map();
register(code: ErrorCode, strategy: RecoveryStrategy) {
this.strategies.set(code, strategy);
}
async tryRecover(error: ROIError, context?: any): Promise<any> {
const strategy = this.strategies.get(error.code);
if (strategy && strategy.canRecover(error)) {
try {
return await strategy.recover(error, context);
} catch (recoveryError) {
logger.error('Recovery failed', recoveryError as Error, { originalError: error });
throw error; // Re-throw original error if recovery fails
}
}
throw error; // No recovery strategy available
}
}
// Default error recovery instance
export const errorRecovery = new ErrorRecovery();
// Register some default recovery strategies
errorRecovery.register(ErrorCode.TIMEOUT_ERROR, {
canRecover: () => true,
recover: async (error, context) => {
// Retry with exponential backoff
const maxRetries = 3;
const baseDelay = 1000;
for (let i = 0; i < maxRetries; i++) {
try {
await new Promise(resolve => setTimeout(resolve, baseDelay * Math.pow(2, i)));
// Retry the operation (context should contain the retry function)
if (context?.retry) {
return await context.retry();
}
} catch (retryError) {
if (i === maxRetries - 1) throw retryError;
}
}
}
});
/**
* Wraps an async function with error handling and optional recovery
*/
export function withErrorHandling<T extends (...args: any[]) => Promise<any>>(
fn: T,
options?: {
errorTransform?: (error: Error) => ROIError;
enableRecovery?: boolean;
recoveryContext?: any;
}
): T {
return (async (...args: Parameters<T>) => {
try {
return await fn(...args);
} catch (error) {
const roiError = error instanceof ROIError
? error
: options?.errorTransform
? options.errorTransform(error as Error)
: handleDatabaseError(error as Error);
if (options?.enableRecovery) {
return await errorRecovery.tryRecover(roiError, options.recoveryContext);
}
throw roiError;
}
}) as T;
}
/**
* Transaction rollback helper
*/
export interface RollbackAction {
description: string;
execute: () => Promise<void>;
}
export class TransactionRollback {
private actions: RollbackAction[] = [];
add(action: RollbackAction) {
this.actions.push(action);
}
async execute() {
logger.info(`Executing rollback actions`, { count: this.actions.length });
// Execute rollback actions in reverse order
for (const action of this.actions.reverse()) {
try {
logger.info(`Executing rollback`, { action: action.description });
await action.execute();
} catch (error) {
logger.error(`Rollback failed`, error as Error, { action: action.description });
// Continue with other rollbacks even if one fails
}
}
}
}
/**
* Logs errors with context for debugging
*/
export function logError(error: Error | ROIError, context?: any) {
const timestamp = new Date().toISOString();
const errorInfo = {
timestamp,
name: error.name,
message: error.message,
code: error instanceof ROIError ? error.code : 'UNKNOWN',
stack: error.stack,
context
};
logger.error('ROI Server Error', errorInfo);
// In production, you might send this to a logging service
// await sendToLoggingService(errorInfo);
}