/** * Server-side validation for entity data * This provides a final safety layer before database writes */ export interface ValidationResult { valid: boolean; errors: string[]; } export interface StrictValidationResult { valid: boolean; blockingErrors: string[]; warnings: string[]; } /** * Strict validation that separates blocking errors from warnings * Used by the approval flow to prevent invalid data from being approved */ export function validateEntityDataStrict( entityType: string, data: any, originalData?: any ): StrictValidationResult { const result: StrictValidationResult = { valid: true, blockingErrors: [], warnings: [] }; // Skip name/slug validations for timeline events (they use title instead) const isTimelineEvent = entityType === 'milestone' || entityType === 'timeline_event'; // Common validations (blocking) - only for entities with name/slug if (!isTimelineEvent) { if (!data.name?.trim()) { result.blockingErrors.push('Name is required'); } if (!data.slug?.trim()) { result.blockingErrors.push('Slug is required'); } if (data.slug && !/^[a-z0-9-]+$/.test(data.slug)) { result.blockingErrors.push('Slug must contain only lowercase letters, numbers, and hyphens'); } if (data.name && data.name.length > 200) { result.blockingErrors.push('Name must be less than 200 characters'); } if (data.description && data.description.length > 2000) { result.blockingErrors.push('Description must be less than 2000 characters'); } // URL validation (warning) if (data.website_url && data.website_url !== '' && !isValidUrl(data.website_url)) { result.warnings.push('Website URL format may be invalid'); } // Email validation (warning) if (data.email && data.email !== '' && !isValidEmail(data.email)) { result.warnings.push('Email format may be invalid'); } } else { // Validations specific to timeline events if (data.description && data.description.length > 2000) { result.blockingErrors.push('Description must be less than 2000 characters'); } } // Entity-specific validations switch (entityType) { case 'park': if (!data.park_type) { result.blockingErrors.push('Park type is required'); } if (!data.status) { result.blockingErrors.push('Status is required'); } // For edits, check if location exists in either new or original data const hasLocation = data.location_id !== null && data.location_id !== undefined; const hadLocation = originalData?.location_id !== null && originalData?.location_id !== undefined; if (!hasLocation && !hadLocation) { result.blockingErrors.push('Location is required for parks'); } // Block explicit removal of required location if (hadLocation && data.location_id === null) { result.blockingErrors.push('Cannot remove location from a park - location is required'); } if (data.opening_date && data.closing_date) { const opening = new Date(data.opening_date); const closing = new Date(data.closing_date); if (closing < opening) { result.blockingErrors.push('Closing date must be after opening date'); } } break; case 'ride': if (!data.category) { result.blockingErrors.push('Category is required'); } if (!data.status) { result.blockingErrors.push('Status is required'); } // For edits, check if park exists in either new or original data const hasPark = data.park_id !== null && data.park_id !== undefined; const hadPark = originalData?.park_id !== null && originalData?.park_id !== undefined; if (!hasPark && !hadPark) { result.blockingErrors.push('Park is required for rides'); } // Block explicit removal of required park assignment if (hadPark && data.park_id === null) { result.blockingErrors.push('Cannot remove park from a ride - park is required'); } if (data.max_speed_kmh && (data.max_speed_kmh < 0 || data.max_speed_kmh > 300)) { result.blockingErrors.push('Max speed must be between 0 and 300 km/h'); } if (data.max_height_meters && (data.max_height_meters < 0 || data.max_height_meters > 200)) { result.blockingErrors.push('Max height must be between 0 and 200 meters'); } if (data.drop_height_meters && (data.drop_height_meters < 0 || data.drop_height_meters > 200)) { result.blockingErrors.push('Drop height must be between 0 and 200 meters'); } if (data.height_requirement && (data.height_requirement < 0 || data.height_requirement > 300)) { result.blockingErrors.push('Height requirement must be between 0 and 300 cm'); } break; case 'manufacturer': case 'designer': case 'operator': case 'property_owner': if (!data.company_type) { result.blockingErrors.push(`Company type is required (expected: ${entityType})`); } else if (data.company_type !== entityType) { result.blockingErrors.push(`Company type mismatch: expected '${entityType}' but got '${data.company_type}'`); } if (data.founded_year) { const year = parseInt(data.founded_year); const currentYear = new Date().getFullYear(); if (year < 1800 || year > currentYear) { result.warnings.push(`Founded year should be between 1800 and ${currentYear}`); } } break; case 'ride_model': if (!data.category) { result.blockingErrors.push('Category is required'); } if (!data.ride_type) { result.blockingErrors.push('Ride type is required'); } break; case 'photo': if (!data.cloudflare_image_id) { result.blockingErrors.push('Image ID is required'); } if (!data.entity_type) { result.blockingErrors.push('Entity type is required'); } if (!data.entity_id) { result.blockingErrors.push('Entity ID is required'); } if (data.caption && data.caption.length > 500) { result.blockingErrors.push('Caption must be less than 500 characters'); } break; case 'photo_edit': if (!data.photo_id) { result.blockingErrors.push('Photo ID is required'); } if (!data.entity_type) { result.blockingErrors.push('Entity type is required'); } if (!data.entity_id) { result.blockingErrors.push('Entity ID is required'); } if (data.caption && data.caption.length > 500) { result.blockingErrors.push('Caption must be less than 500 characters'); } if (data.title && data.title.length > 200) { result.blockingErrors.push('Title must be less than 200 characters'); } break; case 'photo_delete': if (!data.photo_id) { result.blockingErrors.push('Photo ID is required'); } if (!data.cloudflare_image_id && !data.photo_id) { result.blockingErrors.push('Photo identifier is required'); } if (!data.entity_type) { result.blockingErrors.push('Entity type is required'); } if (!data.entity_id) { result.blockingErrors.push('Entity ID is required'); } break; case 'milestone': case 'timeline_event': if (!data.title?.trim()) { result.blockingErrors.push('Event title is required'); } if (data.title && data.title.length > 200) { result.blockingErrors.push('Title must be less than 200 characters'); } if (!data.event_type) { result.blockingErrors.push('Event type is required'); } if (!data.event_date) { result.blockingErrors.push('Event date is required'); } if (!data.entity_type) { result.blockingErrors.push('Entity type is required'); } if (!data.entity_id) { result.blockingErrors.push('Entity ID is required'); } break; } result.valid = result.blockingErrors.length === 0; return result; } // Helper functions function isValidUrl(url: string): boolean { try { new URL(url); return true; } catch { return false; } } function isValidEmail(email: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } /** * Validate entity data before database write (legacy function) */ export function validateEntityData(entityType: string, data: any): ValidationResult { const errors: string[] = []; // Skip name/slug validations for timeline events const isTimelineEvent = entityType === 'milestone' || entityType === 'timeline_event'; // Common validations for entities with name/slug if (!isTimelineEvent) { if (!data.name || data.name.trim().length === 0) { errors.push('Name is required'); } if (!data.slug || data.slug.trim().length === 0) { errors.push('Slug is required'); } if (data.slug && !/^[a-z0-9-]+$/.test(data.slug)) { errors.push('Slug must contain only lowercase letters, numbers, and hyphens'); } if (data.name && data.name.length > 200) { errors.push('Name must be less than 200 characters'); } if (data.description && data.description.length > 2000) { errors.push('Description must be less than 2000 characters'); } if (data.website_url && data.website_url !== '' && !data.website_url.startsWith('http')) { errors.push('Website URL must start with http:// or https://'); } if (data.email && data.email !== '' && !data.email.includes('@')) { errors.push('Invalid email format'); } } else { // Validations for timeline events if (data.description && data.description.length > 2000) { errors.push('Description must be less than 2000 characters'); } } // Entity-specific validations switch (entityType) { case 'park': if (!data.park_type) errors.push('Park type is required'); if (!data.status) errors.push('Status is required'); if (data.opening_date && data.closing_date) { const opening = new Date(data.opening_date); const closing = new Date(data.closing_date); if (closing < opening) { errors.push('Closing date must be after opening date'); } } break; case 'ride': if (!data.category) errors.push('Category is required'); if (!data.status) errors.push('Status is required'); if (data.max_speed_kmh && (data.max_speed_kmh < 0 || data.max_speed_kmh > 300)) { errors.push('Max speed must be between 0 and 300 km/h'); } if (data.max_height_meters && (data.max_height_meters < 0 || data.max_height_meters > 200)) { errors.push('Max height must be between 0 and 200 meters'); } if (data.drop_height_meters && (data.drop_height_meters < 0 || data.drop_height_meters > 200)) { errors.push('Drop height must be between 0 and 200 meters'); } if (data.height_requirement && (data.height_requirement < 0 || data.height_requirement > 300)) { errors.push('Height requirement must be between 0 and 300 cm'); } break; case 'manufacturer': case 'designer': case 'operator': case 'property_owner': if (!data.company_type) { errors.push(`Company type is required (expected: ${entityType})`); } else if (data.company_type !== entityType) { errors.push(`Company type mismatch: expected '${entityType}' but got '${data.company_type}'`); } if (data.founded_year) { const year = parseInt(data.founded_year); const currentYear = new Date().getFullYear(); if (year < 1800 || year > currentYear) { errors.push(`Founded year must be between 1800 and ${currentYear}`); } } break; case 'ride_model': if (!data.category) errors.push('Category is required'); if (!data.ride_type) errors.push('Ride type is required'); break; case 'photo': if (!data.cloudflare_image_id) errors.push('Image ID is required'); if (!data.entity_type) errors.push('Entity type is required'); if (!data.entity_id) errors.push('Entity ID is required'); if (data.caption && data.caption.length > 500) { errors.push('Caption must be less than 500 characters'); } break; case 'milestone': case 'timeline_event': if (!data.title || data.title.trim().length === 0) { errors.push('Event title is required'); } if (data.title && data.title.length > 200) { errors.push('Title must be less than 200 characters'); } if (!data.event_type) errors.push('Event type is required'); if (!data.event_date) errors.push('Event date is required'); if (!data.entity_type) errors.push('Entity type is required'); if (!data.entity_id) errors.push('Entity ID is required'); break; } return { valid: errors.length === 0, errors }; }