mirror of
https://github.com/DeBrosOfficial/network.git
synced 2025-12-15 05:08:49 +00:00
450 lines
13 KiB
Go
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
|
|
}
|