feat: Implement test data generation improvements

This commit is contained in:
gpt-engineer-app[bot]
2025-10-09 14:49:54 +00:00
parent 1d45294703
commit 1557def354
4 changed files with 434 additions and 140 deletions

View File

@@ -23,37 +23,67 @@ The Test Data Generator is a comprehensive testing utility that creates realisti
### Presets
#### Small (~20 submissions)
#### Small (~30 submissions)
- **Use Case**: Quick sanity checks, basic functionality testing
- **Contents**: 5 parks, 10 rides, 3 companies, 2 ride models
- **Time**: ~2-5 seconds
- **Contents**: 5 parks, 10 rides, 3 companies, 2 ride models, 5 photo sets
- **Features**: Mixed field density, photo support
- **Time**: ~3-7 seconds
#### Medium (~100 submissions)
#### Medium (~125 submissions)
- **Use Case**: Standard testing, queue management validation
- **Contents**: 20 parks, 50 rides, 20 companies, 10 ride models
- **Time**: ~10-20 seconds
- **Contents**: 20 parks, 50 rides, 20 companies, 10 ride models, 25 photo sets
- **Features**: Full field variation, technical data, photos
- **Time**: ~15-30 seconds
#### Large (~500 submissions)
#### Large (~600 submissions)
- **Use Case**: Performance testing, pagination verification
- **Contents**: 100 parks, 250 rides, 100 companies, 50 ride models
- **Time**: ~45-90 seconds
- **Contents**: 100 parks, 250 rides, 100 companies, 50 ride models, 100 photo sets
- **Features**: Complete field population, stats, specs, former names
- **Time**: ~60-120 seconds
#### Stress (~2000 submissions)
#### Stress (~2600 submissions)
- **Use Case**: Load testing, database performance
- **Contents**: 400 parks, 1000 rides, 400 companies, 200 ride models
- **Time**: ~3-5 minutes
- **Contents**: 400 parks, 1000 rides, 400 companies, 200 ride models, 500 photo sets
- **Features**: Maximum data density, all technical data, hundreds of photos
- **Time**: ~4-7 minutes
### Entity Types
Select which entity types to generate:
- **Parks**: Theme parks, amusement parks, water parks
- **Rides**: Roller coasters, flat rides, water rides, dark rides
- **Parks**: Theme parks, amusement parks, water parks (with locations, operators, property owners)
- **Rides**: Roller coasters, flat rides, water rides, dark rides (with technical specs, coaster stats, former names)
- **Manufacturers**: Companies that build rides
- **Operators**: Companies that operate parks
- **Property Owners**: Companies that own park properties
- **Designers**: Individuals/companies that design rides
- **Ride Models**: Specific ride model types from manufacturers
- **Photos**: Photo submissions with 1-10 photos each, captions, metadata
### Field Population Density
Control how many optional fields are populated:
#### Mixed (Recommended)
- **Distribution**: 10% minimal, 20% basic, 40% standard, 20% complete, 10% maximum
- **Most Realistic**: Matches real-world usage patterns
- **Tests**: All levels of data completeness
#### Minimal
- **Fields**: Required fields only
- **Use**: Test minimum viable submissions
- **Performance**: Fastest generation
#### Standard
- **Fields**: Required + 50% optional
- **Use**: Balanced testing scenario
- **Performance**: Moderate generation time
#### Maximum
- **Fields**: All fields + technical data
- **Includes**: Coaster stats, technical specs, former names
- **Use**: Complete data testing
- **Performance**: Slowest generation
### Advanced Options

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
@@ -10,19 +10,20 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/component
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
import { supabase } from '@/integrations/supabase/client';
import { useToast } from '@/hooks/use-toast';
import { Beaker, CheckCircle, XCircle, ChevronDown, Trash2, AlertTriangle } from 'lucide-react';
import { Beaker, CheckCircle, ChevronDown, Trash2, AlertTriangle } from 'lucide-react';
import { clearTestData, getTestDataStats } from '@/lib/testDataGenerator';
const PRESETS = {
small: { label: 'Small', description: '~20 submissions - Quick test', counts: '5 parks, 10 rides, 3 companies' },
medium: { label: 'Medium', description: '~100 submissions - Standard testing', counts: '20 parks, 50 rides, 20 companies' },
large: { label: 'Large', description: '~500 submissions - Performance testing', counts: '100 parks, 250 rides, 100 companies' },
stress: { label: 'Stress', description: '~2000 submissions - Load testing', counts: '400 parks, 1000 rides, 400 companies' }
small: { label: 'Small', description: '~30 submissions - Quick test', counts: '5 parks, 10 rides, 3 companies, 2 models, 5 photo sets' },
medium: { label: 'Medium', description: '~125 submissions - Standard testing', counts: '20 parks, 50 rides, 20 companies, 10 models, 25 photo sets' },
large: { label: 'Large', description: '~600 submissions - Performance testing', counts: '100 parks, 250 rides, 100 companies, 50 models, 100 photo sets' },
stress: { label: 'Stress', description: '~2600 submissions - Load testing', counts: '400 parks, 1000 rides, 400 companies, 200 models, 500 photo sets' }
};
export function TestDataGenerator() {
const { toast } = useToast();
const [preset, setPreset] = useState<'small' | 'medium' | 'large' | 'stress'>('small');
const [fieldDensity, setFieldDensity] = useState<'mixed' | 'minimal' | 'standard' | 'maximum'>('mixed');
const [entityTypes, setEntityTypes] = useState({
parks: true,
rides: true,
@@ -30,7 +31,8 @@ export function TestDataGenerator() {
operators: true,
property_owners: true,
designers: true,
ride_models: true
ride_models: true,
photos: true
});
const [options, setOptions] = useState({
includeDependencies: true,
@@ -47,6 +49,10 @@ export function TestDataGenerator() {
.filter(([_, enabled]) => enabled)
.map(([type]) => type);
useEffect(() => {
loadStats();
}, []);
const loadStats = async () => {
try {
const data = await getTestDataStats();
@@ -64,6 +70,7 @@ export function TestDataGenerator() {
const { data, error } = await supabase.functions.invoke('seed-test-data', {
body: {
preset,
fieldDensity,
entityTypes: selectedEntityTypes,
...options
}
@@ -76,7 +83,7 @@ export function TestDataGenerator() {
toast({
title: 'Test Data Generated',
description: `Successfully created ${Object.values(data.summary).reduce((a: number, b: number) => a + b, 0)} submissions in ${data.time}s`
description: `Successfully created test data in ${data.time}s`
});
} catch (error) {
console.error('Generation error:', error);
@@ -122,7 +129,7 @@ export function TestDataGenerator() {
<CardTitle>Test Data Generator</CardTitle>
</div>
<CardDescription>
Generate realistic test submissions for testing the moderation queue and versioning systems
Generate comprehensive test submissions with varying field density and photo support
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
@@ -158,6 +165,39 @@ export function TestDataGenerator() {
</RadioGroup>
</div>
<div>
<Label className="text-base font-semibold">Field Population Density</Label>
<p className="text-sm text-muted-foreground mt-1 mb-3">
Controls how many optional fields are populated in generated entities
</p>
<RadioGroup value={fieldDensity} onValueChange={(v: any) => setFieldDensity(v)} className="space-y-2">
<div className="flex items-center space-x-2">
<RadioGroupItem value="mixed" id="mixed" />
<Label htmlFor="mixed" className="cursor-pointer">
<span className="font-medium">Mixed</span> - Random levels (most realistic)
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="minimal" id="minimal" />
<Label htmlFor="minimal" className="cursor-pointer">
<span className="font-medium">Minimal</span> - Required fields only
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="standard" id="standard" />
<Label htmlFor="standard" className="cursor-pointer">
<span className="font-medium">Standard</span> - 50% optional fields
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="maximum" id="maximum" />
<Label htmlFor="maximum" className="cursor-pointer">
<span className="font-medium">Maximum</span> - All fields + technical data
</Label>
</div>
</RadioGroup>
</div>
<div>
<Label className="text-base font-semibold">Entity Types</Label>
<div className="mt-2 grid grid-cols-2 gap-3">
@@ -219,6 +259,9 @@ export function TestDataGenerator() {
<li> Created {results.summary.rides} ride submissions</li>
<li> Created {results.summary.companies} company submissions</li>
<li> Created {results.summary.rideModels} ride model submissions</li>
{results.summary.photos > 0 && (
<li> Created {results.summary.photos} photo submissions ({results.summary.totalPhotoItems || 0} photos)</li>
)}
<li className="font-medium mt-2">Time taken: {results.time}s</li>
</ul>
</AlertDescription>

View File

@@ -3,10 +3,10 @@ import type { ParkSubmissionData, RideSubmissionData, CompanySubmissionData, Rid
// Preset configurations
export const PRESETS = {
small: { parks: 5, rides: 10, companies: 3, rideModels: 2, photos: 0 },
medium: { parks: 20, rides: 50, companies: 20, rideModels: 10, photos: 0 },
large: { parks: 100, rides: 250, companies: 100, rideModels: 50, photos: 0 },
stress: { parks: 400, rides: 1000, companies: 400, rideModels: 200, photos: 0 }
small: { parks: 5, rides: 10, companies: 3, rideModels: 2, photos: 5 },
medium: { parks: 20, rides: 50, companies: 20, rideModels: 10, photos: 25 },
large: { parks: 100, rides: 250, companies: 100, rideModels: 50, photos: 100 },
stress: { parks: 400, rides: 1000, companies: 400, rideModels: 200, photos: 500 }
} as const;
// Word lists for realistic names

View File

@@ -8,11 +8,12 @@ const corsHeaders = {
interface SeedOptions {
preset: 'small' | 'medium' | 'large' | 'stress';
entityTypes: string[];
includeDependencies: boolean;
includeConflicts: boolean;
includeVersionChains: boolean;
includeEscalated: boolean;
includeExpiredLocks: boolean;
fieldDensity?: 'mixed' | 'minimal' | 'standard' | 'maximum';
includeDependencies?: boolean;
includeConflicts?: boolean;
includeVersionChains?: boolean;
includeEscalated?: boolean;
includeExpiredLocks?: boolean;
}
interface SeedPlan {
@@ -20,33 +21,65 @@ interface SeedPlan {
rides: number;
companies: number;
rideModels: number;
photos: 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 }
small: { parks: 5, rides: 10, companies: 3, rideModels: 2, photos: 5 },
medium: { parks: 20, rides: 50, companies: 20, rideModels: 10, photos: 25 },
large: { parks: 100, rides: 250, companies: 100, rideModels: 50, photos: 100 },
stress: { parks: 400, rides: 1000, companies: 400, rideModels: 200, photos: 500 }
};
const CITIES = [
{ city: 'Orlando', state: 'Florida', country: 'USA' },
{ city: 'Anaheim', state: 'California', country: 'USA' },
{ city: 'Paris', state: 'Île-de-France', country: 'France' },
{ city: 'Tokyo', state: 'Tokyo', country: 'Japan' },
{ city: 'Berlin', state: 'Berlin', country: 'Germany' }
];
// Helper functions
function randomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function randomItem<T>(array: T[]): T {
return array[randomInt(0, array.length - 1)];
}
function randomDate(startYear: number, endYear: number): string {
const year = randomInt(startYear, endYear);
const month = randomInt(1, 12);
const day = randomInt(1, 28);
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
}
function getPopulationLevel(fieldDensity: string, index: number): number {
if (fieldDensity === 'mixed') {
const rand = Math.random();
if (rand < 0.1) return 0; // 10% minimal
if (rand < 0.3) return 1; // 20% basic
if (rand < 0.7) return 2; // 40% standard
if (rand < 0.9) return 3; // 20% complete
return 4; // 10% maximum
}
if (fieldDensity === 'minimal') return 0;
if (fieldDensity === 'standard') return 2;
if (fieldDensity === 'maximum') return 4;
return 2;
}
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' }), {
@@ -55,7 +88,6 @@ Deno.serve(async (req) => {
});
}
// Verify user is moderator
const token = authHeader.replace('Bearer ', '');
const { data: { user }, error: userError } = await supabase.auth.getUser(token);
@@ -67,25 +99,25 @@ Deno.serve(async (req) => {
}
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) {
if (modError || !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];
const {
preset = 'small',
entityTypes = [],
fieldDensity = 'mixed',
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,
@@ -94,7 +126,7 @@ Deno.serve(async (req) => {
}
const startTime = Date.now();
const summary = { parks: 0, rides: 0, companies: 0, rideModels: 0, conflicts: 0, versionChains: 0 };
const summary = { parks: 0, rides: 0, companies: 0, rideModels: 0, photos: 0, totalPhotoItems: 0, conflicts: 0, versionChains: 0 };
const createdParks: string[] = [];
const createdCompanies: Record<string, string[]> = { manufacturer: [], operator: [], designer: [], property_owner: [] };
const createdParkSlugs: string[] = [];
@@ -102,11 +134,6 @@ Deno.serve(async (req) => {
// 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();
@@ -115,12 +142,12 @@ Deno.serve(async (req) => {
metadata: {
is_test_data: true,
generated_at: new Date().toISOString(),
generator_version: '1.0.0',
preset
generator_version: '2.0.0',
preset,
fieldDensity
}
};
// Create content_submission
const submissionData: any = {
id: submissionId,
user_id: userId,
@@ -128,29 +155,23 @@ Deno.serve(async (req) => {
status: 'pending',
content: contentData,
submitted_at: new Date().toISOString(),
priority: options.escalated ? 10 : Math.floor(Math.random() * 5) + 1
priority: options.escalated ? 10 : randomInt(1, 5)
};
if (options.escalated) {
submissionData.escalated = true;
submissionData.escalation_reason = 'Test escalation';
submissionData.escalation_reason = 'Test escalation for priority testing';
}
if (options.expiredLock) {
submissionData.assigned_to = userId;
submissionData.locked_until = new Date(Date.now() - 1000 * 60 * 30).toISOString(); // 30 min ago
submissionData.locked_until = new Date(Date.now() - 1000 * 60 * 30).toISOString();
}
const { error: subError } = await supabase
.from('content_submissions')
.insert(submissionData);
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({
const { error: itemError } = await supabase.from('submission_items').insert({
id: itemId,
submission_id: submissionId,
item_type: type,
@@ -158,10 +179,8 @@ Deno.serve(async (req) => {
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',
@@ -179,43 +198,68 @@ Deno.serve(async (req) => {
typeData.company_type = type;
}
const { error: typeError } = await supabase
.from(table)
.insert(typeData);
const { data: insertedData, error: typeError } = await supabase.from(table).insert(typeData).select('id').single();
if (typeError) throw typeError;
return { submissionId, typeId: insertedData?.id };
}
return submissionId;
return { submissionId, typeId: null };
}
// 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 level = getPopulationLevel(fieldDensity, i);
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)];
slug = randomItem(createdParkSlugs);
summary.conflicts++;
} else if (shouldVersionChain) {
// Reuse an existing slug for a version chain with different data
slug = createdParkSlugs[Math.floor(Math.random() * createdParkSlugs.length)];
slug = randomItem(createdParkSlugs);
summary.versionChains++;
}
const parkData = {
const parkData: any = {
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'
park_type: randomItem(['theme_park', 'amusement_park', 'water_park']),
status: 'operating'
};
if (level >= 1) {
parkData.opening_date = randomDate(1950, 2024);
parkData.description = `A ${parkData.park_type === 'theme_park' ? 'themed' : 'exciting'} park for all ages with various attractions.`;
}
if (level >= 2) {
parkData.website_url = `https://test-park-${i + 1}.example.com`;
parkData.phone = `+1-555-${randomInt(100, 999)}-${randomInt(1000, 9999)}`;
}
if (level >= 3 && createdCompanies.operator.length > 0) {
const { data: operatorData } = await supabase.from('companies').select('id').eq('slug', randomItem(createdCompanies.operator)).maybeSingle();
if (operatorData) parkData.operator_id = operatorData.id;
parkData.email = `info@test-park-${i + 1}.example.com`;
parkData.card_image_id = `test-park-card-${i + 1}`;
parkData.card_image_url = `https://imagedelivery.net/test/park-${i + 1}/card`;
}
if (level >= 4) {
if (createdCompanies.property_owner.length > 0) {
const { data: ownerData } = await supabase.from('companies').select('id').eq('slug', randomItem(createdCompanies.property_owner)).maybeSingle();
if (ownerData) parkData.property_owner_id = ownerData.id;
}
if (Math.random() > 0.9) {
parkData.closing_date = randomDate(2000, 2024);
parkData.status = 'closed';
}
parkData.banner_image_id = `test-park-banner-${i + 1}`;
parkData.banner_image_url = `https://imagedelivery.net/test/park-${i + 1}/banner`;
}
const options = {
escalated: includeEscalated && Math.random() < 0.1,
expiredLock: includeExpiredLocks && Math.random() < 0.1
@@ -233,18 +277,39 @@ Deno.serve(async (req) => {
// Create companies
const companyTypes = ['manufacturer', 'operator', 'designer', 'property_owner'];
for (const compType of companyTypes) {
if (entityTypes.includes(compType)) {
if (entityTypes.includes(compType + 's') || entityTypes.includes(compType === 'manufacturer' ? 'manufacturers' : compType === 'property_owner' ? 'property_owners' : compType + 's')) {
const count = Math.floor(plan.companies / 4);
for (let i = 0; i < count; i++) {
const companyData = {
name: `Test ${compType} ${i + 1}`,
const level = getPopulationLevel(fieldDensity, i);
const companyData: any = {
name: `Test ${compType.replace('_', ' ')} ${i + 1}`,
slug: `test-${compType}-${i + 1}`,
description: `Test ${compType} description`,
company_type: compType,
person_type: 'company',
founded_year: 2000
company_type: compType
};
if (level >= 1) {
companyData.description = `Leading ${compType.replace('_', ' ')} in the amusement industry.`;
companyData.person_type = compType === 'designer' && Math.random() > 0.5 ? 'individual' : 'company';
}
if (level >= 2) {
companyData.founded_year = randomInt(1950, 2020);
const location = randomItem(CITIES);
companyData.headquarters_location = `${location.city}, ${location.country}`;
}
if (level >= 3) {
companyData.website_url = `https://test-${compType}-${i + 1}.example.com`;
companyData.logo_url = `https://imagedelivery.net/test/${compType}-${i + 1}/logo`;
}
if (level >= 4) {
companyData.card_image_id = `test-${compType}-card-${i + 1}`;
companyData.card_image_url = `https://imagedelivery.net/test/${compType}-${i + 1}/card`;
companyData.banner_image_id = `test-${compType}-banner-${i + 1}`;
companyData.banner_image_url = `https://imagedelivery.net/test/${compType}-${i + 1}/banner`;
}
await createSubmission(user.id, compType, companyData);
createdCompanies[compType].push(`test-${compType}-${i + 1}`);
summary.companies++;
@@ -252,41 +317,112 @@ Deno.serve(async (req) => {
}
}
// Create rides (with dependencies if enabled)
// Create rides
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 level = getPopulationLevel(fieldDensity, i);
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)];
slug = randomItem(createdRideSlugs);
summary.conflicts++;
} else if (shouldVersionChain) {
slug = createdRideSlugs[Math.floor(Math.random() * createdRideSlugs.length)];
slug = randomItem(createdRideSlugs);
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 parkSlug = randomItem(createdParks);
const { data: parkData } = await supabase.from('parks').select('id').eq('slug', parkSlug).maybeSingle();
const rideData = {
const category = randomItem(['roller_coaster', 'flat_ride', 'water_ride', 'dark_ride']);
const rideData: any = {
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)],
category: category,
status: 'operating',
park_id: parkData?.id || null,
opening_date: '2010-01-01'
park_id: parkData?.id || null
};
await createSubmission(user.id, 'ride', rideData);
if (level >= 1) {
rideData.opening_date = randomDate(2000, 2024);
rideData.description = `An exciting ${category.replace('_', ' ')} attraction.`;
rideData.height_requirement = randomInt(100, 140);
}
if (level >= 2) {
rideData.max_speed_kmh = randomInt(40, 120);
rideData.max_height_meters = randomInt(20, 100);
rideData.duration_seconds = randomInt(60, 240);
rideData.capacity_per_hour = randomInt(500, 2000);
rideData.intensity_level = randomItem(['family', 'moderate', 'high', 'extreme']);
}
if (level >= 3) {
if (createdCompanies.manufacturer.length > 0) {
const { data: mfgData } = await supabase.from('companies').select('id').eq('slug', randomItem(createdCompanies.manufacturer)).maybeSingle();
if (mfgData) rideData.manufacturer_id = mfgData.id;
}
if (category === 'roller_coaster') {
rideData.inversions = randomInt(0, 7);
rideData.coaster_type = randomItem(['steel', 'wooden', 'hybrid']);
rideData.seating_type = randomItem(['sit_down', 'inverted', 'flying', 'stand_up']);
}
rideData.card_image_id = `test-ride-card-${i + 1}`;
rideData.card_image_url = `https://imagedelivery.net/test/ride-${i + 1}/card`;
}
if (level >= 4) {
if (createdCompanies.designer.length > 0) {
const { data: designerData } = await supabase.from('companies').select('id').eq('slug', randomItem(createdCompanies.designer)).maybeSingle();
if (designerData) rideData.designer_id = designerData.id;
}
rideData.length_meters = randomInt(500, 2000);
rideData.drop_height_meters = randomInt(10, 80);
rideData.max_g_force = (Math.random() * 4 + 2).toFixed(1);
rideData.banner_image_id = `test-ride-banner-${i + 1}`;
rideData.banner_image_url = `https://imagedelivery.net/test/ride-${i + 1}/banner`;
}
const { submissionId, typeId } = await createSubmission(user.id, 'ride', rideData);
// Add technical specs and stats for level 4
if (level >= 4 && typeId && category === 'roller_coaster') {
// Add coaster stats
for (let s = 0; s < randomInt(2, 3); s++) {
await supabase.from('ride_submission_coaster_statistics').insert({
ride_submission_id: typeId,
stat_name: randomItem(['Airtime Duration', 'Zero-G Time', 'Track Gauge']),
stat_value: Math.random() * 100,
unit: randomItem(['seconds', 'mm']),
category: 'technical'
});
}
// Add technical specs
for (let t = 0; t < randomInt(3, 5); t++) {
await supabase.from('ride_submission_technical_specifications').insert({
ride_submission_id: typeId,
spec_name: randomItem(['Lift System', 'Brake System', 'Train Count', 'Track Material']),
spec_value: randomItem(['Chain lift', 'Magnetic brakes', '3 trains', 'Steel tubular']),
spec_type: 'system',
display_order: t
});
}
// Add former name
if (Math.random() > 0.7) {
await supabase.from('ride_submission_name_history').insert({
ride_submission_id: typeId,
former_name: `Original Name ${i + 1}`,
date_changed: randomDate(2010, 2020),
reason: 'Rebranding',
order_index: 0
});
}
}
if (!shouldConflict && !shouldVersionChain) {
createdRideSlugs.push(slug);
}
@@ -297,43 +433,128 @@ Deno.serve(async (req) => {
// 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 level = getPopulationLevel(fieldDensity, i);
const { data: mfgData } = await supabase.from('companies').select('id').eq('slug', randomItem(createdCompanies.manufacturer)).maybeSingle();
const modelData = {
const category = randomItem(['roller_coaster', 'flat_ride', 'water_ride']);
const modelData: any = {
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'
category: category,
manufacturer_id: mfgData?.id || null
};
if (level >= 1) {
modelData.description = `Popular ${category.replace('_', ' ')} model.`;
modelData.ride_type = randomItem(['spinning', 'launch', 'suspended', 'family']);
}
if (level >= 2) {
modelData.card_image_id = `test-model-card-${i + 1}`;
modelData.card_image_url = `https://imagedelivery.net/test/model-${i + 1}/card`;
}
if (level >= 3) {
modelData.banner_image_id = `test-model-banner-${i + 1}`;
modelData.banner_image_url = `https://imagedelivery.net/test/model-${i + 1}/banner`;
}
await createSubmission(user.id, 'ride_model', modelData);
summary.rideModels++;
}
}
const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(2);
// Create photo submissions
if (entityTypes.includes('photos') && plan.photos > 0) {
const { data: approvedParks } = await supabase.from('parks').select('id').limit(Math.min(20, plan.photos));
const { data: approvedRides } = await supabase.from('rides').select('id, park_id').limit(Math.min(20, plan.photos));
for (let i = 0; i < plan.photos; i++) {
const photoCount = randomInt(1, Math.min(10, Math.ceil(plan.photos / 50) + 3));
const submissionId = crypto.randomUUID();
const photoSubmissionId = crypto.randomUUID();
let entityType = 'park';
let entityId = null;
let parentId = null;
if (Math.random() > 0.5 && approvedParks && approvedParks.length > 0) {
entityType = 'park';
entityId = randomItem(approvedParks).id;
} else if (approvedRides && approvedRides.length > 0) {
entityType = 'ride';
const ride = randomItem(approvedRides);
entityId = ride.id;
parentId = ride.park_id;
}
if (!entityId) continue;
// Create content_submission
await supabase.from('content_submissions').insert({
id: submissionId,
user_id: user.id,
submission_type: 'photo',
status: 'pending',
content: {
action: 'create',
metadata: {
is_test_data: true,
generated_at: new Date().toISOString(),
generator_version: '2.0.0',
preset
}
},
submitted_at: new Date().toISOString()
});
// Create photo_submission
await supabase.from('photo_submissions').insert({
id: photoSubmissionId,
submission_id: submissionId,
entity_type: entityType,
entity_id: entityId,
parent_id: parentId,
title: Math.random() > 0.5 ? `${entityType} Photos Collection ${i + 1}` : null
});
// Create photo_submission_items
for (let p = 0; p < photoCount; p++) {
const imageId = `test-photo-${crypto.randomUUID()}`;
await supabase.from('photo_submission_items').insert({
photo_submission_id: photoSubmissionId,
cloudflare_image_id: imageId,
cloudflare_image_url: `https://imagedelivery.net/test/${imageId}/public`,
caption: Math.random() > 0.3 ? `Test photo ${p + 1} - Great view of the ${entityType}` : null,
title: Math.random() > 0.7 ? `Photo ${p + 1}` : null,
filename: `photo-${p + 1}.jpg`,
order_index: p,
file_size: randomInt(500000, 5000000),
mime_type: 'image/jpeg',
date_taken: Math.random() > 0.5 ? randomDate(2015, 2024) : null
});
summary.totalPhotoItems++;
}
summary.photos++;
}
}
const executionTime = Date.now() - startTime;
return new Response(
JSON.stringify({
success: true,
summary,
time: elapsedTime
time: (executionTime / 1000).toFixed(2)
}),
{ 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 }),
JSON.stringify({ error: error.message }),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}