diff --git a/supabase/functions/process-selective-approval/index.ts b/supabase/functions/process-selective-approval/index.ts index 9c053f31..b6f303b9 100644 --- a/supabase/functions/process-selective-approval/index.ts +++ b/supabase/functions/process-selective-approval/index.ts @@ -1,7 +1,6 @@ -// Force redeployment: v100 - JWT verification disabled in config +// Force redeployment: v101 - Inlined validation to fix bundling timeout import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; -import { validateEntityData, validateEntityDataStrict } from "./validation.ts"; import { createErrorResponse } from "../_shared/errorSanitizer.ts"; import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts"; import { rateLimiters, withRateLimit } from "../_shared/rateLimiter.ts"; @@ -11,6 +10,392 @@ const corsHeaders = { 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', }; +// ============================================================================ +// VALIDATION FUNCTIONS (Inlined from validation.ts) +// ============================================================================ + +interface ValidationResult { + valid: boolean; + errors: string[]; +} + +interface StrictValidationResult { + valid: boolean; + blockingErrors: string[]; + warnings: string[]; +} + +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); +} + +function validateEntityDataStrict( + entityType: string, + data: any, + originalData?: any +): StrictValidationResult { + const result: StrictValidationResult = { + valid: true, + blockingErrors: [], + warnings: [] + }; + + const isTimelineEvent = entityType === 'milestone' || entityType === 'timeline_event'; + + 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'); + } + + if (data.website_url && data.website_url !== '' && !isValidUrl(data.website_url)) { + result.warnings.push('Website URL format may be invalid'); + } + + if (data.email && data.email !== '' && !isValidEmail(data.email)) { + result.warnings.push('Email format may be invalid'); + } + } else { + if (data.description && data.description.length > 2000) { + result.blockingErrors.push('Description must be less than 2000 characters'); + } + } + + switch (entityType) { + case 'park': + if (!data.park_type) { + result.blockingErrors.push('Park type is required'); + } + if (!data.status) { + result.blockingErrors.push('Status is required'); + } + 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'); + } + 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'); + } + 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'); + } + 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.cloudflare_image_id && !/^[a-zA-Z0-9-]{36}$/.test(data.cloudflare_image_id)) { + result.blockingErrors.push('Invalid Cloudflare image ID format'); + } + if (!data.entity_type) { + result.blockingErrors.push('Entity type is required'); + } + const validPhotoEntityTypes = ['park', 'ride', 'company', 'ride_model']; + if (data.entity_type && !validPhotoEntityTypes.includes(data.entity_type)) { + result.blockingErrors.push(`Invalid entity type. Must be one of: ${validPhotoEntityTypes.join(', ')}`); + } + if (!data.entity_id) { + result.blockingErrors.push('Entity ID is required'); + } + if (data.entity_id && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(data.entity_id)) { + result.blockingErrors.push('Entity ID must be a valid UUID'); + } + 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.event_date) { + const eventDate = new Date(data.event_date); + const maxFutureDate = new Date(); + maxFutureDate.setFullYear(maxFutureDate.getFullYear() + 5); + if (eventDate > maxFutureDate) { + result.blockingErrors.push('Event date cannot be more than 5 years in the future'); + } + const minDate = new Date('1800-01-01'); + if (eventDate < minDate) { + result.blockingErrors.push('Event date cannot be before year 1800'); + } + } + const changeEventTypes = ['name_change', 'location_change', 'status_change', 'ownership_change']; + if (data.event_type && changeEventTypes.includes(data.event_type)) { + if (!data.from_value && !data.to_value) { + result.blockingErrors.push(`Change event (${data.event_type}) requires at least one of from_value or to_value`); + } + } + if (!data.entity_type) { + result.blockingErrors.push('Entity type is required'); + } + const validTimelineEntityTypes = ['park', 'ride', 'company', 'ride_model']; + if (data.entity_type && !validTimelineEntityTypes.includes(data.entity_type)) { + result.blockingErrors.push(`Invalid entity type. Must be one of: ${validTimelineEntityTypes.join(', ')}`); + } + if (!data.entity_id) { + result.blockingErrors.push('Entity ID is required'); + } + if (data.entity_id && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(data.entity_id)) { + result.blockingErrors.push('Entity ID must be a valid UUID'); + } + break; + } + + result.valid = result.blockingErrors.length === 0; + return result; +} + +function validateEntityData(entityType: string, data: any): ValidationResult { + const errors: string[] = []; + + const isTimelineEvent = entityType === 'milestone' || entityType === 'timeline_event'; + + 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 { + if (data.description && data.description.length > 2000) { + errors.push('Description must be less than 2000 characters'); + } + } + + 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 + }; +} + +// ============================================================================ +// END VALIDATION FUNCTIONS +// ============================================================================ + interface ApprovalRequest { itemIds: string[]; submissionId: string; diff --git a/supabase/functions/process-selective-approval/validation.ts b/supabase/functions/process-selective-approval/validation.ts deleted file mode 100644 index 15dadabc..00000000 --- a/supabase/functions/process-selective-approval/validation.ts +++ /dev/null @@ -1,412 +0,0 @@ -/** - * 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'); - } - // Validate Cloudflare image ID format (standard UUID format) - if (data.cloudflare_image_id && !/^[a-zA-Z0-9-]{36}$/.test(data.cloudflare_image_id)) { - result.blockingErrors.push('Invalid Cloudflare image ID format'); - } - if (!data.entity_type) { - result.blockingErrors.push('Entity type is required'); - } - // Validate entity type is one of the allowed types - const validPhotoEntityTypes = ['park', 'ride', 'company', 'ride_model']; - if (data.entity_type && !validPhotoEntityTypes.includes(data.entity_type)) { - result.blockingErrors.push(`Invalid entity type. Must be one of: ${validPhotoEntityTypes.join(', ')}`); - } - if (!data.entity_id) { - result.blockingErrors.push('Entity ID is required'); - } - // Validate entity_id is a valid UUID - if (data.entity_id && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(data.entity_id)) { - result.blockingErrors.push('Entity ID must be a valid UUID'); - } - 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'); - } - // Validate event date is not too far in the future (max 5 years) - if (data.event_date) { - const eventDate = new Date(data.event_date); - const maxFutureDate = new Date(); - maxFutureDate.setFullYear(maxFutureDate.getFullYear() + 5); - if (eventDate > maxFutureDate) { - result.blockingErrors.push('Event date cannot be more than 5 years in the future'); - } - // Also validate it's not absurdly old (before 1800) - const minDate = new Date('1800-01-01'); - if (eventDate < minDate) { - result.blockingErrors.push('Event date cannot be before year 1800'); - } - } - // For change events (name_change, location_change, status_change), require from/to values - const changeEventTypes = ['name_change', 'location_change', 'status_change', 'ownership_change']; - if (data.event_type && changeEventTypes.includes(data.event_type)) { - if (!data.from_value && !data.to_value) { - result.blockingErrors.push(`Change event (${data.event_type}) requires at least one of from_value or to_value`); - } - } - if (!data.entity_type) { - result.blockingErrors.push('Entity type is required'); - } - // Validate entity type is one of the allowed types - const validTimelineEntityTypes = ['park', 'ride', 'company', 'ride_model']; - if (data.entity_type && !validTimelineEntityTypes.includes(data.entity_type)) { - result.blockingErrors.push(`Invalid entity type. Must be one of: ${validTimelineEntityTypes.join(', ')}`); - } - if (!data.entity_id) { - result.blockingErrors.push('Entity ID is required'); - } - // Validate entity_id is a valid UUID - if (data.entity_id && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(data.entity_id)) { - result.blockingErrors.push('Entity ID must be a valid UUID'); - } - 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 - }; -}