/** * Sanitize a ticker symbol. * - Converts to uppercase * - Trims whitespace * - Validates non-empty * * @param ticker The ticker symbol (e.g. "aapl", " MSFT ", "BRK.B") * @returns Normalized ticker (e.g. "AAPL", "MSFT", "BRK.B") * @throws Error if ticker is empty or invalid */ export function sanitizeTicker(ticker: string): string { if (!ticker || typeof ticker !== 'string') { throw new Error('Invalid ticker: must be a non-empty string'); } const normalized = ticker.trim().toUpperCase(); if (!normalized) { throw new Error('Invalid ticker: cannot be empty or whitespace'); } // Optional: validate ticker format (alphanumeric + dots/hyphens) if (!/^[A-Z0-9-.]+$/.test(normalized)) { throw new Error(`Invalid ticker format: ${normalized}`); } return normalized; } /** * Sanitize an array of tickers. * * @param tickers Array of ticker symbols * @returns Array of normalized tickers * @throws Error if any ticker is invalid */ export function sanitizeTickers(tickers: unknown): string[] { if (!Array.isArray(tickers)) { throw new Error('Invalid tickers: must be an array'); } if (tickers.length === 0) { throw new Error('Invalid tickers: array cannot be empty'); } return tickers.map((t) => { if (typeof t !== 'string') { throw new Error(`Invalid ticker in array: ${t} (expected string)`); } return sanitizeTicker(t); }); } /** * Sanitize a string field. * - Trims whitespace * - Validates non-empty * - Optional: enforces max length * * @param value The string value * @param fieldName Name of the field (for error messages) * @param maxLength Maximum allowed length (optional) * @returns Trimmed string * @throws Error if value is invalid */ export function sanitizeString(value: unknown, fieldName: string, maxLength?: number): string { if (typeof value !== 'string') { throw new Error(`Invalid ${fieldName}: must be a string`); } const trimmed = value.trim(); if (!trimmed) { throw new Error(`Invalid ${fieldName}: cannot be empty or whitespace`); } if (maxLength && trimmed.length > maxLength) { throw new Error(`Invalid ${fieldName}: exceeds max length of ${maxLength} characters`); } return trimmed; } /** * Sanitize a number field. * - Validates it's a number * - Optional: enforces min/max bounds * * @param value The numeric value * @param fieldName Name of the field (for error messages) * @param min Minimum allowed value (optional) * @param max Maximum allowed value (optional) * @returns The validated number * @throws Error if value is invalid */ export function sanitizeNumber( value: unknown, fieldName: string, options?: { min?: number; max?: number }, ): number { const num = typeof value === 'number' ? value : Number(value); if (isNaN(num)) { throw new Error(`Invalid ${fieldName}: must be a valid number`); } if (options?.min !== undefined && num < options.min) { throw new Error(`Invalid ${fieldName}: must be at least ${options.min}`); } if (options?.max !== undefined && num > options.max) { throw new Error(`Invalid ${fieldName}: must be at most ${options.max}`); } return num; } /** * Sanitize an ISO date string. * - Validates it's a valid ISO date * - Converts to string format YYYY-MM-DD * * @param value The date value (ISO string or Date) * @param fieldName Name of the field (for error messages) * @returns Date as YYYY-MM-DD string * @throws Error if date is invalid */ export function sanitizeDate(value: unknown, fieldName: string): string { let date: Date | null = null; if (typeof value === 'string') { date = new Date(value); } else if (value instanceof Date) { date = value; } if (!date || isNaN(date.getTime())) { throw new Error(`Invalid ${fieldName}: must be a valid date`); } return date.toISOString().slice(0, 10); // YYYY-MM-DD }