diff --git a/src/lib/submissionItemsService.ts b/src/lib/submissionItemsService.ts index 6f9e7861..4d585a7c 100644 --- a/src/lib/submissionItemsService.ts +++ b/src/lib/submissionItemsService.ts @@ -2,12 +2,167 @@ import { supabase } from '@/integrations/supabase/client'; import { getErrorMessage } from './errorHandler'; import { logger } from './logger'; +// ============= TYPE DEFINITIONS ============= + +type EntityType = 'park' | 'ride' | 'manufacturer' | 'operator' | 'property_owner' | 'designer' | 'ride_model' | 'photo' | 'timeline_event' | 'milestone' | 'location'; + +interface ImageAssignment { + url: string; + cloudflare_id: string; +} + +interface ImageAssignments { + uploaded?: ImageAssignment[]; + banner_assignment?: number | null; + card_assignment?: number | null; +} + +interface LocationData { + name: string; + city?: string | null; + state_province?: string | null; + country: string; + postal_code?: string | null; + latitude: number; + longitude: number; + timezone?: string | null; +} + +interface PhotoData { + url: string; + cloudflare_image_id?: string; + title?: string; + caption?: string; + photographer_credit?: string; + date?: string; + date_taken?: string; + order?: number; +} + +// Flexible entity form data that accommodates all entity types +// This is intentionally permissive to maintain backwards compatibility +// while adding type safety to function signatures +interface EntityFormData { + // Common fields (not all entities have all fields) + name?: string; + slug?: string; + description?: string | null; + images?: ImageAssignments; + + // Park-specific fields + park_id?: string; + park_type?: string; + status?: string; + opening_date?: string | null; + closing_date?: string | null; + opening_date_precision?: string | null; + closing_date_precision?: string | null; + website_url?: string | null; + phone?: string | null; + email?: string | null; + operator_id?: string | null; + property_owner_id?: string | null; + location_id?: string | null; + location?: LocationData; + + // Ride-specific fields + ride_id?: string; + category?: string; + park_id?: string; + manufacturer_id?: string | null; + manufacturer_name?: string | null; + designer_id?: string | null; + ride_model_id?: string | null; + height_requirement?: number | null; + age_requirement?: number | null; + capacity_per_hour?: number | null; + duration_seconds?: number | null; + max_speed_kmh?: number | null; + max_height_meters?: number | null; + length_meters?: number | null; + inversions?: number | null; + coaster_type?: string | null; + seating_type?: string | null; + intensity_level?: string | null; + drop_height_meters?: number | null; + max_g_force?: number | null; + ride_sub_type?: string | null; + + // Company-specific fields + id?: string; + company_type?: string; + founded_year?: number | null; + founded_date?: string | null; + founded_date_precision?: string | null; + headquarters_location?: string | null; + + // Ride model fields + technical_specs?: Record; + + // Photo submission fields + photos?: PhotoData[]; + entity_id?: string; + context?: EntityType; + title?: string; + + // Timeline/milestone fields + entity_type?: EntityType; + event_type?: string; + event_date?: string; + event_date_precision?: string; + from_value?: string | null; + to_value?: string | null; + from_entity_id?: string | null; + to_entity_id?: string | null; + from_location_id?: string | null; + to_location_id?: string | null; + is_public?: boolean; + + // Allow additional properties for flexibility + [key: string]: unknown; +} + +// Specific typed interfaces for function parameters +interface ParkFormData extends EntityFormData { + name: string; + slug: string; + park_type: string; + status: string; +} + +interface RideFormData extends EntityFormData { + name: string; + slug: string; + category: string; + status: string; + park_id: string; +} + +interface CompanyFormData extends EntityFormData { + name: string; + slug: string; +} + +interface RideModelFormData extends EntityFormData { + name: string; + slug: string; + manufacturer_id: string; +} + +interface PhotoSubmissionData extends EntityFormData { + photos: PhotoData[]; + entity_id: string; + context: EntityType; +} + +// ============= MAIN INTERFACES ============= + export interface SubmissionItemWithDeps { id: string; submission_id: string; item_type: string; - item_data: any; - original_data: any; + item_data: EntityFormData; + original_data: EntityFormData | null; action_type?: 'create' | 'edit' | 'delete'; status: 'pending' | 'approved' | 'rejected'; depends_on: string | null; @@ -310,7 +465,7 @@ function topologicalSort(items: SubmissionItemWithDeps[]): SubmissionItemWithDep /** * Extract image URLs from ImageAssignments structure */ -function extractImageAssignments(images: any) { +function extractImageAssignments(images: Record | undefined) { if (!images || !images.uploaded || !Array.isArray(images.uploaded)) { return { banner_image_url: null, @@ -339,7 +494,7 @@ function extractImageAssignments(images: any) { /** * Helper functions to create entities with dependency resolution */ -async function createPark(data: any, dependencyMap: Map): Promise { +async function createPark(data: Record, dependencyMap: Map): Promise { const { transformParkData, validateSubmissionData } = await import('./entityTransformers'); const { ensureUniqueSlug } = await import('./slugUtils'); @@ -440,9 +595,15 @@ async function createPark(data: any, dependencyMap: Map): Promis /** * Resolve location data to a location_id - * Checks for existing locations by coordinates, creates new ones if needed + * + * SECURITY NOTE: Locations should go through moderation flow. + * Current implementation allows moderators to create locations directly during approval. + * This is acceptable as only moderators can call this function (via RLS on content_submissions). + * + * For user-submitted locations in the future, they should be submitted as separate + * submission_items with item_type='location' and go through the moderation queue. */ -async function resolveLocationId(locationData: any): Promise { +async function resolveLocationId(locationData: Record | undefined): Promise { if (!locationData || !locationData.latitude || !locationData.longitude) { return null; } @@ -460,6 +621,7 @@ async function resolveLocationId(locationData: any): Promise { } // Create new location (moderator has permission via RLS) + // FUTURE TODO: Change this to submission flow for user-submitted locations const { data: newLocation, error } = await supabase .from('locations') .insert({ @@ -487,7 +649,7 @@ async function resolveLocationId(locationData: any): Promise { return newLocation.id; } -async function createRide(data: any, dependencyMap: Map): Promise { +async function createRide(data: Record, dependencyMap: Map): Promise { const { transformRideData, validateSubmissionData } = await import('./entityTransformers'); const { ensureUniqueSlug } = await import('./slugUtils'); @@ -575,7 +737,7 @@ async function createRide(data: any, dependencyMap: Map): Promis } async function createCompany( - data: any, + data: Record, companyType: string, dependencyMap: Map ): Promise { @@ -652,7 +814,7 @@ async function createCompany( return company.id; } -async function createRideModel(data: any, dependencyMap: Map): Promise { +async function createRideModel(data: Record, dependencyMap: Map): Promise { const { transformRideModelData, validateSubmissionData } = await import('./entityTransformers'); const { ensureUniqueSlug } = await import('./slugUtils'); @@ -689,7 +851,7 @@ async function createRideModel(data: any, dependencyMap: Map): P return model.id; } -async function approvePhotos(data: any, dependencyMap: Map, userId: string, submissionId: string): Promise { +async function approvePhotos(data: Record, dependencyMap: Map, userId: string, submissionId: string): Promise { // Photos are already uploaded to Cloudflare // Resolve dependencies for entity associations const resolvedData = resolveDependencies(data, dependencyMap); @@ -723,7 +885,7 @@ async function approvePhotos(data: any, dependencyMap: Map, user } // Insert photos into the photos table - const photosToInsert = resolvedData.photos.map((photo: any, index: number) => { + const photosToInsert = resolvedData.photos.map((photo: Record, index: number) => { // Extract CloudFlare image ID from URL if not provided let cloudflareImageId = photo.cloudflare_image_id; if (!cloudflareImageId && photo.url) { @@ -846,7 +1008,7 @@ async function updateEntityFeaturedImage( * Resolve dependency references in submission data * Replaces submission item IDs with actual database entity IDs */ -function resolveDependencies(data: any, dependencyMap: Map): any { +function resolveDependencies(data: Record, dependencyMap: Map): Record { const resolved = { ...data }; // List of foreign key fields that may need resolution @@ -1021,7 +1183,7 @@ async function updateSubmissionStatusAfterRejection(submissionId: string): Promi */ export async function editSubmissionItem( itemId: string, - newData: any, + newData: Record, userId: string ): Promise { if (!userId) {