/** * Schema Validation Utilities * Provides consistent validation, error handling, and logging for API responses */ import { z, ZodError, ZodSchema } from 'zod'; // Validation result type export interface ValidationResult { success: boolean; data?: T; error?: ValidationError; } // Validation error type export interface ValidationError { message: string; details: z.ZodIssue[]; field?: string; } /** * Creates a validation error from a Zod error * Uses Zod v4's prettifyError for better error messages */ function createValidationError(error: ZodError, context?: string): ValidationError { // Use Zod v4's prettifyError if available for human-readable messages let errorMessage: string; try { // @ts-expect-error - prettifyError may not be in type definitions yet errorMessage = typeof z.prettifyError === 'function' ? z.prettifyError(error) : error.message; } catch { errorMessage = error.message; } return { message: context ? `${context}: ${errorMessage}` : errorMessage, details: error.issues, field: error.issues.length === 1 ? error.issues[0].path.join('.') : undefined, }; } /** * Validates data against a Zod schema with consistent error handling */ export function validateData( schema: ZodSchema, data: unknown, options?: { context?: string; logErrors?: boolean; fallbackData?: T; } ): ValidationResult { // @ts-expect-error - import.meta.env is available in Vite const { context, logErrors = import.meta.env.DEV, fallbackData } = options || {}; try { const validatedData = schema.parse(data); return { success: true, data: validatedData }; } catch (error) { if (!(error instanceof ZodError)) { // Non-validation error const errorMessage = `Unexpected validation error: ${error instanceof Error ? error.message : 'Unknown error'}`; if (logErrors) { console.error(`[Schema Validation] ${context || 'Unknown'}:`, error); } return { success: false, error: { message: errorMessage, details: [], }, }; } const validationError = createValidationError(error, context); if (logErrors) { console.error(`[Schema Validation] ${context || 'Validation failed'}:`, { message: validationError.message, issues: validationError.details.map((issue) => ({ field: issue.path.join('.'), message: issue.message, code: issue.code, })), data: Array.isArray(data) ? `${data.length} items` : typeof data, }); } // Return fallback data if provided if (fallbackData !== undefined) { console.warn(`[Schema Validation] Using fallback data for ${context || 'unknown context'}`); return { success: true, data: fallbackData }; } return { success: false, error: validationError }; } } /** * Validates data and throws on failure * Useful for cases where validation failure should stop execution */ export function validateDataStrict(schema: ZodSchema, data: unknown, context?: string): T { const result = validateData(schema, data, { context, logErrors: true }); if (!result.success || !result.data) { const error = result.error!; const errorMessage = `Schema validation failed${context ? ` for ${context}` : ''}: ${error.message}`; // @ts-expect-error - import.meta.env is available in Vite if (import.meta.env.DEV) { console.error(errorMessage, { issues: error.details, data: Array.isArray(data) ? `${data.length} items` : data, }); } throw new Error(errorMessage); } return result.data; } /** * Validates data with fallback parsing for arrays * Attempts to validate each item individually and filters out invalid ones */ export function validateArrayWithFallback( schema: ZodSchema, data: unknown, options?: { context?: string; logErrors?: boolean; maxErrors?: number; } ): ValidationResult { // @ts-expect-error - import.meta.env is available in Vite const { context = 'Array validation', logErrors = import.meta.env.DEV, maxErrors = 10, } = options || {}; if (!Array.isArray(data)) { return { success: false, error: { message: `${context}: Expected array, received ${typeof data}`, details: [], }, }; } // Check if schema is an array schema, if so, extract the element schema for individual validation // Use safe type checking for Zod v4 compatibility let itemSchema: ZodSchema = schema; try { // @ts-expect-error - Accessing internal Zod structure for array element extraction const def = schema._def; if (def?.typeName === 'ZodArray') { // @ts-expect-error - Accessing internal Zod structure itemSchema = def.type as ZodSchema; } } catch { // If we can't determine the item schema, use the schema as-is itemSchema = schema; } if (data.length === 0) { return { success: true, data: [] }; } try { // Try to validate the entire array first const validatedArray = schema.parse(data); return { success: true, data: validatedArray }; } catch (error) { if (!(error instanceof ZodError)) { return { success: false, error: { message: `${context}: Unexpected error during array validation`, details: [], }, }; } if (logErrors) { console.warn( `[Schema Validation] ${context}: Array validation failed, attempting individual item validation` ); } // Fallback: validate each item individually const validItems: T[] = []; const errors: ValidationError[] = []; let errorCount = 0; for (let i = 0; i < data.length; i++) { const item = data[i]; const itemResult = validateData(itemSchema, item, { context: `${context} item ${i}`, logErrors: false, }); if (itemResult.success && itemResult.data !== undefined) { validItems.push(itemResult.data); } else if (errorCount < maxErrors) { errors.push(itemResult.error!); errorCount++; } } if (errors.length > 0 && logErrors) { console.warn( `[Schema Validation] ${context}: ${errors.length} items failed validation and were filtered out`, { validItemsCount: validItems.length, totalItems: data.length, sampleErrors: errors.slice(0, 3).map((e) => ({ message: e.message, field: e.field, details: e.details.slice(0, 2), // Show first 2 validation issues })), } ); // In development, show detailed errors for first few failed items // @ts-expect-error - import.meta.env is available in Vite if (import.meta.env.DEV && errors.length > 0) { console.error(`[Schema Validation] Detailed validation errors for ${context}:`); errors.slice(0, 3).forEach((error, idx) => { console.error(`❌ Item ${idx} validation error:`, { message: error.message, field: error.field, validationIssues: error.details.map((issue) => ({ field: issue.path.join('.'), code: issue.code, message: issue.message, })), }); }); } } return { success: true, data: validItems }; } } /** * Creates a validated API response handler * Combines HTTP request with schema validation */ export function createValidatedApiCall( apiCall: () => Promise, schema: ZodSchema, options?: { context?: string; fallbackData?: T; useArrayFallback?: boolean; } ) { return async (): Promise> => { const { context, fallbackData, useArrayFallback = false } = options || {}; try { const rawData = await apiCall(); if (useArrayFallback && Array.isArray(rawData)) { // For array fallback, the schema should be for array items const arrayResult = validateArrayWithFallback(schema, rawData, { context }); // Type assertion needed here as we know the schema matches return arrayResult as unknown as ValidationResult; } return validateData(schema, rawData, { context, fallbackData, logErrors: true, }); } catch (error) { const errorMessage = `API call failed${context ? ` for ${context}` : ''}: ${error instanceof Error ? error.message : 'Unknown error'}`; // @ts-expect-error - import.meta.env is available in Vite if (import.meta.env.DEV) { console.error(`[API Validation] ${errorMessage}`, error); } return { success: false, error: { message: errorMessage, details: [], }, }; } }; } /** * Type-safe request validation wrapper */ export function validateRequest(schema: ZodSchema, data: unknown, context?: string): T { try { return schema.parse(data); } catch (error) { if (error instanceof ZodError) { const validationError = createValidationError(error, context); const message = `Request validation failed${context ? ` for ${context}` : ''}: ${validationError.message}`; if (import.meta.env.DEV) { console.error(message, { issues: validationError.details, data, }); } throw new Error(message); } throw error; } } /** * Utility to create validated service methods */ export function createValidatedServiceMethod( schema: ZodSchema, method: (...args: TParams) => Promise, options?: { context?: string; useArrayFallback?: boolean; } ) { return async (...args: TParams): Promise> => { const { context, useArrayFallback } = options || {}; try { const rawData = await method(...args); if (useArrayFallback && Array.isArray(rawData)) { // For array fallback, the schema should be for array items const arrayResult = validateArrayWithFallback(schema, rawData, { context }); // Type assertion needed here as we know the schema matches return arrayResult as unknown as ValidationResult; } return validateData(schema, rawData, { context, logErrors: true, }); } catch (error) { const errorMessage = `Service method failed${context ? ` for ${context}` : ''}: ${error instanceof Error ? error.message : 'Unknown error'}`; // @ts-expect-error - import.meta.env is available in Vite if (import.meta.env.DEV) { console.error(`[Service Validation] ${errorMessage}`, error); } return { success: false, error: { message: errorMessage, details: [], }, }; } }; }