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 SeedOptions { preset: 'small' | 'medium' | 'large' | 'stress'; entityTypes: string[]; includeDependencies: boolean; includeConflicts: boolean; includeVersionChains: boolean; includeEscalated: boolean; includeExpiredLocks: boolean; } interface SeedPlan { parks: number; rides: number; companies: number; rideModels: number; } const PRESETS: Record = { small: { parks: 5, rides: 10, companies: 3, rideModels: 2 }, medium: { parks: 20, rides: 50, companies: 20, rideModels: 10 }, large: { parks: 100, rides: 250, companies: 100, rideModels: 50 }, stress: { parks: 400, rides: 1000, companies: 400, rideModels: 200 } }; Deno.serve(async (req) => { if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } try { // SECURITY: Service Role Key Usage // --------------------------------- // This function uses the service role key to seed test data bypassing RLS. // This is required because: // 1. Test data generation needs to create entities in protected tables // 2. Moderator role is verified via is_moderator() RPC call before proceeding // Scope: Limited to moderators only, for test/development purposes const supabaseUrl = Deno.env.get('SUPABASE_URL')!; const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; const supabase = createClient(supabaseUrl, supabaseServiceKey); // Get auth header const authHeader = req.headers.get('Authorization'); if (!authHeader) { return new Response(JSON.stringify({ error: 'No authorization header' }), { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }); } // Verify user is moderator const token = authHeader.replace('Bearer ', ''); const { data: { user }, error: userError } = await supabase.auth.getUser(token); if (userError || !user) { return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }); } const { data: isMod } = await supabase.rpc('is_moderator', { _user_id: user.id }); if (!isMod) { return new Response(JSON.stringify({ error: 'Must be moderator' }), { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }); } // Parse request const { preset = 'small', entityTypes = [], includeDependencies = true, includeConflicts = false, includeVersionChains = false, includeEscalated = false, includeExpiredLocks = false }: SeedOptions = await req.json(); const plan = PRESETS[preset]; if (!plan) { return new Response(JSON.stringify({ error: 'Invalid preset' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }); } const startTime = Date.now(); const summary = { parks: 0, rides: 0, companies: 0, rideModels: 0, conflicts: 0, versionChains: 0 }; const createdParks: string[] = []; const createdCompanies: Record = { manufacturer: [], operator: [], designer: [], property_owner: [] }; const createdParkSlugs: string[] = []; const createdRideSlugs: string[] = []; // Helper to create submission async function createSubmission(userId: string, type: string, itemData: any, options: { escalated?: boolean; expiredLock?: boolean } = {}) { const submissionId = crypto.randomUUID(); const itemId = crypto.randomUUID(); const contentData = { action: 'create', metadata: { is_test_data: true, generated_at: new Date().toISOString(), generator_version: '1.0.0', preset } }; // Create content_submission const submissionData: any = { id: submissionId, user_id: userId, submission_type: type, status: 'pending', content: contentData, submitted_at: new Date().toISOString(), priority: options.escalated ? 10 : Math.floor(Math.random() * 5) + 1 }; if (options.escalated) { submissionData.escalated = true; submissionData.escalation_reason = 'Test escalation'; } if (options.expiredLock) { submissionData.assigned_to = userId; submissionData.locked_until = new Date(Date.now() - 1000 * 60 * 30).toISOString(); // 30 min ago } const { error: subError } = await supabase .from('content_submissions') .insert(submissionData); if (subError) throw subError; // Create submission_item const { error: itemError } = await supabase .from('submission_items') .insert({ id: itemId, submission_id: submissionId, item_type: type, item_data: itemData, status: 'pending', order_index: 0 }); if (itemError) throw itemError; // Create type-specific submission record const typeTableMap: Record = { park: 'park_submissions', ride: 'ride_submissions', manufacturer: 'company_submissions', operator: 'company_submissions', designer: 'company_submissions', property_owner: 'company_submissions', ride_model: 'ride_model_submissions' }; const table = typeTableMap[type]; if (table) { const typeData = { ...itemData, submission_id: submissionId }; if (table === 'company_submissions') { typeData.company_type = type; } const { error: typeError } = await supabase .from(table) .insert(typeData); if (typeError) throw typeError; } return submissionId; } // Create parks if (entityTypes.includes('parks')) { for (let i = 0; i < plan.parks; i++) { // Determine if this should be a conflict or version chain const shouldConflict = includeConflicts && createdParkSlugs.length > 0 && Math.random() < 0.15; const shouldVersionChain = includeVersionChains && createdParkSlugs.length > 0 && Math.random() < 0.15; let slug = `test-park-${i + 1}`; if (shouldConflict) { // Reuse an existing slug to create a conflict slug = createdParkSlugs[Math.floor(Math.random() * createdParkSlugs.length)]; summary.conflicts++; } else if (shouldVersionChain) { // Reuse an existing slug for a version chain with different data slug = createdParkSlugs[Math.floor(Math.random() * createdParkSlugs.length)]; summary.versionChains++; } const parkData = { name: shouldVersionChain ? `Test Park ${slug} (Updated)` : `Test Park ${i + 1}`, slug: slug, description: shouldVersionChain ? 'Updated test park description' : 'Test park description', park_type: ['theme_park', 'amusement_park', 'water_park'][Math.floor(Math.random() * 3)], status: 'operating', opening_date: '2000-01-01' }; const options = { escalated: includeEscalated && Math.random() < 0.1, expiredLock: includeExpiredLocks && Math.random() < 0.1 }; await createSubmission(user.id, 'park', parkData, options); createdParks.push(slug); if (!shouldConflict && !shouldVersionChain) { createdParkSlugs.push(slug); } summary.parks++; } } // Create companies const companyTypes = ['manufacturer', 'operator', 'designer', 'property_owner']; for (const compType of companyTypes) { if (entityTypes.includes(compType)) { const count = Math.floor(plan.companies / 4); for (let i = 0; i < count; i++) { const companyData = { name: `Test ${compType} ${i + 1}`, slug: `test-${compType}-${i + 1}`, description: `Test ${compType} description`, company_type: compType, person_type: 'company', founded_year: 2000 }; await createSubmission(user.id, compType, companyData); createdCompanies[compType].push(`test-${compType}-${i + 1}`); summary.companies++; } } } // Create rides (with dependencies if enabled) if (entityTypes.includes('rides') && includeDependencies && createdParks.length > 0) { for (let i = 0; i < plan.rides; i++) { // Determine if this should be a conflict or version chain const shouldConflict = includeConflicts && createdRideSlugs.length > 0 && Math.random() < 0.15; const shouldVersionChain = includeVersionChains && createdRideSlugs.length > 0 && Math.random() < 0.15; let slug = `test-ride-${i + 1}`; if (shouldConflict) { slug = createdRideSlugs[Math.floor(Math.random() * createdRideSlugs.length)]; summary.conflicts++; } else if (shouldVersionChain) { slug = createdRideSlugs[Math.floor(Math.random() * createdRideSlugs.length)]; summary.versionChains++; } // Get random park ID from database const parkSlug = createdParks[Math.floor(Math.random() * createdParks.length)]; const { data: parkData } = await supabase .from('parks') .select('id') .eq('slug', parkSlug) .maybeSingle(); const rideData = { name: shouldVersionChain ? `Test Ride ${slug} (Updated)` : `Test Ride ${i + 1}`, slug: slug, description: shouldVersionChain ? 'Updated test ride description' : 'Test ride description', category: ['roller_coaster', 'flat_ride', 'water_ride'][Math.floor(Math.random() * 3)], status: 'operating', park_id: parkData?.id || null, opening_date: '2010-01-01' }; await createSubmission(user.id, 'ride', rideData); if (!shouldConflict && !shouldVersionChain) { createdRideSlugs.push(slug); } summary.rides++; } } // Create ride models if (entityTypes.includes('ride_models') && includeDependencies && createdCompanies.manufacturer.length > 0) { for (let i = 0; i < plan.rideModels; i++) { const mfgSlug = createdCompanies.manufacturer[Math.floor(Math.random() * createdCompanies.manufacturer.length)]; const { data: mfgData } = await supabase .from('companies') .select('id') .eq('slug', mfgSlug) .maybeSingle(); const modelData = { name: `Test Model ${i + 1}`, slug: `test-model-${i + 1}`, manufacturer_id: mfgData?.id || null, category: 'roller_coaster', ride_type: 'steel', description: 'Test ride model' }; await createSubmission(user.id, 'ride_model', modelData); summary.rideModels++; } } const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(2); return new Response( JSON.stringify({ success: true, summary, time: elapsedTime }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } catch (error) { console.error('Seed error:', error); const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; return new Response( JSON.stringify({ error: errorMessage }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } });