280 lines
7.4 KiB
TypeScript
280 lines
7.4 KiB
TypeScript
/**
|
|
* Enhanced Error Utilities for GridPilot
|
|
*
|
|
* Provides comprehensive error handling, validation, and user-friendly error messages
|
|
* for both end users and developers.
|
|
*/
|
|
|
|
import { ApiError } from '@/lib/api/base/ApiError';
|
|
|
|
export interface ValidationError {
|
|
field: string;
|
|
message: string;
|
|
value?: unknown;
|
|
}
|
|
|
|
export interface EnhancedErrorContext {
|
|
timestamp?: string;
|
|
component?: string;
|
|
action?: string;
|
|
formData?: Record<string, unknown>;
|
|
userId?: string;
|
|
sessionId?: string;
|
|
}
|
|
|
|
/**
|
|
* Parse API error response to extract validation errors or user-friendly messages
|
|
*/
|
|
export function parseApiError(error: unknown): {
|
|
userMessage: string;
|
|
developerMessage: string;
|
|
validationErrors: ValidationError[];
|
|
isValidationError: boolean;
|
|
} {
|
|
const result = {
|
|
userMessage: 'An unexpected error occurred',
|
|
developerMessage: '',
|
|
validationErrors: [] as ValidationError[],
|
|
isValidationError: false,
|
|
};
|
|
|
|
if (error instanceof ApiError) {
|
|
result.developerMessage = error.getDeveloperMessage();
|
|
|
|
// Check if it's a validation error
|
|
if (error.type === 'VALIDATION_ERROR') {
|
|
result.isValidationError = true;
|
|
result.userMessage = 'Please check your input and try again';
|
|
|
|
// Try to parse validation details from response
|
|
try {
|
|
if (error.context.responseText) {
|
|
const parsed = JSON.parse(error.context.responseText);
|
|
|
|
// Handle NestJS validation error format
|
|
if (parsed.message && Array.isArray(parsed.message)) {
|
|
result.validationErrors = parsed.message.map((msg: unknown) => {
|
|
const m = msg as { property?: string; field?: string; constraints?: Record<string, string>; message?: string; value?: unknown };
|
|
return {
|
|
field: m.property || m.field || 'unknown',
|
|
message: m.constraints ? Object.values(m.constraints).join(', ') : m.message || 'Invalid value',
|
|
value: m.value,
|
|
};
|
|
});
|
|
}
|
|
// Handle custom error format
|
|
else if (parsed.errors && Array.isArray(parsed.errors)) {
|
|
result.validationErrors = parsed.errors.map((err: unknown) => {
|
|
const e = err as { field?: string; property?: string; message?: string; value?: unknown };
|
|
return {
|
|
field: e.field || e.property || 'unknown',
|
|
message: e.message || 'Invalid value',
|
|
value: e.value,
|
|
};
|
|
});
|
|
}
|
|
// Handle single message
|
|
else if (parsed.message && typeof parsed.message === 'string') {
|
|
result.userMessage = parsed.message;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// If parsing fails, use default messages
|
|
}
|
|
} else {
|
|
result.userMessage = error.getUserMessage();
|
|
}
|
|
} else if (error instanceof Error) {
|
|
result.userMessage = error.message;
|
|
result.developerMessage = error.message;
|
|
} else {
|
|
result.userMessage = 'An unknown error occurred';
|
|
result.developerMessage = String(error);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Format validation errors for display in forms
|
|
*/
|
|
export function formatValidationErrorsForForm(
|
|
validationErrors: ValidationError[]
|
|
): Record<string, string> {
|
|
const formErrors: Record<string, string> = {};
|
|
|
|
validationErrors.forEach((error) => {
|
|
// Map API field names to form field names
|
|
const fieldName = mapApiFieldToFormField(error.field);
|
|
formErrors[fieldName] = error.message;
|
|
});
|
|
|
|
return formErrors;
|
|
}
|
|
|
|
/**
|
|
* Map API field names to form field names
|
|
*/
|
|
function mapApiFieldToFormField(apiField: string): string {
|
|
const fieldMap: Record<string, string> = {
|
|
'rememberMe': 'rememberMe',
|
|
'email': 'email',
|
|
'password': 'password',
|
|
'displayName': 'displayName',
|
|
'firstName': 'firstName',
|
|
'lastName': 'lastName',
|
|
'confirmPassword': 'confirmPassword',
|
|
'role': 'role',
|
|
};
|
|
|
|
return fieldMap[apiField] || apiField;
|
|
}
|
|
|
|
/**
|
|
* Create enhanced error context for debugging
|
|
*/
|
|
export function createErrorContext(
|
|
error: unknown,
|
|
context: EnhancedErrorContext
|
|
): EnhancedErrorContext {
|
|
return {
|
|
timestamp: new Date().toISOString(),
|
|
...context,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if error is retryable
|
|
*/
|
|
export function isRetryable(error: unknown): boolean {
|
|
if (error instanceof ApiError) {
|
|
return error.isRetryable();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if error is a network connectivity issue
|
|
*/
|
|
export function isConnectivityError(error: unknown): boolean {
|
|
if (error instanceof ApiError) {
|
|
return error.isConnectivityIssue();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get error severity for logging and display
|
|
*/
|
|
export function getErrorSeverity(error: unknown): 'error' | 'warning' | 'info' {
|
|
if (error instanceof ApiError) {
|
|
const severity = error.getSeverity();
|
|
if (severity === 'error') return 'error';
|
|
if (severity === 'warn') return 'warning';
|
|
return 'info';
|
|
}
|
|
return 'error';
|
|
}
|
|
|
|
/**
|
|
* Create user-friendly error summary
|
|
*/
|
|
export function createUserErrorSummary(error: unknown): {
|
|
title: string;
|
|
description: string;
|
|
action: string;
|
|
} {
|
|
const parsed = parseApiError(error);
|
|
|
|
if (parsed.isValidationError) {
|
|
return {
|
|
title: 'Invalid Input',
|
|
description: parsed.userMessage,
|
|
action: 'Please review your input and try again',
|
|
};
|
|
}
|
|
|
|
if (isConnectivityError(error)) {
|
|
return {
|
|
title: 'Connection Issue',
|
|
description: 'Unable to connect to the server',
|
|
action: 'Check your internet connection and try again',
|
|
};
|
|
}
|
|
|
|
if (isRetryable(error)) {
|
|
return {
|
|
title: 'Temporary Issue',
|
|
description: parsed.userMessage,
|
|
action: 'Please try again in a moment',
|
|
};
|
|
}
|
|
|
|
return {
|
|
title: 'Error',
|
|
description: parsed.userMessage,
|
|
action: 'Please try again or contact support if the issue persists',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Log error with context (only in development)
|
|
*/
|
|
export function logErrorWithContext(
|
|
error: unknown,
|
|
context: EnhancedErrorContext
|
|
): void {
|
|
if (process.env.NODE_ENV !== 'development') return;
|
|
|
|
const parsed = parseApiError(error);
|
|
const severity = getErrorSeverity(error);
|
|
|
|
console.group(`🚨 [${severity.toUpperCase()}] ${context.component || 'Unknown'} - ${context.action || 'Unknown action'}`);
|
|
console.error('User Message:', parsed.userMessage);
|
|
console.error('Developer Message:', parsed.developerMessage);
|
|
if (parsed.validationErrors.length > 0) {
|
|
console.error('Validation Errors:', parsed.validationErrors);
|
|
}
|
|
if (context.formData) {
|
|
console.error('Form Data:', context.formData);
|
|
}
|
|
console.error('Context:', context);
|
|
console.error('Original Error:', error);
|
|
console.groupEnd();
|
|
}
|
|
|
|
/**
|
|
* Delay execution for retry logic
|
|
*/
|
|
export async function delay(ms: number): Promise<void> {
|
|
await new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
/**
|
|
* Retry operation with exponential backoff
|
|
*/
|
|
export async function retryWithBackoff<T>(
|
|
operation: () => Promise<T>,
|
|
maxRetries: number = 3,
|
|
baseDelay: number = 1000
|
|
): Promise<T> {
|
|
let lastError: unknown;
|
|
|
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
try {
|
|
return await operation();
|
|
} catch (error) {
|
|
lastError = error;
|
|
|
|
if (attempt === maxRetries || !isRetryable(error)) {
|
|
throw error;
|
|
}
|
|
|
|
// Exponential backoff: 1s, 2s, 4s
|
|
const delayMs = baseDelay * Math.pow(2, attempt);
|
|
await delay(delayMs);
|
|
}
|
|
}
|
|
|
|
throw lastError;
|
|
} |