/** * Test Data Seeding Script for PocketBase * * This script creates initial test data for the timing app: * - Admin user * - Timekeeper users * - Test riders * - Sample event with stages * * Usage: * npx tsx scripts/seed-test-data.ts * * Environment Variables (from .env): * PB_TYPEGEN_URL - PocketBase API URL * PB_TYPEGEN_TOKEN - Authentication token for impersonation */ import PocketBase from 'pocketbase' import type { TypedPocketBase } from '../src/lib/types.js' // Validate required environment variables const PB_URL = process.env.PB_TYPEGEN_URL const PB_TOKEN = process.env.PB_TYPEGEN_TOKEN if (!PB_URL) { console.error('❌ Error: PB_TYPEGEN_URL is not set in .env file') console.error(' Please set PB_TYPEGEN_URL in your .env file') process.exit(1) } if (!PB_TOKEN) { console.error('❌ Error: PB_TYPEGEN_TOKEN is not set in .env file') console.error(' Please set PB_TYPEGEN_TOKEN in your .env file') process.exit(1) } // Initialize PocketBase with URL from .env const pb = new PocketBase(PB_URL) as TypedPocketBase // Authenticate using token from .env (impersonation) pb.authStore.save(PB_TOKEN) console.log(`πŸ”— Connecting to PocketBase at: ${PB_URL}`) console.log(`πŸ”‘ Using authentication token from .env\n`) // Configuration const ADMIN_EMAIL = 'admin@stammtisch-hersbruck.de' const ADMIN_PASSWORD = 'Admin123!secure' async function seedData() { console.log('🌱 Starting test data seeding...\n') try { // 1. Create Admin User console.log('πŸ‘€ Creating admin user...') let adminUser try { adminUser = await pb.collection('users').create({ email: ADMIN_EMAIL, password: ADMIN_PASSWORD, passwordConfirm: ADMIN_PASSWORD, name: 'Administrator', role: 'admin', verified: true }) console.log(`βœ… Admin created: ${adminUser.email}`) } catch (e: any) { if (e.message.includes('already exists')) { console.log('⚠️ Admin user already exists, logging in...') await pb.collection('users').authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD) adminUser = pb.authStore.record } else { throw e } } // 2. Create Timekeeper Users console.log('\n⏱️ Creating timekeeper users...') const timekeepers = [] const timekeeperData = [ { email: 'timer1@stammtisch-hersbruck.de', name: 'Max Zeitnehmer', password: 'Timer123!' }, { email: 'timer2@stammtisch-hersbruck.de', name: 'Anna Stoppuhr', password: 'Timer123!' }, { email: 'timer3@stammtisch-hersbruck.de', name: 'Tom Chronograph', password: 'Timer123!' } ] for (const data of timekeeperData) { try { const user = await pb.collection('users').create({ email: data.email, password: data.password, passwordConfirm: data.password, name: data.name, role: 'timekeeper', verified: true }) timekeepers.push(user) console.log(`βœ… Timekeeper created: ${user.name} (${user.email})`) } catch (e: any) { if (e.message.includes('already exists')) { console.log(`⚠️ Timekeeper ${data.email} already exists, skipping...`) // Fetch existing user const existing = await pb.collection('users').getFirstListItem(`email="${data.email}"`) timekeepers.push(existing) } else { console.error(`❌ Error creating ${data.email}:`, e.message) } } } // 3. Create Test Riders console.log('\n🚴 Creating test riders...') const riders = [] const riderData = [ { number: '1', name: 'Max MΓΌller', email: 'max.mueller@example.com', birthday: '1995-03-15' }, { number: '2', name: 'Anna Schmidt', email: 'anna.schmidt@example.com', birthday: '1992-07-22' }, { number: '3', name: 'Tom Weber', email: 'tom.weber@example.com', birthday: '1998-11-08' }, { number: '17', name: 'Lisa Hoffmann', email: 'lisa.hoffmann@example.com', birthday: '2000-01-30' }, { number: '23', name: 'Felix Wagner', email: 'felix.wagner@example.com', birthday: '1996-05-12' }, { number: '42', name: 'Sarah Becker', email: 'sarah.becker@example.com', birthday: '1994-09-25' }, { number: '99', name: 'Lukas Fischer', email: 'lukas.fischer@example.com', birthday: '1999-12-03' }, { number: '101', name: 'Julia Schulz', email: 'julia.schulz@example.com', birthday: '1997-04-18' } ] for (const data of riderData) { try { const rider = await pb.collection('riders').create({ number: data.number, name: data.name, email: data.email, birthday: data.birthday }) riders.push(rider) console.log(`βœ… Rider created: #${rider.number} ${rider.name}`) } catch (e: any) { if (e.message.includes('already exists')) { console.log(`⚠️ Rider #${data.number} already exists, skipping...`) const existing = await pb.collection('riders').getFirstListItem(`number="${data.number}"`) riders.push(existing) } else { console.error(`❌ Error creating rider #${data.number}:`, e.message) } } } // 4. Create Test Event console.log('\n🏁 Creating test event...') let event try { // Event date: 2 weeks from now const eventDate = new Date() eventDate.setDate(eventDate.getDate() + 14) const startTime = new Date(eventDate) startTime.setHours(9, 0, 0, 0) const endTime = new Date(eventDate) endTime.setHours(17, 0, 0, 0) event = await pb.collection('events').create({ name: 'MTB Hersbruck Cup 2025', location: 'Hersbruck, Deutschland', start: startTime.toISOString(), end: endTime.toISOString(), status: 'draft', created_by: adminUser?.id, timekeepers: timekeepers.map(t => t.id) }) console.log(`βœ… Event created: ${event.name}`) console.log(` Date: ${new Date(event.start).toLocaleDateString('de-DE')}`) console.log(` Timekeepers: ${timekeepers.length} assigned`) } catch (e: any) { if (e.message.includes('already exists')) { console.log('⚠️ Event already exists, fetching...') event = await pb.collection('events').getFirstListItem('name~"MTB Hersbruck"') } else { throw e } } // 5. Create Stages console.log('\nπŸ”οΈ Creating stages...') const stages = [] const stageData = [ { name: 'Downhill', location: 'Steinberg Trail', order: 1, start_number: 1, end_number: 50, status: 'pending' }, { name: 'Cross-Country', location: 'Wald-Rundkurs', order: 2, start_number: 51, end_number: 100, status: 'pending' }, { name: 'Enduro Stage 1', location: 'Berg-Passage', order: 3, start_number: 101, end_number: 150, status: 'pending' } ] for (const data of stageData) { try { // Stage times based on event date const stageStart = new Date(event.start) stageStart.setHours(stageStart.getHours() + (data.order - 1) * 2) const stageEnd = new Date(stageStart) stageEnd.setHours(stageEnd.getHours() + 2) const stage = await pb.collection('stages').create({ event: event.id, name: data.name, location: data.location, start: stageStart.toISOString(), end: stageEnd.toISOString(), order: data.order, start_number: data.start_number, end_number: data.end_number, status: data.status }) stages.push(stage) console.log(`βœ… Stage created: ${stage.name} (Startnummern: ${data.start_number}-${data.end_number})`) } catch (e: any) { console.error(`❌ Error creating stage ${data.name}:`, e.message) } } // 6. Create Sample Results (optional - for demo) console.log('\n⏱️ Creating sample results...') // Create 2 finished results for demonstration if (stages.length > 0 && riders.length >= 3 && timekeepers.length > 0) { const firstStage = stages[0] const sampleResults = [ { rider: riders[0], duration: 185.5, // 3:05.5 status: 'finished' }, { rider: riders[1], duration: 192.3, // 3:12.3 status: 'finished' }, { rider: riders[2], duration: 0, // currently running status: 'running' } ] for (const data of sampleResults) { try { const now = new Date() const startTime = new Date(now.getTime() - (data.duration * 1000) - 60000) // Started some time ago const endTime = data.status === 'finished' ? new Date(startTime.getTime() + data.duration * 1000) : undefined const result = await pb.collection('results').create({ stage: firstStage.id, rider: data.rider.id, start: startTime.toISOString(), end: endTime?.toISOString() || null, status: data.status, started_by: timekeepers[0].id, stopped_by: data.status === 'finished' ? timekeepers[0].id : null, correction: 0 }) const timeStr = data.status === 'finished' ? `${Math.floor(data.duration / 60)}:${(data.duration % 60).toFixed(1).padStart(4, '0')}` : 'RUNNING' console.log(`βœ… Result created: #${data.rider.number} ${data.rider.name} - ${timeStr}`) } catch (e: any) { console.error(`❌ Error creating result:`, e.message) } } } // Summary console.log('\n' + '='.repeat(50)) console.log('βœ… Test data seeding completed!') console.log('='.repeat(50)) console.log('\nπŸ“Š Summary:') console.log(` Users: ${1 + timekeepers.length} (1 admin, ${timekeepers.length} timekeepers)`) console.log(` Riders: ${riders.length}`) console.log(` Events: 1`) console.log(` Stages: ${stages.length}`) console.log(` Results: 3 (2 finished, 1 running)`) console.log('\nπŸ”‘ Login Credentials:') console.log(` Admin: ${ADMIN_EMAIL} / ${ADMIN_PASSWORD}`) console.log(` Timekeeper: timer1@stammtisch-hersbruck.de / Timer123!`) console.log(` Timekeeper: timer2@stammtisch-hersbruck.de / Timer123!`) console.log(` Timekeeper: timer3@stammtisch-hersbruck.de / Timer123!`) console.log('\nπŸš€ Next steps:') console.log(' 1. Run: npm run generate-pocketbase-types') console.log(' 2. Start development: npm run dev') console.log(' 3. Login with one of the credentials above') console.log('') } catch (error: any) { console.error('\n❌ Error during seeding:', error) if (error.response) { console.error('Response:', error.response) } process.exit(1) } } // Run the seeding seedData() .then(() => { console.log('βœ… Seeding completed successfully!') process.exit(0) }) .catch((error) => { console.error('❌ Seeding failed:', error) process.exit(1) })