(
field: string,
value: unknown,
opts: KindOpts & { minLength?: number; maxLength?: number; allowEmpty?: boolean } = {},
)
| 54 | * records cannot reach the backend (dogfood P1 fix). |
| 55 | */ |
| 56 | export function requireString( |
| 57 | field: string, |
| 58 | value: unknown, |
| 59 | opts: KindOpts & { minLength?: number; maxLength?: number; allowEmpty?: boolean } = {}, |
| 60 | ): asserts value is string { |
| 61 | const { kind = 'field', allowEmpty = false, minLength, maxLength } = opts; |
| 62 | |
| 63 | // Structural check: must be a string. |
| 64 | const typeResult = v.safeParse(v.string(), value); |
| 65 | if (!typeResult.success) { |
| 66 | throw localValidationError( |
| 67 | field, |
| 68 | 'is required and must be a non-empty string', |
| 69 | undefined, |
| 70 | kind, |
| 71 | ); |
| 72 | } |
| 73 | |
| 74 | // Whitespace-only rejection: a string composed solely of spaces/tabs/ |
| 75 | // newlines is treated as empty unless `allowEmpty` is set. This catches |
| 76 | // cases like `--name " "` that pass the type check but produce junk |
| 77 | // records in the backend (dogfood P1 fix #1). |
| 78 | if (!allowEmpty && typeof value === 'string' && value.trim().length === 0 && value.length > 0) { |
| 79 | throw localValidationError( |
| 80 | field, |
| 81 | 'is required and must be a non-empty string', |
| 82 | undefined, |
| 83 | kind, |
| 84 | ); |
| 85 | } |
| 86 | |
| 87 | // Length lower bound. `allowEmpty` overrides `minLength` per the |
| 88 | // documented precedence above. |
| 89 | const effectiveMin = allowEmpty ? 0 : (minLength ?? 1); |
| 90 | if (effectiveMin > 0) { |
| 91 | const minResult = v.safeParse(v.pipe(v.string(), v.minLength(effectiveMin)), value); |
| 92 | if (!minResult.success) { |
| 93 | throw localValidationError( |
| 94 | field, |
| 95 | 'is required and must be a non-empty string', |
| 96 | undefined, |
| 97 | kind, |
| 98 | ); |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | // Length upper bound. |
| 103 | if (maxLength !== undefined) { |
| 104 | const maxResult = v.safeParse(v.pipe(v.string(), v.maxLength(maxLength)), value); |
| 105 | if (!maxResult.success) { |
| 106 | throw localValidationError(field, `must be at most ${maxLength} characters`, undefined, kind); |
| 107 | } |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | /** |
| 112 | * Assert that `value` is one of the accepted enum members, throwing a |
no test coverage detected