| 206 | * ``` |
| 207 | */ |
| 208 | export function validateNumericId( |
| 209 | value: string | number | null | undefined, |
| 210 | paramName = 'ID', |
| 211 | options: { min?: number; max?: number } = {} |
| 212 | ): ValidationResult { |
| 213 | if (value === null || value === undefined || value === '') { |
| 214 | return { |
| 215 | isValid: false, |
| 216 | error: `${paramName} is required`, |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | const num = typeof value === 'number' ? value : Number(value) |
| 221 | |
| 222 | if (Number.isNaN(num) || !Number.isFinite(num)) { |
| 223 | logger.warn('Invalid numeric ID', { paramName, value }) |
| 224 | return { |
| 225 | isValid: false, |
| 226 | error: `${paramName} must be a valid number`, |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | if (options.min !== undefined && num < options.min) { |
| 231 | return { |
| 232 | isValid: false, |
| 233 | error: `${paramName} must be at least ${options.min}`, |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | if (options.max !== undefined && num > options.max) { |
| 238 | return { |
| 239 | isValid: false, |
| 240 | error: `${paramName} must be at most ${options.max}`, |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | return { isValid: true, sanitized: num.toString() } |
| 245 | } |
| 246 | |
| 247 | /** |
| 248 | * Validates an integer value (from JSON body or other sources) |