Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion middleware/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
"express": "^5.1.0",
"jsonwebtoken": "^9.0.2",
"micromatch": "^4.0.8",
"stellar-sdk": "^13.1.0"
"stellar-sdk": "^13.1.0",
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/uuid": "^9.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.7",
"@typescript-eslint/eslint-plugin": "^8.20.0",
Expand Down
141 changes: 141 additions & 0 deletions middleware/src/errors/constants/error-codes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* Standardized error codes for programmatic handling
*/
export enum ErrorCode {
// Authentication errors (AUTH_*)
AUTH_TOKEN_EXPIRED = 'AUTH_TOKEN_EXPIRED',
AUTH_TOKEN_INVALID = 'AUTH_TOKEN_INVALID',
AUTH_TOKEN_MISSING = 'AUTH_TOKEN_MISSING',
AUTH_INVALID_CREDENTIALS = 'AUTH_INVALID_CREDENTIALS',
AUTH_SESSION_EXPIRED = 'AUTH_SESSION_EXPIRED',

// Authorization errors (AUTHZ_*)
INSUFFICIENT_PERMISSIONS = 'INSUFFICIENT_PERMISSIONS',
ROLE_NOT_ALLOWED = 'ROLE_NOT_ALLOWED',
ACCESS_DENIED = 'ACCESS_DENIED',

// Validation errors (VALIDATION_*)
VALIDATION_FAILED = 'VALIDATION_FAILED',
INVALID_INPUT = 'INVALID_INPUT',
MISSING_REQUIRED_FIELD = 'MISSING_REQUIRED_FIELD',
INVALID_FORMAT = 'INVALID_FORMAT',

// Resource errors (RESOURCE_*)
RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND',
DUPLICATE_RESOURCE = 'DUPLICATE_RESOURCE',
RESOURCE_CONFLICT = 'RESOURCE_CONFLICT',
RESOURCE_LOCKED = 'RESOURCE_LOCKED',

// Rate limiting
RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',

// Database errors (DB_*)
DB_CONNECTION_ERROR = 'DB_CONNECTION_ERROR',
DB_QUERY_ERROR = 'DB_QUERY_ERROR',
DB_CONSTRAINT_VIOLATION = 'DB_CONSTRAINT_VIOLATION',
DB_UNIQUE_VIOLATION = 'DB_UNIQUE_VIOLATION',
DB_FOREIGN_KEY_VIOLATION = 'DB_FOREIGN_KEY_VIOLATION',
DB_TIMEOUT = 'DB_TIMEOUT',

// External service errors
EXTERNAL_SERVICE_ERROR = 'EXTERNAL_SERVICE_ERROR',
EXTERNAL_SERVICE_TIMEOUT = 'EXTERNAL_SERVICE_TIMEOUT',
EXTERNAL_SERVICE_UNAVAILABLE = 'EXTERNAL_SERVICE_UNAVAILABLE',

// Server errors
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
}

