mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 12:11:17 -05:00
feat: Use forwardemail_proxy function
This commit is contained in:
319
supabase/functions/process-selective-approval/index.ts
Normal file
319
supabase/functions/process-selective-approval/index.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user