183 lines
5.2 KiB
Go
183 lines
5.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.ma-al.com/goc_marek/gormcol"
|
|
"github.com/joho/godotenv"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// main is the entry point for the gormcol-gen CLI tool.
|
|
// It parses flags, loads configuration from .env, connects to the database,
|
|
// and generates GORM models with column descriptors.
|
|
func main() {
|
|
dsn := flag.String("dsn", "", "database DSN (e.g. user:pass@tcp(host:3306)/dbname)")
|
|
filter := flag.String("filter", "(ps_|b2b_).*", "regex to match table names")
|
|
all := flag.Bool("all", false, "generate all tables matching filter (shows confirmation)")
|
|
outDir := flag.String("out", "./app/model/dbmodel", "output directory for generated files")
|
|
pkgName := flag.String("pkg", "dbmodel", "Go package name for generated files")
|
|
flag.Usage = func() {
|
|
fmt.Fprintf(os.Stderr, "gormcol-gen - generate GORM models with column descriptors\n\n")
|
|
fmt.Fprintf(os.Stderr, "Usage:\n")
|
|
fmt.Fprintf(os.Stderr, " gormcol-gen [options]\n\n")
|
|
fmt.Fprintf(os.Stderr, "DSN can be provided via --dsn flag or DSN env var (from .env)\n\n")
|
|
fmt.Fprintf(os.Stderr, "Flags:\n")
|
|
flag.PrintDefaults()
|
|
}
|
|
|
|
// Load .env file if present in current directory.
|
|
godotenv.Load()
|
|
|
|
flag.Parse()
|
|
|
|
// Get DSN from flag or environment variable.
|
|
dsnValue := *dsn
|
|
if dsnValue == "" {
|
|
dsnValue = os.Getenv("DSN")
|
|
}
|
|
if dsnValue == "" {
|
|
flag.Usage()
|
|
fmt.Fprintln(os.Stderr, "\nerror: --dsn or DSN env is required")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Connect to the database.
|
|
db, err := gormcol.ConnectDSN(dsnValue)
|
|
if err != nil {
|
|
slog.Error("failed to connect to database", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
defer cancel()
|
|
|
|
cfg := gormcol.GenConfig{
|
|
OutputDir: *outDir,
|
|
PkgName: *pkgName,
|
|
TableFilter: *filter,
|
|
}
|
|
|
|
// Interactive mode (default): select tables using fzf.
|
|
// All mode: generate all tables matching filter with confirmation.
|
|
if *all {
|
|
if !confirmGenerateAll(db) {
|
|
fmt.Println("Aborted.")
|
|
os.Exit(0)
|
|
}
|
|
} else {
|
|
// Check if fzf is available for interactive mode.
|
|
if _, err := exec.LookPath("fzf"); err != nil {
|
|
fmt.Fprintln(os.Stderr, "error: fzf is required for interactive mode. Install from https://github.com/junegunn/fzf")
|
|
os.Exit(1)
|
|
}
|
|
selected, err := selectTablesInteractive(db)
|
|
if err != nil {
|
|
slog.Error("failed to select tables interactively", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
if len(selected) == 0 {
|
|
fmt.Println("No tables selected, exiting.")
|
|
os.Exit(0)
|
|
}
|
|
cfg.SelectedTables = selected
|
|
}
|
|
|
|
// Generate models with column descriptors.
|
|
if err := gormcol.NewWithConfig(db, cfg).GenModels(ctx); err != nil {
|
|
slog.Error("failed to generate models", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// selectTablesInteractive displays all database tables in fzf for interactive selection.
|
|
// Returns a list of selected table names.
|
|
func selectTablesInteractive(db *gorm.DB) ([]string, error) {
|
|
type migratorWithGetTables interface {
|
|
GetTables() ([]string, error)
|
|
}
|
|
|
|
tableNames, err := db.Migrator().(migratorWithGetTables).GetTables()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get tables: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Found %d tables, launching fzf for selection...\n", len(tableNames))
|
|
|
|
selected, err := runFzf(tableNames)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fzf selection failed: %w", err)
|
|
}
|
|
|
|
return selected, nil
|
|
}
|
|
|
|
// confirmGenerateAll displays a confirmation prompt before generating all tables.
|
|
// Returns true if user confirms, false if cancelled or timeout.
|
|
func confirmGenerateAll(db *gorm.DB) bool {
|
|
type migratorWithGetTables interface {
|
|
GetTables() ([]string, error)
|
|
}
|
|
|
|
tableNames, err := db.Migrator().(migratorWithGetTables).GetTables()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
if len(tableNames) == 0 {
|
|
fmt.Println("No tables in database.")
|
|
return false
|
|
}
|
|
|
|
// Display yellow warning message and wait for user input.
|
|
yellow := "\033[33m"
|
|
reset := "\033[0m"
|
|
msg := fmt.Sprintf("%sWARNING:%s Generate all %d tables? [Enter] confirm / [Esc] cancel", yellow, reset, len(tableNames))
|
|
|
|
script := fmt.Sprintf(`echo; echo -e '%s'; read -t 10 -n 1 -s key; if [[ -z "$key" ]]; then exit 0; elif [[ "$key" == $'\e' ]]; then exit 1; else exit 0; fi`, msg)
|
|
cmd := exec.Command("bash", "-c", script)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
err = cmd.Run()
|
|
return err == nil
|
|
}
|
|
|
|
// runFzf launches fzf for interactive table selection.
|
|
// Supports multi-select with Tab, fuzzy search, and keyboard navigation.
|
|
func runFzf(tableNames []string) ([]string, error) {
|
|
header := "Tab: toggle select | Enter: confirm | Esc: cancel"
|
|
cmd := exec.Command("fzf", "-m", "--height=50%", "--border", "--prompt", "Select tables> ", "--header", header)
|
|
cmd.Stdin = strings.NewReader(strings.Join(tableNames, "\n"))
|
|
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
if exitErr.ExitCode() == 130 {
|
|
// User pressed Esc to cancel.
|
|
return []string{}, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("fzf failed: %w", err)
|
|
}
|
|
|
|
// Parse selected lines from fzf output.
|
|
selected := strings.Split(strings.TrimSpace(string(out)), "\n")
|
|
var result []string
|
|
for _, s := range selected {
|
|
s = strings.TrimSpace(s)
|
|
if s != "" {
|
|
result = append(result, s)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|