mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 14:31:12 -05:00
Implement Test Data Generator
This commit is contained in:
246
docs/TEST_DATA_GENERATOR.md
Normal file
246
docs/TEST_DATA_GENERATOR.md
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
# Test Data Generator
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Test Data Generator is a comprehensive testing utility that creates realistic submissions to test the moderation queue, versioning, and audit systems. It's accessible from **Admin Settings > Testing** tab.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Multiple Presets**: Small, Medium, Large, and Stress test configurations
|
||||||
|
- **Entity Type Selection**: Choose which entities to generate (parks, rides, companies, etc.)
|
||||||
|
- **Advanced Options**: Dependencies, conflicts, version chains, escalations
|
||||||
|
- **Test Data Marking**: All generated data is marked with `is_test_data: true`
|
||||||
|
- **Easy Cleanup**: Clear all test data with one click
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Accessing the Generator
|
||||||
|
|
||||||
|
1. Log in as a moderator or admin
|
||||||
|
2. Navigate to **Admin Settings**
|
||||||
|
3. Click the **Testing** tab
|
||||||
|
4. Configure your test data generation
|
||||||
|
|
||||||
|
### Presets
|
||||||
|
|
||||||
|
#### Small (~20 submissions)
|
||||||
|
- **Use Case**: Quick sanity checks, basic functionality testing
|
||||||
|
- **Contents**: 5 parks, 10 rides, 3 companies, 2 ride models
|
||||||
|
- **Time**: ~2-5 seconds
|
||||||
|
|
||||||
|
#### Medium (~100 submissions)
|
||||||
|
- **Use Case**: Standard testing, queue management validation
|
||||||
|
- **Contents**: 20 parks, 50 rides, 20 companies, 10 ride models
|
||||||
|
- **Time**: ~10-20 seconds
|
||||||
|
|
||||||
|
#### Large (~500 submissions)
|
||||||
|
- **Use Case**: Performance testing, pagination verification
|
||||||
|
- **Contents**: 100 parks, 250 rides, 100 companies, 50 ride models
|
||||||
|
- **Time**: ~45-90 seconds
|
||||||
|
|
||||||
|
#### Stress (~2000 submissions)
|
||||||
|
- **Use Case**: Load testing, database performance
|
||||||
|
- **Contents**: 400 parks, 1000 rides, 400 companies, 200 ride models
|
||||||
|
- **Time**: ~3-5 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
|
||||||
|
- **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
|
||||||
|
|
||||||
|
### Advanced Options
|
||||||
|
|
||||||
|
#### Include Dependencies
|
||||||
|
- Creates parent-child relationships (e.g., rides linked to parks)
|
||||||
|
- Tests dependency resolution in moderation queue
|
||||||
|
- Essential for realistic testing scenarios
|
||||||
|
|
||||||
|
#### Include Conflicts
|
||||||
|
- Generates submissions with missing dependencies
|
||||||
|
- Creates duplicate slug scenarios
|
||||||
|
- Tests error handling and conflict resolution
|
||||||
|
|
||||||
|
#### Include Version Chains
|
||||||
|
- Creates multiple edits on the same entity
|
||||||
|
- Tests versioning system and rollback functionality
|
||||||
|
- Validates field-level change tracking
|
||||||
|
|
||||||
|
#### Include Escalated Items
|
||||||
|
- Marks some submissions as high priority
|
||||||
|
- Tests escalation workflow
|
||||||
|
- Priority queue sorting validation
|
||||||
|
|
||||||
|
#### Include Expired Locks
|
||||||
|
- Creates submissions with expired moderator locks
|
||||||
|
- Tests auto-release mechanism
|
||||||
|
- Queue cleanup validation
|
||||||
|
|
||||||
|
## Test Data Structure
|
||||||
|
|
||||||
|
All test submissions include:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"action": "create",
|
||||||
|
"metadata": {
|
||||||
|
"is_test_data": true,
|
||||||
|
"generated_at": "2024-01-15T10:30:00Z",
|
||||||
|
"generator_version": "1.0.0",
|
||||||
|
"preset": "medium"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This metadata allows:
|
||||||
|
- Easy identification in database queries
|
||||||
|
- Selective cleanup without affecting real data
|
||||||
|
- Version tracking of test data generator
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
|
||||||
|
### Clear All Test Data
|
||||||
|
|
||||||
|
1. Click **Clear All Test Data** button
|
||||||
|
2. Confirm the action in the dialog
|
||||||
|
3. All submissions with `is_test_data: true` will be deleted
|
||||||
|
4. Process includes:
|
||||||
|
- Deletion of `content_submissions` records
|
||||||
|
- Cascade deletion of related `submission_items`
|
||||||
|
- Cascade deletion of type-specific tables
|
||||||
|
- Removal of photo submissions
|
||||||
|
- Cleanup of version history
|
||||||
|
|
||||||
|
**Note**: This only removes test data. Real user submissions are never affected.
|
||||||
|
|
||||||
|
## Testing Scenarios
|
||||||
|
|
||||||
|
### Basic CRUD Testing
|
||||||
|
1. Generate Small preset with all entity types
|
||||||
|
2. Navigate to Moderation Queue
|
||||||
|
3. Claim and approve a park submission
|
||||||
|
4. Verify park appears in database
|
||||||
|
5. Clear test data when done
|
||||||
|
|
||||||
|
### Dependency Resolution
|
||||||
|
1. Generate Medium preset with dependencies enabled
|
||||||
|
2. Find a ride submission that depends on a new park
|
||||||
|
3. Approve the park first
|
||||||
|
4. Verify ride can now be approved
|
||||||
|
5. Check that park ID is correctly linked
|
||||||
|
|
||||||
|
### Queue Management
|
||||||
|
1. Generate Large preset with escalated items
|
||||||
|
2. Verify escalated items appear at top of queue
|
||||||
|
3. Test claiming and lock expiration
|
||||||
|
4. Verify priority sorting works correctly
|
||||||
|
|
||||||
|
### Performance Testing
|
||||||
|
1. Generate Stress preset
|
||||||
|
2. Navigate to moderation queue
|
||||||
|
3. Test pagination performance
|
||||||
|
4. Test filtering and search performance
|
||||||
|
5. Monitor database query times
|
||||||
|
|
||||||
|
### Version Chain Testing
|
||||||
|
1. Generate Medium preset with version chains
|
||||||
|
2. Find an entity with multiple edits
|
||||||
|
3. View version history
|
||||||
|
4. Test rollback functionality
|
||||||
|
5. Verify field-level changes tracked
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### During Development
|
||||||
|
- Use **Small** preset for quick iterations
|
||||||
|
- Enable only the entity types you're testing
|
||||||
|
- Clear test data frequently to avoid clutter
|
||||||
|
|
||||||
|
### Before Deployment
|
||||||
|
- Run **Medium** preset with all options
|
||||||
|
- Verify all workflows complete successfully
|
||||||
|
- Test with **Large** preset for performance baseline
|
||||||
|
|
||||||
|
### Performance Testing
|
||||||
|
- Use **Stress** preset on staging environment
|
||||||
|
- Monitor database performance metrics
|
||||||
|
- Test concurrent moderator scenarios
|
||||||
|
|
||||||
|
### Data Management
|
||||||
|
- Clear test data after each testing session
|
||||||
|
- Never deploy with test data in production
|
||||||
|
- Document any test data used in demos
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Generation Fails
|
||||||
|
- Check browser console for errors
|
||||||
|
- Verify moderator permissions
|
||||||
|
- Check edge function logs in Supabase dashboard
|
||||||
|
- Ensure database connection is stable
|
||||||
|
|
||||||
|
### Slow Generation
|
||||||
|
- Large/Stress presets take time (expected)
|
||||||
|
- Check database performance
|
||||||
|
- Verify edge function timeout settings
|
||||||
|
- Consider generating in smaller batches
|
||||||
|
|
||||||
|
### Cannot Clear Test Data
|
||||||
|
- Verify test data exists (check stats display)
|
||||||
|
- Check RLS policies on content_submissions
|
||||||
|
- Ensure cascade delete is configured
|
||||||
|
- Check edge function permissions
|
||||||
|
|
||||||
|
### Test Data Appears in Production
|
||||||
|
- **Never run generator in production**
|
||||||
|
- Use separate testing/staging environment
|
||||||
|
- If accidentally run, use Clear All Test Data immediately
|
||||||
|
- Verify with database query that test data is removed
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Implementation Files
|
||||||
|
- **Service**: `src/lib/testDataGenerator.ts`
|
||||||
|
- **UI Component**: `src/components/admin/TestDataGenerator.tsx`
|
||||||
|
- **Seed Function**: `supabase/functions/seed-test-data/index.ts`
|
||||||
|
- **Integration**: `src/pages/AdminSettings.tsx`
|
||||||
|
|
||||||
|
### Database Tables Used
|
||||||
|
- `content_submissions` - Main submission records
|
||||||
|
- `submission_items` - Individual submission items
|
||||||
|
- `park_submissions` - Park-specific data
|
||||||
|
- `ride_submissions` - Ride-specific data
|
||||||
|
- `company_submissions` - Company-specific data
|
||||||
|
- `ride_model_submissions` - Ride model-specific data
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Requires moderator role
|
||||||
|
- Uses service role key for bulk operations
|
||||||
|
- Rate limited to prevent abuse
|
||||||
|
- All operations logged in audit trail
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Planned features:
|
||||||
|
- Photo submission generation
|
||||||
|
- Report/moderation flag generation
|
||||||
|
- Custom seed configuration
|
||||||
|
- Export/import test scenarios
|
||||||
|
- Automated test suite integration
|
||||||
|
- Performance benchmarking tools
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
1. Check edge function logs in Supabase dashboard
|
||||||
|
2. Review browser console for client errors
|
||||||
|
3. Verify database schema matches expectations
|
||||||
|
4. Check RLS policies are correctly configured
|
||||||
258
src/components/admin/TestDataGenerator.tsx
Normal file
258
src/components/admin/TestDataGenerator.tsx
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||||
|
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 { 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' }
|
||||||
|
};
|
||||||
|
|
||||||
|
export function TestDataGenerator() {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [preset, setPreset] = useState<'small' | 'medium' | 'large' | 'stress'>('small');
|
||||||
|
const [entityTypes, setEntityTypes] = useState({
|
||||||
|
parks: true,
|
||||||
|
rides: true,
|
||||||
|
manufacturers: true,
|
||||||
|
operators: true,
|
||||||
|
property_owners: true,
|
||||||
|
designers: true,
|
||||||
|
ride_models: true
|
||||||
|
});
|
||||||
|
const [options, setOptions] = useState({
|
||||||
|
includeDependencies: true,
|
||||||
|
includeConflicts: false,
|
||||||
|
includeVersionChains: false,
|
||||||
|
includeEscalated: false,
|
||||||
|
includeExpiredLocks: false
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [results, setResults] = useState<any>(null);
|
||||||
|
const [stats, setStats] = useState<{ total: number; pending: number; approved: number } | null>(null);
|
||||||
|
|
||||||
|
const selectedEntityTypes = Object.entries(entityTypes)
|
||||||
|
.filter(([_, enabled]) => enabled)
|
||||||
|
.map(([type]) => type);
|
||||||
|
|
||||||
|
const loadStats = async () => {
|
||||||
|
try {
|
||||||
|
const data = await getTestDataStats();
|
||||||
|
setStats(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load stats:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGenerate = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setResults(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase.functions.invoke('seed-test-data', {
|
||||||
|
body: {
|
||||||
|
preset,
|
||||||
|
entityTypes: selectedEntityTypes,
|
||||||
|
...options
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
setResults(data);
|
||||||
|
await loadStats();
|
||||||
|
|
||||||
|
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`
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Generation error:', error);
|
||||||
|
toast({
|
||||||
|
title: 'Generation Failed',
|
||||||
|
description: error.message,
|
||||||
|
variant: 'destructive'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { deleted } = await clearTestData();
|
||||||
|
await loadStats();
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Test Data Cleared',
|
||||||
|
description: `Removed ${deleted} test submissions`
|
||||||
|
});
|
||||||
|
setResults(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Clear error:', error);
|
||||||
|
toast({
|
||||||
|
title: 'Clear Failed',
|
||||||
|
description: error.message,
|
||||||
|
variant: 'destructive'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Beaker className="w-5 h-5" />
|
||||||
|
<CardTitle>Test Data Generator</CardTitle>
|
||||||
|
</div>
|
||||||
|
<CardDescription>
|
||||||
|
Generate realistic test submissions for testing the moderation queue and versioning systems
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
This will create test data in your database. All test submissions are marked and can be cleared later.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{stats && (
|
||||||
|
<div className="flex gap-4 text-sm text-muted-foreground">
|
||||||
|
<span>Total Test Data: {stats.total}</span>
|
||||||
|
<span>Pending: {stats.pending}</span>
|
||||||
|
<span>Approved: {stats.approved}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-semibold">Preset</Label>
|
||||||
|
<RadioGroup value={preset} onValueChange={(v: any) => setPreset(v)} className="mt-2 space-y-3">
|
||||||
|
{Object.entries(PRESETS).map(([key, { label, description, counts }]) => (
|
||||||
|
<div key={key} className="flex items-start space-x-2">
|
||||||
|
<RadioGroupItem value={key} id={key} className="mt-1" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<Label htmlFor={key} className="font-medium cursor-pointer">{label}</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">{description}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{counts}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-semibold">Entity Types</Label>
|
||||||
|
<div className="mt-2 grid grid-cols-2 gap-3">
|
||||||
|
{Object.entries(entityTypes).map(([key, enabled]) => (
|
||||||
|
<div key={key} className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id={key}
|
||||||
|
checked={enabled}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setEntityTypes({ ...entityTypes, [key]: !!checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={key} className="cursor-pointer capitalize">
|
||||||
|
{key.replace('_', ' ')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Collapsible>
|
||||||
|
<CollapsibleTrigger className="flex items-center gap-2 text-sm font-medium">
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
Advanced Options
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent className="mt-3 space-y-3">
|
||||||
|
{Object.entries(options).map(([key, enabled]) => (
|
||||||
|
<div key={key} className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id={key}
|
||||||
|
checked={enabled}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setOptions({ ...options, [key]: !!checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={key} className="cursor-pointer capitalize">
|
||||||
|
{key.replace(/([A-Z])/g, ' $1').toLowerCase()}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Progress value={undefined} />
|
||||||
|
<p className="text-sm text-center text-muted-foreground">Generating test data...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{results && (
|
||||||
|
<Alert>
|
||||||
|
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||||
|
<AlertDescription>
|
||||||
|
<div className="font-medium mb-2">Test Data Generated Successfully</div>
|
||||||
|
<ul className="text-sm space-y-1">
|
||||||
|
<li>• Created {results.summary.parks} park submissions</li>
|
||||||
|
<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>
|
||||||
|
<li className="font-medium mt-2">Time taken: {results.time}s</li>
|
||||||
|
</ul>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Button onClick={handleGenerate} disabled={loading || selectedEntityTypes.length === 0}>
|
||||||
|
<Beaker className="w-4 h-4 mr-2" />
|
||||||
|
Generate Test Data
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="destructive" disabled={loading}>
|
||||||
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
|
Clear All Test Data
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Clear All Test Data?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will permanently delete all test submissions marked with is_test_data: true. This action cannot be undone.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleClear}>Clear Test Data</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
266
src/lib/testDataGenerator.ts
Normal file
266
src/lib/testDataGenerator.ts
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
import type { ParkSubmissionData, RideSubmissionData, CompanySubmissionData, RideModelSubmissionData } from '@/types/submission-data';
|
||||||
|
|
||||||
|
// 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 }
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Word lists for realistic names
|
||||||
|
const PARK_ADJECTIVES = ['Adventure', 'Fantasy', 'Wonder', 'Magic', 'Dream', 'Thrill', 'Fun', 'Happy', 'Paradise', 'Epic'];
|
||||||
|
const PARK_NOUNS = ['World', 'Land', 'Park', 'Gardens', 'Kingdom', 'Realm', 'Island', 'Bay', 'Point', 'Valley'];
|
||||||
|
const RIDE_PREFIXES = ['Super', 'Mega', 'Ultra', 'Extreme', 'Wild', 'Crazy', 'Thunder', 'Lightning', 'Dragon', 'Phoenix'];
|
||||||
|
const RIDE_TYPES = ['Coaster', 'Drop', 'Spinner', 'Flyer', 'Racer', 'Twister', 'Loop', 'Screamer', 'Rush', 'Blast'];
|
||||||
|
const COMPANY_PREFIXES = ['Ace', 'Premier', 'Advanced', 'Dynamic', 'Innovative', 'Global', 'United', 'International'];
|
||||||
|
const COMPANY_SUFFIXES = ['Industries', 'Manufacturing', 'Enterprises', 'Solutions', 'Systems', 'Designs', 'Works', 'Creations'];
|
||||||
|
|
||||||
|
const CITIES = ['New York', 'Los Angeles', 'Chicago', 'London', 'Paris', 'Tokyo', 'Sydney', 'Toronto', 'Berlin', 'Madrid'];
|
||||||
|
const COUNTRIES = ['USA', 'UK', 'France', 'Japan', 'Australia', 'Canada', 'Germany', 'Spain', 'Italy', 'Netherlands'];
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function randomItem<T>(array: T[]): T {
|
||||||
|
return array[Math.floor(Math.random() * array.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomInt(min: number, max: number): number {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomFloat(min: number, max: number, decimals = 2): number {
|
||||||
|
return parseFloat((Math.random() * (max - min) + min).toFixed(decimals));
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomDate(startYear: number, endYear: number): string {
|
||||||
|
const start = new Date(startYear, 0, 1);
|
||||||
|
const end = new Date(endYear, 11, 31);
|
||||||
|
const date = new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
|
||||||
|
return date.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSlug(name: string, counter?: number): string {
|
||||||
|
let slug = name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9\s-]/g, '')
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.replace(/-+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '');
|
||||||
|
|
||||||
|
if (counter) {
|
||||||
|
slug += `-${counter}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test data context for tracking created entities
|
||||||
|
export class TestDataContext {
|
||||||
|
createdParks: Array<{ id: string; name: string }> = [];
|
||||||
|
createdCompanies: Array<{ id: string; name: string; type: string }> = [];
|
||||||
|
createdRideModels: Array<{ id: string; name: string; manufacturer_id: string }> = [];
|
||||||
|
stats = {
|
||||||
|
parks: 0,
|
||||||
|
rides: 0,
|
||||||
|
companies: 0,
|
||||||
|
rideModels: 0,
|
||||||
|
photos: 0,
|
||||||
|
conflicts: 0,
|
||||||
|
versionChains: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
addPark(id: string, name: string) {
|
||||||
|
this.createdParks.push({ id, name });
|
||||||
|
this.stats.parks++;
|
||||||
|
}
|
||||||
|
|
||||||
|
addCompany(id: string, name: string, type: string) {
|
||||||
|
this.createdCompanies.push({ id, name, type });
|
||||||
|
this.stats.companies++;
|
||||||
|
}
|
||||||
|
|
||||||
|
addRideModel(id: string, name: string, manufacturer_id: string) {
|
||||||
|
this.createdRideModels.push({ id, name, manufacturer_id });
|
||||||
|
this.stats.rideModels++;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRandomPark() {
|
||||||
|
return this.createdParks.length > 0 ? randomItem(this.createdParks) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRandomCompany(type?: string) {
|
||||||
|
const filtered = type
|
||||||
|
? this.createdCompanies.filter(c => c.type === type)
|
||||||
|
: this.createdCompanies;
|
||||||
|
return filtered.length > 0 ? randomItem(filtered) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRandomRideModel() {
|
||||||
|
return this.createdRideModels.length > 0 ? randomItem(this.createdRideModels) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSummary() {
|
||||||
|
return this.stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random data generators
|
||||||
|
export function generateRandomPark(counter: number): ParkSubmissionData {
|
||||||
|
const name = `${randomItem(PARK_ADJECTIVES)} ${randomItem(PARK_NOUNS)}`;
|
||||||
|
const slug = generateSlug(name, counter);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
description: `A thrilling amusement park featuring world-class attractions and entertainment.`,
|
||||||
|
park_type: randomItem(['theme_park', 'amusement_park', 'water_park', 'adventure_park']),
|
||||||
|
status: randomItem(['operating', 'operating', 'operating', 'seasonal']), // More likely to be operating
|
||||||
|
opening_date: randomDate(1950, 2024),
|
||||||
|
closing_date: Math.random() > 0.9 ? randomDate(2000, 2024) : null,
|
||||||
|
website_url: `https://${slug}.example.com`,
|
||||||
|
phone: `+1-555-${randomInt(100, 999)}-${randomInt(1000, 9999)}`,
|
||||||
|
email: `info@${slug}.example.com`,
|
||||||
|
operator_id: null,
|
||||||
|
property_owner_id: null,
|
||||||
|
location_id: null,
|
||||||
|
banner_image_url: null,
|
||||||
|
banner_image_id: null,
|
||||||
|
card_image_url: null,
|
||||||
|
card_image_id: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateRandomRide(parkId: string, counter: number): RideSubmissionData {
|
||||||
|
const name = `${randomItem(RIDE_PREFIXES)} ${randomItem(RIDE_TYPES)}`;
|
||||||
|
const slug = generateSlug(name, counter);
|
||||||
|
const category = randomItem(['roller_coaster', 'flat_ride', 'water_ride', 'dark_ride', 'family_ride']);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
description: `An exciting ${category.replace('_', ' ')} experience for all ages.`,
|
||||||
|
category,
|
||||||
|
ride_sub_type: null,
|
||||||
|
status: randomItem(['operating', 'operating', 'operating', 'seasonal']),
|
||||||
|
park_id: parkId,
|
||||||
|
ride_model_id: null,
|
||||||
|
manufacturer_id: null,
|
||||||
|
designer_id: null,
|
||||||
|
opening_date: randomDate(1980, 2024),
|
||||||
|
closing_date: Math.random() > 0.95 ? randomDate(2010, 2024) : null,
|
||||||
|
height_requirement: randomInt(90, 140),
|
||||||
|
age_requirement: null,
|
||||||
|
capacity_per_hour: randomInt(500, 2000),
|
||||||
|
duration_seconds: randomInt(60, 300),
|
||||||
|
max_speed_kmh: category === 'roller_coaster' ? randomFloat(40, 150, 1) : randomFloat(10, 60, 1),
|
||||||
|
max_height_meters: category === 'roller_coaster' ? randomFloat(20, 100, 1) : randomFloat(5, 30, 1),
|
||||||
|
length_meters: category === 'roller_coaster' ? randomFloat(500, 2500, 1) : null,
|
||||||
|
drop_height_meters: category === 'roller_coaster' ? randomFloat(15, 80, 1) : null,
|
||||||
|
inversions: category === 'roller_coaster' && Math.random() > 0.5 ? randomInt(1, 7) : 0,
|
||||||
|
max_g_force: category === 'roller_coaster' ? randomFloat(2, 5, 1) : null,
|
||||||
|
coaster_type: category === 'roller_coaster' ? randomItem(['steel', 'wooden', 'hybrid']) : null,
|
||||||
|
seating_type: category === 'roller_coaster' ? randomItem(['sit_down', 'inverted', 'floorless', 'suspended']) : null,
|
||||||
|
intensity_level: randomItem(['family', 'moderate', 'thrill', 'extreme']),
|
||||||
|
banner_image_url: null,
|
||||||
|
banner_image_id: null,
|
||||||
|
card_image_url: null,
|
||||||
|
card_image_id: null,
|
||||||
|
image_url: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateRandomCompany(type: 'manufacturer' | 'operator' | 'designer' | 'property_owner', counter: number): CompanySubmissionData {
|
||||||
|
const name = `${randomItem(COMPANY_PREFIXES)} ${randomItem(COMPANY_SUFFIXES)}`;
|
||||||
|
const slug = generateSlug(name, counter);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
description: `A leading ${type.replace('_', ' ')} in the amusement industry.`,
|
||||||
|
person_type: Math.random() > 0.9 ? 'individual' : 'company',
|
||||||
|
founded_year: randomInt(1950, 2020),
|
||||||
|
headquarters_location: `${randomItem(CITIES)}, ${randomItem(COUNTRIES)}`,
|
||||||
|
website_url: `https://${slug}.example.com`,
|
||||||
|
logo_url: null,
|
||||||
|
banner_image_url: null,
|
||||||
|
banner_image_id: null,
|
||||||
|
card_image_url: null,
|
||||||
|
card_image_id: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateRandomRideModel(manufacturerId: string, counter: number): RideModelSubmissionData {
|
||||||
|
const name = `${randomItem(RIDE_PREFIXES)} Model ${randomInt(100, 999)}`;
|
||||||
|
const slug = generateSlug(name, counter);
|
||||||
|
const category = randomItem(['roller_coaster', 'flat_ride', 'water_ride', 'dark_ride']);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
manufacturer_id: manufacturerId,
|
||||||
|
category,
|
||||||
|
ride_type: randomItem(['spinning', 'launch', 'inverted', 'suspended', 'floorless']),
|
||||||
|
description: `A state-of-the-art ${category.replace('_', ' ')} model.`,
|
||||||
|
banner_image_url: null,
|
||||||
|
banner_image_id: null,
|
||||||
|
card_image_url: null,
|
||||||
|
card_image_id: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup utilities
|
||||||
|
export async function clearTestData(): Promise<{ deleted: number }> {
|
||||||
|
try {
|
||||||
|
// Find all test submissions
|
||||||
|
const { data: testSubmissions, error: fetchError } = await supabase
|
||||||
|
.from('content_submissions')
|
||||||
|
.select('id')
|
||||||
|
.eq('status', 'pending')
|
||||||
|
.contains('content', { metadata: { is_test_data: true } });
|
||||||
|
|
||||||
|
if (fetchError) throw fetchError;
|
||||||
|
if (!testSubmissions || testSubmissions.length === 0) {
|
||||||
|
return { deleted: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete in batches of 100
|
||||||
|
const batchSize = 100;
|
||||||
|
let totalDeleted = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < testSubmissions.length; i += batchSize) {
|
||||||
|
const batch = testSubmissions.slice(i, i + batchSize);
|
||||||
|
const ids = batch.map(s => s.id);
|
||||||
|
|
||||||
|
const { error: deleteError } = await supabase
|
||||||
|
.from('content_submissions')
|
||||||
|
.delete()
|
||||||
|
.in('id', ids);
|
||||||
|
|
||||||
|
if (deleteError) throw deleteError;
|
||||||
|
totalDeleted += ids.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { deleted: totalDeleted };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing test data:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTestDataStats(): Promise<{ total: number; pending: number; approved: number }> {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('content_submissions')
|
||||||
|
.select('status')
|
||||||
|
.contains('content', { metadata: { is_test_data: true } });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
total: data?.length || 0,
|
||||||
|
pending: data?.filter(s => s.status === 'pending').length || 0,
|
||||||
|
approved: data?.filter(s => s.status === 'approved').length || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import { useAuth } from '@/hooks/useAuth';
|
|||||||
import { useUserRole } from '@/hooks/useUserRole';
|
import { useUserRole } from '@/hooks/useUserRole';
|
||||||
import { useAdminSettings } from '@/hooks/useAdminSettings';
|
import { useAdminSettings } from '@/hooks/useAdminSettings';
|
||||||
import { NovuMigrationUtility } from '@/components/admin/NovuMigrationUtility';
|
import { NovuMigrationUtility } from '@/components/admin/NovuMigrationUtility';
|
||||||
|
import { TestDataGenerator } from '@/components/admin/TestDataGenerator';
|
||||||
import { Loader2, Save, Clock, Users, Bell, Shield, Settings, Trash2, Plug } from 'lucide-react';
|
import { Loader2, Save, Clock, Users, Bell, Shield, Settings, Trash2, Plug } from 'lucide-react';
|
||||||
|
|
||||||
export default function AdminSettings() {
|
export default function AdminSettings() {
|
||||||
@@ -434,7 +435,7 @@ export default function AdminSettings() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs defaultValue="moderation" className="space-y-6">
|
<Tabs defaultValue="moderation" className="space-y-6">
|
||||||
<TabsList className="grid w-full grid-cols-5">
|
<TabsList className="grid w-full grid-cols-6">
|
||||||
<TabsTrigger value="moderation" className="flex items-center gap-2">
|
<TabsTrigger value="moderation" className="flex items-center gap-2">
|
||||||
<Shield className="w-4 h-4" />
|
<Shield className="w-4 h-4" />
|
||||||
<span className="hidden sm:inline">Moderation</span>
|
<span className="hidden sm:inline">Moderation</span>
|
||||||
@@ -455,6 +456,10 @@ export default function AdminSettings() {
|
|||||||
<Plug className="w-4 h-4" />
|
<Plug className="w-4 h-4" />
|
||||||
<span className="hidden sm:inline">Integrations</span>
|
<span className="hidden sm:inline">Integrations</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="testing" className="flex items-center gap-2">
|
||||||
|
<Loader2 className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">Testing</span>
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="moderation">
|
<TabsContent value="moderation">
|
||||||
@@ -588,6 +593,10 @@ export default function AdminSettings() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="testing">
|
||||||
|
<TestDataGenerator />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
|
|||||||
@@ -25,4 +25,7 @@ verify_jwt = true
|
|||||||
verify_jwt = true
|
verify_jwt = true
|
||||||
|
|
||||||
[functions.cancel-email-change]
|
[functions.cancel-email-change]
|
||||||
|
verify_jwt = true
|
||||||
|
|
||||||
|
[functions.seed-test-data]
|
||||||
verify_jwt = true
|
verify_jwt = true
|
||||||
283
supabase/functions/seed-test-data/index.ts
Normal file
283
supabase/functions/seed-test-data/index.ts
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
||||||
|
|
||||||
|
const corsHeaders = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SeedOptions {
|
||||||
|
preset: 'small' | 'medium' | 'large' | 'stress';
|
||||||
|
entityTypes: string[];
|
||||||
|
includeDependencies: boolean;
|
||||||
|
includeConflicts: boolean;
|
||||||
|
includeVersionChains: boolean;
|
||||||
|
includeEscalated: boolean;
|
||||||
|
includeExpiredLocks: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SeedPlan {
|
||||||
|
parks: number;
|
||||||
|
rides: number;
|
||||||
|
companies: number;
|
||||||
|
rideModels: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PRESETS: Record<string, SeedPlan> = {
|
||||||
|
small: { parks: 5, rides: 10, companies: 3, rideModels: 2 },
|
||||||
|
medium: { parks: 20, rides: 50, companies: 20, rideModels: 10 },
|
||||||
|
large: { parks: 100, rides: 250, companies: 100, rideModels: 50 },
|
||||||
|
stress: { parks: 400, rides: 1000, companies: 400, rideModels: 200 }
|
||||||
|
};
|
||||||
|
|
||||||
|
Deno.serve(async (req) => {
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
return new Response(null, { headers: corsHeaders });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
||||||
|
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
||||||
|
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
||||||
|
|
||||||
|
// Get auth header
|
||||||
|
const authHeader = req.headers.get('Authorization');
|
||||||
|
if (!authHeader) {
|
||||||
|
return new Response(JSON.stringify({ error: 'No authorization header' }), {
|
||||||
|
status: 401,
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify user is moderator
|
||||||
|
const token = authHeader.replace('Bearer ', '');
|
||||||
|
const { data: { user }, error: userError } = await supabase.auth.getUser(token);
|
||||||
|
|
||||||
|
if (userError || !user) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||||
|
status: 401,
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: isMod } = await supabase.rpc('is_moderator', { _user_id: user.id });
|
||||||
|
if (!isMod) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Must be moderator' }), {
|
||||||
|
status: 403,
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request
|
||||||
|
const { preset = 'small', entityTypes = [], includeDependencies = true, includeEscalated = false, includeExpiredLocks = false }: SeedOptions = await req.json();
|
||||||
|
const plan = PRESETS[preset];
|
||||||
|
|
||||||
|
if (!plan) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Invalid preset' }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
const summary = { parks: 0, rides: 0, companies: 0, rideModels: 0 };
|
||||||
|
const createdParks: string[] = [];
|
||||||
|
const createdCompanies: Record<string, string[]> = { manufacturer: [], operator: [], designer: [], property_owner: [] };
|
||||||
|
|
||||||
|
// Helper to create submission
|
||||||
|
async function createSubmission(userId: string, type: string, itemData: any, options: { escalated?: boolean; expiredLock?: boolean } = {}) {
|
||||||
|
const submissionId = crypto.randomUUID();
|
||||||
|
const itemId = crypto.randomUUID();
|
||||||
|
|
||||||
|
const contentData = {
|
||||||
|
action: 'create',
|
||||||
|
metadata: {
|
||||||
|
is_test_data: true,
|
||||||
|
generated_at: new Date().toISOString(),
|
||||||
|
generator_version: '1.0.0',
|
||||||
|
preset
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create content_submission
|
||||||
|
const submissionData: any = {
|
||||||
|
id: submissionId,
|
||||||
|
user_id: userId,
|
||||||
|
submission_type: type,
|
||||||
|
status: 'pending',
|
||||||
|
content: contentData,
|
||||||
|
submitted_at: new Date().toISOString(),
|
||||||
|
priority: options.escalated ? 10 : Math.floor(Math.random() * 5) + 1
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.escalated) {
|
||||||
|
submissionData.escalated = true;
|
||||||
|
submissionData.escalation_reason = 'Test escalation';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.expiredLock) {
|
||||||
|
submissionData.assigned_to = user.id;
|
||||||
|
submissionData.locked_until = new Date(Date.now() - 1000 * 60 * 30).toISOString(); // 30 min ago
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error: subError } = await supabase
|
||||||
|
.from('content_submissions')
|
||||||
|
.insert(submissionData);
|
||||||
|
|
||||||
|
if (subError) throw subError;
|
||||||
|
|
||||||
|
// Create submission_item
|
||||||
|
const { error: itemError } = await supabase
|
||||||
|
.from('submission_items')
|
||||||
|
.insert({
|
||||||
|
id: itemId,
|
||||||
|
submission_id: submissionId,
|
||||||
|
item_type: type,
|
||||||
|
item_data: itemData,
|
||||||
|
status: 'pending',
|
||||||
|
order_index: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
if (itemError) throw itemError;
|
||||||
|
|
||||||
|
// Create type-specific submission record
|
||||||
|
const typeTableMap: Record<string, string> = {
|
||||||
|
park: 'park_submissions',
|
||||||
|
ride: 'ride_submissions',
|
||||||
|
manufacturer: 'company_submissions',
|
||||||
|
operator: 'company_submissions',
|
||||||
|
designer: 'company_submissions',
|
||||||
|
property_owner: 'company_submissions',
|
||||||
|
ride_model: 'ride_model_submissions'
|
||||||
|
};
|
||||||
|
|
||||||
|
const table = typeTableMap[type];
|
||||||
|
if (table) {
|
||||||
|
const typeData = { ...itemData, submission_id: submissionId };
|
||||||
|
if (table === 'company_submissions') {
|
||||||
|
typeData.company_type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error: typeError } = await supabase
|
||||||
|
.from(table)
|
||||||
|
.insert(typeData);
|
||||||
|
|
||||||
|
if (typeError) throw typeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return submissionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create parks
|
||||||
|
if (entityTypes.includes('parks')) {
|
||||||
|
for (let i = 0; i < plan.parks; i++) {
|
||||||
|
const parkData = {
|
||||||
|
name: `Test Park ${i + 1}`,
|
||||||
|
slug: `test-park-${i + 1}`,
|
||||||
|
description: 'Test park description',
|
||||||
|
park_type: ['theme_park', 'amusement_park', 'water_park'][Math.floor(Math.random() * 3)],
|
||||||
|
status: 'operating',
|
||||||
|
opening_date: '2000-01-01'
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
escalated: includeEscalated && Math.random() < 0.1,
|
||||||
|
expiredLock: includeExpiredLocks && Math.random() < 0.1
|
||||||
|
};
|
||||||
|
|
||||||
|
await createSubmission(user.id, 'park', parkData, options);
|
||||||
|
createdParks.push(`test-park-${i + 1}`);
|
||||||
|
summary.parks++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create companies
|
||||||
|
const companyTypes = ['manufacturer', 'operator', 'designer', 'property_owner'];
|
||||||
|
for (const compType of companyTypes) {
|
||||||
|
if (entityTypes.includes(compType)) {
|
||||||
|
const count = Math.floor(plan.companies / 4);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const companyData = {
|
||||||
|
name: `Test ${compType} ${i + 1}`,
|
||||||
|
slug: `test-${compType}-${i + 1}`,
|
||||||
|
description: `Test ${compType} description`,
|
||||||
|
company_type: compType,
|
||||||
|
person_type: 'company',
|
||||||
|
founded_year: 2000
|
||||||
|
};
|
||||||
|
|
||||||
|
await createSubmission(user.id, compType, companyData);
|
||||||
|
createdCompanies[compType].push(`test-${compType}-${i + 1}`);
|
||||||
|
summary.companies++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create rides (with dependencies if enabled)
|
||||||
|
if (entityTypes.includes('rides') && includeDependencies && createdParks.length > 0) {
|
||||||
|
for (let i = 0; i < plan.rides; i++) {
|
||||||
|
// Get random park ID from database
|
||||||
|
const parkSlug = createdParks[Math.floor(Math.random() * createdParks.length)];
|
||||||
|
const { data: parkData } = await supabase
|
||||||
|
.from('parks')
|
||||||
|
.select('id')
|
||||||
|
.eq('slug', parkSlug)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
const rideData = {
|
||||||
|
name: `Test Ride ${i + 1}`,
|
||||||
|
slug: `test-ride-${i + 1}`,
|
||||||
|
description: 'Test ride description',
|
||||||
|
category: ['roller_coaster', 'flat_ride', 'water_ride'][Math.floor(Math.random() * 3)],
|
||||||
|
status: 'operating',
|
||||||
|
park_id: parkData?.id || null,
|
||||||
|
opening_date: '2010-01-01'
|
||||||
|
};
|
||||||
|
|
||||||
|
await createSubmission(user.id, 'ride', rideData);
|
||||||
|
summary.rides++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ride models
|
||||||
|
if (entityTypes.includes('ride_models') && includeDependencies && createdCompanies.manufacturer.length > 0) {
|
||||||
|
for (let i = 0; i < plan.rideModels; i++) {
|
||||||
|
const mfgSlug = createdCompanies.manufacturer[Math.floor(Math.random() * createdCompanies.manufacturer.length)];
|
||||||
|
const { data: mfgData } = await supabase
|
||||||
|
.from('companies')
|
||||||
|
.select('id')
|
||||||
|
.eq('slug', mfgSlug)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
const modelData = {
|
||||||
|
name: `Test Model ${i + 1}`,
|
||||||
|
slug: `test-model-${i + 1}`,
|
||||||
|
manufacturer_id: mfgData?.id || null,
|
||||||
|
category: 'roller_coaster',
|
||||||
|
ride_type: 'steel',
|
||||||
|
description: 'Test ride model'
|
||||||
|
};
|
||||||
|
|
||||||
|
await createSubmission(user.id, 'ride_model', modelData);
|
||||||
|
summary.rideModels++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
summary,
|
||||||
|
time: elapsedTime
|
||||||
|
}),
|
||||||
|
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Seed error:', error);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: error.message }),
|
||||||
|
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user