Files
thrilltrack-explorer/supabase/functions/seed-test-data/index.ts
pac7 13a4d8f64c Improve error handling and display for searches and uploads
Enhance user feedback by displaying search errors, refine photo submission fetching, add rate limiting cleanup logic, improve image upload cleanup, and strengthen moderator permission checks.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 2741d09b-80fb-4f0a-bfd6-ababb2ac4bfc
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
2025-10-08 19:55:55 +00:00

341 lines
12 KiB
TypeScript

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<string, SeedPlan> = {
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, error: modError } = await supabase.rpc('is_moderator', { _user_id: user.id });
if (modError) {
console.error('Failed to check moderator status:', modError);
return new Response(JSON.stringify({ error: 'Failed to verify permissions' }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
if (!isMod) {
return new Response(JSON.stringify({ error: 'Insufficient permissions. Moderator role required.' }), {
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<string, string[]> = { 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 } = {}) {
// Ensure crypto.randomUUID is available
if (typeof crypto === 'undefined' || typeof crypto.randomUUID !== 'function') {
throw new Error('crypto.randomUUID is not available in this environment');
}
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<string, string> = {
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' } }
);
}
});