From 5534f06f21d4a81048e64c5121a5fa3e65fbf5e1 Mon Sep 17 00:00:00 2001 From: david Date: Sat, 4 Oct 2025 17:40:41 +0200 Subject: [PATCH] Cambios --- .../rdx-ddd/src/errors/application-error.ts | 13 +++ packages/rdx-ddd/src/errors/base-error.ts | 38 ++++++++ packages/rdx-ddd/src/errors/domain-error.ts | 21 +++-- .../src/errors/domain-validation-error.ts | 94 +++++++++++++------ packages/rdx-ddd/src/errors/index.ts | 3 + .../src/errors/infrastructure-error.ts | 9 ++ .../src/errors/validation-error-collection.ts | 66 +++++++++++-- .../src/helpers/extract-or-push-error.ts | 20 ++-- 8 files changed, 212 insertions(+), 52 deletions(-) create mode 100644 packages/rdx-ddd/src/errors/application-error.ts create mode 100644 packages/rdx-ddd/src/errors/base-error.ts create mode 100644 packages/rdx-ddd/src/errors/infrastructure-error.ts diff --git a/packages/rdx-ddd/src/errors/application-error.ts b/packages/rdx-ddd/src/errors/application-error.ts new file mode 100644 index 00000000..981e2dde --- /dev/null +++ b/packages/rdx-ddd/src/errors/application-error.ts @@ -0,0 +1,13 @@ +import { BaseError } from "./base-error"; + +/** Errores de aplicación: orquestación, validaciones de caso de uso, seguridad, idempotencia */ +export class ApplicationError extends BaseError<"application"> { + public readonly layer = "application" as const; + constructor( + message: string, + code = "APPLICATION_ERROR", + options?: ErrorOptions & { metadata?: Record } + ) { + super("ApplicationError", message, code, options); + } +} diff --git a/packages/rdx-ddd/src/errors/base-error.ts b/packages/rdx-ddd/src/errors/base-error.ts new file mode 100644 index 00000000..dd63a940 --- /dev/null +++ b/packages/rdx-ddd/src/errors/base-error.ts @@ -0,0 +1,38 @@ +/** + * Error base para todas las capas con soporte de `code`, `cause` y serialización segura. + * Mantiene instanceof correcto y añade un discriminador de capa. + */ + +export abstract class BaseError< + L extends "domain" | "application" | "infrastructure", +> extends Error { + public abstract readonly layer: L; + public readonly code: string; + public readonly metadata?: Record; + + constructor( + name: string, + message: string, + code: string, + options?: ErrorOptions & { metadata?: Record } + ) { + super(message, options); + this.name = name; + this.code = code; + this.metadata = options?.metadata; + Object.setPrototypeOf(this, new.target.prototype); // mantener instanceof + } + + /** Serialización segura para logs/observabilidad */ + toJSON() { + const cause = (this as any).cause as unknown | undefined; + return { + name: this.name, + message: this.message, + code: this.code, + layer: this.layer, + metadata: this.metadata, + cause: cause instanceof Error ? { name: cause.name, message: cause.message } : undefined, + }; + } +} diff --git a/packages/rdx-ddd/src/errors/domain-error.ts b/packages/rdx-ddd/src/errors/domain-error.ts index 1618e4f5..e95ce83b 100644 --- a/packages/rdx-ddd/src/errors/domain-error.ts +++ b/packages/rdx-ddd/src/errors/domain-error.ts @@ -1,12 +1,21 @@ /** - * Errores de capa de dominio. No deben "filtrarse" a cliente tal cual. + * Error base para la capa de dominio. + * Se usa para clasificar y manipular errores de dominio sin depender del texto de `message`. * - * */ + * Ejemplo: throw new DomainError("Customer not found", "CUSTOMER_NOT_FOUND", { cause: err }); + */ -export class DomainError extends Error { +// domain/errors/domain-error.ts +import { BaseError } from "./base-error"; + +/** Error base de dominio: no depende de infra ni HTTP */ +export class DomainError extends BaseError<"domain"> { public readonly layer = "domain" as const; - constructor(message: string, options?: ErrorOptions) { - super(message, options); - Object.setPrototypeOf(this, new.target.prototype); + constructor( + message: string, + code = "DOMAIN_ERROR", + options?: ErrorOptions & { metadata?: Record } + ) { + super("DomainError", message, code, options); } } diff --git a/packages/rdx-ddd/src/errors/domain-validation-error.ts b/packages/rdx-ddd/src/errors/domain-validation-error.ts index 5631888c..3f1c40a0 100644 --- a/packages/rdx-ddd/src/errors/domain-validation-error.ts +++ b/packages/rdx-ddd/src/errors/domain-validation-error.ts @@ -1,60 +1,94 @@ -/** - * Clase DomainValidationError - * Representa un error de validación de dominio. - * - * Esta clase extiende la clase Error de JavaScript y se utiliza para manejar errores - * específicos de validación dentro del dominio de la aplicación. Permite identificar - * el código de error, el campo afectado y un detalle descriptivo del error. - * - * @class DomainValidationError - * @extends {Error} - * @property {string} code - Código del error de validación. - * @property {string} field - Campo afectado por el error de validación. - * @property {string} detail - Detalle descriptivo del error de validación. - * - * @example - * const error = new DomainValidationError("INVALID_EMAIL", "email", "El email no es válido"); - * console.error(error); - */ - import { DomainError } from "./domain-error"; +/** + * Error de validación de dominio. + * + * - Extiende DomainError para aprovechar `code`, `metadata`, `cause` y `toJSON`. + * - Estructura estable para mapeo (Problem+JSON / telemetría). + */ export class DomainValidationError extends DomainError { - // Discriminante estable para mapeo/telemetría + /** Discriminante para routing/telemetría */ public readonly kind = "VALIDATION" as const; + /** Regla/identificador de error de validación (ej. INVALID_EMAIL) */ + public readonly code: string; + + /** Campo afectado (path) */ + public readonly field: string; + + /** Mensaje legible de negocio */ + public readonly detail: string; + constructor( - public readonly code: string, // id de regla del negocio (ej. 'INVALID_FORMAT') - public readonly field: string, // path: 'number' | 'date' | 'lines[0].quantity' - public readonly detail: string, // mensaje legible del negocio - options?: ErrorOptions + code: string, + field: string, + detail: string, + options?: ErrorOptions & { metadata?: Record } ) { - super(`[${field}] ${detail}`, options); + // Mensaje humano compacto y útil para logs + super(`[${field}] ${detail}`, code, { + ...options, + // Aseguramos metadatos ricos y estables + metadata: { + ...(options?.metadata ?? {}), + field, + detail, + kind: "VALIDATION", + }, + }); + this.name = "DomainValidationError"; + this.code = code; + this.field = field; + this.detail = detail; + + Object.setPrototypeOf(this, new.target.prototype); Object.freeze(this); } - // Constructores rápidos - static requiredValue(field: string, options?: ErrorOptions) { + /** Atajos de construcción comunes */ + static requiredValue( + field: string, + options?: ErrorOptions & { metadata?: Record } + ) { return new DomainValidationError("REQUIRED_VALUE", field, "cannot be empty", options); } - static invalidFormat(field: string, detail = "invalid format", options?: ErrorOptions) { + + static invalidFormat( + field: string, + detail = "invalid format", + options?: ErrorOptions & { metadata?: Record } + ) { return new DomainValidationError("INVALID_FORMAT", field, detail, options); } + static invalidValue( field: string, value: unknown, detail = "invalid value", - options?: ErrorOptions + options?: ErrorOptions & { metadata?: Record } ) { + // `cause` preserva el valor problemático para inspección sin exponerlo a cliente return new DomainValidationError("INVALID_VALUE", field, detail, { ...options, cause: value }); } - // Proyección útil para Problem+JSON o colecciones + /** Proyección mínima para Problem+JSON / colecciones */ toDetail() { return { path: this.field, message: this.detail, rule: this.code }; } + + /** Incluye proyección validación en la serialización segura */ + override toJSON() { + const base = super.toJSON(); + return { + ...base, + kind: this.kind, + field: this.field, + detail: this.detail, + }; + } } +/** Type guard */ export const isDomainValidationError = (e: unknown): e is DomainValidationError => e instanceof DomainValidationError; diff --git a/packages/rdx-ddd/src/errors/index.ts b/packages/rdx-ddd/src/errors/index.ts index 93ab6a81..787467af 100644 --- a/packages/rdx-ddd/src/errors/index.ts +++ b/packages/rdx-ddd/src/errors/index.ts @@ -1,3 +1,6 @@ +export * from "./application-error"; +export * from "./base-error"; export * from "./domain-error"; export * from "./domain-validation-error"; +export * from "./infrastructure-error"; export * from "./validation-error-collection"; diff --git a/packages/rdx-ddd/src/errors/infrastructure-error.ts b/packages/rdx-ddd/src/errors/infrastructure-error.ts new file mode 100644 index 00000000..d40c53fb --- /dev/null +++ b/packages/rdx-ddd/src/errors/infrastructure-error.ts @@ -0,0 +1,9 @@ +import { BaseError } from "./base-error"; + +/** Errores de infraestructura: DB, red, serialización, proveedores externos */ +export class InfrastructureError extends BaseError<"infrastructure"> { + public readonly layer = "infrastructure" as const; + constructor(message: string, code = "INFRASTRUCTURE_ERROR", options?: ErrorOptions & { metadata?: Record }) { + super("InfrastructureError", message, code, options); + } +} diff --git a/packages/rdx-ddd/src/errors/validation-error-collection.ts b/packages/rdx-ddd/src/errors/validation-error-collection.ts index aa341fd1..d0ef914b 100644 --- a/packages/rdx-ddd/src/errors/validation-error-collection.ts +++ b/packages/rdx-ddd/src/errors/validation-error-collection.ts @@ -16,30 +16,76 @@ */ import { DomainError } from "./domain-error"; +import { DomainValidationError } from "./domain-validation-error"; export interface ValidationErrorDetail { - path?: string; // ejemplo: "lines[1].unitPrice.amount" - message: string; // ejemplo: "Amount must be a positive number", - value?: unknown; // valor inválido opcional + /** Path del campo inválido, ej. "lines[1].unitPrice.amount" */ + path?: string; + /** Mensaje legible del fallo */ + message: string; + /** Valor inválido (no se debe exponer directamente en API pública si es sensible) */ + value?: unknown; } /** - * Error de validación múltiple. Agrega varios fallos de una sola vez. + * Error de validación múltiple de dominio. + * Permite agrupar varios fallos de validación en una única instancia. + * + * Ejemplo: + * const errors: ValidationErrorDetail[] = [ + * { path: "lines[1].unitPrice.amount", message: "Amount must be positive" }, + * { path: "lines[1].unitPrice.scale", message: "Scale must be non-negative" }, + * ]; + * throw new ValidationErrorCollection(errors); */ - export class ValidationErrorCollection extends DomainError { - public readonly code = "VALIDATION" as const; + public readonly kind = "VALIDATION" as const; + public readonly code = "MULTIPLE_VALIDATION_ERRORS" as const; public readonly details: ValidationErrorDetail[]; - constructor(message: string, details: ValidationErrorDetail[], options?: ErrorOptions) { - super(message, options); - Object.setPrototypeOf(this, ValidationErrorCollection.prototype); - + constructor( + details: ValidationErrorDetail[], + options?: ErrorOptions & { metadata?: Record } + ) { + super("Multiple validation errors", "MULTIPLE_VALIDATION_ERRORS", { + ...options, + metadata: { ...(options?.metadata ?? {}), errors: details }, + }); this.name = "ValidationErrorCollection"; this.details = details; + + Object.setPrototypeOf(this, new.target.prototype); Object.freeze(this); } + + /** Crear a partir de varios DomainValidationError */ + static fromErrors( + errors: DomainValidationError[], + options?: ErrorOptions & { metadata?: Record } + ): ValidationErrorCollection { + const details: ValidationErrorDetail[] = errors.map((e) => ({ + path: e.field, + message: e.detail, + value: (e as any).cause, // opcional: valor que provocó el error + })); + + return new ValidationErrorCollection(details, options); + } + + /** Serialización para Problem+JSON / logs */ + override toJSON() { + const base = super.toJSON(); + return { + ...base, + kind: this.kind, + details: this.details.map((d) => ({ + path: d.path, + message: d.message, + })), + }; + } } +/** Type guard */ export const isValidationErrorCollection = (e: unknown): e is ValidationErrorCollection => e instanceof ValidationErrorCollection; diff --git a/packages/rdx-ddd/src/helpers/extract-or-push-error.ts b/packages/rdx-ddd/src/helpers/extract-or-push-error.ts index f2b56d4e..cce96696 100644 --- a/packages/rdx-ddd/src/helpers/extract-or-push-error.ts +++ b/packages/rdx-ddd/src/helpers/extract-or-push-error.ts @@ -38,17 +38,25 @@ export function extractOrPushError( const error = result.error; if (isValidationErrorCollection(error)) { - // Agrega todos los detalles de error al array proporcionado - error.details.forEach((error) => { + // Copiar todos los detalles, rellenando path si falta + error.details.forEach((detail) => { errors.push({ - ...error, - path: error.path || path, + ...detail, + path: detail.path ?? path, }); }); } else if (isDomainValidationError(error)) { - errors.push({ path, message: error.detail, value: error.cause?.toString() }); + errors.push({ + path, + message: error.detail, + value: (error as any).cause, // mantener la causa original + }); } else { - errors.push({ path, message: error.message }); + // Fallback genérico: Error desconocido tratado como validación simple + errors.push({ + path, + message: error.message ?? "Unknown error", + }); } return undefined;