/** * 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 { /** * 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(): PatchField { return new PatchField(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(value: T | null): PatchField { if (value === null) return new PatchField(true, true); return new PatchField(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 { 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 { 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 { 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(value: T | null | undefined): PatchField { if (value === undefined) return PatchField.unset(); if (value === null) return PatchField.set(null); return PatchField.set(value as T); }