package seeders import ( "encoding/csv" "fmt" "log" "os" "strings" "gorm.io/driver/postgres" "gorm.io/gorm" ) // SeederCLI adalah command-line interface untuk seeder type SeederCLI struct { db *gorm.DB registry *SeederRegistry } // NewSeederCLI membuat CLI baru func NewSeederCLI(db *gorm.DB) *SeederCLI { return &SeederCLI{ db: db, registry: DefaultRegistry(), } } // Run menjalankan CLI seeder func (c *SeederCLI) Run(args []string) error { if len(args) == 0 { return c.showHelp() } command := args[0] switch command { case "list": return c.listTables() case "seed": if len(args) < 2 { return fmt.Errorf("usage: seeder seed [options]") } return c.seedTable(args[1], args[2:]) case "dry-run": if len(args) < 2 { return fmt.Errorf("usage: seeder dry-run [options]") } return c.dryRunTable(args[1], args[2:]) case "validate": if len(args) < 2 { return fmt.Errorf("usage: seeder validate ") } return c.validateTable(args[1]) default: return fmt.Errorf("unknown command: %s", command) } } // showHelp menampilkan bantuan func (c *SeederCLI) showHelp() error { help := ` Seeder CLI - Flexible Database Seeder Usage: seeder [arguments] Commands: list Show all available tables seed Seed data for specific table or all tables dry-run Preview seeding without actual insertion validate Validate CSV files and configuration Examples: seeder list seeder seed province seeder seed all seeder dry-run ethnic seeder validate regency Options: -batch-size=N Set batch size for insertion (default: from config) -delete-before Delete existing data before seeding -no-delete Don't delete existing data (default) -csv-path=PATH Override CSV file path -table-name=NAME Override table name Notes: - CSV files should be in internal/infrastructure/database/csv/ - First row of CSV should be header with column names - Column names in CSV will be mapped to struct fields ` fmt.Println(help) return nil } // listTables menampilkan daftar tabel yang tersedia func (c *SeederCLI) listTables() error { tables := c.registry.List() fmt.Println("Available tables for seeding:") fmt.Println(strings.Repeat("-", 50)) for _, table := range tables { config, _ := c.registry.Get(table) fmt.Printf("%-20s %s\n", table, config.CSVFile) } fmt.Printf("\nTotal: %d tables\n", len(tables)) return nil } // seedTable melakukan seeding untuk tabel tertentu func (c *SeederCLI) seedTable(tableName string, options []string) error { if tableName == "all" { return c.seedAllTables(options) } config, exists := c.registry.Get(tableName) if !exists { return fmt.Errorf("table '%s' not found in registry", tableName) } // Parse options opts := c.parseOptions(options) // Override config dengan options if opts.CSVPath != "" { config.CSVFile = opts.CSVPath } if opts.TableName != "" { config.TableName = opts.TableName } if opts.BatchSize > 0 { config.BatchSize = opts.BatchSize } if opts.DeleteBefore { config.DeleteBefore = true } // Validasi config if err := config.Validate(); err != nil { return fmt.Errorf("validation failed: %w", err) } // Get entity entity := GetEntityByTableName(config.TableName) if entity == nil { return fmt.Errorf("entity not found for table '%s'", config.TableName) } // Create seeder seederConfig := SeederConfig{ CSVPath: config.CSVFile, TableName: config.TableName, ColumnMap: config.ColumnMap, SkipHeader: true, BatchSize: config.BatchSize, DeleteBefore: config.DeleteBefore, DryRun: false, } seeder := NewMasterSeeder(c.db, seederConfig) log.Printf("Starting seed for table: %s", config.TableName) log.Printf("CSV file: %s", config.CSVFile) log.Printf("Batch size: %d", config.BatchSize) log.Printf("Delete before: %v", config.DeleteBefore) if err := seeder.SeedFromCSV(entity); err != nil { return fmt.Errorf("seeding failed: %w", err) } log.Printf("Successfully seeded table: %s", config.TableName) return nil } // dryRunTable melakukan dry-run untuk tabel tertentu func (c *SeederCLI) dryRunTable(tableName string, options []string) error { if tableName == "all" { return c.dryRunAllTables(options) } config, exists := c.registry.Get(tableName) if !exists { return fmt.Errorf("table '%s' not found in registry", tableName) } // Parse options opts := c.parseOptions(options) // Override config dengan options if opts.CSVPath != "" { config.CSVFile = opts.CSVPath } if opts.TableName != "" { config.TableName = opts.TableName } // Validasi config if err := config.Validate(); err != nil { return fmt.Errorf("validation failed: %w", err) } // Get entity entity := GetEntityByTableName(config.TableName) if entity == nil { return fmt.Errorf("entity not found for table '%s'", config.TableName) } // Create seeder dengan dry-run mode seederConfig := SeederConfig{ CSVPath: config.CSVFile, TableName: config.TableName, ColumnMap: config.ColumnMap, SkipHeader: true, BatchSize: 10, // Smaller batch for dry-run DeleteBefore: false, DryRun: true, } seeder := NewMasterSeeder(c.db, seederConfig) log.Printf("Starting dry-run for table: %s", config.TableName) log.Printf("CSV file: %s", config.CSVFile) if err := seeder.SeedFromCSV(entity); err != nil { return fmt.Errorf("dry-run failed: %w", err) } log.Printf("Dry-run completed for table: %s", config.TableName) return nil } // validateTable memvalidasi konfigurasi dan CSV file func (c *SeederCLI) validateTable(tableName string) error { if tableName == "all" { return c.validateAllTables() } config, exists := c.registry.Get(tableName) if !exists { return fmt.Errorf("table '%s' not found in registry", tableName) } // Validasi config if err := config.Validate(); err != nil { return fmt.Errorf("validation failed: %w", err) } // Get entity entity := GetEntityByTableName(config.TableName) if entity == nil { return fmt.Errorf("entity not found for table '%s'", config.TableName) } // Baca CSV file dan validasi file, err := os.Open(config.CSVFile) if err != nil { return fmt.Errorf("failed to open CSV file: %w", err) } defer file.Close() reader := csv.NewReader(file) headers, err := reader.Read() if err != nil { return fmt.Errorf("failed to read CSV header: %w", err) } // Hitung jumlah baris rowCount := 0 for { _, err := reader.Read() if err != nil { break } rowCount++ } fmt.Printf("Validation passed for table: %s\n", tableName) fmt.Printf("CSV file: %s\n", config.CSVFile) fmt.Printf("Headers: %v\n", headers) fmt.Printf("Expected rows: %d\n", rowCount) fmt.Printf("Column mapping: %v\n", config.ColumnMap) fmt.Printf("Entity type: %T\n", entity) return nil } // seedAllTables melakukan seeding untuk semua tabel func (c *SeederCLI) seedAllTables(options []string) error { tables := c.registry.List() log.Printf("Starting seed for all tables (%d tables)", len(tables)) for _, table := range tables { if err := c.seedTable(table, options); err != nil { log.Printf("Error seeding table %s: %v", table, err) continue } } log.Printf("Completed seeding all tables") return nil } // dryRunAllTables melakukan dry-run untuk semua tabel func (c *SeederCLI) dryRunAllTables(options []string) error { tables := c.registry.List() log.Printf("Starting dry-run for all tables (%d tables)", len(tables)) for _, table := range tables { if err := c.dryRunTable(table, options); err != nil { log.Printf("Error in dry-run for table %s: %v", table, err) continue } } log.Printf("Completed dry-run for all tables") return nil } // validateAllTables memvalidasi semua tabel func (c *SeederCLI) validateAllTables() error { tables := c.registry.List() fmt.Printf("Validating all tables (%d tables):\n", len(tables)) fmt.Println(strings.Repeat("=", 60)) for _, table := range tables { fmt.Printf("\n[%s]\n", table) if err := c.validateTable(table); err != nil { fmt.Printf("❌ Validation failed: %v\n", err) } else { fmt.Printf("✅ Validation passed\n") } } return nil } // SeederOptions berisi parsed options type SeederOptions struct { CSVPath string TableName string BatchSize int DeleteBefore bool DryRun bool } // parseOptions mem-parsing command line options func (c *SeederCLI) parseOptions(options []string) SeederOptions { var opts SeederOptions for _, option := range options { switch { case strings.HasPrefix(option, "-batch-size="): fmt.Sscanf(option, "-batch-size=%d", &opts.BatchSize) case option == "-delete-before": opts.DeleteBefore = true case strings.HasPrefix(option, "-csv-path="): opts.CSVPath = strings.TrimPrefix(option, "-csv-path=") case strings.HasPrefix(option, "-table-name="): opts.TableName = strings.TrimPrefix(option, "-table-name=") } } return opts } // MainCLI adalah entry point utama untuk CLI func MainCLI() { // Setup database connection db, err := setupDatabase() if err != nil { log.Fatalf("Failed to connect to database: %v", err) } cli := NewSeederCLI(db) if err := cli.Run(os.Args[1:]); err != nil { log.Fatalf("Error: %v", err) } } // setupDatabase setup koneksi database func setupDatabase() (*gorm.DB, error) { // Bisa diambil dari config atau environment variable dsn := os.Getenv("DATABASE_URL") if dsn == "" { dsn = "host=localhost user=postgres password=postgres dbname=health port=5432 sslmode=disable" } db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { return nil, err } return db, nil }