From 5765b8b678c47bb598b03c1ea9d26019a980e427 Mon Sep 17 00:00:00 2001 From: Anns Shahbaz Date: Thu, 22 Jan 2026 10:40:38 +0500 Subject: [PATCH 1/5] feat: error-builder --- apps/api/src/lib/errors/error-builder.ts | 66 +++++++++++++++++++ apps/api/src/lib/errors/index.ts | 1 + apps/api/src/modules/auth/auth.service.ts | 9 +-- .../crud/services/crud.mongoose.service.ts | 15 ++--- .../crud/services/crud.prisma.service.ts | 7 +- 5 files changed, 82 insertions(+), 16 deletions(-) create mode 100644 apps/api/src/lib/errors/error-builder.ts create mode 100644 apps/api/src/lib/errors/index.ts diff --git a/apps/api/src/lib/errors/error-builder.ts b/apps/api/src/lib/errors/error-builder.ts new file mode 100644 index 0000000..a0d168d --- /dev/null +++ b/apps/api/src/lib/errors/error-builder.ts @@ -0,0 +1,66 @@ +import { + BadRequestException, + ConflictException, + ForbiddenException, + InternalServerErrorException, + NotFoundException, + UnauthorizedException, +} from '@nestjs/common'; + +/** + * Centralized error builder to simplify exception throwing across the application. + * + * @example + * return ErrorBuilder.notFound('Dataset not found'); + * return ErrorBuilder.badRequest('Invalid input data'); + * return ErrorBuilder.unauthorized('Invalid credentials'); + */ +export class ErrorBuilder { + /** + * Throws a 404 Not Found exception + * @param message - Error message describing what was not found + */ + static notFound(message: string): never { + throw new NotFoundException(message); + } + + /** + * Throws a 400 Bad Request exception + * @param message - Error message describing the bad request + */ + static badRequest(message: string): never { + throw new BadRequestException(message); + } + + /** + * Throws a 401 Unauthorized exception + * @param message - Error message describing the authorization failure + */ + static unauthorized(message: string): never { + throw new UnauthorizedException(message); + } + + /** + * Throws a 403 Forbidden exception + * @param message - Error message describing why access is forbidden + */ + static forbidden(message: string): never { + throw new ForbiddenException(message); + } + + /** + * Throws a 409 Conflict exception + * @param message - Error message describing the conflict + */ + static conflict(message: string): never { + throw new ConflictException(message); + } + + /** + * Throws a 500 Internal Server Error exception + * @param message - Error message describing the internal error + */ + static internalError(message: string): never { + throw new InternalServerErrorException(message); + } +} diff --git a/apps/api/src/lib/errors/index.ts b/apps/api/src/lib/errors/index.ts new file mode 100644 index 0000000..c008d35 --- /dev/null +++ b/apps/api/src/lib/errors/index.ts @@ -0,0 +1 @@ +export { ErrorBuilder } from './error-builder'; diff --git a/apps/api/src/modules/auth/auth.service.ts b/apps/api/src/modules/auth/auth.service.ts index 09b50a5..6835f54 100644 --- a/apps/api/src/modules/auth/auth.service.ts +++ b/apps/api/src/modules/auth/auth.service.ts @@ -7,6 +7,7 @@ import { createBetterAuth } from './auth'; import { AppContextType } from '../../app.context'; import { fromNodeHeaders } from 'better-auth/node'; import { DateExtensions, Logger } from '@repo/utils-core'; +import { ErrorBuilder } from '../../lib/errors'; @Injectable() export class AuthService { @@ -107,8 +108,8 @@ export class AuthService { const account = accounts.find((acc) => acc.providerId === provider); if (!account) { - throw new Error( - `No ${provider} account found for provider ${provider}. User may need to link their account.`, + return ErrorBuilder.notFound( + `No ${provider} account found. User may need to link their ${provider} account`, ); } @@ -121,8 +122,8 @@ export class AuthService { }); if (!tokens?.accessToken) { - throw new Error( - `Failed to refresh token for user ${account.userId} and provider ${provider}. User may need to re-authenticate.`, + return ErrorBuilder.unauthorized( + `Failed to refresh ${provider} token. User may need to re-authenticate`, ); } diff --git a/apps/api/src/modules/crud/services/crud.mongoose.service.ts b/apps/api/src/modules/crud/services/crud.mongoose.service.ts index 67f7b9b..7d7c16a 100644 --- a/apps/api/src/modules/crud/services/crud.mongoose.service.ts +++ b/apps/api/src/modules/crud/services/crud.mongoose.service.ts @@ -1,8 +1,4 @@ -import { - BadRequestException, - Injectable, - NotFoundException, -} from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Crud } from '../schemas/crud.schema'; import { NoTransaction } from '../../../decorators/method/no-transaction.decorator'; import { AutoTransaction } from '../../../decorators/class/auto-transaction.decorator'; @@ -10,6 +6,7 @@ import { ServerConstants } from '../../../constants/server.constants'; import { Logger, StringExtensions } from '@repo/utils-core'; import { Propagation } from '@nestjs-cls/transactional'; import { CrudMongooseRepository } from '../repositories/mongoose/crud.mongoose-repository'; +import { ErrorBuilder } from '../../../lib/errors'; @Injectable() @AutoTransaction( @@ -21,7 +18,7 @@ export class CrudMongooseService { async createCrud(data: Partial): Promise { if (StringExtensions.IsNullOrEmpty(data.content)) { - throw new BadRequestException('Content is Empty'); + return ErrorBuilder.badRequest('Content cannot be empty'); } const created = await this.crudRepository.create({ @@ -40,7 +37,7 @@ export class CrudMongooseService { @NoTransaction('dont care if transaction is broken') async findOne(id: string): Promise { const crud = await this.crudRepository.findById(id); - if (!crud) throw new NotFoundException(`Crud with id ${id} not found`); + if (!crud) return ErrorBuilder.notFound(`Crud not found: ${id}`); return crud; } @@ -48,13 +45,13 @@ export class CrudMongooseService { const updated = await this.crudRepository.findByIdAndUpdate(id, { content: data.content, }); - if (!updated) throw new NotFoundException(`Crud with id ${id} not found`); + if (!updated) return ErrorBuilder.notFound(`Crud not found: ${id}`); return updated; } async delete(id: string): Promise { const deleted = await this.crudRepository.findByIdAndDelete(id); - if (!deleted) throw new NotFoundException(`Crud with id ${id} not found`); + if (!deleted) return ErrorBuilder.notFound(`Crud not found: ${id}`); Logger.instance.debug('[Mongoose] Deleted:', deleted); return deleted; } diff --git a/apps/api/src/modules/crud/services/crud.prisma.service.ts b/apps/api/src/modules/crud/services/crud.prisma.service.ts index ddec78d..5e34ce9 100644 --- a/apps/api/src/modules/crud/services/crud.prisma.service.ts +++ b/apps/api/src/modules/crud/services/crud.prisma.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Crud } from '../schemas/crud.schema'; import { NoTransaction } from '../../../decorators/method/no-transaction.decorator'; import { AutoTransaction } from '../../../decorators/class/auto-transaction.decorator'; @@ -6,6 +6,7 @@ import { ServerConstants } from '../../../constants/server.constants'; import { Logger } from '@repo/utils-core'; import { Propagation } from '@nestjs-cls/transactional'; import { CrudPrismaRepository } from '../repositories/prisma/crud.prisma-repository'; +import { ErrorBuilder } from '../../../lib/errors'; @Injectable() @AutoTransaction( @@ -43,7 +44,7 @@ export class CrudPrismaService { where: { id }, data: { content: data.content }, }); - if (!updated) throw new NotFoundException(`Crud with id ${id} not found`); + if (!updated) return ErrorBuilder.notFound(`Crud not found: ${id}`); return updated; } @@ -52,7 +53,7 @@ export class CrudPrismaService { where: { id }, }); - if (!deleted) throw new NotFoundException(`Crud with id ${id} not found`); + if (!deleted) return ErrorBuilder.notFound(`Crud not found: ${id}`); Logger.instance.debug('[Prisma] Deleted:', deleted); return deleted; } From 6334dfbd724ac9fcef0fa0294d47b4de9e94d2e1 Mon Sep 17 00:00:00 2001 From: Anns Shahbaz Date: Thu, 22 Jan 2026 11:06:57 +0500 Subject: [PATCH 2/5] feat: resourceNotFound --- .../modules/crud/services/crud.mongoose.service.ts | 13 +++++++++---- .../modules/crud/services/crud.prisma.service.ts | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/api/src/modules/crud/services/crud.mongoose.service.ts b/apps/api/src/modules/crud/services/crud.mongoose.service.ts index 7d7c16a..25768cb 100644 --- a/apps/api/src/modules/crud/services/crud.mongoose.service.ts +++ b/apps/api/src/modules/crud/services/crud.mongoose.service.ts @@ -18,7 +18,12 @@ export class CrudMongooseService { async createCrud(data: Partial): Promise { if (StringExtensions.IsNullOrEmpty(data.content)) { - return ErrorBuilder.badRequest('Content cannot be empty'); + return ErrorBuilder.validationError([ + { + field: 'content', + message: 'Content cannot be empty', + }, + ]); } const created = await this.crudRepository.create({ @@ -37,7 +42,7 @@ export class CrudMongooseService { @NoTransaction('dont care if transaction is broken') async findOne(id: string): Promise { const crud = await this.crudRepository.findById(id); - if (!crud) return ErrorBuilder.notFound(`Crud not found: ${id}`); + if (!crud) return ErrorBuilder.resourceNotFound('Crud', id); return crud; } @@ -45,13 +50,13 @@ export class CrudMongooseService { const updated = await this.crudRepository.findByIdAndUpdate(id, { content: data.content, }); - if (!updated) return ErrorBuilder.notFound(`Crud not found: ${id}`); + if (!updated) return ErrorBuilder.resourceNotFound('Crud', id); return updated; } async delete(id: string): Promise { const deleted = await this.crudRepository.findByIdAndDelete(id); - if (!deleted) return ErrorBuilder.notFound(`Crud not found: ${id}`); + if (!deleted) return ErrorBuilder.resourceNotFound('Crud', id); Logger.instance.debug('[Mongoose] Deleted:', deleted); return deleted; } diff --git a/apps/api/src/modules/crud/services/crud.prisma.service.ts b/apps/api/src/modules/crud/services/crud.prisma.service.ts index 5e34ce9..c064028 100644 --- a/apps/api/src/modules/crud/services/crud.prisma.service.ts +++ b/apps/api/src/modules/crud/services/crud.prisma.service.ts @@ -44,7 +44,7 @@ export class CrudPrismaService { where: { id }, data: { content: data.content }, }); - if (!updated) return ErrorBuilder.notFound(`Crud not found: ${id}`); + if (!updated) return ErrorBuilder.resourceNotFound('Crud', id); return updated; } @@ -53,7 +53,7 @@ export class CrudPrismaService { where: { id }, }); - if (!deleted) return ErrorBuilder.notFound(`Crud not found: ${id}`); + if (!deleted) return ErrorBuilder.resourceNotFound('Crud', id); Logger.instance.debug('[Prisma] Deleted:', deleted); return deleted; } From ad5d85366a6d78004ff2b92ba56d1675c5e119d9 Mon Sep 17 00:00:00 2001 From: Anns Shahbaz Date: Thu, 22 Jan 2026 11:07:38 +0500 Subject: [PATCH 3/5] feat: extensive error builder --- apps/api/src/lib/errors/error-builder.ts | 1153 +++++++++++++++++++++- apps/api/src/lib/errors/index.ts | 10 +- 2 files changed, 1141 insertions(+), 22 deletions(-) diff --git a/apps/api/src/lib/errors/error-builder.ts b/apps/api/src/lib/errors/error-builder.ts index a0d168d..8296933 100644 --- a/apps/api/src/lib/errors/error-builder.ts +++ b/apps/api/src/lib/errors/error-builder.ts @@ -2,65 +2,1176 @@ import { BadRequestException, ConflictException, ForbiddenException, + GoneException, + HttpException, + HttpStatus, InternalServerErrorException, + MethodNotAllowedException, + NotAcceptableException, NotFoundException, + NotImplementedException, + PayloadTooLargeException, + PreconditionFailedException, + RequestTimeoutException, + ServiceUnavailableException, UnauthorizedException, + UnprocessableEntityException, + UnsupportedMediaTypeException, } from '@nestjs/common'; /** - * Centralized error builder to simplify exception throwing across the application. + * Severity levels for errors + */ +export enum ErrorSeverity { + LOW = 'low', + MEDIUM = 'medium', + HIGH = 'high', + CRITICAL = 'critical', +} + +/** + * Error categories for better classification + */ +export enum ErrorCategory { + AUTHENTICATION = 'authentication', + AUTHORIZATION = 'authorization', + VALIDATION = 'validation', + BUSINESS_LOGIC = 'business_logic', + DATABASE = 'database', + EXTERNAL_SERVICE = 'external_service', + SYSTEM = 'system', + NETWORK = 'network', + FILE_OPERATION = 'file_operation', + RATE_LIMIT = 'rate_limit', + PAYMENT = 'payment', + CONFIGURATION = 'configuration', +} + +/** + * Interface for field-level validation errors + */ +export interface FieldError { + field: string; + message: string; + value?: unknown; + constraints?: Record; +} + +/** + * Interface for error metadata + */ +export interface ErrorMetadata { + code?: string; + errorId?: string; + timestamp?: Date; + severity?: ErrorSeverity; + category?: ErrorCategory; + retryable?: boolean; + retryAfter?: number; + fields?: FieldError[]; + cause?: Error; + context?: Record; + help?: string; + documentation?: string; + userId?: string; + requestId?: string; + stackTrace?: string; + [key: string]: unknown; +} + +/** + * Interface for structured error response + */ +export interface ErrorResponse { + message: string; + timestamp: Date; + code?: string; + errorId?: string; + severity?: ErrorSeverity; + category?: ErrorCategory; + retryable?: boolean; + retryAfter?: number; + fields?: FieldError[]; + context?: Record; + help?: string; + documentation?: string; + userId?: string; + requestId?: string; + [key: string]: unknown; +} + +/** + * Ultimate centralized error builder with comprehensive error handling capabilities. + * Covers all HTTP status codes, validation, metadata, context building, and more. + * + * @example + * // Basic usage + * ErrorBuilder.notFound('User not found'); + * + * @example + * // With metadata + * ErrorBuilder.notFound('User not found', { + * code: 'USER_NOT_FOUND', + * errorId: '12345', + * userId: 'user-123' + * }); * * @example - * return ErrorBuilder.notFound('Dataset not found'); - * return ErrorBuilder.badRequest('Invalid input data'); - * return ErrorBuilder.unauthorized('Invalid credentials'); + * // Validation errors + * ErrorBuilder.validationError([ + * { field: 'email', message: 'Invalid email format' }, + * { field: 'age', message: 'Must be 18 or older' } + * ]); + * + * @example + * // Conditional throwing + * ErrorBuilder.throwIf(!user, () => ErrorBuilder.notFound('User not found')); + * + * @example + * // Business logic errors + * ErrorBuilder.businessLogic('Cannot delete account with active subscriptions', { + * code: 'ACTIVE_SUBSCRIPTIONS_EXIST', + * context: { subscriptionCount: 3 } + * }); */ export class ErrorBuilder { - /** - * Throws a 404 Not Found exception - * @param message - Error message describing what was not found - */ - static notFound(message: string): never { - throw new NotFoundException(message); - } + // ============================================================================ + // STANDARD HTTP ERRORS (4xx - Client Errors) + // ============================================================================ /** * Throws a 400 Bad Request exception * @param message - Error message describing the bad request + * @param metadata - Additional error metadata */ - static badRequest(message: string): never { - throw new BadRequestException(message); + static badRequest(message: string, metadata?: ErrorMetadata): never { + throw new BadRequestException(this.buildErrorResponse(message, metadata)); } /** * Throws a 401 Unauthorized exception * @param message - Error message describing the authorization failure + * @param metadata - Additional error metadata + */ + static unauthorized(message: string, metadata?: ErrorMetadata): never { + throw new UnauthorizedException(this.buildErrorResponse(message, metadata)); + } + + /** + * Throws a 402 Payment Required exception + * @param message - Error message describing payment requirement + * @param metadata - Additional error metadata */ - static unauthorized(message: string): never { - throw new UnauthorizedException(message); + static paymentRequired(message: string, metadata?: ErrorMetadata): never { + throw new HttpException( + this.buildErrorResponse(message, metadata), + HttpStatus.PAYMENT_REQUIRED, + ); } /** * Throws a 403 Forbidden exception * @param message - Error message describing why access is forbidden + * @param metadata - Additional error metadata + */ + static forbidden(message: string, metadata?: ErrorMetadata): never { + throw new ForbiddenException(this.buildErrorResponse(message, metadata)); + } + + /** + * Throws a 404 Not Found exception + * @param message - Error message describing what was not found + * @param metadata - Additional error metadata + */ + static notFound(message: string, metadata?: ErrorMetadata): never { + throw new NotFoundException(this.buildErrorResponse(message, metadata)); + } + + /** + * Throws a 405 Method Not Allowed exception + * @param message - Error message describing the method not allowed + * @param metadata - Additional error metadata */ - static forbidden(message: string): never { - throw new ForbiddenException(message); + static methodNotAllowed(message: string, metadata?: ErrorMetadata): never { + throw new MethodNotAllowedException( + this.buildErrorResponse(message, metadata), + ); + } + + /** + * Throws a 406 Not Acceptable exception + * @param message - Error message describing the not acceptable content + * @param metadata - Additional error metadata + */ + static notAcceptable(message: string, metadata?: ErrorMetadata): never { + throw new NotAcceptableException( + this.buildErrorResponse(message, metadata), + ); + } + + /** + * Throws a 407 Proxy Authentication Required exception + * @param message - Error message describing proxy authentication requirement + * @param metadata - Additional error metadata + */ + static proxyAuthRequired(message: string, metadata?: ErrorMetadata): never { + throw new HttpException( + this.buildErrorResponse(message, metadata), + HttpStatus.PROXY_AUTHENTICATION_REQUIRED, + ); + } + + /** + * Throws a 408 Request Timeout exception + * @param message - Error message describing the timeout + * @param metadata - Additional error metadata + */ + static requestTimeout(message: string, metadata?: ErrorMetadata): never { + throw new RequestTimeoutException( + this.buildErrorResponse(message, metadata), + ); } /** * Throws a 409 Conflict exception * @param message - Error message describing the conflict + * @param metadata - Additional error metadata + */ + static conflict(message: string, metadata?: ErrorMetadata): never { + throw new ConflictException(this.buildErrorResponse(message, metadata)); + } + + /** + * Throws a 410 Gone exception + * @param message - Error message describing what is gone + * @param metadata - Additional error metadata */ - static conflict(message: string): never { - throw new ConflictException(message); + static gone(message: string, metadata?: ErrorMetadata): never { + throw new GoneException(this.buildErrorResponse(message, metadata)); } + /** + * Throws a 411 Length Required exception + * @param message - Error message describing length requirement + * @param metadata - Additional error metadata + */ + static lengthRequired(message: string, metadata?: ErrorMetadata): never { + throw new HttpException( + this.buildErrorResponse(message, metadata), + HttpStatus.LENGTH_REQUIRED, + ); + } + + /** + * Throws a 412 Precondition Failed exception + * @param message - Error message describing the failed precondition + * @param metadata - Additional error metadata + */ + static preconditionFailed(message: string, metadata?: ErrorMetadata): never { + throw new PreconditionFailedException( + this.buildErrorResponse(message, metadata), + ); + } + + /** + * Throws a 413 Payload Too Large exception + * @param message - Error message describing the payload size issue + * @param metadata - Additional error metadata + */ + static payloadTooLarge(message: string, metadata?: ErrorMetadata): never { + throw new PayloadTooLargeException( + this.buildErrorResponse(message, metadata), + ); + } + + /** + * Throws a 414 URI Too Long exception + * @param message - Error message describing URI length issue + * @param metadata - Additional error metadata + */ + static uriTooLong(message: string, metadata?: ErrorMetadata): never { + throw new HttpException( + this.buildErrorResponse(message, metadata), + HttpStatus.URI_TOO_LONG, + ); + } + + /** + * Throws a 415 Unsupported Media Type exception + * @param message - Error message describing the unsupported media type + * @param metadata - Additional error metadata + */ + static unsupportedMediaType( + message: string, + metadata?: ErrorMetadata, + ): never { + throw new UnsupportedMediaTypeException( + this.buildErrorResponse(message, metadata), + ); + } + + /** + * Throws a 416 Range Not Satisfiable exception + * @param message - Error message describing range issue + * @param metadata - Additional error metadata + */ + static rangeNotSatisfiable(message: string, metadata?: ErrorMetadata): never { + throw new HttpException( + this.buildErrorResponse(message, metadata), + HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE, + ); + } + + /** + * Throws a 417 Expectation Failed exception + * @param message - Error message describing expectation failure + * @param metadata - Additional error metadata + */ + static expectationFailed(message: string, metadata?: ErrorMetadata): never { + throw new HttpException( + this.buildErrorResponse(message, metadata), + HttpStatus.EXPECTATION_FAILED, + ); + } + + /** + * Throws a 418 I'm a teapot exception (Easter egg, but why not?) + * @param message - Error message + * @param metadata - Additional error metadata + */ + static imATeapot(message: string, metadata?: ErrorMetadata): never { + throw new HttpException( + this.buildErrorResponse(message, metadata), + HttpStatus.I_AM_A_TEAPOT, + ); + } + + /** + * Throws a 421 Misdirected Request exception + * @param message - Error message describing misdirected request + * @param metadata - Additional error metadata + */ + static misdirected(message: string, metadata?: ErrorMetadata): never { + throw new HttpException( + this.buildErrorResponse(message, metadata), + HttpStatus.MISDIRECTED, + ); + } + + /** + * Throws a 422 Unprocessable Entity exception + * @param message - Error message describing the unprocessable entity + * @param metadata - Additional error metadata + */ + static unprocessableEntity(message: string, metadata?: ErrorMetadata): never { + throw new UnprocessableEntityException( + this.buildErrorResponse(message, metadata), + ); + } + + /** + * Throws a 423 Locked exception + * @param message - Error message describing the locked resource + * @param metadata - Additional error metadata + */ + static locked(message: string, metadata?: ErrorMetadata): never { + throw new HttpException( + this.buildErrorResponse(message, metadata), + HttpStatus.LOCKED, + ); + } + + /** + * Throws a 424 Failed Dependency exception + * @param message - Error message describing the failed dependency + * @param metadata - Additional error metadata + */ + static failedDependency(message: string, metadata?: ErrorMetadata): never { + throw new HttpException( + this.buildErrorResponse(message, metadata), + HttpStatus.FAILED_DEPENDENCY, + ); + } + + /** + * Throws a 426 Upgrade Required exception + * @param message - Error message describing upgrade requirement + * @param metadata - Additional error metadata + */ + static upgradeRequired(message: string, metadata?: ErrorMetadata): never { + throw new HttpException(this.buildErrorResponse(message, metadata), 426); + } + + /** + * Throws a 428 Precondition Required exception + * @param message - Error message describing precondition requirement + * @param metadata - Additional error metadata + */ + static preconditionRequired( + message: string, + metadata?: ErrorMetadata, + ): never { + throw new HttpException( + this.buildErrorResponse(message, metadata), + HttpStatus.PRECONDITION_REQUIRED, + ); + } + + /** + * Throws a 429 Too Many Requests exception + * @param message - Error message describing rate limit + * @param metadata - Additional error metadata (consider setting retryAfter) + */ + static tooManyRequests(message: string, metadata?: ErrorMetadata): never { + throw new HttpException( + this.buildErrorResponse(message, metadata), + HttpStatus.TOO_MANY_REQUESTS, + ); + } + + /** + * Throws a 431 Request Header Fields Too Large exception + * @param message - Error message describing header size issue + * @param metadata - Additional error metadata + */ + static headersTooLarge(message: string, metadata?: ErrorMetadata): never { + throw new HttpException(this.buildErrorResponse(message, metadata), 431); + } + + /** + * Throws a 451 Unavailable For Legal Reasons exception + * @param message - Error message describing legal restriction + * @param metadata - Additional error metadata + */ + static unavailableForLegalReasons( + message: string, + metadata?: ErrorMetadata, + ): never { + throw new HttpException(this.buildErrorResponse(message, metadata), 451); + } + + // ============================================================================ + // STANDARD HTTP ERRORS (5xx - Server Errors) + // ============================================================================ + /** * Throws a 500 Internal Server Error exception * @param message - Error message describing the internal error + * @param metadata - Additional error metadata + */ + static internalError(message: string, metadata?: ErrorMetadata): never { + throw new InternalServerErrorException( + this.buildErrorResponse(message, metadata), + ); + } + + /** + * Throws a 501 Not Implemented exception + * @param message - Error message describing what's not implemented + * @param metadata - Additional error metadata + */ + static notImplemented(message: string, metadata?: ErrorMetadata): never { + throw new NotImplementedException( + this.buildErrorResponse(message, metadata), + ); + } + + /** + * Throws a 502 Bad Gateway exception + * @param message - Error message describing the bad gateway + * @param metadata - Additional error metadata + */ + static badGateway(message: string, metadata?: ErrorMetadata): never { + throw new HttpException( + this.buildErrorResponse(message, metadata), + HttpStatus.BAD_GATEWAY, + ); + } + + /** + * Throws a 503 Service Unavailable exception + * @param message - Error message describing service unavailability + * @param metadata - Additional error metadata + */ + static serviceUnavailable(message: string, metadata?: ErrorMetadata): never { + throw new ServiceUnavailableException( + this.buildErrorResponse(message, metadata), + ); + } + + /** + * Throws a 504 Gateway Timeout exception + * @param message - Error message describing the gateway timeout + * @param metadata - Additional error metadata + */ + static gatewayTimeout(message: string, metadata?: ErrorMetadata): never { + throw new HttpException( + this.buildErrorResponse(message, metadata), + HttpStatus.GATEWAY_TIMEOUT, + ); + } + + /** + * Throws a 505 HTTP Version Not Supported exception + * @param message - Error message describing HTTP version issue + * @param metadata - Additional error metadata + */ + static httpVersionNotSupported( + message: string, + metadata?: ErrorMetadata, + ): never { + throw new HttpException( + this.buildErrorResponse(message, metadata), + HttpStatus.HTTP_VERSION_NOT_SUPPORTED, + ); + } + + /** + * Throws a 506 Variant Also Negotiates exception + * @param message - Error message + * @param metadata - Additional error metadata + */ + static variantAlsoNegotiates( + message: string, + metadata?: ErrorMetadata, + ): never { + throw new HttpException(this.buildErrorResponse(message, metadata), 506); + } + + /** + * Throws a 507 Insufficient Storage exception + * @param message - Error message describing storage issue + * @param metadata - Additional error metadata + */ + static insufficientStorage(message: string, metadata?: ErrorMetadata): never { + throw new HttpException( + this.buildErrorResponse(message, metadata), + HttpStatus.INSUFFICIENT_STORAGE, + ); + } + + /** + * Throws a 508 Loop Detected exception + * @param message - Error message describing loop detection + * @param metadata - Additional error metadata + */ + static loopDetected(message: string, metadata?: ErrorMetadata): never { + throw new HttpException( + this.buildErrorResponse(message, metadata), + HttpStatus.LOOP_DETECTED, + ); + } + + /** + * Throws a 510 Not Extended exception + * @param message - Error message + * @param metadata - Additional error metadata + */ + static notExtended(message: string, metadata?: ErrorMetadata): never { + throw new HttpException(this.buildErrorResponse(message, metadata), 510); + } + + /** + * Throws a 511 Network Authentication Required exception + * @param message - Error message describing network auth requirement + * @param metadata - Additional error metadata + */ + static networkAuthRequired(message: string, metadata?: ErrorMetadata): never { + throw new HttpException(this.buildErrorResponse(message, metadata), 511); + } + + // ============================================================================ + // SPECIALIZED ERROR HELPERS + // ============================================================================ + + /** + * Throws a validation error with detailed field-level errors + * @param fields - Array of field errors + * @param message - Optional custom message + */ + static validationError( + fields: FieldError[], + message = 'Validation failed', + ): never { + throw new BadRequestException( + this.buildErrorResponse(message, { + fields, + category: ErrorCategory.VALIDATION, + }), + ); + } + + /** + * Throws a single field validation error + * @param field - Field name + * @param message - Error message + * @param value - Optional field value + */ + static fieldError(field: string, message: string, value?: unknown): never { + this.validationError([{ field, message, value }]); + } + + /** + * Throws a database-related error + * @param message - Error message + * @param metadata - Additional error metadata + */ + static databaseError(message: string, metadata?: ErrorMetadata): never { + throw new InternalServerErrorException( + this.buildErrorResponse(message, { + ...metadata, + category: ErrorCategory.DATABASE, + }), + ); + } + + /** + * Throws an external service error + * @param service - Service name + * @param message - Error message + * @param metadata - Additional error metadata + */ + static externalServiceError( + service: string, + message: string, + metadata?: ErrorMetadata, + ): never { + throw new ServiceUnavailableException( + this.buildErrorResponse(message, { + ...metadata, + category: ErrorCategory.EXTERNAL_SERVICE, + context: { service, ...metadata?.context }, + }), + ); + } + + /** + * Throws a rate limit error + * @param message - Error message + * @param retryAfter - Seconds until retry is allowed + * @param metadata - Additional error metadata + */ + static rateLimitExceeded( + message: string, + retryAfter?: number, + metadata?: ErrorMetadata, + ): never { + throw new HttpException( + this.buildErrorResponse(message, { + ...metadata, + category: ErrorCategory.RATE_LIMIT, + retryAfter, + retryable: true, + }), + HttpStatus.TOO_MANY_REQUESTS, + ); + } + + /** + * Throws a business logic error + * @param message - Error message + * @param metadata - Additional error metadata + */ + static businessLogic(message: string, metadata?: ErrorMetadata): never { + throw new BadRequestException( + this.buildErrorResponse(message, { + ...metadata, + category: ErrorCategory.BUSINESS_LOGIC, + }), + ); + } + + /** + * Throws a payment/billing related error + * @param message - Error message + * @param metadata - Additional error metadata + */ + static paymentError(message: string, metadata?: ErrorMetadata): never { + throw new HttpException( + this.buildErrorResponse(message, { + ...metadata, + category: ErrorCategory.PAYMENT, + }), + HttpStatus.PAYMENT_REQUIRED, + ); + } + + /** + * Throws a file operation error + * @param message - Error message + * @param metadata - Additional error metadata + */ + static fileOperationError(message: string, metadata?: ErrorMetadata): never { + throw new InternalServerErrorException( + this.buildErrorResponse(message, { + ...metadata, + category: ErrorCategory.FILE_OPERATION, + }), + ); + } + + /** + * Throws a configuration error + * @param message - Error message + * @param metadata - Additional error metadata */ - static internalError(message: string): never { - throw new InternalServerErrorException(message); + static configurationError(message: string, metadata?: ErrorMetadata): never { + throw new InternalServerErrorException( + this.buildErrorResponse(message, { + ...metadata, + category: ErrorCategory.CONFIGURATION, + severity: ErrorSeverity.CRITICAL, + }), + ); + } + + /** + * Throws a network error + * @param message - Error message + * @param metadata - Additional error metadata + */ + static networkError(message: string, metadata?: ErrorMetadata): never { + throw new ServiceUnavailableException( + this.buildErrorResponse(message, { + ...metadata, + category: ErrorCategory.NETWORK, + retryable: true, + }), + ); + } + + /** + * Throws an authentication error with context + * @param message - Error message + * @param metadata - Additional error metadata + */ + static authenticationError(message: string, metadata?: ErrorMetadata): never { + throw new UnauthorizedException( + this.buildErrorResponse(message, { + ...metadata, + category: ErrorCategory.AUTHENTICATION, + }), + ); + } + + /** + * Throws an authorization/permission error + * @param message - Error message + * @param metadata - Additional error metadata + */ + static authorizationError(message: string, metadata?: ErrorMetadata): never { + throw new ForbiddenException( + this.buildErrorResponse(message, { + ...metadata, + category: ErrorCategory.AUTHORIZATION, + }), + ); + } + + /** + * Throws a duplicate resource error + * @param resource - Resource name + * @param identifier - Optional identifier of the duplicate + * @param metadata - Additional error metadata + */ + static duplicate( + resource: string, + identifier?: string, + metadata?: ErrorMetadata, + ): never { + const message = identifier + ? `${resource} with identifier '${identifier}' already exists` + : `${resource} already exists`; + + throw new ConflictException( + this.buildErrorResponse(message, { + ...metadata, + context: { resource, identifier, ...metadata?.context }, + }), + ); + } + + /** + * Throws a resource not found error with context + * @param resource - Resource name + * @param identifier - Optional identifier that was not found + * @param metadata - Additional error metadata + */ + static resourceNotFound( + resource: string, + identifier?: string, + metadata?: ErrorMetadata, + ): never { + const message = identifier + ? `${resource} with identifier '${identifier}' not found` + : `${resource} not found`; + + throw new NotFoundException( + this.buildErrorResponse(message, { + ...metadata, + context: { resource, identifier, ...metadata?.context }, + }), + ); + } + + /** + * Throws a custom error with any HTTP status code + * @param status - HTTP status code + * @param message - Error message + * @param metadata - Additional error metadata + */ + static custom( + status: HttpStatus, + message: string, + metadata?: ErrorMetadata, + ): never { + throw new HttpException(this.buildErrorResponse(message, metadata), status); + } + + // ============================================================================ + // CONDITIONAL ERROR THROWING + // ============================================================================ + + /** + * Throws an error if condition is true + * @param condition - Condition to check + * @param errorFactory - Function that throws the error + */ + static throwIf(condition: boolean, errorFactory: () => never): void { + if (condition) { + errorFactory(); + } + } + + /** + * Throws an error if condition is false + * @param condition - Condition to check + * @param errorFactory - Function that throws the error + */ + static throwUnless(condition: boolean, errorFactory: () => never): void { + if (!condition) { + errorFactory(); + } + } + + /** + * Throws not found if value is null or undefined + * @param value - Value to check + * @param message - Error message + * @param metadata - Additional error metadata + */ + static throwIfNotFound( + value: T | null | undefined, + message: string, + metadata?: ErrorMetadata, + ): asserts value is T { + if (value === null || value === undefined) { + this.notFound(message, metadata); + } + } + + /** + * Throws forbidden if condition is true + * @param condition - Condition to check + * @param message - Error message + * @param metadata - Additional error metadata + */ + static throwIfForbidden( + condition: boolean, + message: string, + metadata?: ErrorMetadata, + ): void { + if (condition) { + this.forbidden(message, metadata); + } + } + + /** + * Throws unauthorized if condition is true + * @param condition - Condition to check + * @param message - Error message + * @param metadata - Additional error metadata + */ + static throwIfUnauthorized( + condition: boolean, + message: string, + metadata?: ErrorMetadata, + ): void { + if (condition) { + this.unauthorized(message, metadata); + } + } + + // ============================================================================ + // ERROR AGGREGATION + // ============================================================================ + + /** + * Collects multiple errors and throws them together + * Useful for batch validation or multiple operation failures + * @param errors - Array of error messages or FieldError objects + * @param message - Overall error message + */ + static aggregateErrors( + errors: (string | FieldError)[], + message = 'Multiple errors occurred', + ): never { + const fields: FieldError[] = errors.map((error, index) => + typeof error === 'string' + ? { field: `error_${index}`, message: error } + : error, + ); + + this.validationError(fields, message); + } + + // ============================================================================ + // ERROR WRAPPING AND TRANSFORMATION + // ============================================================================ + + /** + * Wraps an unknown error into a structured error + * @param error - The original error + * @param message - Custom message + * @param metadata - Additional error metadata + */ + static wrap( + error: unknown, + message: string, + metadata?: ErrorMetadata, + ): never { + const cause = error instanceof Error ? error : new Error(String(error)); + + throw new InternalServerErrorException( + this.buildErrorResponse(message, { + ...metadata, + cause, + stackTrace: cause.stack, + }), + ); + } + + /** + * Re-throws an error with additional context + * @param error - The original error + * @param additionalContext - Additional context to add + */ + static rethrowWithContext( + error: unknown, + additionalContext: Record, + ): never { + if (error instanceof HttpException) { + const response = error.getResponse(); + + let existingContext: Record = {}; + if ( + typeof response === 'object' && + response !== null && + 'context' in response + ) { + const ctx = (response as Record).context; + existingContext = + typeof ctx === 'object' && ctx !== null + ? (ctx as Record) + : {}; + } + + throw new HttpException( + { + ...(typeof response === 'object' ? response : { message: response }), + context: { ...existingContext, ...additionalContext }, + }, + error.getStatus(), + ); + } + + this.wrap(error, 'An error occurred', { context: additionalContext }); + } + + // ============================================================================ + // HELPER METHODS + // ============================================================================ + + /** + * Generates a unique error ID + */ + static generateErrorId(): string { + return `ERR-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; + } + + /** + * Creates an error metadata builder for fluent API + */ + static metadata(): ErrorMetadataBuilder { + return new ErrorMetadataBuilder(); + } + + /** + * Builds a structured error response with metadata + * @param message - Error message + * @param metadata - Error metadata + */ + private static buildErrorResponse( + message: string, + metadata?: ErrorMetadata, + ): string | ErrorResponse { + if (!metadata || Object.keys(metadata).length === 0) { + return message; + } + + const response: ErrorResponse = { + message, + timestamp: metadata.timestamp || new Date(), + }; + + if (metadata.code) response.code = metadata.code; + if (metadata.errorId) response.errorId = metadata.errorId; + if (metadata.severity) response.severity = metadata.severity; + if (metadata.category) response.category = metadata.category; + if (metadata.retryable !== undefined) + response.retryable = metadata.retryable; + if (metadata.retryAfter) response.retryAfter = metadata.retryAfter; + if (metadata.fields) response.fields = metadata.fields; + if (metadata.context) response.context = metadata.context; + if (metadata.help) response.help = metadata.help; + if (metadata.documentation) response.documentation = metadata.documentation; + if (metadata.userId) response.userId = metadata.userId; + if (metadata.requestId) response.requestId = metadata.requestId; + + // Add any additional custom metadata + Object.keys(metadata).forEach((key) => { + if ( + ![ + 'code', + 'errorId', + 'timestamp', + 'severity', + 'category', + 'retryable', + 'retryAfter', + 'fields', + 'cause', + 'context', + 'help', + 'documentation', + 'userId', + 'requestId', + 'stackTrace', + ].includes(key) + ) { + response[key] = metadata[key]; + } + }); + + return response; + } +} + +/** + * Fluent builder for error metadata + * + * @example + * ErrorBuilder.notFound('User not found', + * ErrorBuilder.metadata() + * .code('USER_NOT_FOUND') + * .severity(ErrorSeverity.MEDIUM) + * .retryable(false) + * .context({ userId: '123' }) + * .build() + * ); + */ +export class ErrorMetadataBuilder { + private metadata: ErrorMetadata = {}; + + code(code: string): this { + this.metadata.code = code; + return this; + } + + errorId(errorId: string): this { + this.metadata.errorId = errorId; + return this; + } + + generateErrorId(): this { + this.metadata.errorId = ErrorBuilder.generateErrorId(); + return this; + } + + timestamp(timestamp: Date): this { + this.metadata.timestamp = timestamp; + return this; + } + + severity(severity: ErrorSeverity): this { + this.metadata.severity = severity; + return this; + } + + category(category: ErrorCategory): this { + this.metadata.category = category; + return this; + } + + retryable(retryable: boolean): this { + this.metadata.retryable = retryable; + return this; + } + + retryAfter(seconds: number): this { + this.metadata.retryAfter = seconds; + return this; + } + + fields(fields: FieldError[]): this { + this.metadata.fields = fields; + return this; + } + + addField(field: FieldError): this { + if (!this.metadata.fields) { + this.metadata.fields = []; + } + this.metadata.fields.push(field); + return this; + } + + cause(cause: Error): this { + this.metadata.cause = cause; + return this; + } + + context(context: Record): this { + this.metadata.context = { ...this.metadata.context, ...context }; + return this; + } + + help(help: string): this { + this.metadata.help = help; + return this; + } + + documentation(url: string): this { + this.metadata.documentation = url; + return this; + } + + userId(userId: string): this { + this.metadata.userId = userId; + return this; + } + + requestId(requestId: string): this { + this.metadata.requestId = requestId; + return this; + } + + custom(key: string, value: unknown): this { + this.metadata[key] = value; + return this; + } + + build(): ErrorMetadata { + return this.metadata; } } diff --git a/apps/api/src/lib/errors/index.ts b/apps/api/src/lib/errors/index.ts index c008d35..cb462af 100644 --- a/apps/api/src/lib/errors/index.ts +++ b/apps/api/src/lib/errors/index.ts @@ -1 +1,9 @@ -export { ErrorBuilder } from './error-builder'; +export { + ErrorBuilder, + ErrorMetadataBuilder, + ErrorSeverity, + ErrorCategory, + type ErrorMetadata, + type ErrorResponse, + type FieldError, +} from './error-builder'; From 45bd07b2de81ca96d88d9f6e64776f13bea52703 Mon Sep 17 00:00:00 2001 From: Anns Shahbaz Date: Thu, 22 Jan 2026 11:24:51 +0500 Subject: [PATCH 4/5] feat: extras removed --- apps/api/src/lib/errors/error-builder.ts | 897 +++-------------------- 1 file changed, 116 insertions(+), 781 deletions(-) diff --git a/apps/api/src/lib/errors/error-builder.ts b/apps/api/src/lib/errors/error-builder.ts index 8296933..7907d2e 100644 --- a/apps/api/src/lib/errors/error-builder.ts +++ b/apps/api/src/lib/errors/error-builder.ts @@ -20,119 +20,16 @@ import { } from '@nestjs/common'; /** - * Severity levels for errors - */ -export enum ErrorSeverity { - LOW = 'low', - MEDIUM = 'medium', - HIGH = 'high', - CRITICAL = 'critical', -} - -/** - * Error categories for better classification - */ -export enum ErrorCategory { - AUTHENTICATION = 'authentication', - AUTHORIZATION = 'authorization', - VALIDATION = 'validation', - BUSINESS_LOGIC = 'business_logic', - DATABASE = 'database', - EXTERNAL_SERVICE = 'external_service', - SYSTEM = 'system', - NETWORK = 'network', - FILE_OPERATION = 'file_operation', - RATE_LIMIT = 'rate_limit', - PAYMENT = 'payment', - CONFIGURATION = 'configuration', -} - -/** - * Interface for field-level validation errors - */ -export interface FieldError { - field: string; - message: string; - value?: unknown; - constraints?: Record; -} - -/** - * Interface for error metadata - */ -export interface ErrorMetadata { - code?: string; - errorId?: string; - timestamp?: Date; - severity?: ErrorSeverity; - category?: ErrorCategory; - retryable?: boolean; - retryAfter?: number; - fields?: FieldError[]; - cause?: Error; - context?: Record; - help?: string; - documentation?: string; - userId?: string; - requestId?: string; - stackTrace?: string; - [key: string]: unknown; -} - -/** - * Interface for structured error response - */ -export interface ErrorResponse { - message: string; - timestamp: Date; - code?: string; - errorId?: string; - severity?: ErrorSeverity; - category?: ErrorCategory; - retryable?: boolean; - retryAfter?: number; - fields?: FieldError[]; - context?: Record; - help?: string; - documentation?: string; - userId?: string; - requestId?: string; - [key: string]: unknown; -} - -/** - * Ultimate centralized error builder with comprehensive error handling capabilities. - * Covers all HTTP status codes, validation, metadata, context building, and more. + * Centralized error builder with comprehensive error handling capabilities. * * @example * // Basic usage * ErrorBuilder.notFound('User not found'); * * @example - * // With metadata - * ErrorBuilder.notFound('User not found', { - * code: 'USER_NOT_FOUND', - * errorId: '12345', - * userId: 'user-123' - * }); - * - * @example - * // Validation errors - * ErrorBuilder.validationError([ - * { field: 'email', message: 'Invalid email format' }, - * { field: 'age', message: 'Must be 18 or older' } - * ]); - * - * @example * // Conditional throwing * ErrorBuilder.throwIf(!user, () => ErrorBuilder.notFound('User not found')); * - * @example - * // Business logic errors - * ErrorBuilder.businessLogic('Cannot delete account with active subscriptions', { - * code: 'ACTIVE_SUBSCRIPTIONS_EXIST', - * context: { subscriptionCount: 3 } - * }); */ export class ErrorBuilder { // ============================================================================ @@ -142,182 +39,138 @@ export class ErrorBuilder { /** * Throws a 400 Bad Request exception * @param message - Error message describing the bad request - * @param metadata - Additional error metadata */ - static badRequest(message: string, metadata?: ErrorMetadata): never { - throw new BadRequestException(this.buildErrorResponse(message, metadata)); + static badRequest(message: string): never { + throw new BadRequestException(message); } /** * Throws a 401 Unauthorized exception * @param message - Error message describing the authorization failure - * @param metadata - Additional error metadata */ - static unauthorized(message: string, metadata?: ErrorMetadata): never { - throw new UnauthorizedException(this.buildErrorResponse(message, metadata)); + static unauthorized(message: string): never { + throw new UnauthorizedException(message); } /** * Throws a 402 Payment Required exception * @param message - Error message describing payment requirement - * @param metadata - Additional error metadata */ - static paymentRequired(message: string, metadata?: ErrorMetadata): never { - throw new HttpException( - this.buildErrorResponse(message, metadata), - HttpStatus.PAYMENT_REQUIRED, - ); + static paymentRequired(message: string): never { + throw new HttpException(message, HttpStatus.PAYMENT_REQUIRED); } /** * Throws a 403 Forbidden exception * @param message - Error message describing why access is forbidden - * @param metadata - Additional error metadata */ - static forbidden(message: string, metadata?: ErrorMetadata): never { - throw new ForbiddenException(this.buildErrorResponse(message, metadata)); + static forbidden(message: string): never { + throw new ForbiddenException(message); } /** * Throws a 404 Not Found exception * @param message - Error message describing what was not found - * @param metadata - Additional error metadata */ - static notFound(message: string, metadata?: ErrorMetadata): never { - throw new NotFoundException(this.buildErrorResponse(message, metadata)); + static notFound(message: string): never { + throw new NotFoundException(message); } /** * Throws a 405 Method Not Allowed exception * @param message - Error message describing the method not allowed - * @param metadata - Additional error metadata */ - static methodNotAllowed(message: string, metadata?: ErrorMetadata): never { - throw new MethodNotAllowedException( - this.buildErrorResponse(message, metadata), - ); + static methodNotAllowed(message: string): never { + throw new MethodNotAllowedException(message); } /** * Throws a 406 Not Acceptable exception * @param message - Error message describing the not acceptable content - * @param metadata - Additional error metadata */ - static notAcceptable(message: string, metadata?: ErrorMetadata): never { - throw new NotAcceptableException( - this.buildErrorResponse(message, metadata), - ); + static notAcceptable(message: string): never { + throw new NotAcceptableException(message); } /** * Throws a 407 Proxy Authentication Required exception * @param message - Error message describing proxy authentication requirement - * @param metadata - Additional error metadata */ - static proxyAuthRequired(message: string, metadata?: ErrorMetadata): never { - throw new HttpException( - this.buildErrorResponse(message, metadata), - HttpStatus.PROXY_AUTHENTICATION_REQUIRED, - ); + static proxyAuthRequired(message: string): never { + throw new HttpException(message, HttpStatus.PROXY_AUTHENTICATION_REQUIRED); } /** * Throws a 408 Request Timeout exception * @param message - Error message describing the timeout - * @param metadata - Additional error metadata */ - static requestTimeout(message: string, metadata?: ErrorMetadata): never { - throw new RequestTimeoutException( - this.buildErrorResponse(message, metadata), - ); + static requestTimeout(message: string): never { + throw new RequestTimeoutException(message); } /** * Throws a 409 Conflict exception * @param message - Error message describing the conflict - * @param metadata - Additional error metadata */ - static conflict(message: string, metadata?: ErrorMetadata): never { - throw new ConflictException(this.buildErrorResponse(message, metadata)); + static conflict(message: string): never { + throw new ConflictException(message); } /** * Throws a 410 Gone exception * @param message - Error message describing what is gone - * @param metadata - Additional error metadata */ - static gone(message: string, metadata?: ErrorMetadata): never { - throw new GoneException(this.buildErrorResponse(message, metadata)); + static gone(message: string): never { + throw new GoneException(message); } /** * Throws a 411 Length Required exception * @param message - Error message describing length requirement - * @param metadata - Additional error metadata */ - static lengthRequired(message: string, metadata?: ErrorMetadata): never { - throw new HttpException( - this.buildErrorResponse(message, metadata), - HttpStatus.LENGTH_REQUIRED, - ); + static lengthRequired(message: string): never { + throw new HttpException(message, HttpStatus.LENGTH_REQUIRED); } /** * Throws a 412 Precondition Failed exception * @param message - Error message describing the failed precondition - * @param metadata - Additional error metadata */ - static preconditionFailed(message: string, metadata?: ErrorMetadata): never { - throw new PreconditionFailedException( - this.buildErrorResponse(message, metadata), - ); + static preconditionFailed(message: string): never { + throw new PreconditionFailedException(message); } /** * Throws a 413 Payload Too Large exception * @param message - Error message describing the payload size issue - * @param metadata - Additional error metadata */ - static payloadTooLarge(message: string, metadata?: ErrorMetadata): never { - throw new PayloadTooLargeException( - this.buildErrorResponse(message, metadata), - ); + static payloadTooLarge(message: string): never { + throw new PayloadTooLargeException(message); } /** * Throws a 414 URI Too Long exception * @param message - Error message describing URI length issue - * @param metadata - Additional error metadata */ - static uriTooLong(message: string, metadata?: ErrorMetadata): never { - throw new HttpException( - this.buildErrorResponse(message, metadata), - HttpStatus.URI_TOO_LONG, - ); + static uriTooLong(message: string): never { + throw new HttpException(message, HttpStatus.URI_TOO_LONG); } /** * Throws a 415 Unsupported Media Type exception * @param message - Error message describing the unsupported media type - * @param metadata - Additional error metadata */ - static unsupportedMediaType( - message: string, - metadata?: ErrorMetadata, - ): never { - throw new UnsupportedMediaTypeException( - this.buildErrorResponse(message, metadata), - ); + static unsupportedMediaType(message: string): never { + throw new UnsupportedMediaTypeException(message); } /** * Throws a 416 Range Not Satisfiable exception * @param message - Error message describing range issue - * @param metadata - Additional error metadata */ - static rangeNotSatisfiable(message: string, metadata?: ErrorMetadata): never { + static rangeNotSatisfiable(message: string): never { throw new HttpException( - this.buildErrorResponse(message, metadata), + message, HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE, ); } @@ -325,129 +178,90 @@ export class ErrorBuilder { /** * Throws a 417 Expectation Failed exception * @param message - Error message describing expectation failure - * @param metadata - Additional error metadata */ - static expectationFailed(message: string, metadata?: ErrorMetadata): never { - throw new HttpException( - this.buildErrorResponse(message, metadata), - HttpStatus.EXPECTATION_FAILED, - ); + static expectationFailed(message: string): never { + throw new HttpException(message, HttpStatus.EXPECTATION_FAILED); } /** * Throws a 418 I'm a teapot exception (Easter egg, but why not?) * @param message - Error message - * @param metadata - Additional error metadata */ - static imATeapot(message: string, metadata?: ErrorMetadata): never { - throw new HttpException( - this.buildErrorResponse(message, metadata), - HttpStatus.I_AM_A_TEAPOT, - ); + static imATeapot(message: string): never { + throw new HttpException(message, HttpStatus.I_AM_A_TEAPOT); } /** * Throws a 421 Misdirected Request exception * @param message - Error message describing misdirected request - * @param metadata - Additional error metadata */ - static misdirected(message: string, metadata?: ErrorMetadata): never { - throw new HttpException( - this.buildErrorResponse(message, metadata), - HttpStatus.MISDIRECTED, - ); + static misdirected(message: string): never { + throw new HttpException(message, HttpStatus.MISDIRECTED); } /** * Throws a 422 Unprocessable Entity exception * @param message - Error message describing the unprocessable entity - * @param metadata - Additional error metadata */ - static unprocessableEntity(message: string, metadata?: ErrorMetadata): never { - throw new UnprocessableEntityException( - this.buildErrorResponse(message, metadata), - ); + static unprocessableEntity(message: string): never { + throw new UnprocessableEntityException(message); } /** * Throws a 423 Locked exception * @param message - Error message describing the locked resource - * @param metadata - Additional error metadata */ - static locked(message: string, metadata?: ErrorMetadata): never { - throw new HttpException( - this.buildErrorResponse(message, metadata), - HttpStatus.LOCKED, - ); + static locked(message: string): never { + throw new HttpException(message, HttpStatus.LOCKED); } /** * Throws a 424 Failed Dependency exception * @param message - Error message describing the failed dependency - * @param metadata - Additional error metadata */ - static failedDependency(message: string, metadata?: ErrorMetadata): never { - throw new HttpException( - this.buildErrorResponse(message, metadata), - HttpStatus.FAILED_DEPENDENCY, - ); + static failedDependency(message: string): never { + throw new HttpException(message, HttpStatus.FAILED_DEPENDENCY); } /** * Throws a 426 Upgrade Required exception * @param message - Error message describing upgrade requirement - * @param metadata - Additional error metadata */ - static upgradeRequired(message: string, metadata?: ErrorMetadata): never { - throw new HttpException(this.buildErrorResponse(message, metadata), 426); + static upgradeRequired(message: string): never { + throw new HttpException(message, 426); } /** * Throws a 428 Precondition Required exception * @param message - Error message describing precondition requirement - * @param metadata - Additional error metadata */ - static preconditionRequired( - message: string, - metadata?: ErrorMetadata, - ): never { - throw new HttpException( - this.buildErrorResponse(message, metadata), - HttpStatus.PRECONDITION_REQUIRED, - ); + static preconditionRequired(message: string): never { + throw new HttpException(message, HttpStatus.PRECONDITION_REQUIRED); } /** * Throws a 429 Too Many Requests exception * @param message - Error message describing rate limit - * @param metadata - Additional error metadata (consider setting retryAfter) + (consider setting retryAfter) */ - static tooManyRequests(message: string, metadata?: ErrorMetadata): never { - throw new HttpException( - this.buildErrorResponse(message, metadata), - HttpStatus.TOO_MANY_REQUESTS, - ); + static tooManyRequests(message: string): never { + throw new HttpException(message, HttpStatus.TOO_MANY_REQUESTS); } /** * Throws a 431 Request Header Fields Too Large exception * @param message - Error message describing header size issue - * @param metadata - Additional error metadata */ - static headersTooLarge(message: string, metadata?: ErrorMetadata): never { - throw new HttpException(this.buildErrorResponse(message, metadata), 431); + static headersTooLarge(message: string): never { + throw new HttpException(message, 431); } /** * Throws a 451 Unavailable For Legal Reasons exception * @param message - Error message describing legal restriction - * @param metadata - Additional error metadata */ - static unavailableForLegalReasons( - message: string, - metadata?: ErrorMetadata, - ): never { - throw new HttpException(this.buildErrorResponse(message, metadata), 451); + static unavailableForLegalReasons(message: string): never { + throw new HttpException(message, 451); } // ============================================================================ @@ -457,375 +271,168 @@ export class ErrorBuilder { /** * Throws a 500 Internal Server Error exception * @param message - Error message describing the internal error - * @param metadata - Additional error metadata */ - static internalError(message: string, metadata?: ErrorMetadata): never { - throw new InternalServerErrorException( - this.buildErrorResponse(message, metadata), - ); + static internalError(message: string): never { + throw new InternalServerErrorException(message); } /** * Throws a 501 Not Implemented exception * @param message - Error message describing what's not implemented - * @param metadata - Additional error metadata */ - static notImplemented(message: string, metadata?: ErrorMetadata): never { - throw new NotImplementedException( - this.buildErrorResponse(message, metadata), - ); + static notImplemented(message: string): never { + throw new NotImplementedException(message); } /** * Throws a 502 Bad Gateway exception * @param message - Error message describing the bad gateway - * @param metadata - Additional error metadata */ - static badGateway(message: string, metadata?: ErrorMetadata): never { - throw new HttpException( - this.buildErrorResponse(message, metadata), - HttpStatus.BAD_GATEWAY, - ); + static badGateway(message: string): never { + throw new HttpException(message, HttpStatus.BAD_GATEWAY); } /** * Throws a 503 Service Unavailable exception * @param message - Error message describing service unavailability - * @param metadata - Additional error metadata */ - static serviceUnavailable(message: string, metadata?: ErrorMetadata): never { - throw new ServiceUnavailableException( - this.buildErrorResponse(message, metadata), - ); + static serviceUnavailable(message: string): never { + throw new ServiceUnavailableException(message); } /** * Throws a 504 Gateway Timeout exception * @param message - Error message describing the gateway timeout - * @param metadata - Additional error metadata */ - static gatewayTimeout(message: string, metadata?: ErrorMetadata): never { - throw new HttpException( - this.buildErrorResponse(message, metadata), - HttpStatus.GATEWAY_TIMEOUT, - ); + static gatewayTimeout(message: string): never { + throw new HttpException(message, HttpStatus.GATEWAY_TIMEOUT); } /** * Throws a 505 HTTP Version Not Supported exception * @param message - Error message describing HTTP version issue - * @param metadata - Additional error metadata */ - static httpVersionNotSupported( - message: string, - metadata?: ErrorMetadata, - ): never { - throw new HttpException( - this.buildErrorResponse(message, metadata), - HttpStatus.HTTP_VERSION_NOT_SUPPORTED, - ); + static httpVersionNotSupported(message: string): never { + throw new HttpException(message, HttpStatus.HTTP_VERSION_NOT_SUPPORTED); } /** * Throws a 506 Variant Also Negotiates exception * @param message - Error message - * @param metadata - Additional error metadata */ - static variantAlsoNegotiates( - message: string, - metadata?: ErrorMetadata, - ): never { - throw new HttpException(this.buildErrorResponse(message, metadata), 506); + static variantAlsoNegotiates(message: string): never { + throw new HttpException(message, 506); } /** * Throws a 507 Insufficient Storage exception * @param message - Error message describing storage issue - * @param metadata - Additional error metadata */ - static insufficientStorage(message: string, metadata?: ErrorMetadata): never { - throw new HttpException( - this.buildErrorResponse(message, metadata), - HttpStatus.INSUFFICIENT_STORAGE, - ); + static insufficientStorage(message: string): never { + throw new HttpException(message, HttpStatus.INSUFFICIENT_STORAGE); } /** * Throws a 508 Loop Detected exception * @param message - Error message describing loop detection - * @param metadata - Additional error metadata */ - static loopDetected(message: string, metadata?: ErrorMetadata): never { - throw new HttpException( - this.buildErrorResponse(message, metadata), - HttpStatus.LOOP_DETECTED, - ); + static loopDetected(message: string): never { + throw new HttpException(message, HttpStatus.LOOP_DETECTED); } /** * Throws a 510 Not Extended exception * @param message - Error message - * @param metadata - Additional error metadata */ - static notExtended(message: string, metadata?: ErrorMetadata): never { - throw new HttpException(this.buildErrorResponse(message, metadata), 510); + static notExtended(message: string): never { + throw new HttpException(message, 510); } /** * Throws a 511 Network Authentication Required exception * @param message - Error message describing network auth requirement - * @param metadata - Additional error metadata */ - static networkAuthRequired(message: string, metadata?: ErrorMetadata): never { - throw new HttpException(this.buildErrorResponse(message, metadata), 511); + static networkAuthRequired(message: string): never { + throw new HttpException(message, 511); } // ============================================================================ // SPECIALIZED ERROR HELPERS // ============================================================================ - /** - * Throws a validation error with detailed field-level errors - * @param fields - Array of field errors - * @param message - Optional custom message - */ - static validationError( - fields: FieldError[], - message = 'Validation failed', - ): never { - throw new BadRequestException( - this.buildErrorResponse(message, { - fields, - category: ErrorCategory.VALIDATION, - }), - ); - } - - /** - * Throws a single field validation error - * @param field - Field name - * @param message - Error message - * @param value - Optional field value - */ - static fieldError(field: string, message: string, value?: unknown): never { - this.validationError([{ field, message, value }]); - } - /** * Throws a database-related error * @param message - Error message - * @param metadata - Additional error metadata */ - static databaseError(message: string, metadata?: ErrorMetadata): never { - throw new InternalServerErrorException( - this.buildErrorResponse(message, { - ...metadata, - category: ErrorCategory.DATABASE, - }), - ); + static databaseError(message: string): never { + throw new InternalServerErrorException(message); } /** * Throws an external service error * @param service - Service name - * @param message - Error message - * @param metadata - Additional error metadata */ - static externalServiceError( - service: string, - message: string, - metadata?: ErrorMetadata, - ): never { - throw new ServiceUnavailableException( - this.buildErrorResponse(message, { - ...metadata, - category: ErrorCategory.EXTERNAL_SERVICE, - context: { service, ...metadata?.context }, - }), - ); + static externalServiceError(service: string): never { + throw new ServiceUnavailableException(service); } /** * Throws a rate limit error * @param message - Error message - * @param retryAfter - Seconds until retry is allowed - * @param metadata - Additional error metadata */ - static rateLimitExceeded( - message: string, - retryAfter?: number, - metadata?: ErrorMetadata, - ): never { - throw new HttpException( - this.buildErrorResponse(message, { - ...metadata, - category: ErrorCategory.RATE_LIMIT, - retryAfter, - retryable: true, - }), - HttpStatus.TOO_MANY_REQUESTS, - ); - } - - /** - * Throws a business logic error - * @param message - Error message - * @param metadata - Additional error metadata - */ - static businessLogic(message: string, metadata?: ErrorMetadata): never { - throw new BadRequestException( - this.buildErrorResponse(message, { - ...metadata, - category: ErrorCategory.BUSINESS_LOGIC, - }), - ); - } - - /** - * Throws a payment/billing related error - * @param message - Error message - * @param metadata - Additional error metadata - */ - static paymentError(message: string, metadata?: ErrorMetadata): never { - throw new HttpException( - this.buildErrorResponse(message, { - ...metadata, - category: ErrorCategory.PAYMENT, - }), - HttpStatus.PAYMENT_REQUIRED, - ); - } - - /** - * Throws a file operation error - * @param message - Error message - * @param metadata - Additional error metadata - */ - static fileOperationError(message: string, metadata?: ErrorMetadata): never { - throw new InternalServerErrorException( - this.buildErrorResponse(message, { - ...metadata, - category: ErrorCategory.FILE_OPERATION, - }), - ); - } - - /** - * Throws a configuration error - * @param message - Error message - * @param metadata - Additional error metadata - */ - static configurationError(message: string, metadata?: ErrorMetadata): never { - throw new InternalServerErrorException( - this.buildErrorResponse(message, { - ...metadata, - category: ErrorCategory.CONFIGURATION, - severity: ErrorSeverity.CRITICAL, - }), - ); - } - - /** - * Throws a network error - * @param message - Error message - * @param metadata - Additional error metadata - */ - static networkError(message: string, metadata?: ErrorMetadata): never { - throw new ServiceUnavailableException( - this.buildErrorResponse(message, { - ...metadata, - category: ErrorCategory.NETWORK, - retryable: true, - }), - ); - } - - /** - * Throws an authentication error with context - * @param message - Error message - * @param metadata - Additional error metadata - */ - static authenticationError(message: string, metadata?: ErrorMetadata): never { - throw new UnauthorizedException( - this.buildErrorResponse(message, { - ...metadata, - category: ErrorCategory.AUTHENTICATION, - }), - ); - } - - /** - * Throws an authorization/permission error - * @param message - Error message - * @param metadata - Additional error metadata - */ - static authorizationError(message: string, metadata?: ErrorMetadata): never { - throw new ForbiddenException( - this.buildErrorResponse(message, { - ...metadata, - category: ErrorCategory.AUTHORIZATION, - }), - ); + static rateLimitExceeded(message: string): never { + throw new HttpException(message, HttpStatus.TOO_MANY_REQUESTS); } /** * Throws a duplicate resource error * @param resource - Resource name * @param identifier - Optional identifier of the duplicate - * @param metadata - Additional error metadata */ - static duplicate( - resource: string, - identifier?: string, - metadata?: ErrorMetadata, - ): never { + static duplicate(resource: string, identifier?: string): never { const message = identifier ? `${resource} with identifier '${identifier}' already exists` : `${resource} already exists`; - throw new ConflictException( - this.buildErrorResponse(message, { - ...metadata, - context: { resource, identifier, ...metadata?.context }, - }), - ); + throw new ConflictException(message); } /** * Throws a resource not found error with context * @param resource - Resource name * @param identifier - Optional identifier that was not found - * @param metadata - Additional error metadata */ - static resourceNotFound( - resource: string, - identifier?: string, - metadata?: ErrorMetadata, - ): never { + static resourceNotFound(resource: string, identifier?: string): never { const message = identifier ? `${resource} with identifier '${identifier}' not found` : `${resource} not found`; - throw new NotFoundException( - this.buildErrorResponse(message, { - ...metadata, - context: { resource, identifier, ...metadata?.context }, - }), - ); + throw new NotFoundException(message); + } + + /** + * Throws a type error new TypeError(message) + * @param message - Error message + */ + static typeMismatch(message: string): never { + throw new TypeError(message); } /** - * Throws a custom error with any HTTP status code + * Throws a custom exception with any HTTP status code * @param status - HTTP status code * @param message - Error message - * @param metadata - Additional error metadata */ - static custom( - status: HttpStatus, - message: string, - metadata?: ErrorMetadata, - ): never { - throw new HttpException(this.buildErrorResponse(message, metadata), status); + static customException(status: HttpStatus, message: string): never { + throw new HttpException(message, status); + } + + /** + * Throws a custom error new Error(message) + * @param message - Error message + */ + static customError(message: string): never { + throw new Error(message); } // ============================================================================ @@ -858,15 +465,13 @@ export class ErrorBuilder { * Throws not found if value is null or undefined * @param value - Value to check * @param message - Error message - * @param metadata - Additional error metadata */ static throwIfNotFound( value: T | null | undefined, message: string, - metadata?: ErrorMetadata, ): asserts value is T { if (value === null || value === undefined) { - this.notFound(message, metadata); + this.notFound(message); } } @@ -874,15 +479,10 @@ export class ErrorBuilder { * Throws forbidden if condition is true * @param condition - Condition to check * @param message - Error message - * @param metadata - Additional error metadata */ - static throwIfForbidden( - condition: boolean, - message: string, - metadata?: ErrorMetadata, - ): void { + static throwIfForbidden(condition: boolean, message: string): void { if (condition) { - this.forbidden(message, metadata); + this.forbidden(message); } } @@ -890,41 +490,13 @@ export class ErrorBuilder { * Throws unauthorized if condition is true * @param condition - Condition to check * @param message - Error message - * @param metadata - Additional error metadata */ - static throwIfUnauthorized( - condition: boolean, - message: string, - metadata?: ErrorMetadata, - ): void { + static throwIfUnauthorized(condition: boolean, message: string): void { if (condition) { - this.unauthorized(message, metadata); + this.unauthorized(message); } } - // ============================================================================ - // ERROR AGGREGATION - // ============================================================================ - - /** - * Collects multiple errors and throws them together - * Useful for batch validation or multiple operation failures - * @param errors - Array of error messages or FieldError objects - * @param message - Overall error message - */ - static aggregateErrors( - errors: (string | FieldError)[], - message = 'Multiple errors occurred', - ): never { - const fields: FieldError[] = errors.map((error, index) => - typeof error === 'string' - ? { field: `error_${index}`, message: error } - : error, - ); - - this.validationError(fields, message); - } - // ============================================================================ // ERROR WRAPPING AND TRANSFORMATION // ============================================================================ @@ -932,246 +504,9 @@ export class ErrorBuilder { /** * Wraps an unknown error into a structured error * @param error - The original error - * @param message - Custom message - * @param metadata - Additional error metadata */ - static wrap( - error: unknown, - message: string, - metadata?: ErrorMetadata, - ): never { + static wrap(error: unknown): never { const cause = error instanceof Error ? error : new Error(String(error)); - - throw new InternalServerErrorException( - this.buildErrorResponse(message, { - ...metadata, - cause, - stackTrace: cause.stack, - }), - ); - } - - /** - * Re-throws an error with additional context - * @param error - The original error - * @param additionalContext - Additional context to add - */ - static rethrowWithContext( - error: unknown, - additionalContext: Record, - ): never { - if (error instanceof HttpException) { - const response = error.getResponse(); - - let existingContext: Record = {}; - if ( - typeof response === 'object' && - response !== null && - 'context' in response - ) { - const ctx = (response as Record).context; - existingContext = - typeof ctx === 'object' && ctx !== null - ? (ctx as Record) - : {}; - } - - throw new HttpException( - { - ...(typeof response === 'object' ? response : { message: response }), - context: { ...existingContext, ...additionalContext }, - }, - error.getStatus(), - ); - } - - this.wrap(error, 'An error occurred', { context: additionalContext }); - } - - // ============================================================================ - // HELPER METHODS - // ============================================================================ - - /** - * Generates a unique error ID - */ - static generateErrorId(): string { - return `ERR-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; - } - - /** - * Creates an error metadata builder for fluent API - */ - static metadata(): ErrorMetadataBuilder { - return new ErrorMetadataBuilder(); - } - - /** - * Builds a structured error response with metadata - * @param message - Error message - * @param metadata - Error metadata - */ - private static buildErrorResponse( - message: string, - metadata?: ErrorMetadata, - ): string | ErrorResponse { - if (!metadata || Object.keys(metadata).length === 0) { - return message; - } - - const response: ErrorResponse = { - message, - timestamp: metadata.timestamp || new Date(), - }; - - if (metadata.code) response.code = metadata.code; - if (metadata.errorId) response.errorId = metadata.errorId; - if (metadata.severity) response.severity = metadata.severity; - if (metadata.category) response.category = metadata.category; - if (metadata.retryable !== undefined) - response.retryable = metadata.retryable; - if (metadata.retryAfter) response.retryAfter = metadata.retryAfter; - if (metadata.fields) response.fields = metadata.fields; - if (metadata.context) response.context = metadata.context; - if (metadata.help) response.help = metadata.help; - if (metadata.documentation) response.documentation = metadata.documentation; - if (metadata.userId) response.userId = metadata.userId; - if (metadata.requestId) response.requestId = metadata.requestId; - - // Add any additional custom metadata - Object.keys(metadata).forEach((key) => { - if ( - ![ - 'code', - 'errorId', - 'timestamp', - 'severity', - 'category', - 'retryable', - 'retryAfter', - 'fields', - 'cause', - 'context', - 'help', - 'documentation', - 'userId', - 'requestId', - 'stackTrace', - ].includes(key) - ) { - response[key] = metadata[key]; - } - }); - - return response; - } -} - -/** - * Fluent builder for error metadata - * - * @example - * ErrorBuilder.notFound('User not found', - * ErrorBuilder.metadata() - * .code('USER_NOT_FOUND') - * .severity(ErrorSeverity.MEDIUM) - * .retryable(false) - * .context({ userId: '123' }) - * .build() - * ); - */ -export class ErrorMetadataBuilder { - private metadata: ErrorMetadata = {}; - - code(code: string): this { - this.metadata.code = code; - return this; - } - - errorId(errorId: string): this { - this.metadata.errorId = errorId; - return this; - } - - generateErrorId(): this { - this.metadata.errorId = ErrorBuilder.generateErrorId(); - return this; - } - - timestamp(timestamp: Date): this { - this.metadata.timestamp = timestamp; - return this; - } - - severity(severity: ErrorSeverity): this { - this.metadata.severity = severity; - return this; - } - - category(category: ErrorCategory): this { - this.metadata.category = category; - return this; - } - - retryable(retryable: boolean): this { - this.metadata.retryable = retryable; - return this; - } - - retryAfter(seconds: number): this { - this.metadata.retryAfter = seconds; - return this; - } - - fields(fields: FieldError[]): this { - this.metadata.fields = fields; - return this; - } - - addField(field: FieldError): this { - if (!this.metadata.fields) { - this.metadata.fields = []; - } - this.metadata.fields.push(field); - return this; - } - - cause(cause: Error): this { - this.metadata.cause = cause; - return this; - } - - context(context: Record): this { - this.metadata.context = { ...this.metadata.context, ...context }; - return this; - } - - help(help: string): this { - this.metadata.help = help; - return this; - } - - documentation(url: string): this { - this.metadata.documentation = url; - return this; - } - - userId(userId: string): this { - this.metadata.userId = userId; - return this; - } - - requestId(requestId: string): this { - this.metadata.requestId = requestId; - return this; - } - - custom(key: string, value: unknown): this { - this.metadata[key] = value; - return this; - } - - build(): ErrorMetadata { - return this.metadata; + throw new InternalServerErrorException(cause); } } From 91bdb9174b783f1d718aba73a2fbd63746362a82 Mon Sep 17 00:00:00 2001 From: Anns Shahbaz Date: Thu, 22 Jan 2026 11:30:56 +0500 Subject: [PATCH 5/5] misc. --- apps/api/src/lib/errors/error-builder.ts | 8 ++++++++ apps/api/src/lib/errors/index.ts | 10 +--------- .../src/modules/crud/services/crud.mongoose.service.ts | 7 +------ 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/apps/api/src/lib/errors/error-builder.ts b/apps/api/src/lib/errors/error-builder.ts index 7907d2e..9de1612 100644 --- a/apps/api/src/lib/errors/error-builder.ts +++ b/apps/api/src/lib/errors/error-builder.ts @@ -360,6 +360,14 @@ export class ErrorBuilder { // SPECIALIZED ERROR HELPERS // ============================================================================ + /** + * Throws a database-related error + * @param message - Error message + */ + static validationError(message: string): never { + throw new BadRequestException(message); + } + /** * Throws a database-related error * @param message - Error message diff --git a/apps/api/src/lib/errors/index.ts b/apps/api/src/lib/errors/index.ts index cb462af..c008d35 100644 --- a/apps/api/src/lib/errors/index.ts +++ b/apps/api/src/lib/errors/index.ts @@ -1,9 +1 @@ -export { - ErrorBuilder, - ErrorMetadataBuilder, - ErrorSeverity, - ErrorCategory, - type ErrorMetadata, - type ErrorResponse, - type FieldError, -} from './error-builder'; +export { ErrorBuilder } from './error-builder'; diff --git a/apps/api/src/modules/crud/services/crud.mongoose.service.ts b/apps/api/src/modules/crud/services/crud.mongoose.service.ts index 25768cb..501e259 100644 --- a/apps/api/src/modules/crud/services/crud.mongoose.service.ts +++ b/apps/api/src/modules/crud/services/crud.mongoose.service.ts @@ -18,12 +18,7 @@ export class CrudMongooseService { async createCrud(data: Partial): Promise { if (StringExtensions.IsNullOrEmpty(data.content)) { - return ErrorBuilder.validationError([ - { - field: 'content', - message: 'Content cannot be empty', - }, - ]); + return ErrorBuilder.validationError('Content cannot be empty'); } const created = await this.crudRepository.create({