network/pkg/gateway/database_handlers.go
2025-10-13 07:41:46 +03:00

450 lines
13 KiB
Go

package gateway
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/DeBrosOfficial/network/pkg/logging"
"go.uber.org/zap"
)
// Database request/response types
type ExecRequest struct {
Database string `json:"database"`
SQL string `json:"sql"`
Args []interface{} `json:"args,omitempty"`
}
type ExecResponse struct {
RowsAffected int64 `json:"rows_affected"`
LastInsertID int64 `json:"last_insert_id,omitempty"`
Error string `json:"error,omitempty"`
}
type QueryRequest struct {
Database string `json:"database"`
SQL string `json:"sql"`
Args []interface{} `json:"args,omitempty"`
}
type QueryResponse struct {
Items []map[string]interface{} `json:"items"`
Count int `json:"count"`
Error string `json:"error,omitempty"`
}
type TransactionRequest struct {
Database string `json:"database"`
Queries []string `json:"queries"`
}
type TransactionResponse struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
type CreateTableRequest struct {
Database string `json:"database"`
Schema string `json:"schema"`
}
type DropTableRequest struct {
Database string `json:"database"`
TableName string `json:"table_name"`
}
type SchemaResponse struct {
Tables []TableSchema `json:"tables"`
Error string `json:"error,omitempty"`
}
type TableSchema struct {
Name string `json:"name"`
CreateSQL string `json:"create_sql"`
Columns []string `json:"columns,omitempty"`
}
// Database handlers
// databaseExecHandler handles SQL execution (INSERT, UPDATE, DELETE, DDL)
func (g *Gateway) databaseExecHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req ExecRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
g.respondJSON(w, http.StatusBadRequest, ExecResponse{Error: "Invalid request body"})
return
}
if req.Database == "" {
g.respondJSON(w, http.StatusBadRequest, ExecResponse{Error: "database field is required"})
return
}
if req.SQL == "" {
g.respondJSON(w, http.StatusBadRequest, ExecResponse{Error: "sql field is required"})
return
}
// Get database client
db, err := g.client.Database().Database(req.Database)
if err != nil {
g.logger.ComponentError(logging.ComponentDatabase, "Failed to get database client", zap.Error(err))
g.respondJSON(w, http.StatusInternalServerError, ExecResponse{Error: fmt.Sprintf("Failed to access database: %v", err)})
return
}
// Execute with timeout
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
// For simplicity, we'll use Query and check if it's a write operation
// In production, you'd want to detect write vs read and route accordingly
result, err := db.Query(ctx, req.SQL, req.Args...)
if err != nil {
g.logger.ComponentError(logging.ComponentDatabase, "Query execution failed",
zap.String("database", req.Database),
zap.Error(err))
g.respondJSON(w, http.StatusInternalServerError, ExecResponse{Error: err.Error()})
return
}
// For exec operations, return affected rows
g.respondJSON(w, http.StatusOK, ExecResponse{
RowsAffected: result.Count,
})
}
// databaseQueryHandler handles SELECT queries
func (g *Gateway) databaseQueryHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req QueryRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
g.respondJSON(w, http.StatusBadRequest, QueryResponse{Error: "Invalid request body"})
return
}
if req.Database == "" {
g.respondJSON(w, http.StatusBadRequest, QueryResponse{Error: "database field is required"})
return
}
if req.SQL == "" {
g.respondJSON(w, http.StatusBadRequest, QueryResponse{Error: "sql field is required"})
return
}
// Get database client
db, err := g.client.Database().Database(req.Database)
if err != nil {
g.logger.ComponentError(logging.ComponentDatabase, "Failed to get database client", zap.Error(err))
g.respondJSON(w, http.StatusInternalServerError, QueryResponse{Error: fmt.Sprintf("Failed to access database: %v", err)})
return
}
// Execute with timeout
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
result, err := db.Query(ctx, req.SQL, req.Args...)
if err != nil {
g.logger.ComponentError(logging.ComponentDatabase, "Query execution failed",
zap.String("database", req.Database),
zap.Error(err))
g.respondJSON(w, http.StatusInternalServerError, QueryResponse{Error: err.Error()})
return
}
// Convert result to map format
items := make([]map[string]interface{}, len(result.Rows))
for i, row := range result.Rows {
item := make(map[string]interface{})
for j, col := range result.Columns {
if j < len(row) {
item[col] = row[j]
}
}
items[i] = item
}
g.respondJSON(w, http.StatusOK, QueryResponse{
Items: items,
Count: len(items),
})
}
// databaseTransactionHandler handles atomic transactions
func (g *Gateway) databaseTransactionHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req TransactionRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
g.respondJSON(w, http.StatusBadRequest, TransactionResponse{Success: false, Error: "Invalid request body"})
return
}
if req.Database == "" {
g.respondJSON(w, http.StatusBadRequest, TransactionResponse{Success: false, Error: "database field is required"})
return
}
if len(req.Queries) == 0 {
g.respondJSON(w, http.StatusBadRequest, TransactionResponse{Success: false, Error: "queries field is required and must not be empty"})
return
}
// Get database client
db, err := g.client.Database().Database(req.Database)
if err != nil {
g.logger.ComponentError(logging.ComponentDatabase, "Failed to get database client", zap.Error(err))
g.respondJSON(w, http.StatusInternalServerError, TransactionResponse{Success: false, Error: fmt.Sprintf("Failed to access database: %v", err)})
return
}
// Execute with timeout
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
defer cancel()
err = db.Transaction(ctx, req.Queries)
if err != nil {
g.logger.ComponentError(logging.ComponentDatabase, "Transaction failed",
zap.String("database", req.Database),
zap.Error(err))
g.respondJSON(w, http.StatusInternalServerError, TransactionResponse{Success: false, Error: err.Error()})
return
}
g.respondJSON(w, http.StatusOK, TransactionResponse{Success: true})
}
// databaseSchemaHandler returns database schema information
func (g *Gateway) databaseSchemaHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Support both GET with query param and POST with JSON body
var database string
if r.Method == http.MethodPost {
var req struct {
Database string `json:"database"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
g.respondJSON(w, http.StatusBadRequest, SchemaResponse{Error: "Invalid request body"})
return
}
database = req.Database
} else {
database = r.URL.Query().Get("database")
}
if database == "" {
g.respondJSON(w, http.StatusBadRequest, SchemaResponse{Error: "database parameter is required"})
return
}
// Get database client
db, err := g.client.Database().Database(database)
if err != nil {
g.logger.ComponentError(logging.ComponentDatabase, "Failed to get database client", zap.Error(err))
g.respondJSON(w, http.StatusInternalServerError, SchemaResponse{Error: fmt.Sprintf("Failed to access database: %v", err)})
return
}
// Execute with timeout
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
schemaInfo, err := db.GetSchema(ctx)
if err != nil {
g.logger.ComponentError(logging.ComponentDatabase, "Failed to get schema",
zap.String("database", database),
zap.Error(err))
g.respondJSON(w, http.StatusInternalServerError, SchemaResponse{Error: err.Error()})
return
}
// Convert to response format
tables := make([]TableSchema, len(schemaInfo.Tables))
for i, table := range schemaInfo.Tables {
columns := make([]string, len(table.Columns))
for j, col := range table.Columns {
columns[j] = col.Name
}
tables[i] = TableSchema{
Name: table.Name,
Columns: columns,
}
}
g.respondJSON(w, http.StatusOK, SchemaResponse{Tables: tables})
}
// databaseCreateTableHandler creates a new table
func (g *Gateway) databaseCreateTableHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req CreateTableRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
g.respondJSON(w, http.StatusBadRequest, ExecResponse{Error: "Invalid request body"})
return
}
if req.Database == "" {
g.respondJSON(w, http.StatusBadRequest, ExecResponse{Error: "database field is required"})
return
}
if req.Schema == "" {
g.respondJSON(w, http.StatusBadRequest, ExecResponse{Error: "schema field is required"})
return
}
// Get database client
db, err := g.client.Database().Database(req.Database)
if err != nil {
g.logger.ComponentError(logging.ComponentDatabase, "Failed to get database client", zap.Error(err))
g.respondJSON(w, http.StatusInternalServerError, ExecResponse{Error: fmt.Sprintf("Failed to access database: %v", err)})
return
}
// Execute with timeout
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
err = db.CreateTable(ctx, req.Schema)
if err != nil {
g.logger.ComponentError(logging.ComponentDatabase, "Failed to create table",
zap.String("database", req.Database),
zap.Error(err))
g.respondJSON(w, http.StatusInternalServerError, ExecResponse{Error: err.Error()})
return
}
g.respondJSON(w, http.StatusOK, ExecResponse{RowsAffected: 0})
}
// databaseDropTableHandler drops a table
func (g *Gateway) databaseDropTableHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req DropTableRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
g.respondJSON(w, http.StatusBadRequest, ExecResponse{Error: "Invalid request body"})
return
}
if req.Database == "" {
g.respondJSON(w, http.StatusBadRequest, ExecResponse{Error: "database field is required"})
return
}
if req.TableName == "" {
g.respondJSON(w, http.StatusBadRequest, ExecResponse{Error: "table_name field is required"})
return
}
// Validate table name (basic SQL injection prevention)
if !isValidIdentifier(req.TableName) {
g.respondJSON(w, http.StatusBadRequest, ExecResponse{Error: "invalid table name"})
return
}
// Get database client
db, err := g.client.Database().Database(req.Database)
if err != nil {
g.logger.ComponentError(logging.ComponentDatabase, "Failed to get database client", zap.Error(err))
g.respondJSON(w, http.StatusInternalServerError, ExecResponse{Error: fmt.Sprintf("Failed to access database: %v", err)})
return
}
// Execute with timeout
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
err = db.DropTable(ctx, req.TableName)
if err != nil {
g.logger.ComponentError(logging.ComponentDatabase, "Failed to drop table",
zap.String("database", req.Database),
zap.String("table", req.TableName),
zap.Error(err))
g.respondJSON(w, http.StatusInternalServerError, ExecResponse{Error: err.Error()})
return
}
g.respondJSON(w, http.StatusOK, ExecResponse{RowsAffected: 0})
}
// databaseListHandler lists all available databases for the current app
func (g *Gateway) databaseListHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// TODO: This would require the ClusterManager to expose a list of databases
// For now, return a placeholder
g.respondJSON(w, http.StatusOK, map[string]interface{}{
"databases": []string{},
"message": "Database listing not yet implemented - query metadata store directly",
})
}
// Helper functions
func (g *Gateway) respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(data); err != nil {
g.logger.ComponentError(logging.ComponentGeneral, "Failed to encode JSON response", zap.Error(err))
}
}
func isValidIdentifier(name string) bool {
if len(name) == 0 || len(name) > 128 {
return false
}
// Only allow alphanumeric, underscore, and hyphen
for _, r := range name {
if !(r >= 'a' && r <= 'z') && !(r >= 'A' && r <= 'Z') && !(r >= '0' && r <= '9') && r != '_' && r != '-' {
return false
}
}
// Don't start with number
firstRune := []rune(name)[0]
if firstRune >= '0' && firstRune <= '9' {
return false
}
// Avoid SQL keywords
upperName := strings.ToUpper(name)
sqlKeywords := []string{"SELECT", "INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER", "TABLE", "DATABASE", "INDEX"}
for _, keyword := range sqlKeywords {
if upperName == keyword {
return false
}
}
return true
}