2025-09-01 14:07:59 +00:00
|
|
|
|
2026-04-20 17:20:42 +00:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
* ```
|
|
|
|
|
*/
|
2025-09-01 14:07:59 +00:00
|
|
|
export class PatchField<T> {
|
2026-04-20 17:20:42 +00:00
|
|
|
/**
|
|
|
|
|
* Constructor privado. Se fuerza el uso de fábricas estáticas
|
|
|
|
|
* para garantizar consistencia en los estados.
|
|
|
|
|
*/
|
2025-09-01 14:07:59 +00:00
|
|
|
private constructor(
|
|
|
|
|
private readonly _isSet: boolean,
|
2026-04-20 17:20:42 +00:00
|
|
|
private readonly _isNull: boolean,
|
|
|
|
|
private readonly _value?: T
|
2025-09-01 14:07:59 +00:00
|
|
|
) {}
|
|
|
|
|
|
2026-04-20 17:20:42 +00:00
|
|
|
/**
|
|
|
|
|
* Crea un PatchField en estado "unset".
|
|
|
|
|
*
|
|
|
|
|
* Indica que el campo no fue enviado en el request.
|
|
|
|
|
*
|
|
|
|
|
* @returns Instancia en estado unset
|
|
|
|
|
*/
|
2025-09-01 14:07:59 +00:00
|
|
|
static unset<T>(): PatchField<T> {
|
2026-04-20 17:20:42 +00:00
|
|
|
return new PatchField<T>(false, false);
|
2025-09-01 14:07:59 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 17:20:42 +00:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
2025-09-01 14:07:59 +00:00
|
|
|
static set<T>(value: T | null): PatchField<T> {
|
2026-04-20 17:20:42 +00:00
|
|
|
if (value === null) return new PatchField<T>(true, true);
|
|
|
|
|
return new PatchField<T>(true, false, value);
|
2025-09-01 14:07:59 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 17:20:42 +00:00
|
|
|
/**
|
|
|
|
|
* Indica si el campo fue enviado (ya sea con valor o con null).
|
|
|
|
|
*/
|
2025-09-01 14:07:59 +00:00
|
|
|
get isSet(): boolean {
|
|
|
|
|
return this._isSet;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 17:20:42 +00:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
2025-09-01 14:07:59 +00:00
|
|
|
get value(): T | null | undefined {
|
2026-04-20 17:20:42 +00:00
|
|
|
if (this._isSet) {
|
|
|
|
|
return this._value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this._isNull) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 14:07:59 +00:00
|
|
|
return this._value;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 17:20:42 +00:00
|
|
|
/**
|
|
|
|
|
* 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;
|
2025-09-01 14:07:59 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 17:20:42 +00:00
|
|
|
/**
|
|
|
|
|
* 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());
|
|
|
|
|
* ```
|
|
|
|
|
*/
|
2025-09-01 14:07:59 +00:00
|
|
|
export function toPatchField<T>(value: T | null | undefined): PatchField<T> {
|
|
|
|
|
if (value === undefined) return PatchField.unset<T>();
|
2026-04-20 17:20:42 +00:00
|
|
|
if (value === null) return PatchField.set<T>(null);
|
2025-09-01 14:07:59 +00:00
|
|
|
return PatchField.set<T>(value as T);
|
|
|
|
|
}
|