mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 09:41:09 +02:00
style: auto-format codebase with Prettier
Applied formatting to 1487+ files using pnpm format:write - TypeScript/JavaScript files - Svelte components - Astro pages - JSON configs - Markdown docs 13 files still need manual review (Astro JSX comments)
This commit is contained in:
parent
0241f5554c
commit
d36b321d9d
3952 changed files with 661498 additions and 739751 deletions
|
|
@ -1,26 +1,22 @@
|
|||
import {
|
||||
ErrorCode,
|
||||
ERROR_CODE_TO_HTTP_STATUS,
|
||||
ERROR_CODE_RETRYABLE,
|
||||
} from '../types/error-codes';
|
||||
import { ErrorCode, ERROR_CODE_TO_HTTP_STATUS, ERROR_CODE_RETRYABLE } from '../types/error-codes';
|
||||
|
||||
/**
|
||||
* Additional context that can be attached to errors.
|
||||
*/
|
||||
export interface ErrorContext {
|
||||
[key: string]: unknown;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating an AppError.
|
||||
*/
|
||||
export interface AppErrorOptions {
|
||||
code: ErrorCode;
|
||||
message: string;
|
||||
cause?: Error | AppError;
|
||||
context?: ErrorContext;
|
||||
httpStatus?: number;
|
||||
retryable?: boolean;
|
||||
code: ErrorCode;
|
||||
message: string;
|
||||
cause?: Error | AppError;
|
||||
context?: ErrorContext;
|
||||
httpStatus?: number;
|
||||
retryable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -50,130 +46,126 @@ export interface AppErrorOptions {
|
|||
* ```
|
||||
*/
|
||||
export class AppError extends Error {
|
||||
/** Standardized error code */
|
||||
readonly code: ErrorCode;
|
||||
/** Standardized error code */
|
||||
readonly code: ErrorCode;
|
||||
|
||||
/** HTTP status code for API responses */
|
||||
readonly httpStatus: number;
|
||||
/** HTTP status code for API responses */
|
||||
readonly httpStatus: number;
|
||||
|
||||
/** Whether the operation can be retried */
|
||||
readonly retryable: boolean;
|
||||
/** Whether the operation can be retried */
|
||||
readonly retryable: boolean;
|
||||
|
||||
/** Original error that caused this error (for wrapping) */
|
||||
readonly cause?: Error | AppError;
|
||||
/** Original error that caused this error (for wrapping) */
|
||||
readonly cause?: Error | AppError;
|
||||
|
||||
/** Additional context information */
|
||||
readonly context: ErrorContext;
|
||||
/** Additional context information */
|
||||
readonly context: ErrorContext;
|
||||
|
||||
/** Timestamp when error was created */
|
||||
readonly timestamp: string;
|
||||
/** Timestamp when error was created */
|
||||
readonly timestamp: string;
|
||||
|
||||
constructor(options: AppErrorOptions) {
|
||||
super(options.message);
|
||||
this.name = 'AppError';
|
||||
this.code = options.code;
|
||||
this.cause = options.cause;
|
||||
this.context = options.context ?? {};
|
||||
this.timestamp = new Date().toISOString();
|
||||
constructor(options: AppErrorOptions) {
|
||||
super(options.message);
|
||||
this.name = 'AppError';
|
||||
this.code = options.code;
|
||||
this.cause = options.cause;
|
||||
this.context = options.context ?? {};
|
||||
this.timestamp = new Date().toISOString();
|
||||
|
||||
// Use provided values or defaults from mappings
|
||||
this.httpStatus =
|
||||
options.httpStatus ?? ERROR_CODE_TO_HTTP_STATUS[options.code];
|
||||
this.retryable = options.retryable ?? ERROR_CODE_RETRYABLE[options.code];
|
||||
// Use provided values or defaults from mappings
|
||||
this.httpStatus = options.httpStatus ?? ERROR_CODE_TO_HTTP_STATUS[options.code];
|
||||
this.retryable = options.retryable ?? ERROR_CODE_RETRYABLE[options.code];
|
||||
|
||||
// Capture stack trace
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
// Capture stack trace
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapped error with additional context.
|
||||
* Similar to Go's `fmt.Errorf("context: %w", err)`.
|
||||
*
|
||||
* @param contextMessage - Description of the operation that failed
|
||||
* @param additionalContext - Extra context data to include
|
||||
* @returns A new AppError with the original as its cause
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const wrapped = originalError.wrap('fetching user data');
|
||||
* // Message: "fetching user data: original message"
|
||||
* ```
|
||||
*/
|
||||
wrap(contextMessage: string, additionalContext?: ErrorContext): AppError {
|
||||
return new AppError({
|
||||
code: this.code,
|
||||
message: `${contextMessage}: ${this.message}`,
|
||||
cause: this,
|
||||
context: { ...this.context, ...additionalContext },
|
||||
httpStatus: this.httpStatus,
|
||||
retryable: this.retryable,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Create a wrapped error with additional context.
|
||||
* Similar to Go's `fmt.Errorf("context: %w", err)`.
|
||||
*
|
||||
* @param contextMessage - Description of the operation that failed
|
||||
* @param additionalContext - Extra context data to include
|
||||
* @returns A new AppError with the original as its cause
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const wrapped = originalError.wrap('fetching user data');
|
||||
* // Message: "fetching user data: original message"
|
||||
* ```
|
||||
*/
|
||||
wrap(contextMessage: string, additionalContext?: ErrorContext): AppError {
|
||||
return new AppError({
|
||||
code: this.code,
|
||||
message: `${contextMessage}: ${this.message}`,
|
||||
cause: this,
|
||||
context: { ...this.context, ...additionalContext },
|
||||
httpStatus: this.httpStatus,
|
||||
retryable: this.retryable,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the root cause of the error chain.
|
||||
* Traverses the cause chain to find the original error.
|
||||
*/
|
||||
rootCause(): Error {
|
||||
let current: Error = this;
|
||||
while (current instanceof AppError && current.cause) {
|
||||
current = current.cause;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
/**
|
||||
* Get the root cause of the error chain.
|
||||
* Traverses the cause chain to find the original error.
|
||||
*/
|
||||
rootCause(): Error {
|
||||
let current: Error = this;
|
||||
while (current instanceof AppError && current.cause) {
|
||||
current = current.cause;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this error or any in the chain has the given code.
|
||||
* Similar to Go's `errors.Is()`.
|
||||
*
|
||||
* @param code - The error code to check for
|
||||
* @returns true if this error or any cause has the given code
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* if (error.hasCode(ErrorCode.INSUFFICIENT_CREDITS)) {
|
||||
* // Show upgrade prompt
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
hasCode(code: ErrorCode): boolean {
|
||||
let current: Error | undefined = this;
|
||||
while (current) {
|
||||
if (current instanceof AppError && current.code === code) {
|
||||
return true;
|
||||
}
|
||||
current = current instanceof AppError ? current.cause : undefined;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Check if this error or any in the chain has the given code.
|
||||
* Similar to Go's `errors.Is()`.
|
||||
*
|
||||
* @param code - The error code to check for
|
||||
* @returns true if this error or any cause has the given code
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* if (error.hasCode(ErrorCode.INSUFFICIENT_CREDITS)) {
|
||||
* // Show upgrade prompt
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
hasCode(code: ErrorCode): boolean {
|
||||
let current: Error | undefined = this;
|
||||
while (current) {
|
||||
if (current instanceof AppError && current.code === code) {
|
||||
return true;
|
||||
}
|
||||
current = current instanceof AppError ? current.cause : undefined;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to JSON for API responses.
|
||||
* Excludes stack traces and internal details.
|
||||
*/
|
||||
toJSON(): Record<string, unknown> {
|
||||
return {
|
||||
code: this.code,
|
||||
message: this.message,
|
||||
httpStatus: this.httpStatus,
|
||||
retryable: this.retryable,
|
||||
timestamp: this.timestamp,
|
||||
...(Object.keys(this.context).length > 0 && { details: this.context }),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Convert to JSON for API responses.
|
||||
* Excludes stack traces and internal details.
|
||||
*/
|
||||
toJSON(): Record<string, unknown> {
|
||||
return {
|
||||
code: this.code,
|
||||
message: this.message,
|
||||
httpStatus: this.httpStatus,
|
||||
retryable: this.retryable,
|
||||
timestamp: this.timestamp,
|
||||
...(Object.keys(this.context).length > 0 && { details: this.context }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to full JSON including stack and cause (for logging).
|
||||
* Use this for server-side logging, not client responses.
|
||||
*/
|
||||
toFullJSON(): Record<string, unknown> {
|
||||
return {
|
||||
...this.toJSON(),
|
||||
stack: this.stack,
|
||||
cause:
|
||||
this.cause instanceof AppError
|
||||
? this.cause.toFullJSON()
|
||||
: this.cause?.message,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Convert to full JSON including stack and cause (for logging).
|
||||
* Use this for server-side logging, not client responses.
|
||||
*/
|
||||
toFullJSON(): Record<string, unknown> {
|
||||
return {
|
||||
...this.toJSON(),
|
||||
stack: this.stack,
|
||||
cause: this.cause instanceof AppError ? this.cause.toFullJSON() : this.cause?.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import { ErrorCode } from '../types/error-codes';
|
|||
import { AppError, type ErrorContext } from './app-error';
|
||||
|
||||
type AuthErrorCode =
|
||||
| ErrorCode.AUTHENTICATION_REQUIRED
|
||||
| ErrorCode.INVALID_TOKEN
|
||||
| ErrorCode.TOKEN_EXPIRED
|
||||
| ErrorCode.PERMISSION_DENIED
|
||||
| ErrorCode.RESOURCE_NOT_OWNED;
|
||||
| ErrorCode.AUTHENTICATION_REQUIRED
|
||||
| ErrorCode.INVALID_TOKEN
|
||||
| ErrorCode.TOKEN_EXPIRED
|
||||
| ErrorCode.PERMISSION_DENIED
|
||||
| ErrorCode.RESOURCE_NOT_OWNED;
|
||||
|
||||
/**
|
||||
* Error for authentication and authorization failures.
|
||||
|
|
@ -25,55 +25,54 @@ type AuthErrorCode =
|
|||
* ```
|
||||
*/
|
||||
export class AuthError extends AppError {
|
||||
constructor(code: AuthErrorCode, message: string, context?: ErrorContext) {
|
||||
super({ code, message, context });
|
||||
this.name = 'AuthError';
|
||||
}
|
||||
constructor(code: AuthErrorCode, message: string, context?: ErrorContext) {
|
||||
super({ code, message, context });
|
||||
this.name = 'AuthError';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an error for missing authentication.
|
||||
* HTTP 401 Unauthorized
|
||||
*/
|
||||
static unauthorized(message = 'Authentication required'): AuthError {
|
||||
return new AuthError(ErrorCode.AUTHENTICATION_REQUIRED, message);
|
||||
}
|
||||
/**
|
||||
* Create an error for missing authentication.
|
||||
* HTTP 401 Unauthorized
|
||||
*/
|
||||
static unauthorized(message = 'Authentication required'): AuthError {
|
||||
return new AuthError(ErrorCode.AUTHENTICATION_REQUIRED, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an error for an invalid token.
|
||||
* HTTP 401 Unauthorized
|
||||
*/
|
||||
static invalidToken(message = 'Invalid or malformed token'): AuthError {
|
||||
return new AuthError(ErrorCode.INVALID_TOKEN, message);
|
||||
}
|
||||
/**
|
||||
* Create an error for an invalid token.
|
||||
* HTTP 401 Unauthorized
|
||||
*/
|
||||
static invalidToken(message = 'Invalid or malformed token'): AuthError {
|
||||
return new AuthError(ErrorCode.INVALID_TOKEN, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an error for an expired token.
|
||||
* HTTP 401 Unauthorized
|
||||
*/
|
||||
static tokenExpired(message = 'Token has expired'): AuthError {
|
||||
return new AuthError(ErrorCode.TOKEN_EXPIRED, message);
|
||||
}
|
||||
/**
|
||||
* Create an error for an expired token.
|
||||
* HTTP 401 Unauthorized
|
||||
*/
|
||||
static tokenExpired(message = 'Token has expired'): AuthError {
|
||||
return new AuthError(ErrorCode.TOKEN_EXPIRED, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an error for insufficient permissions.
|
||||
* HTTP 403 Forbidden
|
||||
*/
|
||||
static forbidden(message = 'Permission denied'): AuthError {
|
||||
return new AuthError(ErrorCode.PERMISSION_DENIED, message);
|
||||
}
|
||||
/**
|
||||
* Create an error for insufficient permissions.
|
||||
* HTTP 403 Forbidden
|
||||
*/
|
||||
static forbidden(message = 'Permission denied'): AuthError {
|
||||
return new AuthError(ErrorCode.PERMISSION_DENIED, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an error when a user tries to access a resource they don't own.
|
||||
* HTTP 403 Forbidden
|
||||
*
|
||||
* @param resourceType - Type of resource (e.g., 'Story', 'Character')
|
||||
* @param resourceId - ID of the resource
|
||||
*/
|
||||
static notOwned(resourceType: string, resourceId: string): AuthError {
|
||||
return new AuthError(
|
||||
ErrorCode.RESOURCE_NOT_OWNED,
|
||||
`${resourceType} does not belong to you`,
|
||||
{ resourceType, resourceId }
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Create an error when a user tries to access a resource they don't own.
|
||||
* HTTP 403 Forbidden
|
||||
*
|
||||
* @param resourceType - Type of resource (e.g., 'Story', 'Character')
|
||||
* @param resourceId - ID of the resource
|
||||
*/
|
||||
static notOwned(resourceType: string, resourceId: string): AuthError {
|
||||
return new AuthError(ErrorCode.RESOURCE_NOT_OWNED, `${resourceType} does not belong to you`, {
|
||||
resourceType,
|
||||
resourceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,24 +12,20 @@ import { AppError } from './app-error';
|
|||
* ```
|
||||
*/
|
||||
export class CreditError extends AppError {
|
||||
/** Credits required for the operation */
|
||||
readonly requiredCredits: number;
|
||||
/** Credits required for the operation */
|
||||
readonly requiredCredits: number;
|
||||
|
||||
/** Credits currently available */
|
||||
readonly availableCredits: number;
|
||||
/** Credits currently available */
|
||||
readonly availableCredits: number;
|
||||
|
||||
constructor(
|
||||
requiredCredits: number,
|
||||
availableCredits: number,
|
||||
operation?: string
|
||||
) {
|
||||
super({
|
||||
code: ErrorCode.INSUFFICIENT_CREDITS,
|
||||
message: `Insufficient credits. Required: ${requiredCredits}, Available: ${availableCredits}`,
|
||||
context: { requiredCredits, availableCredits, operation },
|
||||
});
|
||||
this.name = 'CreditError';
|
||||
this.requiredCredits = requiredCredits;
|
||||
this.availableCredits = availableCredits;
|
||||
}
|
||||
constructor(requiredCredits: number, availableCredits: number, operation?: string) {
|
||||
super({
|
||||
code: ErrorCode.INSUFFICIENT_CREDITS,
|
||||
message: `Insufficient credits. Required: ${requiredCredits}, Available: ${availableCredits}`,
|
||||
context: { requiredCredits, availableCredits, operation },
|
||||
});
|
||||
this.name = 'CreditError';
|
||||
this.requiredCredits = requiredCredits;
|
||||
this.availableCredits = availableCredits;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,38 +17,28 @@ type DatabaseErrorCode = ErrorCode.DATABASE_ERROR | ErrorCode.CONSTRAINT_VIOLATI
|
|||
* ```
|
||||
*/
|
||||
export class DatabaseError extends AppError {
|
||||
constructor(
|
||||
code: DatabaseErrorCode,
|
||||
message: string,
|
||||
cause?: Error,
|
||||
context?: ErrorContext
|
||||
) {
|
||||
super({ code, message, cause, context });
|
||||
this.name = 'DatabaseError';
|
||||
}
|
||||
constructor(code: DatabaseErrorCode, message: string, cause?: Error, context?: ErrorContext) {
|
||||
super({ code, message, cause, context });
|
||||
this.name = 'DatabaseError';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a constraint violation error (e.g., unique constraint).
|
||||
*
|
||||
* @param field - The field that violated the constraint
|
||||
* @param message - Description of the violation
|
||||
*/
|
||||
static constraintViolation(field: string, message: string): DatabaseError {
|
||||
return new DatabaseError(
|
||||
ErrorCode.CONSTRAINT_VIOLATION,
|
||||
message,
|
||||
undefined,
|
||||
{ field }
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Create a constraint violation error (e.g., unique constraint).
|
||||
*
|
||||
* @param field - The field that violated the constraint
|
||||
* @param message - Description of the violation
|
||||
*/
|
||||
static constraintViolation(field: string, message: string): DatabaseError {
|
||||
return new DatabaseError(ErrorCode.CONSTRAINT_VIOLATION, message, undefined, { field });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a generic database query error.
|
||||
*
|
||||
* @param message - Description of what went wrong
|
||||
* @param cause - Original error if available
|
||||
*/
|
||||
static queryFailed(message: string, cause?: Error): DatabaseError {
|
||||
return new DatabaseError(ErrorCode.DATABASE_ERROR, message, cause);
|
||||
}
|
||||
/**
|
||||
* Create a generic database query error.
|
||||
*
|
||||
* @param message - Description of what went wrong
|
||||
* @param cause - Original error if available
|
||||
*/
|
||||
static queryFailed(message: string, cause?: Error): DatabaseError {
|
||||
return new DatabaseError(ErrorCode.DATABASE_ERROR, message, cause);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import { ErrorCode } from '../types/error-codes';
|
||||
import { AppError, type ErrorContext } from './app-error';
|
||||
|
||||
type NetworkErrorCode =
|
||||
| ErrorCode.NETWORK_ERROR
|
||||
| ErrorCode.TIMEOUT
|
||||
| ErrorCode.CONNECTION_REFUSED;
|
||||
type NetworkErrorCode = ErrorCode.NETWORK_ERROR | ErrorCode.TIMEOUT | ErrorCode.CONNECTION_REFUSED;
|
||||
|
||||
/**
|
||||
* Error for network-level failures (timeouts, connection issues, etc.).
|
||||
|
|
@ -23,41 +20,33 @@ type NetworkErrorCode =
|
|||
* ```
|
||||
*/
|
||||
export class NetworkError extends AppError {
|
||||
constructor(
|
||||
code: NetworkErrorCode,
|
||||
message: string,
|
||||
cause?: Error,
|
||||
context?: ErrorContext
|
||||
) {
|
||||
super({ code, message, cause, context });
|
||||
this.name = 'NetworkError';
|
||||
}
|
||||
constructor(code: NetworkErrorCode, message: string, cause?: Error, context?: ErrorContext) {
|
||||
super({ code, message, cause, context });
|
||||
this.name = 'NetworkError';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a timeout error.
|
||||
*
|
||||
* @param operation - Description of the operation that timed out
|
||||
*/
|
||||
static timeout(operation: string): NetworkError {
|
||||
return new NetworkError(
|
||||
ErrorCode.TIMEOUT,
|
||||
`Operation timed out: ${operation}`,
|
||||
undefined,
|
||||
{ operation }
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Create a timeout error.
|
||||
*
|
||||
* @param operation - Description of the operation that timed out
|
||||
*/
|
||||
static timeout(operation: string): NetworkError {
|
||||
return new NetworkError(ErrorCode.TIMEOUT, `Operation timed out: ${operation}`, undefined, {
|
||||
operation,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a connection refused error.
|
||||
*
|
||||
* @param service - Name of the service that refused connection
|
||||
*/
|
||||
static connectionRefused(service: string): NetworkError {
|
||||
return new NetworkError(
|
||||
ErrorCode.CONNECTION_REFUSED,
|
||||
`Connection refused: ${service}`,
|
||||
undefined,
|
||||
{ service }
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Create a connection refused error.
|
||||
*
|
||||
* @param service - Name of the service that refused connection
|
||||
*/
|
||||
static connectionRefused(service: string): NetworkError {
|
||||
return new NetworkError(
|
||||
ErrorCode.CONNECTION_REFUSED,
|
||||
`Connection refused: ${service}`,
|
||||
undefined,
|
||||
{ service }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,30 +16,26 @@ import { AppError, type ErrorContext } from './app-error';
|
|||
* ```
|
||||
*/
|
||||
export class NotFoundError extends AppError {
|
||||
constructor(
|
||||
resourceType: string,
|
||||
identifier: string,
|
||||
context?: ErrorContext
|
||||
) {
|
||||
super({
|
||||
code: ErrorCode.RESOURCE_NOT_FOUND,
|
||||
message: `${resourceType} not found: ${identifier}`,
|
||||
context: { resourceType, identifier, ...context },
|
||||
});
|
||||
this.name = 'NotFoundError';
|
||||
}
|
||||
constructor(resourceType: string, identifier: string, context?: ErrorContext) {
|
||||
super({
|
||||
code: ErrorCode.RESOURCE_NOT_FOUND,
|
||||
message: `${resourceType} not found: ${identifier}`,
|
||||
context: { resourceType, identifier, ...context },
|
||||
});
|
||||
this.name = 'NotFoundError';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a not found error for a user.
|
||||
*/
|
||||
static user(userId: string): NotFoundError {
|
||||
return new NotFoundError('User', userId);
|
||||
}
|
||||
/**
|
||||
* Create a not found error for a user.
|
||||
*/
|
||||
static user(userId: string): NotFoundError {
|
||||
return new NotFoundError('User', userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a not found error for any resource type.
|
||||
*/
|
||||
static resource(resourceType: string, identifier: string): NotFoundError {
|
||||
return new NotFoundError(resourceType, identifier);
|
||||
}
|
||||
/**
|
||||
* Create a not found error for any resource type.
|
||||
*/
|
||||
static resource(resourceType: string, identifier: string): NotFoundError {
|
||||
return new NotFoundError(resourceType, identifier);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,16 +16,16 @@ import { AppError } from './app-error';
|
|||
* ```
|
||||
*/
|
||||
export class RateLimitError extends AppError {
|
||||
/** Seconds to wait before retrying (if known) */
|
||||
readonly retryAfter?: number;
|
||||
/** Seconds to wait before retrying (if known) */
|
||||
readonly retryAfter?: number;
|
||||
|
||||
constructor(message = 'Rate limit exceeded', retryAfter?: number) {
|
||||
super({
|
||||
code: ErrorCode.RATE_LIMIT_EXCEEDED,
|
||||
message,
|
||||
context: retryAfter ? { retryAfter } : {},
|
||||
});
|
||||
this.name = 'RateLimitError';
|
||||
this.retryAfter = retryAfter;
|
||||
}
|
||||
constructor(message = 'Rate limit exceeded', retryAfter?: number) {
|
||||
super({
|
||||
code: ErrorCode.RATE_LIMIT_EXCEEDED,
|
||||
message,
|
||||
context: retryAfter ? { retryAfter } : {},
|
||||
});
|
||||
this.name = 'RateLimitError';
|
||||
this.retryAfter = retryAfter;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import { ErrorCode } from '../types/error-codes';
|
|||
import { AppError, type ErrorContext } from './app-error';
|
||||
|
||||
type ServiceErrorCode =
|
||||
| ErrorCode.INTERNAL_ERROR
|
||||
| ErrorCode.SERVICE_UNAVAILABLE
|
||||
| ErrorCode.GENERATION_FAILED
|
||||
| ErrorCode.EXTERNAL_SERVICE_ERROR;
|
||||
| ErrorCode.INTERNAL_ERROR
|
||||
| ErrorCode.SERVICE_UNAVAILABLE
|
||||
| ErrorCode.GENERATION_FAILED
|
||||
| ErrorCode.EXTERNAL_SERVICE_ERROR;
|
||||
|
||||
/**
|
||||
* Error for service-level failures (internal errors, external API failures, etc.).
|
||||
|
|
@ -27,77 +27,64 @@ type ServiceErrorCode =
|
|||
* ```
|
||||
*/
|
||||
export class ServiceError extends AppError {
|
||||
constructor(
|
||||
code: ServiceErrorCode,
|
||||
message: string,
|
||||
cause?: Error,
|
||||
context?: ErrorContext
|
||||
) {
|
||||
super({ code, message, cause, context });
|
||||
this.name = 'ServiceError';
|
||||
}
|
||||
constructor(code: ServiceErrorCode, message: string, cause?: Error, context?: ErrorContext) {
|
||||
super({ code, message, cause, context });
|
||||
this.name = 'ServiceError';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an error for AI/content generation failures.
|
||||
*
|
||||
* @param service - Name of the service (e.g., 'OpenAI', 'Azure OpenAI')
|
||||
* @param reason - Why the generation failed
|
||||
* @param cause - Original error if available
|
||||
*/
|
||||
static generationFailed(
|
||||
service: string,
|
||||
reason: string,
|
||||
cause?: Error
|
||||
): ServiceError {
|
||||
return new ServiceError(
|
||||
ErrorCode.GENERATION_FAILED,
|
||||
`${service} generation failed: ${reason}`,
|
||||
cause,
|
||||
{ service }
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Create an error for AI/content generation failures.
|
||||
*
|
||||
* @param service - Name of the service (e.g., 'OpenAI', 'Azure OpenAI')
|
||||
* @param reason - Why the generation failed
|
||||
* @param cause - Original error if available
|
||||
*/
|
||||
static generationFailed(service: string, reason: string, cause?: Error): ServiceError {
|
||||
return new ServiceError(
|
||||
ErrorCode.GENERATION_FAILED,
|
||||
`${service} generation failed: ${reason}`,
|
||||
cause,
|
||||
{ service }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an error for a service that is temporarily unavailable.
|
||||
*
|
||||
* @param service - Name of the unavailable service
|
||||
*/
|
||||
static unavailable(service: string): ServiceError {
|
||||
return new ServiceError(
|
||||
ErrorCode.SERVICE_UNAVAILABLE,
|
||||
`${service} is temporarily unavailable`,
|
||||
undefined,
|
||||
{ service }
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Create an error for a service that is temporarily unavailable.
|
||||
*
|
||||
* @param service - Name of the unavailable service
|
||||
*/
|
||||
static unavailable(service: string): ServiceError {
|
||||
return new ServiceError(
|
||||
ErrorCode.SERVICE_UNAVAILABLE,
|
||||
`${service} is temporarily unavailable`,
|
||||
undefined,
|
||||
{ service }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an error for external API failures.
|
||||
*
|
||||
* @param service - Name of the external service
|
||||
* @param message - Error message or description
|
||||
* @param cause - Original error if available
|
||||
*/
|
||||
static externalError(
|
||||
service: string,
|
||||
message: string,
|
||||
cause?: Error
|
||||
): ServiceError {
|
||||
return new ServiceError(
|
||||
ErrorCode.EXTERNAL_SERVICE_ERROR,
|
||||
`${service} error: ${message}`,
|
||||
cause,
|
||||
{ service }
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Create an error for external API failures.
|
||||
*
|
||||
* @param service - Name of the external service
|
||||
* @param message - Error message or description
|
||||
* @param cause - Original error if available
|
||||
*/
|
||||
static externalError(service: string, message: string, cause?: Error): ServiceError {
|
||||
return new ServiceError(
|
||||
ErrorCode.EXTERNAL_SERVICE_ERROR,
|
||||
`${service} error: ${message}`,
|
||||
cause,
|
||||
{ service }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an internal server error.
|
||||
*
|
||||
* @param message - Description of what went wrong
|
||||
* @param cause - Original error if available
|
||||
*/
|
||||
static internal(message: string, cause?: Error): ServiceError {
|
||||
return new ServiceError(ErrorCode.INTERNAL_ERROR, message, cause);
|
||||
}
|
||||
/**
|
||||
* Create an internal server error.
|
||||
*
|
||||
* @param message - Description of what went wrong
|
||||
* @param cause - Original error if available
|
||||
*/
|
||||
static internal(message: string, cause?: Error): ServiceError {
|
||||
return new ServiceError(ErrorCode.INTERNAL_ERROR, message, cause);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,44 +16,44 @@ import { AppError, type ErrorContext } from './app-error';
|
|||
* ```
|
||||
*/
|
||||
export class ValidationError extends AppError {
|
||||
constructor(message: string, context?: ErrorContext) {
|
||||
super({
|
||||
code: ErrorCode.VALIDATION_FAILED,
|
||||
message,
|
||||
context,
|
||||
});
|
||||
this.name = 'ValidationError';
|
||||
}
|
||||
constructor(message: string, context?: ErrorContext) {
|
||||
super({
|
||||
code: ErrorCode.VALIDATION_FAILED,
|
||||
message,
|
||||
context,
|
||||
});
|
||||
this.name = 'ValidationError';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a validation error for an invalid field value.
|
||||
*
|
||||
* @param field - The field name that failed validation
|
||||
* @param reason - Why the validation failed
|
||||
*/
|
||||
static invalidInput(field: string, reason: string): ValidationError {
|
||||
return new ValidationError(`Invalid ${field}: ${reason}`, { field, reason });
|
||||
}
|
||||
/**
|
||||
* Create a validation error for an invalid field value.
|
||||
*
|
||||
* @param field - The field name that failed validation
|
||||
* @param reason - Why the validation failed
|
||||
*/
|
||||
static invalidInput(field: string, reason: string): ValidationError {
|
||||
return new ValidationError(`Invalid ${field}: ${reason}`, { field, reason });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a validation error for a missing required field.
|
||||
*
|
||||
* @param field - The field name that is missing
|
||||
*/
|
||||
static missingField(field: string): ValidationError {
|
||||
return new ValidationError(`Missing required field: ${field}`, { field });
|
||||
}
|
||||
/**
|
||||
* Create a validation error for a missing required field.
|
||||
*
|
||||
* @param field - The field name that is missing
|
||||
*/
|
||||
static missingField(field: string): ValidationError {
|
||||
return new ValidationError(`Missing required field: ${field}`, { field });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a validation error for an invalid format.
|
||||
*
|
||||
* @param field - The field name with invalid format
|
||||
* @param expectedFormat - Description of the expected format
|
||||
*/
|
||||
static invalidFormat(field: string, expectedFormat: string): ValidationError {
|
||||
return new ValidationError(
|
||||
`Invalid format for ${field}: expected ${expectedFormat}`,
|
||||
{ field, expectedFormat }
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Create a validation error for an invalid format.
|
||||
*
|
||||
* @param field - The field name with invalid format
|
||||
* @param expectedFormat - Description of the expected format
|
||||
*/
|
||||
static invalidFormat(field: string, expectedFormat: string): ValidationError {
|
||||
return new ValidationError(`Invalid format for ${field}: expected ${expectedFormat}`, {
|
||||
field,
|
||||
expectedFormat,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,63 +21,63 @@ import { ErrorCode } from '../types/error-codes';
|
|||
* ```
|
||||
*/
|
||||
export function isAppError(error: unknown): error is AppError {
|
||||
return error instanceof AppError;
|
||||
return error instanceof AppError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is a ValidationError.
|
||||
*/
|
||||
export function isValidationError(error: unknown): error is ValidationError {
|
||||
return error instanceof ValidationError;
|
||||
return error instanceof ValidationError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is an AuthError.
|
||||
*/
|
||||
export function isAuthError(error: unknown): error is AuthError {
|
||||
return error instanceof AuthError;
|
||||
return error instanceof AuthError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is a NotFoundError.
|
||||
*/
|
||||
export function isNotFoundError(error: unknown): error is NotFoundError {
|
||||
return error instanceof NotFoundError;
|
||||
return error instanceof NotFoundError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is a CreditError.
|
||||
*/
|
||||
export function isCreditError(error: unknown): error is CreditError {
|
||||
return error instanceof CreditError;
|
||||
return error instanceof CreditError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is a ServiceError.
|
||||
*/
|
||||
export function isServiceError(error: unknown): error is ServiceError {
|
||||
return error instanceof ServiceError;
|
||||
return error instanceof ServiceError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is a RateLimitError.
|
||||
*/
|
||||
export function isRateLimitError(error: unknown): error is RateLimitError {
|
||||
return error instanceof RateLimitError;
|
||||
return error instanceof RateLimitError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is a NetworkError.
|
||||
*/
|
||||
export function isNetworkError(error: unknown): error is NetworkError {
|
||||
return error instanceof NetworkError;
|
||||
return error instanceof NetworkError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is a DatabaseError.
|
||||
*/
|
||||
export function isDatabaseError(error: unknown): error is DatabaseError {
|
||||
return error instanceof DatabaseError;
|
||||
return error instanceof DatabaseError;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -92,10 +92,10 @@ export function isDatabaseError(error: unknown): error is DatabaseError {
|
|||
* ```
|
||||
*/
|
||||
export function hasErrorCode(error: unknown, code: ErrorCode): boolean {
|
||||
if (!isAppError(error)) {
|
||||
return false;
|
||||
}
|
||||
return error.hasCode(code);
|
||||
if (!isAppError(error)) {
|
||||
return false;
|
||||
}
|
||||
return error.hasCode(code);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -111,17 +111,17 @@ export function hasErrorCode(error: unknown, code: ErrorCode): boolean {
|
|||
* ```
|
||||
*/
|
||||
export function findError<T extends AppError>(
|
||||
error: unknown,
|
||||
predicate: (e: AppError) => e is T
|
||||
error: unknown,
|
||||
predicate: (e: AppError) => e is T
|
||||
): T | undefined {
|
||||
let current: unknown = error;
|
||||
while (current) {
|
||||
if (isAppError(current) && predicate(current)) {
|
||||
return current;
|
||||
}
|
||||
current = isAppError(current) ? current.cause : undefined;
|
||||
}
|
||||
return undefined;
|
||||
let current: unknown = error;
|
||||
while (current) {
|
||||
if (isAppError(current) && predicate(current)) {
|
||||
return current;
|
||||
}
|
||||
current = isAppError(current) ? current.cause : undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -129,10 +129,10 @@ export function findError<T extends AppError>(
|
|||
* Works with both AppError and standard Error.
|
||||
*/
|
||||
export function isRetryable(error: unknown): boolean {
|
||||
if (isAppError(error)) {
|
||||
return error.retryable;
|
||||
}
|
||||
return false;
|
||||
if (isAppError(error)) {
|
||||
return error.retryable;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -140,10 +140,10 @@ export function isRetryable(error: unknown): boolean {
|
|||
* Returns 500 for non-AppError errors.
|
||||
*/
|
||||
export function getHttpStatus(error: unknown): number {
|
||||
if (isAppError(error)) {
|
||||
return error.httpStatus;
|
||||
}
|
||||
return 500;
|
||||
if (isAppError(error)) {
|
||||
return error.httpStatus;
|
||||
}
|
||||
return 500;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -151,8 +151,8 @@ export function getHttpStatus(error: unknown): number {
|
|||
* Returns UNKNOWN_ERROR for non-AppError errors.
|
||||
*/
|
||||
export function getErrorCode(error: unknown): ErrorCode {
|
||||
if (isAppError(error)) {
|
||||
return error.code;
|
||||
}
|
||||
return ErrorCode.UNKNOWN_ERROR;
|
||||
if (isAppError(error)) {
|
||||
return error.code;
|
||||
}
|
||||
return ErrorCode.UNKNOWN_ERROR;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,39 +43,31 @@
|
|||
*/
|
||||
|
||||
// Types
|
||||
export {
|
||||
ErrorCode,
|
||||
ERROR_CODE_TO_HTTP_STATUS,
|
||||
ERROR_CODE_RETRYABLE,
|
||||
} from './types/error-codes';
|
||||
export { ErrorCode, ERROR_CODE_TO_HTTP_STATUS, ERROR_CODE_RETRYABLE } from './types/error-codes';
|
||||
|
||||
export {
|
||||
type Result,
|
||||
type AsyncResult,
|
||||
ok,
|
||||
err,
|
||||
isOk,
|
||||
isErr,
|
||||
unwrap,
|
||||
unwrapOr,
|
||||
unwrapOrElse,
|
||||
map,
|
||||
mapErr,
|
||||
andThen,
|
||||
match,
|
||||
tryCatch,
|
||||
tryCatchAsync,
|
||||
combine,
|
||||
fromNullable,
|
||||
toNullable,
|
||||
type Result,
|
||||
type AsyncResult,
|
||||
ok,
|
||||
err,
|
||||
isOk,
|
||||
isErr,
|
||||
unwrap,
|
||||
unwrapOr,
|
||||
unwrapOrElse,
|
||||
map,
|
||||
mapErr,
|
||||
andThen,
|
||||
match,
|
||||
tryCatch,
|
||||
tryCatchAsync,
|
||||
combine,
|
||||
fromNullable,
|
||||
toNullable,
|
||||
} from './types/result';
|
||||
|
||||
// Errors
|
||||
export {
|
||||
AppError,
|
||||
type ErrorContext,
|
||||
type AppErrorOptions,
|
||||
} from './errors/app-error';
|
||||
export { AppError, type ErrorContext, type AppErrorOptions } from './errors/app-error';
|
||||
|
||||
export { ValidationError } from './errors/validation-error';
|
||||
export { AuthError } from './errors/auth-error';
|
||||
|
|
@ -88,20 +80,20 @@ export { DatabaseError } from './errors/database-error';
|
|||
|
||||
// Guards
|
||||
export {
|
||||
isAppError,
|
||||
isValidationError,
|
||||
isAuthError,
|
||||
isNotFoundError,
|
||||
isCreditError,
|
||||
isServiceError,
|
||||
isRateLimitError,
|
||||
isNetworkError,
|
||||
isDatabaseError,
|
||||
hasErrorCode,
|
||||
findError,
|
||||
isRetryable,
|
||||
getHttpStatus,
|
||||
getErrorCode,
|
||||
isAppError,
|
||||
isValidationError,
|
||||
isAuthError,
|
||||
isNotFoundError,
|
||||
isCreditError,
|
||||
isServiceError,
|
||||
isRateLimitError,
|
||||
isNetworkError,
|
||||
isDatabaseError,
|
||||
hasErrorCode,
|
||||
findError,
|
||||
isRetryable,
|
||||
getHttpStatus,
|
||||
getErrorCode,
|
||||
} from './guards/type-guards';
|
||||
|
||||
// Utils
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import {
|
||||
type ExceptionFilter,
|
||||
Catch,
|
||||
type ArgumentsHost,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
type ExceptionFilter,
|
||||
Catch,
|
||||
type ArgumentsHost,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import type { Request, Response } from 'express';
|
||||
import { AppError } from '../errors/app-error';
|
||||
|
|
@ -15,13 +15,13 @@ import { ErrorCode } from '../types/error-codes';
|
|||
* Standard error response format returned by all backends.
|
||||
*/
|
||||
export interface ErrorResponseBody {
|
||||
statusCode: number;
|
||||
error: string;
|
||||
message: string;
|
||||
retryable: boolean;
|
||||
timestamp: string;
|
||||
path: string;
|
||||
details?: Record<string, unknown>;
|
||||
statusCode: number;
|
||||
error: string;
|
||||
message: string;
|
||||
retryable: boolean;
|
||||
timestamp: string;
|
||||
path: string;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -47,213 +47,198 @@ export interface ErrorResponseBody {
|
|||
*/
|
||||
@Catch()
|
||||
export class AppExceptionFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(AppExceptionFilter.name);
|
||||
private readonly logger = new Logger(AppExceptionFilter.name);
|
||||
|
||||
catch(exception: unknown, host: ArgumentsHost): void {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
catch(exception: unknown, host: ArgumentsHost): void {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
const errorResponse = this.buildErrorResponse(exception, request);
|
||||
const errorResponse = this.buildErrorResponse(exception, request);
|
||||
|
||||
this.logError(exception, request, errorResponse);
|
||||
this.logError(exception, request, errorResponse);
|
||||
|
||||
response.status(errorResponse.statusCode).json(errorResponse);
|
||||
}
|
||||
response.status(errorResponse.statusCode).json(errorResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the error response body based on the exception type.
|
||||
*/
|
||||
private buildErrorResponse(
|
||||
exception: unknown,
|
||||
request: Request
|
||||
): ErrorResponseBody {
|
||||
// Handle AppError and subclasses
|
||||
if (isAppError(exception)) {
|
||||
return this.buildAppErrorResponse(exception, request);
|
||||
}
|
||||
/**
|
||||
* Build the error response body based on the exception type.
|
||||
*/
|
||||
private buildErrorResponse(exception: unknown, request: Request): ErrorResponseBody {
|
||||
// Handle AppError and subclasses
|
||||
if (isAppError(exception)) {
|
||||
return this.buildAppErrorResponse(exception, request);
|
||||
}
|
||||
|
||||
// Handle NestJS HttpException
|
||||
if (exception instanceof HttpException) {
|
||||
return this.buildHttpExceptionResponse(exception, request);
|
||||
}
|
||||
// Handle NestJS HttpException
|
||||
if (exception instanceof HttpException) {
|
||||
return this.buildHttpExceptionResponse(exception, request);
|
||||
}
|
||||
|
||||
// Handle standard Error
|
||||
if (exception instanceof Error) {
|
||||
return this.buildStandardErrorResponse(exception, request);
|
||||
}
|
||||
// Handle standard Error
|
||||
if (exception instanceof Error) {
|
||||
return this.buildStandardErrorResponse(exception, request);
|
||||
}
|
||||
|
||||
// Handle unknown errors
|
||||
return this.buildUnknownErrorResponse(request);
|
||||
}
|
||||
// Handle unknown errors
|
||||
return this.buildUnknownErrorResponse(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build response for AppError and subclasses.
|
||||
*/
|
||||
private buildAppErrorResponse(
|
||||
exception: AppError,
|
||||
request: Request
|
||||
): ErrorResponseBody {
|
||||
const baseResponse: ErrorResponseBody = {
|
||||
statusCode: exception.httpStatus,
|
||||
error: exception.code,
|
||||
message: exception.message,
|
||||
retryable: exception.retryable,
|
||||
timestamp: exception.timestamp,
|
||||
path: request.url,
|
||||
};
|
||||
/**
|
||||
* Build response for AppError and subclasses.
|
||||
*/
|
||||
private buildAppErrorResponse(exception: AppError, request: Request): ErrorResponseBody {
|
||||
const baseResponse: ErrorResponseBody = {
|
||||
statusCode: exception.httpStatus,
|
||||
error: exception.code,
|
||||
message: exception.message,
|
||||
retryable: exception.retryable,
|
||||
timestamp: exception.timestamp,
|
||||
path: request.url,
|
||||
};
|
||||
|
||||
// Add credit-specific fields for CreditError
|
||||
if (isCreditError(exception)) {
|
||||
baseResponse.details = {
|
||||
requiredCredits: exception.requiredCredits,
|
||||
availableCredits: exception.availableCredits,
|
||||
...exception.context,
|
||||
};
|
||||
}
|
||||
// Add retry-after for RateLimitError
|
||||
else if (isRateLimitError(exception) && exception.retryAfter) {
|
||||
baseResponse.details = {
|
||||
retryAfter: exception.retryAfter,
|
||||
...exception.context,
|
||||
};
|
||||
}
|
||||
// Add other context if present
|
||||
else if (Object.keys(exception.context).length > 0) {
|
||||
baseResponse.details = exception.context;
|
||||
}
|
||||
// Add credit-specific fields for CreditError
|
||||
if (isCreditError(exception)) {
|
||||
baseResponse.details = {
|
||||
requiredCredits: exception.requiredCredits,
|
||||
availableCredits: exception.availableCredits,
|
||||
...exception.context,
|
||||
};
|
||||
}
|
||||
// Add retry-after for RateLimitError
|
||||
else if (isRateLimitError(exception) && exception.retryAfter) {
|
||||
baseResponse.details = {
|
||||
retryAfter: exception.retryAfter,
|
||||
...exception.context,
|
||||
};
|
||||
}
|
||||
// Add other context if present
|
||||
else if (Object.keys(exception.context).length > 0) {
|
||||
baseResponse.details = exception.context;
|
||||
}
|
||||
|
||||
return baseResponse;
|
||||
}
|
||||
return baseResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build response for NestJS HttpException.
|
||||
*/
|
||||
private buildHttpExceptionResponse(
|
||||
exception: HttpException,
|
||||
request: Request
|
||||
): ErrorResponseBody {
|
||||
const status = exception.getStatus();
|
||||
const exceptionResponse = exception.getResponse();
|
||||
/**
|
||||
* Build response for NestJS HttpException.
|
||||
*/
|
||||
private buildHttpExceptionResponse(
|
||||
exception: HttpException,
|
||||
request: Request
|
||||
): ErrorResponseBody {
|
||||
const status = exception.getStatus();
|
||||
const exceptionResponse = exception.getResponse();
|
||||
|
||||
let message: string;
|
||||
let details: Record<string, unknown> | undefined;
|
||||
let message: string;
|
||||
let details: Record<string, unknown> | undefined;
|
||||
|
||||
if (typeof exceptionResponse === 'object') {
|
||||
const responseObj = exceptionResponse as Record<string, unknown>;
|
||||
message =
|
||||
typeof responseObj.message === 'string'
|
||||
? responseObj.message
|
||||
: Array.isArray(responseObj.message)
|
||||
? (responseObj.message as string[]).join(', ')
|
||||
: exception.message;
|
||||
if (typeof exceptionResponse === 'object') {
|
||||
const responseObj = exceptionResponse as Record<string, unknown>;
|
||||
message =
|
||||
typeof responseObj.message === 'string'
|
||||
? responseObj.message
|
||||
: Array.isArray(responseObj.message)
|
||||
? (responseObj.message as string[]).join(', ')
|
||||
: exception.message;
|
||||
|
||||
// Extract any additional details
|
||||
const { message: _, error: __, statusCode: ___, ...rest } = responseObj;
|
||||
if (Object.keys(rest).length > 0) {
|
||||
details = rest;
|
||||
}
|
||||
} else {
|
||||
message = String(exceptionResponse);
|
||||
}
|
||||
// Extract any additional details
|
||||
const { message: _, error: __, statusCode: ___, ...rest } = responseObj;
|
||||
if (Object.keys(rest).length > 0) {
|
||||
details = rest;
|
||||
}
|
||||
} else {
|
||||
message = String(exceptionResponse);
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: status,
|
||||
error: this.httpStatusToErrorCode(status),
|
||||
message,
|
||||
retryable: status >= 500,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
...(details && { details }),
|
||||
};
|
||||
}
|
||||
return {
|
||||
statusCode: status,
|
||||
error: this.httpStatusToErrorCode(status),
|
||||
message,
|
||||
retryable: status >= 500,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
...(details && { details }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build response for standard JavaScript Error.
|
||||
*/
|
||||
private buildStandardErrorResponse(
|
||||
exception: Error,
|
||||
request: Request
|
||||
): ErrorResponseBody {
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
/**
|
||||
* Build response for standard JavaScript Error.
|
||||
*/
|
||||
private buildStandardErrorResponse(exception: Error, request: Request): ErrorResponseBody {
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
return {
|
||||
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
error: ErrorCode.INTERNAL_ERROR,
|
||||
message: isProduction
|
||||
? 'An unexpected error occurred'
|
||||
: exception.message,
|
||||
retryable: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
};
|
||||
}
|
||||
return {
|
||||
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
error: ErrorCode.INTERNAL_ERROR,
|
||||
message: isProduction ? 'An unexpected error occurred' : exception.message,
|
||||
retryable: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build response for unknown error types.
|
||||
*/
|
||||
private buildUnknownErrorResponse(request: Request): ErrorResponseBody {
|
||||
return {
|
||||
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
error: ErrorCode.UNKNOWN_ERROR,
|
||||
message: 'An unexpected error occurred',
|
||||
retryable: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Build response for unknown error types.
|
||||
*/
|
||||
private buildUnknownErrorResponse(request: Request): ErrorResponseBody {
|
||||
return {
|
||||
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
error: ErrorCode.UNKNOWN_ERROR,
|
||||
message: 'An unexpected error occurred',
|
||||
retryable: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map HTTP status code to ErrorCode.
|
||||
*/
|
||||
private httpStatusToErrorCode(status: number): string {
|
||||
const statusToCode: Record<number, string> = {
|
||||
400: ErrorCode.VALIDATION_FAILED,
|
||||
401: ErrorCode.AUTHENTICATION_REQUIRED,
|
||||
402: ErrorCode.PAYMENT_REQUIRED,
|
||||
403: ErrorCode.PERMISSION_DENIED,
|
||||
404: ErrorCode.RESOURCE_NOT_FOUND,
|
||||
409: ErrorCode.CONFLICT,
|
||||
429: ErrorCode.RATE_LIMIT_EXCEEDED,
|
||||
500: ErrorCode.INTERNAL_ERROR,
|
||||
502: ErrorCode.EXTERNAL_SERVICE_ERROR,
|
||||
503: ErrorCode.SERVICE_UNAVAILABLE,
|
||||
504: ErrorCode.TIMEOUT,
|
||||
};
|
||||
return statusToCode[status] || ErrorCode.UNKNOWN_ERROR;
|
||||
}
|
||||
/**
|
||||
* Map HTTP status code to ErrorCode.
|
||||
*/
|
||||
private httpStatusToErrorCode(status: number): string {
|
||||
const statusToCode: Record<number, string> = {
|
||||
400: ErrorCode.VALIDATION_FAILED,
|
||||
401: ErrorCode.AUTHENTICATION_REQUIRED,
|
||||
402: ErrorCode.PAYMENT_REQUIRED,
|
||||
403: ErrorCode.PERMISSION_DENIED,
|
||||
404: ErrorCode.RESOURCE_NOT_FOUND,
|
||||
409: ErrorCode.CONFLICT,
|
||||
429: ErrorCode.RATE_LIMIT_EXCEEDED,
|
||||
500: ErrorCode.INTERNAL_ERROR,
|
||||
502: ErrorCode.EXTERNAL_SERVICE_ERROR,
|
||||
503: ErrorCode.SERVICE_UNAVAILABLE,
|
||||
504: ErrorCode.TIMEOUT,
|
||||
};
|
||||
return statusToCode[status] || ErrorCode.UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the error with appropriate level based on status code.
|
||||
*/
|
||||
private logError(
|
||||
exception: unknown,
|
||||
request: Request,
|
||||
response: ErrorResponseBody
|
||||
): void {
|
||||
const logData = {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
statusCode: response.statusCode,
|
||||
error: response.error,
|
||||
message: response.message,
|
||||
userId: (request as Request & { user?: { sub?: string } }).user?.sub,
|
||||
};
|
||||
/**
|
||||
* Log the error with appropriate level based on status code.
|
||||
*/
|
||||
private logError(exception: unknown, request: Request, response: ErrorResponseBody): void {
|
||||
const logData = {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
statusCode: response.statusCode,
|
||||
error: response.error,
|
||||
message: response.message,
|
||||
userId: (request as Request & { user?: { sub?: string } }).user?.sub,
|
||||
};
|
||||
|
||||
// Log 5xx errors as errors, others as warnings
|
||||
if (response.statusCode >= 500) {
|
||||
this.logger.error(
|
||||
`[${logData.method}] ${logData.url} - ${logData.statusCode}: ${logData.message}`,
|
||||
isAppError(exception)
|
||||
? JSON.stringify(exception.toFullJSON(), null, 2)
|
||||
: exception instanceof Error
|
||||
? exception.stack
|
||||
: undefined
|
||||
);
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`[${logData.method}] ${logData.url} - ${logData.statusCode}: ${logData.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
// Log 5xx errors as errors, others as warnings
|
||||
if (response.statusCode >= 500) {
|
||||
this.logger.error(
|
||||
`[${logData.method}] ${logData.url} - ${logData.statusCode}: ${logData.message}`,
|
||||
isAppError(exception)
|
||||
? JSON.stringify(exception.toFullJSON(), null, 2)
|
||||
: exception instanceof Error
|
||||
? exception.stack
|
||||
: undefined
|
||||
);
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`[${logData.method}] ${logData.url} - ${logData.statusCode}: ${logData.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,160 +3,160 @@
|
|||
* Follows pattern: CATEGORY_SPECIFIC_ERROR
|
||||
*/
|
||||
export enum ErrorCode {
|
||||
// Validation Errors (400)
|
||||
VALIDATION_FAILED = 'VALIDATION_FAILED',
|
||||
INVALID_INPUT = 'INVALID_INPUT',
|
||||
MISSING_REQUIRED_FIELD = 'MISSING_REQUIRED_FIELD',
|
||||
INVALID_FORMAT = 'INVALID_FORMAT',
|
||||
// Validation Errors (400)
|
||||
VALIDATION_FAILED = 'VALIDATION_FAILED',
|
||||
INVALID_INPUT = 'INVALID_INPUT',
|
||||
MISSING_REQUIRED_FIELD = 'MISSING_REQUIRED_FIELD',
|
||||
INVALID_FORMAT = 'INVALID_FORMAT',
|
||||
|
||||
// Authentication Errors (401)
|
||||
AUTHENTICATION_REQUIRED = 'AUTHENTICATION_REQUIRED',
|
||||
INVALID_TOKEN = 'INVALID_TOKEN',
|
||||
TOKEN_EXPIRED = 'TOKEN_EXPIRED',
|
||||
// Authentication Errors (401)
|
||||
AUTHENTICATION_REQUIRED = 'AUTHENTICATION_REQUIRED',
|
||||
INVALID_TOKEN = 'INVALID_TOKEN',
|
||||
TOKEN_EXPIRED = 'TOKEN_EXPIRED',
|
||||
|
||||
// Authorization Errors (403)
|
||||
PERMISSION_DENIED = 'PERMISSION_DENIED',
|
||||
RESOURCE_NOT_OWNED = 'RESOURCE_NOT_OWNED',
|
||||
// Authorization Errors (403)
|
||||
PERMISSION_DENIED = 'PERMISSION_DENIED',
|
||||
RESOURCE_NOT_OWNED = 'RESOURCE_NOT_OWNED',
|
||||
|
||||
// Not Found Errors (404)
|
||||
RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND',
|
||||
USER_NOT_FOUND = 'USER_NOT_FOUND',
|
||||
// Not Found Errors (404)
|
||||
RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND',
|
||||
USER_NOT_FOUND = 'USER_NOT_FOUND',
|
||||
|
||||
// Payment/Credit Errors (402)
|
||||
INSUFFICIENT_CREDITS = 'INSUFFICIENT_CREDITS',
|
||||
PAYMENT_REQUIRED = 'PAYMENT_REQUIRED',
|
||||
// Payment/Credit Errors (402)
|
||||
INSUFFICIENT_CREDITS = 'INSUFFICIENT_CREDITS',
|
||||
PAYMENT_REQUIRED = 'PAYMENT_REQUIRED',
|
||||
|
||||
// Conflict Errors (409)
|
||||
CONFLICT = 'CONFLICT',
|
||||
DUPLICATE_ENTRY = 'DUPLICATE_ENTRY',
|
||||
// Conflict Errors (409)
|
||||
CONFLICT = 'CONFLICT',
|
||||
DUPLICATE_ENTRY = 'DUPLICATE_ENTRY',
|
||||
|
||||
// Rate Limiting (429)
|
||||
RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',
|
||||
TOO_MANY_REQUESTS = 'TOO_MANY_REQUESTS',
|
||||
// Rate Limiting (429)
|
||||
RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',
|
||||
TOO_MANY_REQUESTS = 'TOO_MANY_REQUESTS',
|
||||
|
||||
// Service Errors (500)
|
||||
INTERNAL_ERROR = 'INTERNAL_ERROR',
|
||||
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
|
||||
GENERATION_FAILED = 'GENERATION_FAILED',
|
||||
EXTERNAL_SERVICE_ERROR = 'EXTERNAL_SERVICE_ERROR',
|
||||
// Service Errors (500)
|
||||
INTERNAL_ERROR = 'INTERNAL_ERROR',
|
||||
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
|
||||
GENERATION_FAILED = 'GENERATION_FAILED',
|
||||
EXTERNAL_SERVICE_ERROR = 'EXTERNAL_SERVICE_ERROR',
|
||||
|
||||
// Network Errors (502/503/504)
|
||||
NETWORK_ERROR = 'NETWORK_ERROR',
|
||||
TIMEOUT = 'TIMEOUT',
|
||||
CONNECTION_REFUSED = 'CONNECTION_REFUSED',
|
||||
// Network Errors (502/503/504)
|
||||
NETWORK_ERROR = 'NETWORK_ERROR',
|
||||
TIMEOUT = 'TIMEOUT',
|
||||
CONNECTION_REFUSED = 'CONNECTION_REFUSED',
|
||||
|
||||
// Database Errors
|
||||
DATABASE_ERROR = 'DATABASE_ERROR',
|
||||
CONSTRAINT_VIOLATION = 'CONSTRAINT_VIOLATION',
|
||||
// Database Errors
|
||||
DATABASE_ERROR = 'DATABASE_ERROR',
|
||||
CONSTRAINT_VIOLATION = 'CONSTRAINT_VIOLATION',
|
||||
|
||||
// Unknown
|
||||
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
|
||||
// Unknown
|
||||
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps error codes to default HTTP status codes.
|
||||
*/
|
||||
export const ERROR_CODE_TO_HTTP_STATUS: Record<ErrorCode, number> = {
|
||||
// Validation (400)
|
||||
[ErrorCode.VALIDATION_FAILED]: 400,
|
||||
[ErrorCode.INVALID_INPUT]: 400,
|
||||
[ErrorCode.MISSING_REQUIRED_FIELD]: 400,
|
||||
[ErrorCode.INVALID_FORMAT]: 400,
|
||||
// Validation (400)
|
||||
[ErrorCode.VALIDATION_FAILED]: 400,
|
||||
[ErrorCode.INVALID_INPUT]: 400,
|
||||
[ErrorCode.MISSING_REQUIRED_FIELD]: 400,
|
||||
[ErrorCode.INVALID_FORMAT]: 400,
|
||||
|
||||
// Authentication (401)
|
||||
[ErrorCode.AUTHENTICATION_REQUIRED]: 401,
|
||||
[ErrorCode.INVALID_TOKEN]: 401,
|
||||
[ErrorCode.TOKEN_EXPIRED]: 401,
|
||||
// Authentication (401)
|
||||
[ErrorCode.AUTHENTICATION_REQUIRED]: 401,
|
||||
[ErrorCode.INVALID_TOKEN]: 401,
|
||||
[ErrorCode.TOKEN_EXPIRED]: 401,
|
||||
|
||||
// Authorization (403)
|
||||
[ErrorCode.PERMISSION_DENIED]: 403,
|
||||
[ErrorCode.RESOURCE_NOT_OWNED]: 403,
|
||||
// Authorization (403)
|
||||
[ErrorCode.PERMISSION_DENIED]: 403,
|
||||
[ErrorCode.RESOURCE_NOT_OWNED]: 403,
|
||||
|
||||
// Not Found (404)
|
||||
[ErrorCode.RESOURCE_NOT_FOUND]: 404,
|
||||
[ErrorCode.USER_NOT_FOUND]: 404,
|
||||
// Not Found (404)
|
||||
[ErrorCode.RESOURCE_NOT_FOUND]: 404,
|
||||
[ErrorCode.USER_NOT_FOUND]: 404,
|
||||
|
||||
// Payment (402)
|
||||
[ErrorCode.INSUFFICIENT_CREDITS]: 402,
|
||||
[ErrorCode.PAYMENT_REQUIRED]: 402,
|
||||
// Payment (402)
|
||||
[ErrorCode.INSUFFICIENT_CREDITS]: 402,
|
||||
[ErrorCode.PAYMENT_REQUIRED]: 402,
|
||||
|
||||
// Conflict (409)
|
||||
[ErrorCode.CONFLICT]: 409,
|
||||
[ErrorCode.DUPLICATE_ENTRY]: 409,
|
||||
// Conflict (409)
|
||||
[ErrorCode.CONFLICT]: 409,
|
||||
[ErrorCode.DUPLICATE_ENTRY]: 409,
|
||||
|
||||
// Rate Limit (429)
|
||||
[ErrorCode.RATE_LIMIT_EXCEEDED]: 429,
|
||||
[ErrorCode.TOO_MANY_REQUESTS]: 429,
|
||||
// Rate Limit (429)
|
||||
[ErrorCode.RATE_LIMIT_EXCEEDED]: 429,
|
||||
[ErrorCode.TOO_MANY_REQUESTS]: 429,
|
||||
|
||||
// Service Errors (500)
|
||||
[ErrorCode.INTERNAL_ERROR]: 500,
|
||||
[ErrorCode.SERVICE_UNAVAILABLE]: 503,
|
||||
[ErrorCode.GENERATION_FAILED]: 500,
|
||||
[ErrorCode.EXTERNAL_SERVICE_ERROR]: 502,
|
||||
// Service Errors (500)
|
||||
[ErrorCode.INTERNAL_ERROR]: 500,
|
||||
[ErrorCode.SERVICE_UNAVAILABLE]: 503,
|
||||
[ErrorCode.GENERATION_FAILED]: 500,
|
||||
[ErrorCode.EXTERNAL_SERVICE_ERROR]: 502,
|
||||
|
||||
// Network Errors
|
||||
[ErrorCode.NETWORK_ERROR]: 502,
|
||||
[ErrorCode.TIMEOUT]: 504,
|
||||
[ErrorCode.CONNECTION_REFUSED]: 503,
|
||||
// Network Errors
|
||||
[ErrorCode.NETWORK_ERROR]: 502,
|
||||
[ErrorCode.TIMEOUT]: 504,
|
||||
[ErrorCode.CONNECTION_REFUSED]: 503,
|
||||
|
||||
// Database Errors
|
||||
[ErrorCode.DATABASE_ERROR]: 500,
|
||||
[ErrorCode.CONSTRAINT_VIOLATION]: 409,
|
||||
// Database Errors
|
||||
[ErrorCode.DATABASE_ERROR]: 500,
|
||||
[ErrorCode.CONSTRAINT_VIOLATION]: 409,
|
||||
|
||||
// Unknown
|
||||
[ErrorCode.UNKNOWN_ERROR]: 500,
|
||||
// Unknown
|
||||
[ErrorCode.UNKNOWN_ERROR]: 500,
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps error codes to default retryable status.
|
||||
*/
|
||||
export const ERROR_CODE_RETRYABLE: Record<ErrorCode, boolean> = {
|
||||
// Validation - not retryable (user needs to fix input)
|
||||
[ErrorCode.VALIDATION_FAILED]: false,
|
||||
[ErrorCode.INVALID_INPUT]: false,
|
||||
[ErrorCode.MISSING_REQUIRED_FIELD]: false,
|
||||
[ErrorCode.INVALID_FORMAT]: false,
|
||||
// Validation - not retryable (user needs to fix input)
|
||||
[ErrorCode.VALIDATION_FAILED]: false,
|
||||
[ErrorCode.INVALID_INPUT]: false,
|
||||
[ErrorCode.MISSING_REQUIRED_FIELD]: false,
|
||||
[ErrorCode.INVALID_FORMAT]: false,
|
||||
|
||||
// Authentication - not retryable (need new credentials)
|
||||
[ErrorCode.AUTHENTICATION_REQUIRED]: false,
|
||||
[ErrorCode.INVALID_TOKEN]: false,
|
||||
[ErrorCode.TOKEN_EXPIRED]: false,
|
||||
// Authentication - not retryable (need new credentials)
|
||||
[ErrorCode.AUTHENTICATION_REQUIRED]: false,
|
||||
[ErrorCode.INVALID_TOKEN]: false,
|
||||
[ErrorCode.TOKEN_EXPIRED]: false,
|
||||
|
||||
// Authorization - not retryable (permission issue)
|
||||
[ErrorCode.PERMISSION_DENIED]: false,
|
||||
[ErrorCode.RESOURCE_NOT_OWNED]: false,
|
||||
// Authorization - not retryable (permission issue)
|
||||
[ErrorCode.PERMISSION_DENIED]: false,
|
||||
[ErrorCode.RESOURCE_NOT_OWNED]: false,
|
||||
|
||||
// Not Found - not retryable (resource doesn't exist)
|
||||
[ErrorCode.RESOURCE_NOT_FOUND]: false,
|
||||
[ErrorCode.USER_NOT_FOUND]: false,
|
||||
// Not Found - not retryable (resource doesn't exist)
|
||||
[ErrorCode.RESOURCE_NOT_FOUND]: false,
|
||||
[ErrorCode.USER_NOT_FOUND]: false,
|
||||
|
||||
// Payment - not retryable (need more credits)
|
||||
[ErrorCode.INSUFFICIENT_CREDITS]: false,
|
||||
[ErrorCode.PAYMENT_REQUIRED]: false,
|
||||
// Payment - not retryable (need more credits)
|
||||
[ErrorCode.INSUFFICIENT_CREDITS]: false,
|
||||
[ErrorCode.PAYMENT_REQUIRED]: false,
|
||||
|
||||
// Conflict - not retryable (data issue)
|
||||
[ErrorCode.CONFLICT]: false,
|
||||
[ErrorCode.DUPLICATE_ENTRY]: false,
|
||||
// Conflict - not retryable (data issue)
|
||||
[ErrorCode.CONFLICT]: false,
|
||||
[ErrorCode.DUPLICATE_ENTRY]: false,
|
||||
|
||||
// Rate Limit - retryable (after waiting)
|
||||
[ErrorCode.RATE_LIMIT_EXCEEDED]: true,
|
||||
[ErrorCode.TOO_MANY_REQUESTS]: true,
|
||||
// Rate Limit - retryable (after waiting)
|
||||
[ErrorCode.RATE_LIMIT_EXCEEDED]: true,
|
||||
[ErrorCode.TOO_MANY_REQUESTS]: true,
|
||||
|
||||
// Service Errors - retryable (transient issues)
|
||||
[ErrorCode.INTERNAL_ERROR]: true,
|
||||
[ErrorCode.SERVICE_UNAVAILABLE]: true,
|
||||
[ErrorCode.GENERATION_FAILED]: true,
|
||||
[ErrorCode.EXTERNAL_SERVICE_ERROR]: true,
|
||||
// Service Errors - retryable (transient issues)
|
||||
[ErrorCode.INTERNAL_ERROR]: true,
|
||||
[ErrorCode.SERVICE_UNAVAILABLE]: true,
|
||||
[ErrorCode.GENERATION_FAILED]: true,
|
||||
[ErrorCode.EXTERNAL_SERVICE_ERROR]: true,
|
||||
|
||||
// Network Errors - retryable (transient issues)
|
||||
[ErrorCode.NETWORK_ERROR]: true,
|
||||
[ErrorCode.TIMEOUT]: true,
|
||||
[ErrorCode.CONNECTION_REFUSED]: true,
|
||||
// Network Errors - retryable (transient issues)
|
||||
[ErrorCode.NETWORK_ERROR]: true,
|
||||
[ErrorCode.TIMEOUT]: true,
|
||||
[ErrorCode.CONNECTION_REFUSED]: true,
|
||||
|
||||
// Database Errors - not retryable (except transient, but safer to say no)
|
||||
[ErrorCode.DATABASE_ERROR]: false,
|
||||
[ErrorCode.CONSTRAINT_VIOLATION]: false,
|
||||
// Database Errors - not retryable (except transient, but safer to say no)
|
||||
[ErrorCode.DATABASE_ERROR]: false,
|
||||
[ErrorCode.CONSTRAINT_VIOLATION]: false,
|
||||
|
||||
// Unknown - retryable (might be transient)
|
||||
[ErrorCode.UNKNOWN_ERROR]: true,
|
||||
// Unknown - retryable (might be transient)
|
||||
[ErrorCode.UNKNOWN_ERROR]: true,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ import { ErrorCode } from './error-codes';
|
|||
* ```
|
||||
*/
|
||||
export type Result<T, E extends AppError = AppError> =
|
||||
| { readonly ok: true; readonly value: T; readonly error?: never }
|
||||
| { readonly ok: false; readonly error: E; readonly value?: never };
|
||||
| { readonly ok: true; readonly value: T; readonly error?: never }
|
||||
| { readonly ok: false; readonly error: E; readonly value?: never };
|
||||
|
||||
/**
|
||||
* Async version of Result - use this as return type for async functions.
|
||||
|
|
@ -42,7 +42,7 @@ export type AsyncResult<T, E extends AppError = AppError> = Promise<Result<T, E>
|
|||
* ```
|
||||
*/
|
||||
export function ok<T>(value: T): Result<T, never> {
|
||||
return { ok: true, value };
|
||||
return { ok: true, value };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -55,7 +55,7 @@ export function ok<T>(value: T): Result<T, never> {
|
|||
* ```
|
||||
*/
|
||||
export function err<E extends AppError>(error: E): Result<never, E> {
|
||||
return { ok: false, error };
|
||||
return { ok: false, error };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -71,9 +71,9 @@ export function err<E extends AppError>(error: E): Result<never, E> {
|
|||
* ```
|
||||
*/
|
||||
export function isOk<T, E extends AppError>(
|
||||
result: Result<T, E>
|
||||
result: Result<T, E>
|
||||
): result is { ok: true; value: T } {
|
||||
return result.ok === true;
|
||||
return result.ok === true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -89,9 +89,9 @@ export function isOk<T, E extends AppError>(
|
|||
* ```
|
||||
*/
|
||||
export function isErr<T, E extends AppError>(
|
||||
result: Result<T, E>
|
||||
result: Result<T, E>
|
||||
): result is { ok: false; error: E } {
|
||||
return result.ok === false;
|
||||
return result.ok === false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -107,10 +107,10 @@ export function isErr<T, E extends AppError>(
|
|||
* ```
|
||||
*/
|
||||
export function unwrap<T, E extends AppError>(result: Result<T, E>): T {
|
||||
if (isOk(result)) {
|
||||
return result.value;
|
||||
}
|
||||
throw result.error;
|
||||
if (isOk(result)) {
|
||||
return result.value;
|
||||
}
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -121,11 +121,8 @@ export function unwrap<T, E extends AppError>(result: Result<T, E>): T {
|
|||
* const users = unwrapOr(result, []); // Returns [] if error
|
||||
* ```
|
||||
*/
|
||||
export function unwrapOr<T, E extends AppError>(
|
||||
result: Result<T, E>,
|
||||
defaultValue: T
|
||||
): T {
|
||||
return isOk(result) ? result.value : defaultValue;
|
||||
export function unwrapOr<T, E extends AppError>(result: Result<T, E>, defaultValue: T): T {
|
||||
return isOk(result) ? result.value : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -139,11 +136,8 @@ export function unwrapOr<T, E extends AppError>(
|
|||
* });
|
||||
* ```
|
||||
*/
|
||||
export function unwrapOrElse<T, E extends AppError>(
|
||||
result: Result<T, E>,
|
||||
fn: (error: E) => T
|
||||
): T {
|
||||
return isOk(result) ? result.value : fn(result.error);
|
||||
export function unwrapOrElse<T, E extends AppError>(result: Result<T, E>, fn: (error: E) => T): T {
|
||||
return isOk(result) ? result.value : fn(result.error);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -156,10 +150,10 @@ export function unwrapOrElse<T, E extends AppError>(
|
|||
* ```
|
||||
*/
|
||||
export function map<T, U, E extends AppError>(
|
||||
result: Result<T, E>,
|
||||
fn: (value: T) => U
|
||||
result: Result<T, E>,
|
||||
fn: (value: T) => U
|
||||
): Result<U, E> {
|
||||
return isOk(result) ? ok(fn(result.value)) : result;
|
||||
return isOk(result) ? ok(fn(result.value)) : result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -173,10 +167,10 @@ export function map<T, U, E extends AppError>(
|
|||
* ```
|
||||
*/
|
||||
export function mapErr<T, E extends AppError, F extends AppError>(
|
||||
result: Result<T, E>,
|
||||
fn: (error: E) => F
|
||||
result: Result<T, E>,
|
||||
fn: (error: E) => F
|
||||
): Result<T, F> {
|
||||
return isErr(result) ? err(fn(result.error)) : result;
|
||||
return isErr(result) ? err(fn(result.error)) : result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -190,10 +184,10 @@ export function mapErr<T, E extends AppError, F extends AppError>(
|
|||
* ```
|
||||
*/
|
||||
export function andThen<T, U, E extends AppError>(
|
||||
result: Result<T, E>,
|
||||
fn: (value: T) => Result<U, E>
|
||||
result: Result<T, E>,
|
||||
fn: (value: T) => Result<U, E>
|
||||
): Result<U, E> {
|
||||
return isOk(result) ? fn(result.value) : result;
|
||||
return isOk(result) ? fn(result.value) : result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -208,13 +202,13 @@ export function andThen<T, U, E extends AppError>(
|
|||
* ```
|
||||
*/
|
||||
export function match<T, E extends AppError, U>(
|
||||
result: Result<T, E>,
|
||||
handlers: {
|
||||
ok: (value: T) => U;
|
||||
err: (error: E) => U;
|
||||
}
|
||||
result: Result<T, E>,
|
||||
handlers: {
|
||||
ok: (value: T) => U;
|
||||
err: (error: E) => U;
|
||||
}
|
||||
): U {
|
||||
return isOk(result) ? handlers.ok(result.value) : handlers.err(result.error);
|
||||
return isOk(result) ? handlers.ok(result.value) : handlers.err(result.error);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -226,20 +220,20 @@ export function match<T, E extends AppError, U>(
|
|||
* ```
|
||||
*/
|
||||
export function tryCatch<T>(fn: () => T): Result<T, AppError> {
|
||||
try {
|
||||
return ok(fn());
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
return err(error);
|
||||
}
|
||||
return err(
|
||||
new AppError({
|
||||
code: ErrorCode.UNKNOWN_ERROR,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
cause: error instanceof Error ? error : undefined,
|
||||
})
|
||||
);
|
||||
}
|
||||
try {
|
||||
return ok(fn());
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
return err(error);
|
||||
}
|
||||
return err(
|
||||
new AppError({
|
||||
code: ErrorCode.UNKNOWN_ERROR,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
cause: error instanceof Error ? error : undefined,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -250,23 +244,21 @@ export function tryCatch<T>(fn: () => T): Result<T, AppError> {
|
|||
* const result = await tryCatchAsync(() => fetch(url).then(r => r.json()));
|
||||
* ```
|
||||
*/
|
||||
export async function tryCatchAsync<T>(
|
||||
fn: () => Promise<T>
|
||||
): AsyncResult<T, AppError> {
|
||||
try {
|
||||
return ok(await fn());
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
return err(error);
|
||||
}
|
||||
return err(
|
||||
new AppError({
|
||||
code: ErrorCode.UNKNOWN_ERROR,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
cause: error instanceof Error ? error : undefined,
|
||||
})
|
||||
);
|
||||
}
|
||||
export async function tryCatchAsync<T>(fn: () => Promise<T>): AsyncResult<T, AppError> {
|
||||
try {
|
||||
return ok(await fn());
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
return err(error);
|
||||
}
|
||||
return err(
|
||||
new AppError({
|
||||
code: ErrorCode.UNKNOWN_ERROR,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
cause: error instanceof Error ? error : undefined,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -285,17 +277,15 @@ export async function tryCatchAsync<T>(
|
|||
* }
|
||||
* ```
|
||||
*/
|
||||
export function combine<T, E extends AppError>(
|
||||
results: Result<T, E>[]
|
||||
): Result<T[], E> {
|
||||
const values: T[] = [];
|
||||
for (const result of results) {
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
values.push(result.value);
|
||||
}
|
||||
return ok(values);
|
||||
export function combine<T, E extends AppError>(results: Result<T, E>[]): Result<T[], E> {
|
||||
const values: T[] = [];
|
||||
for (const result of results) {
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
values.push(result.value);
|
||||
}
|
||||
return ok(values);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -310,10 +300,10 @@ export function combine<T, E extends AppError>(
|
|||
* ```
|
||||
*/
|
||||
export function fromNullable<T, E extends AppError>(
|
||||
value: T | null | undefined,
|
||||
errorFn: () => E
|
||||
value: T | null | undefined,
|
||||
errorFn: () => E
|
||||
): Result<T, E> {
|
||||
return value != null ? ok(value) : err(errorFn());
|
||||
return value != null ? ok(value) : err(errorFn());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -324,8 +314,6 @@ export function fromNullable<T, E extends AppError>(
|
|||
* const user = toNullable(result); // User | null
|
||||
* ```
|
||||
*/
|
||||
export function toNullable<T, E extends AppError>(
|
||||
result: Result<T, E>
|
||||
): T | null {
|
||||
return isOk(result) ? result.value : null;
|
||||
export function toNullable<T, E extends AppError>(result: Result<T, E>): T | null {
|
||||
return isOk(result) ? result.value : null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,22 +20,18 @@ import { isAppError } from '../guards/type-guards';
|
|||
* }
|
||||
* ```
|
||||
*/
|
||||
export function wrap(
|
||||
error: unknown,
|
||||
context: string,
|
||||
additionalContext?: ErrorContext
|
||||
): AppError {
|
||||
if (isAppError(error)) {
|
||||
return error.wrap(context, additionalContext);
|
||||
}
|
||||
export function wrap(error: unknown, context: string, additionalContext?: ErrorContext): AppError {
|
||||
if (isAppError(error)) {
|
||||
return error.wrap(context, additionalContext);
|
||||
}
|
||||
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return new AppError({
|
||||
code: ErrorCode.UNKNOWN_ERROR,
|
||||
message: `${context}: ${message}`,
|
||||
cause: error instanceof Error ? error : undefined,
|
||||
context: additionalContext,
|
||||
});
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return new AppError({
|
||||
code: ErrorCode.UNKNOWN_ERROR,
|
||||
message: `${context}: ${message}`,
|
||||
cause: error instanceof Error ? error : undefined,
|
||||
context: additionalContext,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -52,22 +48,22 @@ export function wrap(
|
|||
* ```
|
||||
*/
|
||||
export function toAppError(error: unknown): AppError {
|
||||
if (isAppError(error)) {
|
||||
return error;
|
||||
}
|
||||
if (isAppError(error)) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return new AppError({
|
||||
code: ErrorCode.UNKNOWN_ERROR,
|
||||
message: error.message,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return new AppError({
|
||||
code: ErrorCode.UNKNOWN_ERROR,
|
||||
message: error.message,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
|
||||
return new AppError({
|
||||
code: ErrorCode.UNKNOWN_ERROR,
|
||||
message: String(error),
|
||||
});
|
||||
return new AppError({
|
||||
code: ErrorCode.UNKNOWN_ERROR,
|
||||
message: String(error),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -79,7 +75,7 @@ export function toAppError(error: unknown): AppError {
|
|||
* ```
|
||||
*/
|
||||
export function cause(error: AppError): Error | undefined {
|
||||
return error.cause;
|
||||
return error.cause;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -92,5 +88,5 @@ export function cause(error: AppError): Error | undefined {
|
|||
* ```
|
||||
*/
|
||||
export function rootCause(error: AppError): Error {
|
||||
return error.rootCause();
|
||||
return error.rootCause();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue