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;
|
public readonly layer = "domain" as const;
|
||||||
constructor(message: string, options?: ErrorOptions) {
|
constructor(
|
||||||
super(message, options);
|
message: string,
|
||||||
Object.setPrototypeOf(this, new.target.prototype);
|
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";
|
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 {
|
export class DomainValidationError extends DomainError {
|
||||||
// Discriminante estable para mapeo/telemetría
|
/** Discriminante para routing/telemetría */
|
||||||
public readonly kind = "VALIDATION" as const;
|
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(
|
constructor(
|
||||||
public readonly code: string, // id de regla del negocio (ej. 'INVALID_FORMAT')
|
code: string,
|
||||||
public readonly field: string, // path: 'number' | 'date' | 'lines[0].quantity'
|
field: string,
|
||||||
public readonly detail: string, // mensaje legible del negocio
|
detail: string,
|
||||||
options?: ErrorOptions
|
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.name = "DomainValidationError";
|
||||||
|
this.code = code;
|
||||||
|
this.field = field;
|
||||||
|
this.detail = detail;
|
||||||
|
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype);
|
||||||
Object.freeze(this);
|
Object.freeze(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constructores rápidos
|
/** Atajos de construcción comunes */
|
||||||
static requiredValue(field: string, options?: ErrorOptions) {
|
static requiredValue(
|
||||||
|
field: string,
|
||||||
|
options?: ErrorOptions & { metadata?: Record<string, unknown> }
|
||||||
|
) {
|
||||||
return new DomainValidationError("REQUIRED_VALUE", field, "cannot be empty", options);
|
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);
|
return new DomainValidationError("INVALID_FORMAT", field, detail, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
static invalidValue(
|
static invalidValue(
|
||||||
field: string,
|
field: string,
|
||||||
value: unknown,
|
value: unknown,
|
||||||
detail = "invalid value",
|
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 });
|
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() {
|
toDetail() {
|
||||||
return { path: this.field, message: this.detail, rule: this.code };
|
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 =>
|
export const isDomainValidationError = (e: unknown): e is DomainValidationError =>
|
||||||
e instanceof DomainValidationError;
|
e instanceof DomainValidationError;
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
export * from "./application-error";
|
||||||
|
export * from "./base-error";
|
||||||
export * from "./domain-error";
|
export * from "./domain-error";
|
||||||
export * from "./domain-validation-error";
|
export * from "./domain-validation-error";
|
||||||
|
export * from "./infrastructure-error";
|
||||||
export * from "./validation-error-collection";
|
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 { DomainError } from "./domain-error";
|
||||||
|
import { DomainValidationError } from "./domain-validation-error";
|
||||||
|
|
||||||
export interface ValidationErrorDetail {
|
export interface ValidationErrorDetail {
|
||||||
path?: string; // ejemplo: "lines[1].unitPrice.amount"
|
/** Path del campo inválido, ej. "lines[1].unitPrice.amount" */
|
||||||
message: string; // ejemplo: "Amount must be a positive number",
|
path?: string;
|
||||||
value?: unknown; // valor inválido opcional
|
/** 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 {
|
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[];
|
public readonly details: ValidationErrorDetail[];
|
||||||
|
|
||||||
constructor(message: string, details: ValidationErrorDetail[], options?: ErrorOptions) {
|
constructor(
|
||||||
super(message, options);
|
details: ValidationErrorDetail[],
|
||||||
Object.setPrototypeOf(this, ValidationErrorCollection.prototype);
|
options?: ErrorOptions & { metadata?: Record<string, unknown> }
|
||||||
|
) {
|
||||||
|
super("Multiple validation errors", "MULTIPLE_VALIDATION_ERRORS", {
|
||||||
|
...options,
|
||||||
|
metadata: { ...(options?.metadata ?? {}), errors: details },
|
||||||
|
});
|
||||||
this.name = "ValidationErrorCollection";
|
this.name = "ValidationErrorCollection";
|
||||||
this.details = details;
|
this.details = details;
|
||||||
|
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype);
|
||||||
Object.freeze(this);
|
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 =>
|
export const isValidationErrorCollection = (e: unknown): e is ValidationErrorCollection =>
|
||||||
e instanceof ValidationErrorCollection;
|
e instanceof ValidationErrorCollection;
|
||||||
|
|||||||
@ -38,17 +38,25 @@ export function extractOrPushError<T>(
|
|||||||
const error = result.error;
|
const error = result.error;
|
||||||
|
|
||||||
if (isValidationErrorCollection(error)) {
|
if (isValidationErrorCollection(error)) {
|
||||||
// Agrega todos los detalles de error al array proporcionado
|
// Copiar todos los detalles, rellenando path si falta
|
||||||
error.details.forEach((error) => {
|
error.details.forEach((detail) => {
|
||||||
errors.push({
|
errors.push({
|
||||||
...error,
|
...detail,
|
||||||
path: error.path || path,
|
path: detail.path ?? path,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (isDomainValidationError(error)) {
|
} 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 {
|
} 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;
|
return undefined;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user