/**
* HTTP status code mapping for error codes
*/
export const ErrorCodeHttpStatus: Record<ErrorCode, number> = {
// 401 Unauthorized
[ErrorCode.AUTH_TOKEN_EXPIRED]: 401,
[ErrorCode.AUTH_TOKEN_INVALID]: 401,
[ErrorCode.AUTH_TOKEN_MISSING]: 401,
[ErrorCode.AUTH_INVALID_CREDENTIALS]: 401,
[ErrorCode.AUTH_SESSION_EXPIRED]: 401,

// 403 Forbidden
[ErrorCode.INSUFFICIENT_PERMISSIONS]: 403,
[ErrorCode.ROLE_NOT_ALLOWED]: 403,
[ErrorCode.ACCESS_DENIED]: 403,

// 400 Bad Request
[ErrorCode.VALIDATION_FAILED]: 400,
[ErrorCode.INVALID_INPUT]: 400,
[ErrorCode.MISSING_REQUIRED_FIELD]: 400,
[ErrorCode.INVALID_FORMAT]: 400,

// 404 Not Found
[ErrorCode.RESOURCE_NOT_FOUND]: 404,

// 409 Conflict
[ErrorCode.DUPLICATE_RESOURCE]: 409,
[ErrorCode.RESOURCE_CONFLICT]: 409,
[ErrorCode.RESOURCE_LOCKED]: 423,

// 429 Too Many Requests
[ErrorCode.RATE_LIMIT_EXCEEDED]: 429,

// 500+ Server Errors
[ErrorCode.DB_CONNECTION_ERROR]: 503,
[ErrorCode.DB_QUERY_ERROR]: 500,
[ErrorCode.DB_CONSTRAINT_VIOLATION]: 409,
[ErrorCode.DB_UNIQUE_VIOLATION]: 409,
[ErrorCode.DB_FOREIGN_KEY_VIOLATION]: 409,
[ErrorCode.DB_TIMEOUT]: 504,

[ErrorCode.EXTERNAL_SERVICE_ERROR]: 502,
[ErrorCode.EXTERNAL_SERVICE_TIMEOUT]: 504,
[ErrorCode.EXTERNAL_SERVICE_UNAVAILABLE]: 503,

[ErrorCode.INTERNAL_SERVER_ERROR]: 500,
[ErrorCode.SERVICE_UNAVAILABLE]: 503,
[ErrorCode.UNKNOWN_ERROR]: 500,
};

/**
* Default user-friendly messages for error codes
*/
export const ErrorCodeMessages: Record<ErrorCode, string> = {
[ErrorCode.AUTH_TOKEN_EXPIRED]: 'Your session has expired. Please log in again.',
[ErrorCode.AUTH_TOKEN_INVALID]: 'Invalid authentication token.',
[ErrorCode.AUTH_TOKEN_MISSING]: 'Authentication required.',
[ErrorCode.AUTH_INVALID_CREDENTIALS]: 'Invalid email or password.',
[ErrorCode.AUTH_SESSION_EXPIRED]: 'Your session has expired.',

[ErrorCode.INSUFFICIENT_PERMISSIONS]: 'You do not have permission to perform this action.',
[ErrorCode.ROLE_NOT_ALLOWED]: 'Your role does not allow this action.',
[ErrorCode.ACCESS_DENIED]: 'Access denied.',

[ErrorCode.VALIDATION_FAILED]: 'Validation failed. Please check your input.',
[ErrorCode.INVALID_INPUT]: 'Invalid input provided.',
[ErrorCode.MISSING_REQUIRED_FIELD]: 'Required field is missing.',
[ErrorCode.INVALID_FORMAT]: 'Invalid format.',

[ErrorCode.RESOURCE_NOT_FOUND]: 'The requested resource was not found.',
[ErrorCode.DUPLICATE_RESOURCE]: 'A resource with the same identifier already exists.',
[ErrorCode.RESOURCE_CONFLICT]: 'Resource conflict detected.',
[ErrorCode.RESOURCE_LOCKED]: 'Resource is currently locked.',

[ErrorCode.RATE_LIMIT_EXCEEDED]: 'Too many requests. Please try again later.',

[ErrorCode.DB_CONNECTION_ERROR]: 'Service temporarily unavailable. Please try again.',
[ErrorCode.DB_QUERY_ERROR]: 'An error occurred while processing your request.',
[ErrorCode.DB_CONSTRAINT_VIOLATION]: 'Operation violates data constraints.',
[ErrorCode.DB_UNIQUE_VIOLATION]: 'A record with this value already exists.',
[ErrorCode.DB_FOREIGN_KEY_VIOLATION]: 'Referenced record does not exist.',
[ErrorCode.DB_TIMEOUT]: 'Request timed out. Please try again.',

[ErrorCode.EXTERNAL_SERVICE_ERROR]: 'External service error. Please try again.',
[ErrorCode.EXTERNAL_SERVICE_TIMEOUT]: 'External service timed out.',
[ErrorCode.EXTERNAL_SERVICE_UNAVAILABLE]: 'External service is unavailable.',

[ErrorCode.INTERNAL_SERVER_ERROR]: 'An unexpected error occurred.',
[ErrorCode.SERVICE_UNAVAILABLE]: 'Service is temporarily unavailable.',
[ErrorCode.UNKNOWN_ERROR]: 'An unknown error occurred.',
};
57 changes: 57 additions & 0 deletions middleware/src/errors/exceptions/base.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { HttpException, HttpStatus } from '@nestjs/common';
import { ErrorCode, ErrorCodeHttpStatus, ErrorCodeMessages } from '../constants/error-codes';
import { ValidationErrorDetail } from '../interfaces/error.interface';

