Spaces:
Runtime error
Runtime error
File size: 3,189 Bytes
46252cd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | import { Transform, TransformFnParams } from 'class-transformer';
/**
* Accept a boolean only when the caller spelled one unambiguously, and leave anything else
* untouched so `@IsBoolean()` rejects it.
*
* The global `ValidationPipe` runs with `transformOptions.enableImplicitConversion: true`
* (`src/config/app-validation.ts`). For a `boolean`-typed property that makes class-transformer
* cast *any* non-empty string to `true` — `'false'`, `'0'` and `'no'` all become `true` — and it
* happens before `@IsBoolean()` ever runs, so the validator can never reject it. Requests reach a
* DTO as strings whenever the body arrives through the global `express.urlencoded` parser
* (`src/main.ts`), whose scalars are always strings.
*
* The callback deliberately reads `obj[key]` (the untouched plain source) instead of `value`:
* implicit conversion has already run by the time a `@Transform` callback is invoked, so `value`
* is the coerced `true` and the caller's original spelling is only still recoverable from `obj`.
*
* Only exact `'true'` / `'false'` are mapped. Anything else keeps its original value and fails
* validation — for a permission flag, an ambiguous spelling is safer refused than guessed.
*
* Because it reads `obj` and never `value`, it does NOT compose: class-transformer threads each
* `@Transform` result into the next, and this one discards whatever a previously-registered
* transform produced. Do not stack another `@Transform` on a property that uses it.
*/
export function coerceStrictBoolean({ obj, key }: Pick<TransformFnParams, 'obj' | 'key'>): unknown {
const raw = (obj as Record<string, unknown> | undefined)?.[key];
if (typeof raw === 'boolean') return raw;
if (raw === 'true') return true;
if (raw === 'false') return false;
return raw;
}
/** Property decorator form of {@link coerceStrictBoolean}. Pair it with `@IsBoolean()`. */
export const ToStrictBoolean = (): PropertyDecorator => Transform(coerceStrictBoolean);
/**
* The numeric counterpart, for the same reason and with the same `obj[key]` trick.
*
* Implicit conversion applies `Number(value)` to a `number`-typed property, and `Number('')` and
* `Number(' ')` are both `0` — so an empty form field arrives as a real, valid zero rather than
* being rejected as missing. On a field where `0` is itself meaningful (a disappearing-message
* timer, where `0` means "off") that silently performs an action the caller never asked for.
*
* Only a genuine number or a string that is entirely a finite number is converted. Everything else
* keeps its original value and fails `@IsInt()`/`@IsNumber()`.
*/
export function coerceStrictNumber({ obj, key }: Pick<TransformFnParams, 'obj' | 'key'>): unknown {
const raw = (obj as Record<string, unknown> | undefined)?.[key];
if (typeof raw === 'number') return raw;
if (typeof raw !== 'string' || raw.trim() === '') return raw;
const parsed = Number(raw);
return Number.isFinite(parsed) ? parsed : raw;
}
/** Property decorator form of {@link coerceStrictNumber}. Pair it with `@IsInt()` / `@IsNumber()`. */
export const ToStrictNumber = (): PropertyDecorator => Transform(coerceStrictNumber);
|