package main import ( "fmt" "os" "strings" "github.com/spf13/cobra" "git.warky.dev/wdevs/relspecgo/pkg/models" "git.warky.dev/wdevs/relspecgo/pkg/readers" "git.warky.dev/wdevs/relspecgo/pkg/readers/bun" "git.warky.dev/wdevs/relspecgo/pkg/readers/dbml" "git.warky.dev/wdevs/relspecgo/pkg/readers/dctx" "git.warky.dev/wdevs/relspecgo/pkg/readers/drawdb" "git.warky.dev/wdevs/relspecgo/pkg/readers/drizzle" "git.warky.dev/wdevs/relspecgo/pkg/readers/gorm" "git.warky.dev/wdevs/relspecgo/pkg/readers/graphql" "git.warky.dev/wdevs/relspecgo/pkg/readers/json" "git.warky.dev/wdevs/relspecgo/pkg/readers/pgsql" "git.warky.dev/wdevs/relspecgo/pkg/readers/prisma" "git.warky.dev/wdevs/relspecgo/pkg/readers/typeorm" "git.warky.dev/wdevs/relspecgo/pkg/readers/yaml" "git.warky.dev/wdevs/relspecgo/pkg/ui" "git.warky.dev/wdevs/relspecgo/pkg/writers" wbun "git.warky.dev/wdevs/relspecgo/pkg/writers/bun" wdbml "git.warky.dev/wdevs/relspecgo/pkg/writers/dbml" wdctx "git.warky.dev/wdevs/relspecgo/pkg/writers/dctx" wdrawdb "git.warky.dev/wdevs/relspecgo/pkg/writers/drawdb" wdrizzle "git.warky.dev/wdevs/relspecgo/pkg/writers/drizzle" wgorm "git.warky.dev/wdevs/relspecgo/pkg/writers/gorm" wgraphql "git.warky.dev/wdevs/relspecgo/pkg/writers/graphql" wjson "git.warky.dev/wdevs/relspecgo/pkg/writers/json" wpgsql "git.warky.dev/wdevs/relspecgo/pkg/writers/pgsql" wprisma "git.warky.dev/wdevs/relspecgo/pkg/writers/prisma" wtypeorm "git.warky.dev/wdevs/relspecgo/pkg/writers/typeorm" wyaml "git.warky.dev/wdevs/relspecgo/pkg/writers/yaml" ) var ( editSourceType string editSourcePath string editSourceConn string editTargetType string editTargetPath string editSchemaFilter string ) var editCmd = &cobra.Command{ Use: "edit", Short: "Edit database schema interactively with TUI", Long: `Edit database schemas from various formats using an interactive terminal UI. Allows you to: - List and navigate schemas and tables - Create, edit, and delete schemas - Create, edit, and delete tables - Add, edit, and delete columns - Set table and column properties - Add constraints, indexes, and relationships Supports reading from and writing to all supported formats: Input formats: - dbml: DBML schema files - dctx: DCTX schema files - drawdb: DrawDB JSON files - graphql: GraphQL schema files (.graphql, SDL) - json: JSON database schema - yaml: YAML database schema - gorm: GORM model files (Go, file or directory) - bun: Bun model files (Go, file or directory) - drizzle: Drizzle ORM schema files (TypeScript, file or directory) - prisma: Prisma schema files (.prisma) - typeorm: TypeORM entity files (TypeScript) - pgsql: PostgreSQL database (live connection) Output formats: - dbml: DBML schema files - dctx: DCTX schema files - drawdb: DrawDB JSON files - graphql: GraphQL schema files (.graphql, SDL) - json: JSON database schema - yaml: YAML database schema - gorm: GORM model files (Go) - bun: Bun model files (Go) - drizzle: Drizzle ORM schema files (TypeScript) - prisma: Prisma schema files (.prisma) - typeorm: TypeORM entity files (TypeScript) - pgsql: PostgreSQL SQL schema PostgreSQL Connection String Examples: postgres://username:password@localhost:5432/database_name postgres://username:password@localhost/database_name postgresql://user:pass@host:5432/dbname?sslmode=disable postgresql://user:pass@host/dbname?sslmode=require host=localhost port=5432 user=username password=pass dbname=mydb sslmode=disable Examples: # Edit a DBML schema file relspec edit --from dbml --from-path schema.dbml --to dbml --to-path schema.dbml # Edit a PostgreSQL database relspec edit --from pgsql --from-conn "postgres://user:pass@localhost/mydb" \ --to pgsql --to-conn "postgres://user:pass@localhost/mydb" # Edit JSON schema and output to GORM relspec edit --from json --from-path db.json --to gorm --to-path models/ # Edit GORM models in place relspec edit --from gorm --from-path ./models --to gorm --to-path ./models`, RunE: runEdit, } func init() { editCmd.Flags().StringVar(&editSourceType, "from", "", "Source format (dbml, dctx, drawdb, graphql, json, yaml, gorm, bun, drizzle, prisma, typeorm, pgsql)") editCmd.Flags().StringVar(&editSourcePath, "from-path", "", "Source file path (for file-based formats)") editCmd.Flags().StringVar(&editSourceConn, "from-conn", "", "Source connection string (for database formats)") editCmd.Flags().StringVar(&editTargetType, "to", "", "Target format (dbml, dctx, drawdb, graphql, json, yaml, gorm, bun, drizzle, prisma, typeorm, pgsql)") editCmd.Flags().StringVar(&editTargetPath, "to-path", "", "Target file path (for file-based formats)") editCmd.Flags().StringVar(&editSchemaFilter, "schema", "", "Filter to a specific schema by name") // Flags are now optional - if not provided, UI will prompt for load/save options } func runEdit(cmd *cobra.Command, args []string) error { fmt.Fprintf(os.Stderr, "\n=== RelSpec Schema Editor ===\n") fmt.Fprintf(os.Stderr, "Started at: %s\n\n", getCurrentTimestamp()) var db *models.Database var loadConfig *ui.LoadConfig var saveConfig *ui.SaveConfig var err error // Check if source parameters are provided if editSourceType != "" { // Read source database fmt.Fprintf(os.Stderr, "[1/3] Reading source schema...\n") fmt.Fprintf(os.Stderr, " Format: %s\n", editSourceType) if editSourcePath != "" { fmt.Fprintf(os.Stderr, " Path: %s\n", editSourcePath) } if editSourceConn != "" { fmt.Fprintf(os.Stderr, " Conn: %s\n", maskPassword(editSourceConn)) } db, err = readDatabaseForEdit(editSourceType, editSourcePath, editSourceConn, "Source") if err != nil { return fmt.Errorf("failed to read source: %w", err) } // Apply schema filter if specified if editSchemaFilter != "" { db = filterDatabaseBySchema(db, editSchemaFilter) } fmt.Fprintf(os.Stderr, " ✓ Successfully read database '%s'\n", db.Name) fmt.Fprintf(os.Stderr, " Found: %d schema(s)\n", len(db.Schemas)) totalTables := 0 for _, schema := range db.Schemas { totalTables += len(schema.Tables) } fmt.Fprintf(os.Stderr, " Found: %d table(s)\n\n", totalTables) // Store load config loadConfig = &ui.LoadConfig{ SourceType: editSourceType, FilePath: editSourcePath, ConnString: editSourceConn, } } else { // No source parameters provided, UI will show load screen fmt.Fprintf(os.Stderr, "[1/2] No source specified, editor will prompt for database\n\n") } // Store save config if target parameters are provided if editTargetType != "" { saveConfig = &ui.SaveConfig{ TargetType: editTargetType, FilePath: editTargetPath, } } // Launch interactive TUI if editSourceType != "" { fmt.Fprintf(os.Stderr, "[2/3] Launching interactive editor...\n") } else { fmt.Fprintf(os.Stderr, "[2/2] Launching interactive editor...\n") } fmt.Fprintf(os.Stderr, " Use arrow keys and shortcuts to navigate\n") fmt.Fprintf(os.Stderr, " Press ? for help\n\n") editor := ui.NewSchemaEditorWithConfigs(db, loadConfig, saveConfig) if err := editor.Run(); err != nil { return fmt.Errorf("editor failed: %w", err) } // Only write to output if target parameters were provided and database was loaded from command line if editTargetType != "" && editSourceType != "" && db != nil { fmt.Fprintf(os.Stderr, "[3/3] Writing changes to output...\n") fmt.Fprintf(os.Stderr, " Format: %s\n", editTargetType) if editTargetPath != "" { fmt.Fprintf(os.Stderr, " Path: %s\n", editTargetPath) } // Get the potentially modified database from the editor err = writeDatabaseForEdit(editTargetType, editTargetPath, "", editor.GetDatabase(), "Target") if err != nil { return fmt.Errorf("failed to write output: %w", err) } fmt.Fprintf(os.Stderr, " ✓ Successfully written database\n") } fmt.Fprintf(os.Stderr, "\n=== Edit complete ===\n") return nil } func readDatabaseForEdit(dbType, filePath, connString, label string) (*models.Database, error) { var reader readers.Reader switch strings.ToLower(dbType) { case "dbml": if filePath == "" { return nil, fmt.Errorf("%s: file path is required for DBML format", label) } reader = dbml.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "dctx": if filePath == "" { return nil, fmt.Errorf("%s: file path is required for DCTX format", label) } reader = dctx.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "drawdb": if filePath == "" { return nil, fmt.Errorf("%s: file path is required for DrawDB format", label) } reader = drawdb.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "graphql": if filePath == "" { return nil, fmt.Errorf("%s: file path is required for GraphQL format", label) } reader = graphql.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "json": if filePath == "" { return nil, fmt.Errorf("%s: file path is required for JSON format", label) } reader = json.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "yaml": if filePath == "" { return nil, fmt.Errorf("%s: file path is required for YAML format", label) } reader = yaml.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "gorm": if filePath == "" { return nil, fmt.Errorf("%s: file path is required for GORM format", label) } reader = gorm.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "bun": if filePath == "" { return nil, fmt.Errorf("%s: file path is required for Bun format", label) } reader = bun.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "drizzle": if filePath == "" { return nil, fmt.Errorf("%s: file path is required for Drizzle format", label) } reader = drizzle.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "prisma": if filePath == "" { return nil, fmt.Errorf("%s: file path is required for Prisma format", label) } reader = prisma.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "typeorm": if filePath == "" { return nil, fmt.Errorf("%s: file path is required for TypeORM format", label) } reader = typeorm.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "pgsql": if connString == "" { return nil, fmt.Errorf("%s: connection string is required for PostgreSQL format", label) } reader = pgsql.NewReader(&readers.ReaderOptions{ConnectionString: connString}) default: return nil, fmt.Errorf("%s: unsupported format: %s", label, dbType) } db, err := reader.ReadDatabase() if err != nil { return nil, fmt.Errorf("%s: %w", label, err) } return db, nil } func writeDatabaseForEdit(dbType, filePath, connString string, db *models.Database, label string) error { var writer writers.Writer switch strings.ToLower(dbType) { case "dbml": writer = wdbml.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "dctx": writer = wdctx.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "drawdb": writer = wdrawdb.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "graphql": writer = wgraphql.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "json": writer = wjson.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "yaml": writer = wyaml.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "gorm": writer = wgorm.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "bun": writer = wbun.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "drizzle": writer = wdrizzle.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "prisma": writer = wprisma.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "typeorm": writer = wtypeorm.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "pgsql": writer = wpgsql.NewWriter(&writers.WriterOptions{OutputPath: filePath}) default: return fmt.Errorf("%s: unsupported format: %s", label, dbType) } err := writer.WriteDatabase(db) if err != nil { return fmt.Errorf("%s: %w", label, err) } return nil }