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"; const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', }; interface ProcessApprovalRequest { submissionId: string; selectedItemIds: string[]; moderatorId: string; } interface ProcessedItem { id: string; entityType: string; entityId: string | null; status: 'success' | 'failed'; error?: string; } serve(async (req) => { if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } try { const supabaseClient = createClient( Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '', { auth: { autoRefreshToken: false, persistSession: false } } ); const authHeader = req.headers.get('Authorization'); if (!authHeader) { throw new Error('No authorization header'); } const token = authHeader.replace('Bearer ', ''); const { data: { user }, error: authError } = await supabaseClient.auth.getUser(token); if (authError || !user) { throw new Error('Unauthorized'); } // Verify user is moderator or admin const { data: userRoles } = await supabaseClient .from('user_roles') .select('role') .eq('user_id', user.id); const isModerator = userRoles?.some(r => ['moderator', 'admin', 'superuser'].includes(r.role) ); if (!isModerator) { throw new Error('Insufficient permissions'); } const { submissionId, selectedItemIds, moderatorId }: ProcessApprovalRequest = await req.json(); console.log('Processing selective approval:', { submissionId, selectedItemIds, moderatorId }); // Fetch all selected items with their dependencies const { data: items, error: itemsError } = await supabaseClient .from('submission_items') .select('*') .in('id', selectedItemIds) .eq('submission_id', submissionId); if (itemsError) { throw new Error(`Failed to fetch items: ${itemsError.message}`); } // Topological sort to process items in correct order const sortedItems = topologicalSort(items || []); const processedItems: ProcessedItem[] = []; const entityMap = new Map(); // temp_id -> real_id mapping // Process each item in dependency order for (const item of sortedItems) { try { let entityId: string | null = null; // Resolve parent dependency if exists if (item.depends_on && entityMap.has(item.depends_on)) { item.item_data.parent_id = entityMap.get(item.depends_on); } // Create the entity based on type switch (item.item_type) { case 'manufacturer': const { data: manufacturer, error: mfgError } = await supabaseClient .from('companies') .insert({ name: item.item_data.name, slug: item.item_data.slug, company_type: 'MANUFACTURER', description: item.item_data.description, founded_year: item.item_data.founded_year, headquarters_location: item.item_data.headquarters_location, website_url: item.item_data.website_url, }) .select('id') .single(); if (mfgError) throw mfgError; entityId = manufacturer.id; break; case 'designer': const { data: designer, error: designerError } = await supabaseClient .from('companies') .insert({ name: item.item_data.name, slug: item.item_data.slug, company_type: 'DESIGNER', description: item.item_data.description, founded_year: item.item_data.founded_year, headquarters_location: item.item_data.headquarters_location, website_url: item.item_data.website_url, }) .select('id') .single(); if (designerError) throw designerError; entityId = designer.id; break; case 'ride_model': const { data: model, error: modelError } = await supabaseClient .from('ride_models') .insert({ name: item.item_data.name, slug: item.item_data.slug, manufacturer_id: item.item_data.manufacturer_id || item.item_data.parent_id, description: item.item_data.description, model_type: item.item_data.model_type, }) .select('id') .single(); if (modelError) throw modelError; entityId = model.id; break; case 'ride': const { data: ride, error: rideError } = await supabaseClient .from('rides') .insert({ name: item.item_data.name, slug: item.item_data.slug, park_id: item.item_data.park_id, manufacturer_id: item.item_data.manufacturer_id, designer_id: item.item_data.designer_id, model_id: item.item_data.model_id || item.item_data.parent_id, ride_type: item.item_data.ride_type, status: item.item_data.status, opening_date: item.item_data.opening_date, closing_date: item.item_data.closing_date, description: item.item_data.description, }) .select('id') .single(); if (rideError) throw rideError; entityId = ride.id; break; case 'park': const { data: park, error: parkError } = await supabaseClient .from('parks') .insert({ name: item.item_data.name, slug: item.item_data.slug, operator_id: item.item_data.operator_id, property_owner_id: item.item_data.property_owner_id, location_id: item.item_data.location_id, status: item.item_data.status, opening_date: item.item_data.opening_date, closing_date: item.item_data.closing_date, description: item.item_data.description, website_url: item.item_data.website_url, }) .select('id') .single(); if (parkError) throw parkError; entityId = park.id; break; default: throw new Error(`Unknown item type: ${item.item_type}`); } // Update submission item status await supabaseClient .from('submission_items') .update({ status: 'approved', entity_id: entityId, reviewed_by: moderatorId, reviewed_at: new Date().toISOString(), }) .eq('id', item.id); // Store mapping for dependent items entityMap.set(item.id, entityId!); processedItems.push({ id: item.id, entityType: item.item_type, entityId, status: 'success', }); console.log(`Successfully processed ${item.item_type} ${item.id} -> ${entityId}`); } catch (error: any) { console.error(`Failed to process item ${item.id}:`, error); // Mark item as failed await supabaseClient .from('submission_items') .update({ status: 'rejected', rejection_reason: error.message, reviewed_by: moderatorId, reviewed_at: new Date().toISOString(), }) .eq('id', item.id); processedItems.push({ id: item.id, entityType: item.item_type, entityId: null, status: 'failed', error: error.message, }); } } // Update submission status if all items are processed const { data: remainingItems } = await supabaseClient .from('submission_items') .select('id') .eq('submission_id', submissionId) .eq('status', 'pending'); if (!remainingItems || remainingItems.length === 0) { await supabaseClient .from('content_submissions') .update({ status: 'approved', reviewed_at: new Date().toISOString(), }) .eq('id', submissionId); } return new Response( JSON.stringify({ success: true, processedItems, totalProcessed: processedItems.length, successCount: processedItems.filter(p => p.status === 'success').length, failureCount: processedItems.filter(p => p.status === 'failed').length, }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200, } ); } catch (error: any) { console.error('Error in process-selective-approval:', error); return new Response( JSON.stringify({ error: error.message }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 500, } ); } }); // Topological sort helper function topologicalSort(items: any[]): any[] { const sorted: any[] = []; const visited = new Set(); const visiting = new Set(); function visit(item: any) { if (visited.has(item.id)) return; if (visiting.has(item.id)) { throw new Error('Circular dependency detected'); } visiting.add(item.id); // Visit dependencies first if (item.depends_on) { const parent = items.find(i => i.id === item.depends_on); if (parent) { visit(parent); } } visiting.delete(item.id); visited.add(item.id); sorted.push(item); } items.forEach(item => visit(item)); return sorted; }