Files
satusehat-service/internal/infrastructure/database/seeders/seeder_cli.go
T
2026-04-14 01:23:34 +00:00

400 lines
9.6 KiB
Go

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 <table-name|all> [options]")
}
return c.seedTable(args[1], args[2:])
case "dry-run":
if len(args) < 2 {
return fmt.Errorf("usage: seeder dry-run <table-name|all> [options]")
}
return c.dryRunTable(args[1], args[2:])
case "validate":
if len(args) < 2 {
return fmt.Errorf("usage: seeder validate <table-name|all>")
}
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 <command> [arguments]
Commands:
list Show all available tables
seed <table|all> Seed data for specific table or all tables
dry-run <table|all> Preview seeding without actual insertion
validate <table|all> 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
}