* 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
335 lines
12 KiB
Go
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
|
|
}
|