Files
relspecgo/cmd/relspec/edit.go
Hein 1795eb64d1
Some checks failed
CI / Test (1.24) (push) Failing after 1m3s
CI / Lint (push) Failing after -27m11s
CI / Build (push) Successful in 40s
Integration Tests / Integration Tests (push) Failing after -28m11s
CI / Test (1.25) (push) Failing after -26m33s
feat(ui): 🎨 Implement schema and table management screens
* Add schema management screen with list and editor
* Implement table management screen with list and editor
* Create data operations for schema and table management
* Define UI rules and guidelines for consistency
* Ensure circular tab navigation and keyboard shortcuts
* Add forms for creating and editing schemas and tables
* Implement confirmation dialogs for destructive actions
2026-01-04 18:29:29 +02:00

335 lines
12 KiB
Go

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
}