FactuGES_ERP/packages/rdx-utils/src/helpers/patch-field.ts
2026-04-20 19:20:42 +02:00

166 lines
4.1 KiB
TypeScript

/**
* Representa un campo PATCH con semántica tri-estado:
*
* - unset → el campo no fue enviado (no modificar)
* - set(value) → el campo fue enviado con valor (actualizar)
* - set(null) → el campo fue enviado explícitamente como null (borrar / desvincular)
*
* Este wrapper evita ambigüedades entre `undefined`, `null` y valores reales,
* alineándose con semántica PATCH en APIs.
*
* @typeParam T - Tipo del valor del campo (sin incluir null ni undefined)
*
* @example
* ```ts
* const field = toPatchField(dto.name);
*
* field
* .ifSet((value) => { ... }) // solo si viene con valor
* .ifNull(() => { ... }); // solo si viene explícitamente null
* ```
*/
export class PatchField<T> {
/**
* Constructor privado. Se fuerza el uso de fábricas estáticas
* para garantizar consistencia en los estados.
*/
private constructor(
private readonly _isSet: boolean,
private readonly _isNull: boolean,
private readonly _value?: T
) {}
/**
* Crea un PatchField en estado "unset".
*
* Indica que el campo no fue enviado en el request.
*
* @returns Instancia en estado unset
*/
static unset<T>(): PatchField<T> {
return new PatchField<T>(false, false);
}
/**
* Crea un PatchField en estado "set".
*
* - Si el valor es `null`, representa una eliminación explícita.
* - Si el valor es distinto de null, representa una asignación.
*
* @param value - Valor a asignar o null
* @returns Instancia en estado set
*/
static set<T>(value: T | null): PatchField<T> {
if (value === null) return new PatchField<T>(true, true);
return new PatchField<T>(true, false, value);
}
/**
* Indica si el campo fue enviado (ya sea con valor o con null).
*/
get isSet(): boolean {
return this._isSet;
}
/**
* Indica si el campo fue enviado explícitamente como null.
*/
get isNull(): boolean {
return this._isNull;
}
/**
* Devuelve el valor del campo según su estado:
*
* - set(value) → devuelve value
* - set(null) → devuelve null
* - unset → devuelve undefined
*
* @returns Valor, null o undefined según estado
*/
get value(): T | null | undefined {
if (this._isSet) {
return this._value;
}
if (this._isNull) {
return null;
}
return this._value;
}
/**
* Ejecuta la función solo si el campo está en estado set(value).
*
* No se ejecuta si:
* - unset
* - set(null)
*
* @param fn - Callback a ejecutar con el valor
* @returns this (permite chaining)
*/
ifSet(fn: (v: T) => void): PatchField<T> {
if (this._isSet && !this._isNull) fn(this._value!);
return this;
}
/**
* Ejecuta la función solo si el campo está en estado set(null).
*
* No se ejecuta si:
* - unset
* - set(value)
*
* @param fn - Callback a ejecutar
* @returns this (permite chaining)
*/
ifNull(fn: () => void): PatchField<T> {
if (this._isSet && this._isNull) fn();
return this;
}
/**
* Ejecuta la función si el campo está en estado set(value) o set(null).
*
* No se ejecuta si:
* - unset
*
* @param fn - Callback que recibe el valor o null
* @returns this (permite chaining)
*/
ifSetOrNull(fn: (v: T | null) => void): PatchField<T> {
if (this._isSet) fn(this._isNull ? null : this._value!);
return this;
}
}
/**
* Convierte un valor DTO (`T | null | undefined`) en un PatchField.
*
* Reglas:
* - undefined → unset
* - null → set(null)
* - valor → set(value)
*
* Este helper se usa típicamente en mappers de input para aplicar
* semántica PATCH de forma explícita y segura.
*
* @typeParam T - Tipo del valor original
* @param value - Valor procedente del DTO
* @returns PatchField correspondiente
*
* @example
* ```ts
* toPatchField(dto.email)
* .ifSet(v => updateEmail(v))
* .ifNull(() => removeEmail());
* ```
*/
export function toPatchField<T>(value: T | null | undefined): PatchField<T> {
if (value === undefined) return PatchField.unset<T>();
if (value === null) return PatchField.set<T>(null);
return PatchField.set<T>(value as T);
}