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
|
### Presets
|
||||||
|
|
||||||
#### Small (~20 submissions)
|
#### Small (~30 submissions)
|
||||||
- **Use Case**: Quick sanity checks, basic functionality testing
|
- **Use Case**: Quick sanity checks, basic functionality testing
|
||||||
- **Contents**: 5 parks, 10 rides, 3 companies, 2 ride models
|
- **Contents**: 5 parks, 10 rides, 3 companies, 2 ride models, 5 photo sets
|
||||||
- **Time**: ~2-5 seconds
|
- **Features**: Mixed field density, photo support
|
||||||
|
- **Time**: ~3-7 seconds
|
||||||
|
|
||||||
#### Medium (~100 submissions)
|
#### Medium (~125 submissions)
|
||||||
- **Use Case**: Standard testing, queue management validation
|
- **Use Case**: Standard testing, queue management validation
|
||||||
- **Contents**: 20 parks, 50 rides, 20 companies, 10 ride models
|
- **Contents**: 20 parks, 50 rides, 20 companies, 10 ride models, 25 photo sets
|
||||||
- **Time**: ~10-20 seconds
|
- **Features**: Full field variation, technical data, photos
|
||||||
|
- **Time**: ~15-30 seconds
|
||||||
|
|
||||||
#### Large (~500 submissions)
|
#### Large (~600 submissions)
|
||||||
- **Use Case**: Performance testing, pagination verification
|
- **Use Case**: Performance testing, pagination verification
|
||||||
- **Contents**: 100 parks, 250 rides, 100 companies, 50 ride models
|
- **Contents**: 100 parks, 250 rides, 100 companies, 50 ride models, 100 photo sets
|
||||||
- **Time**: ~45-90 seconds
|
- **Features**: Complete field population, stats, specs, former names
|
||||||
|
- **Time**: ~60-120 seconds
|
||||||
|
|
||||||
#### Stress (~2000 submissions)
|
#### Stress (~2600 submissions)
|
||||||
- **Use Case**: Load testing, database performance
|
- **Use Case**: Load testing, database performance
|
||||||
- **Contents**: 400 parks, 1000 rides, 400 companies, 200 ride models
|
- **Contents**: 400 parks, 1000 rides, 400 companies, 200 ride models, 500 photo sets
|
||||||
- **Time**: ~3-5 minutes
|
- **Features**: Maximum data density, all technical data, hundreds of photos
|
||||||
|
- **Time**: ~4-7 minutes
|
||||||
|
|
||||||
### Entity Types
|
### Entity Types
|
||||||
|
|
||||||
Select which entity types to generate:
|
Select which entity types to generate:
|
||||||
|
|
||||||
- **Parks**: Theme parks, amusement parks, water parks
|
- **Parks**: Theme parks, amusement parks, water parks (with locations, operators, property owners)
|
||||||
- **Rides**: Roller coasters, flat rides, water rides, dark rides
|
- **Rides**: Roller coasters, flat rides, water rides, dark rides (with technical specs, coaster stats, former names)
|
||||||
- **Manufacturers**: Companies that build rides
|
- **Manufacturers**: Companies that build rides
|
||||||
- **Operators**: Companies that operate parks
|
- **Operators**: Companies that operate parks
|
||||||
- **Property Owners**: Companies that own park properties
|
- **Property Owners**: Companies that own park properties
|
||||||
- **Designers**: Individuals/companies that design rides
|
- **Designers**: Individuals/companies that design rides
|
||||||
- **Ride Models**: Specific ride model types from manufacturers
|
- **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
|
### Advanced Options
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
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 { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
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';
|
import { clearTestData, getTestDataStats } from '@/lib/testDataGenerator';
|
||||||
|
|
||||||
const PRESETS = {
|
const PRESETS = {
|
||||||
small: { label: 'Small', description: '~20 submissions - Quick test', counts: '5 parks, 10 rides, 3 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: '~100 submissions - Standard testing', counts: '20 parks, 50 rides, 20 companies' },
|
medium: { label: 'Medium', description: '~125 submissions - Standard testing', counts: '20 parks, 50 rides, 20 companies, 10 models, 25 photo sets' },
|
||||||
large: { label: 'Large', description: '~500 submissions - Performance testing', counts: '100 parks, 250 rides, 100 companies' },
|
large: { label: 'Large', description: '~600 submissions - Performance testing', counts: '100 parks, 250 rides, 100 companies, 50 models, 100 photo sets' },
|
||||||
stress: { label: 'Stress', description: '~2000 submissions - Load testing', counts: '400 parks, 1000 rides, 400 companies' }
|
stress: { label: 'Stress', description: '~2600 submissions - Load testing', counts: '400 parks, 1000 rides, 400 companies, 200 models, 500 photo sets' }
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TestDataGenerator() {
|
export function TestDataGenerator() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [preset, setPreset] = useState<'small' | 'medium' | 'large' | 'stress'>('small');
|
const [preset, setPreset] = useState<'small' | 'medium' | 'large' | 'stress'>('small');
|
||||||
|
const [fieldDensity, setFieldDensity] = useState<'mixed' | 'minimal' | 'standard' | 'maximum'>('mixed');
|
||||||
const [entityTypes, setEntityTypes] = useState({
|
const [entityTypes, setEntityTypes] = useState({
|
||||||
parks: true,
|
parks: true,
|
||||||
rides: true,
|
rides: true,
|
||||||
@@ -30,7 +31,8 @@ export function TestDataGenerator() {
|
|||||||
operators: true,
|
operators: true,
|
||||||
property_owners: true,
|
property_owners: true,
|
||||||
designers: true,
|
designers: true,
|
||||||
ride_models: true
|
ride_models: true,
|
||||||
|
photos: true
|
||||||
});
|
});
|
||||||
const [options, setOptions] = useState({
|
const [options, setOptions] = useState({
|
||||||
includeDependencies: true,
|
includeDependencies: true,
|
||||||
@@ -47,6 +49,10 @@ export function TestDataGenerator() {
|
|||||||
.filter(([_, enabled]) => enabled)
|
.filter(([_, enabled]) => enabled)
|
||||||
.map(([type]) => type);
|
.map(([type]) => type);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadStats();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const loadStats = async () => {
|
const loadStats = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getTestDataStats();
|
const data = await getTestDataStats();
|
||||||
@@ -64,6 +70,7 @@ export function TestDataGenerator() {
|
|||||||
const { data, error } = await supabase.functions.invoke('seed-test-data', {
|
const { data, error } = await supabase.functions.invoke('seed-test-data', {
|
||||||
body: {
|
body: {
|
||||||
preset,
|
preset,
|
||||||
|
fieldDensity,
|
||||||
entityTypes: selectedEntityTypes,
|
entityTypes: selectedEntityTypes,
|
||||||
...options
|
...options
|
||||||
}
|
}
|
||||||
@@ -76,7 +83,7 @@ export function TestDataGenerator() {
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Test Data Generated',
|
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) {
|
} catch (error) {
|
||||||
console.error('Generation error:', error);
|
console.error('Generation error:', error);
|
||||||
@@ -122,7 +129,7 @@ export function TestDataGenerator() {
|
|||||||
<CardTitle>Test Data Generator</CardTitle>
|
<CardTitle>Test Data Generator</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<CardDescription>
|
<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>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
@@ -158,6 +165,39 @@ export function TestDataGenerator() {
|
|||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<Label className="text-base font-semibold">Entity Types</Label>
|
<Label className="text-base font-semibold">Entity Types</Label>
|
||||||
<div className="mt-2 grid grid-cols-2 gap-3">
|
<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.rides} ride submissions</li>
|
||||||
<li>• Created {results.summary.companies} company submissions</li>
|
<li>• Created {results.summary.companies} company submissions</li>
|
||||||
<li>• Created {results.summary.rideModels} ride model 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>
|
<li className="font-medium mt-2">Time taken: {results.time}s</li>
|
||||||
</ul>
|
</ul>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import type { ParkSubmissionData, RideSubmissionData, CompanySubmissionData, Rid
|
|||||||
|
|
||||||
// Preset configurations
|
// Preset configurations
|
||||||
export const PRESETS = {
|
export const PRESETS = {
|
||||||
small: { parks: 5, rides: 10, companies: 3, rideModels: 2, photos: 0 },
|
small: { parks: 5, rides: 10, companies: 3, rideModels: 2, photos: 5 },
|
||||||
medium: { parks: 20, rides: 50, companies: 20, rideModels: 10, photos: 0 },
|
medium: { parks: 20, rides: 50, companies: 20, rideModels: 10, photos: 25 },
|
||||||
large: { parks: 100, rides: 250, companies: 100, rideModels: 50, photos: 0 },
|
large: { parks: 100, rides: 250, companies: 100, rideModels: 50, photos: 100 },
|
||||||
stress: { parks: 400, rides: 1000, companies: 400, rideModels: 200, photos: 0 }
|
stress: { parks: 400, rides: 1000, companies: 400, rideModels: 200, photos: 500 }
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Word lists for realistic names
|
// Word lists for realistic names
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ const corsHeaders = {
|
|||||||
interface SeedOptions {
|
interface SeedOptions {
|
||||||
preset: 'small' | 'medium' | 'large' | 'stress';
|
preset: 'small' | 'medium' | 'large' | 'stress';
|
||||||
entityTypes: string[];
|
entityTypes: string[];
|
||||||
includeDependencies: boolean;
|
fieldDensity?: 'mixed' | 'minimal' | 'standard' | 'maximum';
|
||||||
includeConflicts: boolean;
|
includeDependencies?: boolean;
|
||||||
includeVersionChains: boolean;
|
includeConflicts?: boolean;
|
||||||
includeEscalated: boolean;
|
includeVersionChains?: boolean;
|
||||||
includeExpiredLocks: boolean;
|
includeEscalated?: boolean;
|
||||||
|
includeExpiredLocks?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SeedPlan {
|
interface SeedPlan {
|
||||||
@@ -20,33 +21,65 @@ interface SeedPlan {
|
|||||||
rides: number;
|
rides: number;
|
||||||
companies: number;
|
companies: number;
|
||||||
rideModels: number;
|
rideModels: number;
|
||||||
|
photos: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PRESETS: Record<string, SeedPlan> = {
|
const PRESETS: Record<string, SeedPlan> = {
|
||||||
small: { parks: 5, rides: 10, companies: 3, rideModels: 2 },
|
small: { parks: 5, rides: 10, companies: 3, rideModels: 2, photos: 5 },
|
||||||
medium: { parks: 20, rides: 50, companies: 20, rideModels: 10 },
|
medium: { parks: 20, rides: 50, companies: 20, rideModels: 10, photos: 25 },
|
||||||
large: { parks: 100, rides: 250, companies: 100, rideModels: 50 },
|
large: { parks: 100, rides: 250, companies: 100, rideModels: 50, photos: 100 },
|
||||||
stress: { parks: 400, rides: 1000, companies: 400, rideModels: 200 }
|
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) => {
|
Deno.serve(async (req) => {
|
||||||
if (req.method === 'OPTIONS') {
|
if (req.method === 'OPTIONS') {
|
||||||
return new Response(null, { headers: corsHeaders });
|
return new Response(null, { headers: corsHeaders });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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 supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
||||||
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
||||||
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
||||||
|
|
||||||
// Get auth header
|
|
||||||
const authHeader = req.headers.get('Authorization');
|
const authHeader = req.headers.get('Authorization');
|
||||||
if (!authHeader) {
|
if (!authHeader) {
|
||||||
return new Response(JSON.stringify({ error: 'No authorization header' }), {
|
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 token = authHeader.replace('Bearer ', '');
|
||||||
const { data: { user }, error: userError } = await supabase.auth.getUser(token);
|
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 });
|
const { data: isMod, error: modError } = await supabase.rpc('is_moderator', { _user_id: user.id });
|
||||||
if (modError) {
|
if (modError || !isMod) {
|
||||||
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.' }), {
|
return new Response(JSON.stringify({ error: 'Insufficient permissions. Moderator role required.' }), {
|
||||||
status: 403,
|
status: 403,
|
||||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse request
|
const {
|
||||||
const { preset = 'small', entityTypes = [], includeDependencies = true, includeConflicts = false, includeVersionChains = false, includeEscalated = false, includeExpiredLocks = false }: SeedOptions = await req.json();
|
preset = 'small',
|
||||||
|
entityTypes = [],
|
||||||
|
fieldDensity = 'mixed',
|
||||||
|
includeDependencies = true,
|
||||||
|
includeConflicts = false,
|
||||||
|
includeVersionChains = false,
|
||||||
|
includeEscalated = false,
|
||||||
|
includeExpiredLocks = false
|
||||||
|
}: SeedOptions = await req.json();
|
||||||
|
|
||||||
const plan = PRESETS[preset];
|
const plan = PRESETS[preset];
|
||||||
|
|
||||||
if (!plan) {
|
if (!plan) {
|
||||||
return new Response(JSON.stringify({ error: 'Invalid preset' }), {
|
return new Response(JSON.stringify({ error: 'Invalid preset' }), {
|
||||||
status: 400,
|
status: 400,
|
||||||
@@ -94,7 +126,7 @@ Deno.serve(async (req) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startTime = Date.now();
|
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 createdParks: string[] = [];
|
||||||
const createdCompanies: Record<string, string[]> = { manufacturer: [], operator: [], designer: [], property_owner: [] };
|
const createdCompanies: Record<string, string[]> = { manufacturer: [], operator: [], designer: [], property_owner: [] };
|
||||||
const createdParkSlugs: string[] = [];
|
const createdParkSlugs: string[] = [];
|
||||||
@@ -102,11 +134,6 @@ Deno.serve(async (req) => {
|
|||||||
|
|
||||||
// Helper to create submission
|
// Helper to create submission
|
||||||
async function createSubmission(userId: string, type: string, itemData: any, options: { escalated?: boolean; expiredLock?: boolean } = {}) {
|
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 submissionId = crypto.randomUUID();
|
||||||
const itemId = crypto.randomUUID();
|
const itemId = crypto.randomUUID();
|
||||||
|
|
||||||
@@ -115,12 +142,12 @@ Deno.serve(async (req) => {
|
|||||||
metadata: {
|
metadata: {
|
||||||
is_test_data: true,
|
is_test_data: true,
|
||||||
generated_at: new Date().toISOString(),
|
generated_at: new Date().toISOString(),
|
||||||
generator_version: '1.0.0',
|
generator_version: '2.0.0',
|
||||||
preset
|
preset,
|
||||||
|
fieldDensity
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create content_submission
|
|
||||||
const submissionData: any = {
|
const submissionData: any = {
|
||||||
id: submissionId,
|
id: submissionId,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
@@ -128,40 +155,32 @@ Deno.serve(async (req) => {
|
|||||||
status: 'pending',
|
status: 'pending',
|
||||||
content: contentData,
|
content: contentData,
|
||||||
submitted_at: new Date().toISOString(),
|
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) {
|
if (options.escalated) {
|
||||||
submissionData.escalated = true;
|
submissionData.escalated = true;
|
||||||
submissionData.escalation_reason = 'Test escalation';
|
submissionData.escalation_reason = 'Test escalation for priority testing';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.expiredLock) {
|
if (options.expiredLock) {
|
||||||
submissionData.assigned_to = userId;
|
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
|
const { error: subError } = await supabase.from('content_submissions').insert(submissionData);
|
||||||
.from('content_submissions')
|
|
||||||
.insert(submissionData);
|
|
||||||
|
|
||||||
if (subError) throw subError;
|
if (subError) throw subError;
|
||||||
|
|
||||||
// Create submission_item
|
const { error: itemError } = await supabase.from('submission_items').insert({
|
||||||
const { error: itemError } = await supabase
|
id: itemId,
|
||||||
.from('submission_items')
|
submission_id: submissionId,
|
||||||
.insert({
|
item_type: type,
|
||||||
id: itemId,
|
item_data: itemData,
|
||||||
submission_id: submissionId,
|
status: 'pending',
|
||||||
item_type: type,
|
order_index: 0
|
||||||
item_data: itemData,
|
});
|
||||||
status: 'pending',
|
|
||||||
order_index: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
if (itemError) throw itemError;
|
if (itemError) throw itemError;
|
||||||
|
|
||||||
// Create type-specific submission record
|
|
||||||
const typeTableMap: Record<string, string> = {
|
const typeTableMap: Record<string, string> = {
|
||||||
park: 'park_submissions',
|
park: 'park_submissions',
|
||||||
ride: 'ride_submissions',
|
ride: 'ride_submissions',
|
||||||
@@ -179,43 +198,68 @@ Deno.serve(async (req) => {
|
|||||||
typeData.company_type = type;
|
typeData.company_type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { error: typeError } = await supabase
|
const { data: insertedData, error: typeError } = await supabase.from(table).insert(typeData).select('id').single();
|
||||||
.from(table)
|
|
||||||
.insert(typeData);
|
|
||||||
|
|
||||||
if (typeError) throw typeError;
|
if (typeError) throw typeError;
|
||||||
|
return { submissionId, typeId: insertedData?.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
return submissionId;
|
return { submissionId, typeId: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create parks
|
// Create parks
|
||||||
if (entityTypes.includes('parks')) {
|
if (entityTypes.includes('parks')) {
|
||||||
for (let i = 0; i < plan.parks; i++) {
|
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 shouldConflict = includeConflicts && createdParkSlugs.length > 0 && Math.random() < 0.15;
|
||||||
const shouldVersionChain = includeVersionChains && createdParkSlugs.length > 0 && Math.random() < 0.15;
|
const shouldVersionChain = includeVersionChains && createdParkSlugs.length > 0 && Math.random() < 0.15;
|
||||||
|
|
||||||
let slug = `test-park-${i + 1}`;
|
let slug = `test-park-${i + 1}`;
|
||||||
if (shouldConflict) {
|
if (shouldConflict) {
|
||||||
// Reuse an existing slug to create a conflict
|
slug = randomItem(createdParkSlugs);
|
||||||
slug = createdParkSlugs[Math.floor(Math.random() * createdParkSlugs.length)];
|
|
||||||
summary.conflicts++;
|
summary.conflicts++;
|
||||||
} else if (shouldVersionChain) {
|
} else if (shouldVersionChain) {
|
||||||
// Reuse an existing slug for a version chain with different data
|
slug = randomItem(createdParkSlugs);
|
||||||
slug = createdParkSlugs[Math.floor(Math.random() * createdParkSlugs.length)];
|
|
||||||
summary.versionChains++;
|
summary.versionChains++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parkData = {
|
const parkData: any = {
|
||||||
name: shouldVersionChain ? `Test Park ${slug} (Updated)` : `Test Park ${i + 1}`,
|
name: shouldVersionChain ? `Test Park ${slug} (Updated)` : `Test Park ${i + 1}`,
|
||||||
slug: slug,
|
slug: slug,
|
||||||
description: shouldVersionChain ? 'Updated test park description' : 'Test park description',
|
park_type: randomItem(['theme_park', 'amusement_park', 'water_park']),
|
||||||
park_type: ['theme_park', 'amusement_park', 'water_park'][Math.floor(Math.random() * 3)],
|
status: 'operating'
|
||||||
status: 'operating',
|
|
||||||
opening_date: '2000-01-01'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 = {
|
const options = {
|
||||||
escalated: includeEscalated && Math.random() < 0.1,
|
escalated: includeEscalated && Math.random() < 0.1,
|
||||||
expiredLock: includeExpiredLocks && Math.random() < 0.1
|
expiredLock: includeExpiredLocks && Math.random() < 0.1
|
||||||
@@ -233,18 +277,39 @@ Deno.serve(async (req) => {
|
|||||||
// Create companies
|
// Create companies
|
||||||
const companyTypes = ['manufacturer', 'operator', 'designer', 'property_owner'];
|
const companyTypes = ['manufacturer', 'operator', 'designer', 'property_owner'];
|
||||||
for (const compType of companyTypes) {
|
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);
|
const count = Math.floor(plan.companies / 4);
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const companyData = {
|
const level = getPopulationLevel(fieldDensity, i);
|
||||||
name: `Test ${compType} ${i + 1}`,
|
const companyData: any = {
|
||||||
|
name: `Test ${compType.replace('_', ' ')} ${i + 1}`,
|
||||||
slug: `test-${compType}-${i + 1}`,
|
slug: `test-${compType}-${i + 1}`,
|
||||||
description: `Test ${compType} description`,
|
company_type: compType
|
||||||
company_type: compType,
|
|
||||||
person_type: 'company',
|
|
||||||
founded_year: 2000
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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);
|
await createSubmission(user.id, compType, companyData);
|
||||||
createdCompanies[compType].push(`test-${compType}-${i + 1}`);
|
createdCompanies[compType].push(`test-${compType}-${i + 1}`);
|
||||||
summary.companies++;
|
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) {
|
if (entityTypes.includes('rides') && includeDependencies && createdParks.length > 0) {
|
||||||
for (let i = 0; i < plan.rides; i++) {
|
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 shouldConflict = includeConflicts && createdRideSlugs.length > 0 && Math.random() < 0.15;
|
||||||
const shouldVersionChain = includeVersionChains && createdRideSlugs.length > 0 && Math.random() < 0.15;
|
const shouldVersionChain = includeVersionChains && createdRideSlugs.length > 0 && Math.random() < 0.15;
|
||||||
|
|
||||||
let slug = `test-ride-${i + 1}`;
|
let slug = `test-ride-${i + 1}`;
|
||||||
if (shouldConflict) {
|
if (shouldConflict) {
|
||||||
slug = createdRideSlugs[Math.floor(Math.random() * createdRideSlugs.length)];
|
slug = randomItem(createdRideSlugs);
|
||||||
summary.conflicts++;
|
summary.conflicts++;
|
||||||
} else if (shouldVersionChain) {
|
} else if (shouldVersionChain) {
|
||||||
slug = createdRideSlugs[Math.floor(Math.random() * createdRideSlugs.length)];
|
slug = randomItem(createdRideSlugs);
|
||||||
summary.versionChains++;
|
summary.versionChains++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get random park ID from database
|
const parkSlug = randomItem(createdParks);
|
||||||
const parkSlug = createdParks[Math.floor(Math.random() * createdParks.length)];
|
const { data: parkData } = await supabase.from('parks').select('id').eq('slug', parkSlug).maybeSingle();
|
||||||
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}`,
|
name: shouldVersionChain ? `Test Ride ${slug} (Updated)` : `Test Ride ${i + 1}`,
|
||||||
slug: slug,
|
slug: slug,
|
||||||
description: shouldVersionChain ? 'Updated test ride description' : 'Test ride description',
|
category: category,
|
||||||
category: ['roller_coaster', 'flat_ride', 'water_ride'][Math.floor(Math.random() * 3)],
|
|
||||||
status: 'operating',
|
status: 'operating',
|
||||||
park_id: parkData?.id || null,
|
park_id: parkData?.id || null
|
||||||
opening_date: '2010-01-01'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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) {
|
if (!shouldConflict && !shouldVersionChain) {
|
||||||
createdRideSlugs.push(slug);
|
createdRideSlugs.push(slug);
|
||||||
}
|
}
|
||||||
@@ -297,43 +433,128 @@ Deno.serve(async (req) => {
|
|||||||
// Create ride models
|
// Create ride models
|
||||||
if (entityTypes.includes('ride_models') && includeDependencies && createdCompanies.manufacturer.length > 0) {
|
if (entityTypes.includes('ride_models') && includeDependencies && createdCompanies.manufacturer.length > 0) {
|
||||||
for (let i = 0; i < plan.rideModels; i++) {
|
for (let i = 0; i < plan.rideModels; i++) {
|
||||||
const mfgSlug = createdCompanies.manufacturer[Math.floor(Math.random() * createdCompanies.manufacturer.length)];
|
const level = getPopulationLevel(fieldDensity, i);
|
||||||
const { data: mfgData } = await supabase
|
const { data: mfgData } = await supabase.from('companies').select('id').eq('slug', randomItem(createdCompanies.manufacturer)).maybeSingle();
|
||||||
.from('companies')
|
|
||||||
.select('id')
|
|
||||||
.eq('slug', mfgSlug)
|
|
||||||
.maybeSingle();
|
|
||||||
|
|
||||||
const modelData = {
|
const category = randomItem(['roller_coaster', 'flat_ride', 'water_ride']);
|
||||||
|
const modelData: any = {
|
||||||
name: `Test Model ${i + 1}`,
|
name: `Test Model ${i + 1}`,
|
||||||
slug: `test-model-${i + 1}`,
|
slug: `test-model-${i + 1}`,
|
||||||
manufacturer_id: mfgData?.id || null,
|
category: category,
|
||||||
category: 'roller_coaster',
|
manufacturer_id: mfgData?.id || null
|
||||||
ride_type: 'steel',
|
|
||||||
description: 'Test ride model'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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);
|
await createSubmission(user.id, 'ride_model', modelData);
|
||||||
summary.rideModels++;
|
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(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
summary,
|
summary,
|
||||||
time: elapsedTime
|
time: (executionTime / 1000).toFixed(2)
|
||||||
}),
|
}),
|
||||||
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
);
|
);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Seed error:', error);
|
console.error('Seed error:', error);
|
||||||
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: errorMessage }),
|
JSON.stringify({ error: error.message }),
|
||||||
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user