feat: add TableList component with styles and functionality for displaying database tables
feat: implement NewConnectionDialog component for creating and editing database connections with form validation chore: generate TypeScript definitions and JavaScript bindings for app functions chore: add models for configuration, connection requests, and database entities
This commit is contained in:
@@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
@@ -13,14 +14,25 @@ import (
|
||||
"uzdb/internal/services"
|
||||
)
|
||||
|
||||
// rootErrMsg unwraps the full error chain and returns the deepest error message.
|
||||
func rootErrMsg(err error) string {
|
||||
for {
|
||||
unwrapped := errors.Unwrap(err)
|
||||
if unwrapped == nil {
|
||||
return err.Error()
|
||||
}
|
||||
err = unwrapped
|
||||
}
|
||||
}
|
||||
|
||||
// App is the main application structure for Wails bindings
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
config *config.Config
|
||||
connectionSvc *services.ConnectionService
|
||||
querySvc *services.QueryService
|
||||
httpServer *handler.HTTPServer
|
||||
shutdownFunc context.CancelFunc
|
||||
ctx context.Context
|
||||
config *config.Config
|
||||
connectionSvc *services.ConnectionService
|
||||
querySvc *services.QueryService
|
||||
httpServer *handler.HTTPServer
|
||||
shutdownFunc context.CancelFunc
|
||||
}
|
||||
|
||||
// NewApp creates a new App instance
|
||||
@@ -44,7 +56,7 @@ func (a *App) Initialize(
|
||||
// OnStartup is called when the app starts (public method for Wails)
|
||||
func (a *App) OnStartup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
|
||||
|
||||
config.GetLogger().Info("Wails application started")
|
||||
}
|
||||
|
||||
@@ -124,6 +136,24 @@ func (a *App) DeleteConnection(id string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// DisconnectConnection removes an active connection from the connection manager
|
||||
// Returns error message or empty string on success
|
||||
func (a *App) DisconnectConnection(id string) string {
|
||||
if a.connectionSvc == nil {
|
||||
return "Service not initialized"
|
||||
}
|
||||
|
||||
err := a.connectionSvc.DisconnectConnection(a.ctx, id)
|
||||
if err != nil {
|
||||
config.GetLogger().Error("failed to disconnect connection",
|
||||
zap.String("id", id),
|
||||
zap.Error(err))
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// TestConnection tests a database connection
|
||||
// Returns (success, error_message)
|
||||
func (a *App) TestConnection(id string) (bool, string) {
|
||||
@@ -137,14 +167,19 @@ func (a *App) TestConnection(id string) (bool, string) {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
return result.Success, result.Message
|
||||
return result.Success, func() string {
|
||||
if result.Success {
|
||||
return ""
|
||||
}
|
||||
return result.Message
|
||||
}()
|
||||
}
|
||||
|
||||
// ExecuteQuery executes a SQL query on a connection
|
||||
// Returns query result or error message
|
||||
func (a *App) ExecuteQuery(connectionID, sql string) (*models.QueryResult, string) {
|
||||
if a.connectionSvc == nil {
|
||||
return nil, "Service not initialized"
|
||||
return &models.QueryResult{Success: false, Error: "Service not initialized"}, ""
|
||||
}
|
||||
|
||||
result, err := a.connectionSvc.ExecuteQuery(a.ctx, connectionID, sql)
|
||||
@@ -153,7 +188,7 @@ func (a *App) ExecuteQuery(connectionID, sql string) (*models.QueryResult, strin
|
||||
zap.String("connection_id", connectionID),
|
||||
zap.String("sql", sql),
|
||||
zap.Error(err))
|
||||
return nil, err.Error()
|
||||
return &models.QueryResult{Success: false, Error: rootErrMsg(err)}, ""
|
||||
}
|
||||
|
||||
return result, ""
|
||||
@@ -188,7 +223,7 @@ func (a *App) GetTableData(connectionID, tableName string, limit, offset int) (*
|
||||
zap.String("connection_id", connectionID),
|
||||
zap.String("table", tableName),
|
||||
zap.Error(err))
|
||||
return nil, err.Error()
|
||||
return nil, rootErrMsg(err)
|
||||
}
|
||||
|
||||
return result, ""
|
||||
@@ -306,19 +341,19 @@ func (a *App) StartHTTPServer() string {
|
||||
// Shutdown gracefully shuts down the application
|
||||
func (a *App) Shutdown() {
|
||||
config.GetLogger().Info("shutting down application")
|
||||
|
||||
|
||||
if a.shutdownFunc != nil {
|
||||
a.shutdownFunc()
|
||||
}
|
||||
|
||||
|
||||
if a.httpServer != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
a.httpServer.Shutdown(ctx)
|
||||
}
|
||||
|
||||
|
||||
// Close all database connections
|
||||
database.CloseSQLite()
|
||||
|
||||
|
||||
config.Sync()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
@@ -16,9 +17,9 @@ import (
|
||||
|
||||
// ConnectionService manages database connections
|
||||
type ConnectionService struct {
|
||||
db *gorm.DB
|
||||
connManager *database.ConnectionManager
|
||||
encryptSvc *EncryptionService
|
||||
db *gorm.DB
|
||||
connManager *database.ConnectionManager
|
||||
encryptSvc *EncryptionService
|
||||
}
|
||||
|
||||
// NewConnectionService creates a new connection service
|
||||
@@ -37,7 +38,7 @@ func NewConnectionService(
|
||||
// GetAllConnections returns all user connections
|
||||
func (s *ConnectionService) GetAllConnections(ctx context.Context) ([]models.UserConnection, error) {
|
||||
var connections []models.UserConnection
|
||||
|
||||
|
||||
result := s.db.WithContext(ctx).Find(&connections)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("failed to get connections: %w", result.Error)
|
||||
@@ -57,7 +58,7 @@ func (s *ConnectionService) GetAllConnections(ctx context.Context) ([]models.Use
|
||||
// GetConnectionByID returns a connection by ID
|
||||
func (s *ConnectionService) GetConnectionByID(ctx context.Context, id string) (*models.UserConnection, error) {
|
||||
var conn models.UserConnection
|
||||
|
||||
|
||||
result := s.db.WithContext(ctx).First(&conn, "id = ?", id)
|
||||
if result.Error != nil {
|
||||
if result.Error == gorm.ErrRecordNotFound {
|
||||
@@ -87,16 +88,16 @@ func (s *ConnectionService) CreateConnection(ctx context.Context, req *models.Cr
|
||||
}
|
||||
|
||||
conn := &models.UserConnection{
|
||||
ID: utils.GenerateID(),
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
Host: req.Host,
|
||||
Port: req.Port,
|
||||
Username: req.Username,
|
||||
Password: encryptedPassword,
|
||||
Database: req.Database,
|
||||
SSLMode: req.SSLMode,
|
||||
Timeout: req.Timeout,
|
||||
ID: utils.GenerateID(),
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
Host: req.Host,
|
||||
Port: req.Port,
|
||||
Username: req.Username,
|
||||
Password: encryptedPassword,
|
||||
Database: req.Database,
|
||||
SSLMode: req.SSLMode,
|
||||
Timeout: req.Timeout,
|
||||
}
|
||||
|
||||
if conn.Timeout <= 0 {
|
||||
@@ -204,6 +205,17 @@ func (s *ConnectionService) DeleteConnection(ctx context.Context, id string) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisconnectConnection removes an active connection from the connection manager
|
||||
func (s *ConnectionService) DisconnectConnection(ctx context.Context, id string) error {
|
||||
if err := s.connManager.RemoveConnection(id); err != nil {
|
||||
return fmt.Errorf("failed to disconnect connection: %w", err)
|
||||
}
|
||||
|
||||
config.GetLogger().Info("connection disconnected", zap.String("id", id))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestConnection tests a database connection
|
||||
func (s *ConnectionService) TestConnection(ctx context.Context, id string) (*models.ConnectionTestResult, error) {
|
||||
// Get connection config
|
||||
@@ -266,7 +278,7 @@ func (s *ConnectionService) ExecuteQuery(ctx context.Context, connectionID, sql
|
||||
|
||||
// Execute query
|
||||
startTime := time.Now()
|
||||
|
||||
|
||||
var result *models.QueryResult
|
||||
if utils.IsReadOnlyQuery(sql) {
|
||||
result, err = dbConn.ExecuteQuery(sql)
|
||||
@@ -283,7 +295,7 @@ func (s *ConnectionService) ExecuteQuery(ctx context.Context, connectionID, sql
|
||||
Duration: duration.Milliseconds(),
|
||||
Success: err == nil,
|
||||
}
|
||||
|
||||
|
||||
if result != nil {
|
||||
history.RowsAffected = result.AffectedRows
|
||||
}
|
||||
@@ -343,21 +355,35 @@ func (s *ConnectionService) GetTableData(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var query string
|
||||
switch conn.Type {
|
||||
case models.ConnectionTypeMySQL:
|
||||
query = fmt.Sprintf("SELECT * FROM `%s` LIMIT %d OFFSET %d", tableName, limit, offset)
|
||||
case models.ConnectionTypePostgreSQL:
|
||||
query = fmt.Sprintf(`SELECT * FROM "%s" LIMIT %d OFFSET %d`, tableName, limit, offset)
|
||||
case models.ConnectionTypeSQLite:
|
||||
query = fmt.Sprintf(`SELECT * FROM "%s" LIMIT %d OFFSET %d`, tableName, limit, offset)
|
||||
default:
|
||||
return nil, models.ErrValidationFailed
|
||||
}
|
||||
// Build a properly-quoted table reference that handles 'schema.table' notation
|
||||
tableRef := buildTableRef(conn.Type, tableName)
|
||||
|
||||
query := fmt.Sprintf("SELECT * FROM %s LIMIT %d OFFSET %d", tableRef, limit, offset)
|
||||
|
||||
return s.ExecuteQuery(ctx, connectionID, query)
|
||||
}
|
||||
|
||||
// buildTableRef returns a properly-quoted table reference.
|
||||
// tableName may be plain 'table' or schema-qualified 'schema.table'.
|
||||
func buildTableRef(dbType models.ConnectionType, tableName string) string {
|
||||
if strings.Contains(tableName, ".") {
|
||||
parts := strings.SplitN(tableName, ".", 2)
|
||||
schema, table := parts[0], parts[1]
|
||||
switch dbType {
|
||||
case models.ConnectionTypeMySQL:
|
||||
return fmt.Sprintf("`%s`.`%s`", schema, table)
|
||||
default:
|
||||
return fmt.Sprintf(`"%s"."%s"`, schema, table)
|
||||
}
|
||||
}
|
||||
switch dbType {
|
||||
case models.ConnectionTypeMySQL:
|
||||
return fmt.Sprintf("`%s`", tableName)
|
||||
default:
|
||||
return fmt.Sprintf(`"%s"`, tableName)
|
||||
}
|
||||
}
|
||||
|
||||
// GetTableStructure returns the structure of a table
|
||||
func (s *ConnectionService) GetTableStructure(ctx context.Context, connectionID, tableName string) (*models.TableStructure, error) {
|
||||
// Get connection config
|
||||
|
||||
Reference in New Issue
Block a user