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);