Cambios
This commit is contained in:
parent
6e5b14305e
commit
5534f06f21
13
packages/rdx-ddd/src/errors/application-error.ts
Normal file
13
packages/rdx-ddd/src/errors/application-error.ts
Normal file
@ -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<string, unknown> }
|
||||
) {
|
||||
super("ApplicationError", message, code, options);
|
||||
}
|
||||
}
|
||||
38
packages/rdx-ddd/src/errors/base-error.ts
Normal file
38
packages/rdx-ddd/src/errors/base-error.ts
Normal file
@ -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<string, unknown>;
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
message: string,
|
||||
code: string,
|
||||
options?: ErrorOptions & { metadata?: Record<string, unknown> }
|
||||
) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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<string, unknown> }
|
||||
) {
|
||||
super("DomainError", message, code, options);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<string, unknown> }
|
||||
) {
|
||||
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<string, unknown> }
|
||||
) {
|
||||
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<string, unknown> }
|
||||
) {
|
||||
return new DomainValidationError("INVALID_FORMAT", field, detail, options);
|
||||
}
|
||||
|
||||
static invalidValue(
|
||||
field: string,
|
||||
value: unknown,
|
||||
detail = "invalid value",
|
||||
options?: ErrorOptions
|
||||
options?: ErrorOptions & { metadata?: Record<string, unknown> }
|
||||
) {
|
||||
// `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;
|
||||
|
||||
@ -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";
|
||||
|
||||
9
packages/rdx-ddd/src/errors/infrastructure-error.ts
Normal file
9
packages/rdx-ddd/src/errors/infrastructure-error.ts
Normal file
@ -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<string, unknown> }) {
|
||||
super("InfrastructureError", message, code, options);
|
||||
}
|
||||
}
|
||||
@ -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<string, unknown> }
|
||||
) {
|
||||
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<string, unknown> }
|
||||
): 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;
|
||||
|
||||
@ -38,17 +38,25 @@ export function extractOrPushError<T>(
|
||||
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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user