/**
* Base exception class for all application errors
*/
export class BaseException extends HttpException {
public readonly errorCode: ErrorCode;
public readonly details?: ValidationErrorDetail[];
public readonly context?: Record<string, unknown>;

constructor(
errorCode: ErrorCode,
message?: string,
details?: ValidationErrorDetail[],
context?: Record<string, unknown>,
) {
const statusCode = ErrorCodeHttpStatus[errorCode] ?? HttpStatus.INTERNAL_SERVER_ERROR;
const errorMessage = message ?? ErrorCodeMessages[errorCode] ?? 'An error occurred';

super(
{
statusCode,
errorCode,
message: errorMessage,
details,
},
statusCode,
);

this.errorCode = errorCode;
this.details = details;
this.context = context;
}

/**
* Get the error code
*/
getErrorCode(): ErrorCode {
return this.errorCode;
}

/**
* Get validation error details
*/
getDetails(): ValidationErrorDetail[] | undefined {
return this.details;
}

/**
* Get additional context
*/
getContext(): Record<string, unknown> | undefined {
return this.context;
}
}
86 changes: 86 additions & 0 deletions middleware/src/errors/exceptions/database.exceptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { ErrorCode } from '../constants/error-codes';
import { BaseException } from './base.exception';

/**
* Database connection error
*/
export class DatabaseConnectionException extends BaseException {
constructor(message?: string) {
super(
ErrorCode.DB_CONNECTION_ERROR,
message ?? 'Database connection failed',
);
}
}

/**
* Database query error
*/
export class DatabaseQueryException extends BaseException {
constructor(message?: string, context?: Record<string, unknown>) {
super(
ErrorCode.DB_QUERY_ERROR,
message ?? 'Database query failed',
undefined,
context,
);
}
}

/**
* Database unique constraint violation
*/
export class UniqueConstraintException extends BaseException {
public readonly constraintName?: string;
public readonly columnName?: string;

constructor(constraintName?: string, columnName?: string) {
const message = columnName
? `A record with this ${columnName} already exists`
: 'Duplicate entry violates unique constraint';
super(ErrorCode.DB_UNIQUE_VIOLATION, message);
this.constraintName = constraintName;
this.columnName = columnName;
}
}

/**
* Database foreign key constraint violation
*/
export class ForeignKeyConstraintException extends BaseException {
public readonly constraintName?: string;
public readonly referencedTable?: string;

constructor(constraintName?: string, referencedTable?: string) {
const message = referencedTable
? `Referenced ${referencedTable} does not exist`
: 'Foreign key constraint violation';
super(ErrorCode.DB_FOREIGN_KEY_VIOLATION, message);
this.constraintName = constraintName;
this.referencedTable = referencedTable;
}
}

/**
* Database timeout error
*/
export class DatabaseTimeoutException extends BaseException {
constructor(message?: string) {
super(ErrorCode.DB_TIMEOUT, message ?? 'Database operation timed out');
}
}

/**
* General database constraint violation
*/
export class ConstraintViolationException extends BaseException {
public readonly constraintName?: string;

constructor(constraintName?: string, message?: string) {
super(
ErrorCode.DB_CONSTRAINT_VIOLATION,
message ?? 'Database constraint violation',
);
this.constraintName = constraintName;
}
}
Loading
Loading