mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 04:31:13 -05:00
feat: Implement test data generation improvements
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,40 +155,32 @@ 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({
|
||||
id: itemId,
|
||||
submission_id: submissionId,
|
||||
item_type: type,
|
||||
item_data: itemData,
|
||||
status: 'pending',
|
||||
order_index: 0
|
||||
});
|
||||
|
||||
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',
|
||||
@@ -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' } }
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user