525 lines
15 KiB
Go
525 lines
15 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/gdamore/tcell/v2"
|
|
"github.com/rivo/tview"
|
|
|
|
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
|
"git.warky.dev/wdevs/relspecgo/pkg/readers"
|
|
rbun "git.warky.dev/wdevs/relspecgo/pkg/readers/bun"
|
|
rdbml "git.warky.dev/wdevs/relspecgo/pkg/readers/dbml"
|
|
rdctx "git.warky.dev/wdevs/relspecgo/pkg/readers/dctx"
|
|
rdrawdb "git.warky.dev/wdevs/relspecgo/pkg/readers/drawdb"
|
|
rdrizzle "git.warky.dev/wdevs/relspecgo/pkg/readers/drizzle"
|
|
rgorm "git.warky.dev/wdevs/relspecgo/pkg/readers/gorm"
|
|
rgraphql "git.warky.dev/wdevs/relspecgo/pkg/readers/graphql"
|
|
rjson "git.warky.dev/wdevs/relspecgo/pkg/readers/json"
|
|
rpgsql "git.warky.dev/wdevs/relspecgo/pkg/readers/pgsql"
|
|
rprisma "git.warky.dev/wdevs/relspecgo/pkg/readers/prisma"
|
|
rtypeorm "git.warky.dev/wdevs/relspecgo/pkg/readers/typeorm"
|
|
ryaml "git.warky.dev/wdevs/relspecgo/pkg/readers/yaml"
|
|
"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"
|
|
)
|
|
|
|
// LoadConfig holds the configuration for loading a database
|
|
type LoadConfig struct {
|
|
SourceType string
|
|
FilePath string
|
|
ConnString string
|
|
}
|
|
|
|
// SaveConfig holds the configuration for saving a database
|
|
type SaveConfig struct {
|
|
TargetType string
|
|
FilePath string
|
|
ConnString string
|
|
}
|
|
|
|
// showLoadScreen displays the database load screen
|
|
func (se *SchemaEditor) showLoadScreen() {
|
|
flex := tview.NewFlex().SetDirection(tview.FlexRow)
|
|
|
|
// Title
|
|
title := tview.NewTextView().
|
|
SetText("[::b]Load Database Schema").
|
|
SetTextAlign(tview.AlignCenter).
|
|
SetDynamicColors(true)
|
|
|
|
// Form
|
|
form := tview.NewForm()
|
|
form.SetBorder(true).SetTitle(" Load Configuration ").SetTitleAlign(tview.AlignLeft)
|
|
|
|
// Format selection
|
|
formatOptions := []string{
|
|
"dbml", "dctx", "drawdb", "graphql", "json", "yaml",
|
|
"gorm", "bun", "drizzle", "prisma", "typeorm", "pgsql",
|
|
}
|
|
selectedFormat := 0
|
|
currentFormat := formatOptions[selectedFormat]
|
|
|
|
// File path input
|
|
filePath := ""
|
|
connString := ""
|
|
|
|
form.AddDropDown("Format", formatOptions, 0, func(option string, index int) {
|
|
selectedFormat = index
|
|
currentFormat = option
|
|
})
|
|
|
|
form.AddInputField("File Path", "", 50, nil, func(value string) {
|
|
filePath = value
|
|
})
|
|
|
|
form.AddInputField("Connection String", "", 50, nil, func(value string) {
|
|
connString = value
|
|
})
|
|
|
|
form.AddTextView("Help", getLoadHelpText(), 0, 5, true, false)
|
|
|
|
// Buttons
|
|
form.AddButton("Load [l]", func() {
|
|
se.loadDatabase(currentFormat, filePath, connString)
|
|
})
|
|
|
|
form.AddButton("Create New [n]", func() {
|
|
se.createNewDatabase()
|
|
})
|
|
|
|
form.AddButton("Exit [q]", func() {
|
|
se.app.Stop()
|
|
})
|
|
|
|
// Keyboard shortcuts
|
|
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
if event.Key() == tcell.KeyEscape {
|
|
se.app.Stop()
|
|
return nil
|
|
}
|
|
switch event.Rune() {
|
|
case 'l':
|
|
se.loadDatabase(currentFormat, filePath, connString)
|
|
return nil
|
|
case 'n':
|
|
se.createNewDatabase()
|
|
return nil
|
|
case 'q':
|
|
se.app.Stop()
|
|
return nil
|
|
}
|
|
return event
|
|
})
|
|
|
|
// Tab navigation
|
|
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
if event.Key() == tcell.KeyEscape {
|
|
se.app.Stop()
|
|
return nil
|
|
}
|
|
if event.Rune() == 'l' || event.Rune() == 'n' || event.Rune() == 'q' {
|
|
return event
|
|
}
|
|
return event
|
|
})
|
|
|
|
flex.AddItem(title, 1, 0, false).
|
|
AddItem(form, 0, 1, true)
|
|
|
|
se.pages.AddAndSwitchToPage("load-database", flex, true)
|
|
}
|
|
|
|
// showSaveScreen displays the save database screen
|
|
func (se *SchemaEditor) showSaveScreen() {
|
|
flex := tview.NewFlex().SetDirection(tview.FlexRow)
|
|
|
|
// Title
|
|
title := tview.NewTextView().
|
|
SetText("[::b]Save Database Schema").
|
|
SetTextAlign(tview.AlignCenter).
|
|
SetDynamicColors(true)
|
|
|
|
// Form
|
|
form := tview.NewForm()
|
|
form.SetBorder(true).SetTitle(" Save Configuration ").SetTitleAlign(tview.AlignLeft)
|
|
|
|
// Format selection
|
|
formatOptions := []string{
|
|
"dbml", "dctx", "drawdb", "graphql", "json", "yaml",
|
|
"gorm", "bun", "drizzle", "prisma", "typeorm", "pgsql",
|
|
}
|
|
selectedFormat := 0
|
|
currentFormat := formatOptions[selectedFormat]
|
|
|
|
// File path input
|
|
filePath := ""
|
|
if se.saveConfig != nil {
|
|
// Pre-populate with existing save config
|
|
for i, format := range formatOptions {
|
|
if format == se.saveConfig.TargetType {
|
|
selectedFormat = i
|
|
currentFormat = format
|
|
break
|
|
}
|
|
}
|
|
filePath = se.saveConfig.FilePath
|
|
}
|
|
|
|
form.AddDropDown("Format", formatOptions, selectedFormat, func(option string, index int) {
|
|
selectedFormat = index
|
|
currentFormat = option
|
|
})
|
|
|
|
form.AddInputField("File Path", filePath, 50, nil, func(value string) {
|
|
filePath = value
|
|
})
|
|
|
|
form.AddTextView("Help", getSaveHelpText(), 0, 5, true, false)
|
|
|
|
// Buttons
|
|
form.AddButton("Save [s]", func() {
|
|
se.saveDatabase(currentFormat, filePath)
|
|
})
|
|
|
|
form.AddButton("Update Existing Database [u]", func() {
|
|
// Use saveConfig if available, otherwise use loadConfig
|
|
if se.saveConfig != nil {
|
|
se.showUpdateExistingDatabaseConfirm()
|
|
} else if se.loadConfig != nil {
|
|
se.showUpdateExistingDatabaseConfirm()
|
|
} else {
|
|
se.showErrorDialog("Error", "No database source found. Use Save instead.")
|
|
}
|
|
})
|
|
|
|
form.AddButton("Back [b]", func() {
|
|
se.pages.RemovePage("save-database")
|
|
se.pages.SwitchToPage("main")
|
|
})
|
|
|
|
// Keyboard shortcuts
|
|
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
if event.Key() == tcell.KeyEscape {
|
|
se.pages.RemovePage("save-database")
|
|
se.pages.SwitchToPage("main")
|
|
return nil
|
|
}
|
|
switch event.Rune() {
|
|
case 's':
|
|
se.saveDatabase(currentFormat, filePath)
|
|
return nil
|
|
case 'u':
|
|
// Use saveConfig if available, otherwise use loadConfig
|
|
if se.saveConfig != nil {
|
|
se.showUpdateExistingDatabaseConfirm()
|
|
} else if se.loadConfig != nil {
|
|
se.showUpdateExistingDatabaseConfirm()
|
|
} else {
|
|
se.showErrorDialog("Error", "No database source found. Use Save instead.")
|
|
}
|
|
return nil
|
|
case 'b':
|
|
se.pages.RemovePage("save-database")
|
|
se.pages.SwitchToPage("main")
|
|
return nil
|
|
}
|
|
return event
|
|
})
|
|
|
|
flex.AddItem(title, 1, 0, false).
|
|
AddItem(form, 0, 1, true)
|
|
|
|
se.pages.AddAndSwitchToPage("save-database", flex, true)
|
|
}
|
|
|
|
// loadDatabase loads a database from the specified configuration
|
|
func (se *SchemaEditor) loadDatabase(format, filePath, connString string) {
|
|
// Validate input
|
|
if format == "pgsql" {
|
|
if connString == "" {
|
|
se.showErrorDialog("Error", "Connection string is required for PostgreSQL")
|
|
return
|
|
}
|
|
} else {
|
|
if filePath == "" {
|
|
se.showErrorDialog("Error", "File path is required for "+format)
|
|
return
|
|
}
|
|
// Expand home directory
|
|
if len(filePath) > 0 && filePath[0] == '~' {
|
|
home, err := os.UserHomeDir()
|
|
if err == nil {
|
|
filePath = filepath.Join(home, filePath[1:])
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create reader
|
|
var reader readers.Reader
|
|
switch format {
|
|
case "dbml":
|
|
reader = rdbml.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
|
case "dctx":
|
|
reader = rdctx.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
|
case "drawdb":
|
|
reader = rdrawdb.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
|
case "graphql":
|
|
reader = rgraphql.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
|
case "json":
|
|
reader = rjson.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
|
case "yaml":
|
|
reader = ryaml.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
|
case "gorm":
|
|
reader = rgorm.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
|
case "bun":
|
|
reader = rbun.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
|
case "drizzle":
|
|
reader = rdrizzle.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
|
case "prisma":
|
|
reader = rprisma.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
|
case "typeorm":
|
|
reader = rtypeorm.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
|
case "pgsql":
|
|
reader = rpgsql.NewReader(&readers.ReaderOptions{ConnectionString: connString})
|
|
default:
|
|
se.showErrorDialog("Error", "Unsupported format: "+format)
|
|
return
|
|
}
|
|
|
|
// Read database
|
|
db, err := reader.ReadDatabase()
|
|
if err != nil {
|
|
se.showErrorDialog("Load Error", fmt.Sprintf("Failed to load database: %v", err))
|
|
return
|
|
}
|
|
|
|
// Store load config
|
|
se.loadConfig = &LoadConfig{
|
|
SourceType: format,
|
|
FilePath: filePath,
|
|
ConnString: connString,
|
|
}
|
|
|
|
// Update database
|
|
se.db = db
|
|
|
|
// Show success and switch to main menu
|
|
se.showSuccessDialog("Load Complete", fmt.Sprintf("Successfully loaded database '%s'", db.Name), func() {
|
|
se.pages.RemovePage("load-database")
|
|
se.pages.RemovePage("main")
|
|
se.pages.AddPage("main", se.createMainMenu(), true, true)
|
|
})
|
|
}
|
|
|
|
// saveDatabase saves the database to the specified configuration
|
|
func (se *SchemaEditor) saveDatabase(format, filePath string) {
|
|
// Validate input
|
|
if format == "pgsql" {
|
|
se.showErrorDialog("Error", "Direct PostgreSQL save is not supported from the UI. Use --to pgsql --to-path output.sql")
|
|
return
|
|
}
|
|
|
|
if filePath == "" {
|
|
se.showErrorDialog("Error", "File path is required")
|
|
return
|
|
}
|
|
|
|
// Expand home directory
|
|
if len(filePath) > 0 && filePath[0] == '~' {
|
|
home, err := os.UserHomeDir()
|
|
if err == nil {
|
|
filePath = filepath.Join(home, filePath[1:])
|
|
}
|
|
}
|
|
|
|
// Create writer
|
|
var writer writers.Writer
|
|
switch format {
|
|
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:
|
|
se.showErrorDialog("Error", "Unsupported format: "+format)
|
|
return
|
|
}
|
|
|
|
// Write database
|
|
err := writer.WriteDatabase(se.db)
|
|
if err != nil {
|
|
se.showErrorDialog("Save Error", fmt.Sprintf("Failed to save database: %v", err))
|
|
return
|
|
}
|
|
|
|
// Store save config
|
|
se.saveConfig = &SaveConfig{
|
|
TargetType: format,
|
|
FilePath: filePath,
|
|
}
|
|
|
|
// Show success
|
|
se.showSuccessDialog("Save Complete", fmt.Sprintf("Successfully saved database to %s", filePath), func() {
|
|
se.pages.RemovePage("save-database")
|
|
se.pages.SwitchToPage("main")
|
|
})
|
|
}
|
|
|
|
// createNewDatabase creates a new empty database
|
|
func (se *SchemaEditor) createNewDatabase() {
|
|
// Create a new empty database
|
|
se.db = &models.Database{
|
|
Name: "New Database",
|
|
Schemas: []*models.Schema{},
|
|
}
|
|
|
|
// Clear load config
|
|
se.loadConfig = nil
|
|
|
|
// Show success and switch to main menu
|
|
se.showSuccessDialog("New Database", "Created new empty database", func() {
|
|
se.pages.RemovePage("load-database")
|
|
se.pages.AddPage("main", se.createMainMenu(), true, true)
|
|
})
|
|
}
|
|
|
|
// showErrorDialog displays an error dialog
|
|
func (se *SchemaEditor) showErrorDialog(_title, message string) {
|
|
modal := tview.NewModal().
|
|
SetText(message).
|
|
AddButtons([]string{"OK"}).
|
|
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
|
se.pages.RemovePage("error-dialog")
|
|
})
|
|
|
|
modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
if event.Key() == tcell.KeyEscape {
|
|
se.pages.RemovePage("error-dialog")
|
|
return nil
|
|
}
|
|
return event
|
|
})
|
|
|
|
se.pages.AddPage("error-dialog", modal, true, true)
|
|
}
|
|
|
|
// showSuccessDialog displays a success dialog
|
|
func (se *SchemaEditor) showSuccessDialog(_title, message string, onClose func()) {
|
|
modal := tview.NewModal().
|
|
SetText(message).
|
|
AddButtons([]string{"OK"}).
|
|
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
|
se.pages.RemovePage("success-dialog")
|
|
if onClose != nil {
|
|
onClose()
|
|
}
|
|
})
|
|
|
|
modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
if event.Key() == tcell.KeyEscape {
|
|
se.pages.RemovePage("success-dialog")
|
|
if onClose != nil {
|
|
onClose()
|
|
}
|
|
return nil
|
|
}
|
|
return event
|
|
})
|
|
|
|
se.pages.AddPage("success-dialog", modal, true, true)
|
|
}
|
|
|
|
// getLoadHelpText returns the help text for the load screen
|
|
func getLoadHelpText() string {
|
|
return `File-based formats: dbml, dctx, drawdb, graphql, json, yaml, gorm, bun, drizzle, prisma, typeorm
|
|
Database formats: pgsql (requires connection string)
|
|
|
|
Examples:
|
|
- File path: ~/schemas/mydb.dbml or /path/to/schema.json
|
|
- Connection: postgres://user:pass@localhost/dbname`
|
|
}
|
|
|
|
// showUpdateExistingDatabaseConfirm displays a confirmation dialog before updating existing database
|
|
func (se *SchemaEditor) showUpdateExistingDatabaseConfirm() {
|
|
// Use saveConfig if available, otherwise use loadConfig
|
|
var targetType, targetPath string
|
|
if se.saveConfig != nil {
|
|
targetType = se.saveConfig.TargetType
|
|
targetPath = se.saveConfig.FilePath
|
|
} else if se.loadConfig != nil {
|
|
targetType = se.loadConfig.SourceType
|
|
targetPath = se.loadConfig.FilePath
|
|
} else {
|
|
return
|
|
}
|
|
|
|
confirmText := fmt.Sprintf("Update existing database?\n\nFormat: %s\nPath: %s\n\nThis will overwrite the source.",
|
|
targetType, targetPath)
|
|
|
|
modal := tview.NewModal().
|
|
SetText(confirmText).
|
|
AddButtons([]string{"Cancel", "Update"}).
|
|
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
|
if buttonLabel == "Update" {
|
|
se.pages.RemovePage("update-confirm")
|
|
se.pages.RemovePage("save-database")
|
|
se.saveDatabase(targetType, targetPath)
|
|
se.pages.SwitchToPage("main")
|
|
} else {
|
|
se.pages.RemovePage("update-confirm")
|
|
}
|
|
})
|
|
|
|
modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
if event.Key() == tcell.KeyEscape {
|
|
se.pages.RemovePage("update-confirm")
|
|
return nil
|
|
}
|
|
return event
|
|
})
|
|
|
|
se.pages.AddAndSwitchToPage("update-confirm", modal, true)
|
|
}
|
|
|
|
// getSaveHelpText returns the help text for the save screen
|
|
func getSaveHelpText() string {
|
|
return `File-based formats: dbml, dctx, drawdb, graphql, json, yaml, gorm, bun, drizzle, prisma, typeorm, pgsql (SQL export)
|
|
|
|
Examples:
|
|
- File: ~/schemas/mydb.dbml
|
|
- Directory (for code formats): ./models/`
|
|
}
|