mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:11:13 -05:00
320 lines
9.8 KiB
TypeScript
320 lines
9.8 KiB
TypeScript
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<string, string>(); // 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<string>();
|
|
const visiting = new Set<string>();
|
|
|
|
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;
|
|
}
|