diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 48149c85..ea4d2057 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -4147,6 +4147,7 @@ export type Database = { id: string is_test_data: boolean | null item_data: Json + item_data_id: string | null item_type: string order_index: number | null original_data: Json | null @@ -4163,6 +4164,7 @@ export type Database = { id?: string is_test_data?: boolean | null item_data: Json + item_data_id?: string | null item_type: string order_index?: number | null original_data?: Json | null @@ -4179,6 +4181,7 @@ export type Database = { id?: string is_test_data?: boolean | null item_data?: Json + item_data_id?: string | null item_type?: string order_index?: number | null original_data?: Json | null diff --git a/src/lib/entitySubmissionHelpers.ts b/src/lib/entitySubmissionHelpers.ts index e2a104d1..5d5cf8ed 100644 --- a/src/lib/entitySubmissionHelpers.ts +++ b/src/lib/entitySubmissionHelpers.ts @@ -465,20 +465,48 @@ export async function submitParkCreation( if (submissionError) throw submissionError; - // Create the submission item with actual park data + // Extract images for assignment + const changedFields = extractChangedFields(data, {}); + + // Insert into relational park_submissions table + const { data: parkSubmission, error: parkSubmissionError } = await supabase + .from('park_submissions' as any) + .insert({ + submission_id: submissionData.id, + name: data.name, + slug: data.slug, + description: data.description || null, + park_type: data.park_type, + status: data.status, + opening_date: data.opening_date ? new Date(data.opening_date).toISOString().split('T')[0] : null, + closing_date: data.closing_date ? new Date(data.closing_date).toISOString().split('T')[0] : null, + website_url: data.website_url || null, + phone: data.phone || null, + email: data.email || null, + operator_id: data.operator_id || null, + property_owner_id: data.property_owner_id || null, + location_id: data.location_id || null, + banner_image_url: changedFields.banner_image_url as string || null, + banner_image_id: changedFields.banner_image_id as string || null, + card_image_url: changedFields.card_image_url as string || null, + card_image_id: changedFields.card_image_id as string || null + } as any) + .select('id') + .single(); + + if (parkSubmissionError) throw parkSubmissionError; + + // Create submission_items record linking to park_submissions const { error: itemError } = await supabase .from('submission_items') .insert({ submission_id: submissionData.id, item_type: 'park', action_type: 'create', - item_data: { - ...extractChangedFields(data, {}), - images: processedImages as unknown as Json - }, + item_data_id: (parkSubmission as any).id, status: 'pending' as const, order_index: 0 - }); + } as any); if (itemError) throw itemError; @@ -748,20 +776,61 @@ export async function submitRideCreation( if (submissionError) throw submissionError; - // Create the submission item with actual ride data + // Extract images for assignment + const changedFields = extractChangedFields(data, {}); + + // Insert into relational ride_submissions table + const { data: rideSubmission, error: rideSubmissionError } = await supabase + .from('ride_submissions' as any) + .insert({ + submission_id: submissionData.id, + park_id: data.park_id || null, + name: data.name, + slug: data.slug, + description: data.description || null, + category: data.category, + ride_sub_type: data.ride_sub_type || null, + status: data.status, + opening_date: data.opening_date ? new Date(data.opening_date).toISOString().split('T')[0] : null, + closing_date: data.closing_date ? new Date(data.closing_date).toISOString().split('T')[0] : null, + manufacturer_id: data.manufacturer_id || null, + designer_id: data.designer_id || null, + ride_model_id: data.ride_model_id || null, + height_requirement: data.height_requirement || null, + age_requirement: data.age_requirement || null, + capacity_per_hour: data.capacity_per_hour || null, + duration_seconds: data.duration_seconds || null, + max_speed_kmh: data.max_speed_kmh || null, + max_height_meters: data.max_height_meters || null, + length_meters: data.length_meters || null, + drop_height_meters: data.drop_height_meters || null, + inversions: data.inversions || 0, + max_g_force: data.max_g_force || null, + coaster_type: data.coaster_type || null, + seating_type: data.seating_type || null, + intensity_level: data.intensity_level || null, + banner_image_url: changedFields.banner_image_url as string || null, + banner_image_id: changedFields.banner_image_id as string || null, + card_image_url: changedFields.card_image_url as string || null, + card_image_id: changedFields.card_image_id as string || null, + image_url: null + } as any) + .select('id') + .single(); + + if (rideSubmissionError) throw rideSubmissionError; + + // Create submission_items record linking to ride_submissions const { error: itemError } = await supabase .from('submission_items') .insert({ submission_id: submissionData.id, item_type: 'ride', action_type: 'create', - item_data: { - ...extractChangedFields(data, {}), - images: processedImages as unknown as Json - }, + item_data_id: (rideSubmission as any).id, status: 'pending' as const, order_index: 0 - }); + } as any); if (itemError) throw itemError; diff --git a/supabase/functions/process-selective-approval/index.ts b/supabase/functions/process-selective-approval/index.ts index 5444ab26..2da442ce 100644 --- a/supabase/functions/process-selective-approval/index.ts +++ b/supabase/functions/process-selective-approval/index.ts @@ -335,10 +335,16 @@ serve(async (req) => { edgeLogger.info('Processing selective approval', { action: 'approval_start', itemCount: itemIds.length, userId: authenticatedUserId, submissionId }); - // Fetch all items for the submission + // Fetch all items with relational data for the submission const { data: items, error: fetchError } = await supabase .from('submission_items') - .select('*') + .select(` + *, + park_submission:park_submissions!item_data_id(*), + ride_submission:ride_submissions!item_data_id(*), + company_submission:company_submissions!item_data_id(*), + ride_model_submission:ride_model_submissions!item_data_id(*) + `) .in('id', itemIds); if (fetchError) { @@ -405,8 +411,36 @@ serve(async (req) => { try { edgeLogger.info('Processing item', { action: 'approval_process_item', itemId: item.id, itemType: item.item_type }); + // Extract data from relational tables based on item_type + let itemData: any; + switch (item.item_type) { + case 'park': + itemData = (item as any).park_submission; + break; + case 'ride': + itemData = (item as any).ride_submission; + break; + case 'manufacturer': + case 'operator': + case 'property_owner': + case 'designer': + itemData = (item as any).company_submission; + break; + case 'ride_model': + itemData = (item as any).ride_model_submission; + break; + default: + // For photo/timeline items, fall back to item_data (these still use JSONB) + itemData = item.item_data; + } + + if (!itemData && item.item_data) { + // Fallback to item_data if relational data not found (for backwards compatibility) + itemData = item.item_data; + } + // Validate entity data with strict validation, passing original_data for edits - const validation = validateEntityDataStrict(item.item_type, item.item_data, item.original_data); + const validation = validateEntityDataStrict(item.item_type, itemData, item.original_data); if (validation.blockingErrors.length > 0) { edgeLogger.error('Blocking validation errors', { @@ -467,7 +501,7 @@ serve(async (req) => { } // Resolve dependencies in item data - const resolvedData = resolveDependencies(item.item_data, dependencyMap, sortedItems); + const resolvedData = resolveDependencies(itemData, dependencyMap, sortedItems); // Add submitter ID to the data for photo tracking resolvedData._submitter_id = submitterId; diff --git a/supabase/migrations/20251103035256_0ff94070-9336-406c-98e3-d55937709cf2.sql b/supabase/migrations/20251103035256_0ff94070-9336-406c-98e3-d55937709cf2.sql new file mode 100644 index 00000000..50b0cd5d --- /dev/null +++ b/supabase/migrations/20251103035256_0ff94070-9336-406c-98e3-d55937709cf2.sql @@ -0,0 +1,25 @@ +-- ============================================================================ +-- ELIMINATE JSONB FROM submission_items +-- ============================================================================ +-- Problem: submission_items.item_data stores entire submission payloads as JSONB +-- Solution: Use existing relational tables (*_submissions) and link via foreign key +-- +-- CRITICAL PROJECT RULE: NO JSON OR JSONB IN SQL COLUMNS FOR RELATIONAL DATA +-- ============================================================================ + +-- Step 1: Add foreign key column to link to relational submission tables +ALTER TABLE submission_items + ADD COLUMN item_data_id UUID; + +-- Step 2: Create index for performance +CREATE INDEX idx_submission_items_item_data_id ON submission_items(item_data_id); + +-- Step 3: Add comments explaining the architecture +COMMENT ON COLUMN submission_items.item_data_id IS + 'Foreign key to *_submissions tables based on item_type. park -> park_submissions, ride -> ride_submissions, company types -> company_submissions, ride_model -> ride_model_submissions. Replaces the deprecated item_data JSONB column.'; + +COMMENT ON COLUMN submission_items.item_data IS + 'DEPRECATED: Use item_data_id to reference relational *_submissions tables. This JSONB column violates the NO JSON IN DATABASE rule and will be dropped.'; + +COMMENT ON COLUMN submission_items.original_data IS + 'DEPRECATED: Use item_data_id to reference relational *_submissions tables. This JSONB column violates the NO JSON IN DATABASE rule and will be dropped.'; \ No newline at end of file