package services import ( "context" "database/sql" "fmt" "math/rand" "os" "strings" "time" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) type SeederService struct { DB *sql.DB } func NewSeederService(db *sql.DB) *SeederService { return &SeederService{DB: db} } // SendEvent is a helper to stream logs via SSE func (s *SeederService) SendEvent(logChan chan string, msg string) { if logChan != nil { logChan <- msg } else { fmt.Println("[SEEDER]", msg) } } func (s *SeederService) Reset(ctx context.Context) error { // Dangerous operation: Truncate tables queries := []string{ "TRUNCATE TABLE applications CASCADE", "TRUNCATE TABLE jobs CASCADE", "TRUNCATE TABLE companies CASCADE", "TRUNCATE TABLE users CASCADE", // Add other tables as needed } tx, err := s.DB.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() for _, q := range queries { if _, err := tx.ExecContext(ctx, q); err != nil { // Ignore if table doesn't exist, but log it fmt.Printf("Error executing %s: %v\n", q, err) } } // Re-create SuperAdmin if needed, or leave it to manual registration // For dev, it's nice to have a default admin. return tx.Commit() } func (s *SeederService) Seed(ctx context.Context, logChan chan string) error { s.SendEvent(logChan, "🚀 Starting Database Seed...") // 0. Auto-Create Superadmin from Env adminEmail := os.Getenv("ADMIN_EMAIL") adminPass := os.Getenv("ADMIN_PASSWORD") if adminEmail != "" && adminPass != "" { s.SendEvent(logChan, fmt.Sprintf("🛡️ Found ADMIN_EMAIL env. Creating Superadmin: %s", adminEmail)) // Hash password hash, err := bcrypt.GenerateFromPassword([]byte(adminPass), bcrypt.DefaultCost) if err == nil { tx, err := s.DB.BeginTx(ctx, nil) if err == nil { adminID := uuid.New().String() _, execErr := tx.ExecContext(ctx, ` INSERT INTO users (id, identifier, full_name, password_hash, role, created_at, updated_at) VALUES ($1, $2, 'System Master', $3, 'superadmin', NOW(), NOW()) ON CONFLICT (identifier) DO UPDATE SET password_hash = EXCLUDED.password_hash, role = 'superadmin', updated_at = NOW() `, adminID, adminEmail, string(hash)) if execErr == nil { tx.Commit() s.SendEvent(logChan, "✅ Superadmin created/verified successfully.") } else { tx.Rollback() s.SendEvent(logChan, fmt.Sprintf("❌ Failed to create Superadmin: %v", execErr)) } } } } // Create Random Source rnd := rand.New(rand.NewSource(time.Now().UnixNano())) tx, err := s.DB.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() // 1. Create Users (Candidates & Recruiters) s.SendEvent(logChan, "👤 Creating Users...") candidates := []string{"Alice Johnson", "Bob Smith", "Charlie Brown", "Diana Prince", "Evan Wright"} var candidateIDs []string passwordHash, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) for _, name := range candidates { id := uuid.New().String() // Using email format as identifier identifier := strings.ToLower(strings.ReplaceAll(name, " ", ".")) + "@example.com" // UPSERT to ensure we get the correct ID back if it exists row := tx.QueryRowContext(ctx, ` INSERT INTO users (id, identifier, full_name, password_hash, role, created_at, updated_at) VALUES ($1, $2, $3, $4, 'candidate', NOW(), NOW()) ON CONFLICT (identifier) DO UPDATE SET updated_at = NOW() RETURNING id `, id, identifier, name, string(passwordHash)) if err := row.Scan(&id); err != nil { s.SendEvent(logChan, fmt.Sprintf("❌ Error creating candidate %s: %v", name, err)) continue } candidateIDs = append(candidateIDs, id) s.SendEvent(logChan, fmt.Sprintf(" - Created Candidate: %s (%s)", name, identifier)) } // 2. Create Companies & Recruiters s.SendEvent(logChan, "🏢 Creating Companies...") companyNames := []string{"TechCorp", "InnovateX", "GlobalSolutions", "CodeFactory", "DesignStudio"} var companyIDs []string for _, compName := range companyNames { // Create Recruiter recruiterID := uuid.New().String() recIdentifier := "hr@" + strings.ToLower(compName) + ".com" row := tx.QueryRowContext(ctx, ` INSERT INTO users (id, identifier, full_name, password_hash, role, created_at, updated_at) VALUES ($1, $2, $3, $4, 'recruiter', NOW(), NOW()) ON CONFLICT (identifier) DO UPDATE SET updated_at = NOW() RETURNING id `, recruiterID, recIdentifier, compName+" Recruiter", string(passwordHash)) if err := row.Scan(&recruiterID); err != nil { s.SendEvent(logChan, fmt.Sprintf("❌ Error creating recruiter %s: %v", recIdentifier, err)) continue } // Create Company compID := uuid.New().String() slug := strings.ToLower(compName) row = tx.QueryRowContext(ctx, ` INSERT INTO companies (id, name, slug, type, verified, active, created_at, updated_at) VALUES ($1, $2, $3, 'COMPANY', true, true, NOW(), NOW()) ON CONFLICT (slug) DO UPDATE SET updated_at = NOW() RETURNING id `, compID, compName, slug) if err := row.Scan(&compID); err != nil { s.SendEvent(logChan, fmt.Sprintf("❌ Error creating company %s: %v", compName, err)) continue } companyIDs = append(companyIDs, compID) // Link Recruiter - Use 'admin' as role (per schema constraint: 'admin', 'recruiter') _, err = tx.ExecContext(ctx, ` INSERT INTO user_companies (user_id, company_id, role) VALUES ($1, $2, 'admin') ON CONFLICT DO NOTHING `, recruiterID, compID) if err != nil { s.SendEvent(logChan, fmt.Sprintf("⚠️ Failed to link recruiter: %v", err)) } s.SendEvent(logChan, fmt.Sprintf(" - Created Company: %s (HR: %s)", compName, recIdentifier)) } // 3. Create Jobs s.SendEvent(logChan, "💼 Creating Jobs...") jobTitles := []string{"Software Engineer", "Frontend Developer", "Backend Developer", "Product Manager", "UX Designer"} var jobIDs []string for _, compID := range companyIDs { // Create 2-3 jobs per company numJobs := rnd.Intn(2) + 2 for i := 0; i < numJobs; i++ { jobID := uuid.New().String() title := jobTitles[rnd.Intn(len(jobTitles))] _, err := tx.ExecContext(ctx, ` INSERT INTO jobs (id, company_id, title, description, location, type, status, created_at, updated_at) VALUES ($1, $2, $3, 'This is a great job opportunity.', 'Remote', 'full-time', 'published', NOW(), NOW()) ON CONFLICT DO NOTHING `, jobID, compID, title) if err != nil { s.SendEvent(logChan, fmt.Sprintf("❌ Error creating job %s: %v", title, err)) continue } jobIDs = append(jobIDs, jobID) s.SendEvent(logChan, fmt.Sprintf(" - Posted Job: %s at %s", title, compID[:8])) } } // 4. Create Applications s.SendEvent(logChan, "📝 Creating Applications...") for _, candID := range candidateIDs { // Apply to 1-3 random jobs numApps := rnd.Intn(3) + 1 for i := 0; i < numApps; i++ { if len(jobIDs) == 0 { break } jobID := jobIDs[rnd.Intn(len(jobIDs))] appID := uuid.New().String() _, err := tx.ExecContext(ctx, ` INSERT INTO applications (id, job_id, user_id, status, created_at, updated_at) VALUES ($1, $2, $3, 'applied', NOW(), NOW()) ON CONFLICT DO NOTHING `, appID, jobID, candID) if err == nil { s.SendEvent(logChan, fmt.Sprintf(" - Candidate %s applied to Job %s", candID[:8], jobID[:8])) } } } if err := tx.Commit(); err != nil { return err } s.SendEvent(logChan, "✅ Seed Completed Successfully!") return nil }