package controllers import ( "bytes" "encoding/csv" "encoding/json" "fmt" "math" "strconv" "strings" "time" "github.com/gofiber/fiber/v2" "github.com/sujit-baniya/fiber-boilerplate/app" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" "github.com/sujit-baniya/fiber-boilerplate/pkg/services" ) // QueryRunnerPage renderiza la vista del editor SQL. // GET /app/query-runner?conx_db_id=1 func QueryRunnerPage(c *fiber.Ctx) error { conxIDStr := c.Query("conx_db_id", "") var conx models.ConxDb if conxIDStr != "" { id, _ := strconv.ParseUint(conxIDStr, 10, 32) app.Http.Database.DB.Preload("TipoDb").Preload("Servidor").First(&conx, id) } data := fiber.Map{ "user": c.Locals("user").(map[string]interface{}), "modules": c.Locals("userModules"), "conx_db_id": conxIDStr, "conx": conx, } if err := c.Render("query_runner", data, "layouts/main"); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": err.Error(), }) } return nil } // GetConxDbList devuelve todas las conexiones DB disponibles (para el selector). func GetConxDbList(c *fiber.Ctx) error { var items []models.ConxDb app.Http.Database.DB.Preload("TipoDb").Preload("Servidor").Find(&items) return c.JSON(fiber.Map{"data": items}) } // GetDatabases lista las bases de datos de una conexión. // GET /app/query-runner/databases?conx_db_id=1 func GetDatabases(c *fiber.Ctx) error { conx, err := loadConxDb(c.Query("conx_db_id", "0")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } dbs, err := services.ListDatabases(conx) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"data": dbs}) } // GetTables lista las tablas de una base de datos. // GET /app/query-runner/tables?conx_db_id=1&db=mydb func GetTables(c *fiber.Ctx) error { conx, err := loadConxDb(c.Query("conx_db_id", "0")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } dbName := c.Query("db", "") tables, err := services.ListTables(conx, dbName) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"data": tables}) } // RunQuery ejecuta una consulta SQL. // POST /app/query-runner/run // Body: { conx_db_id, database, sql } func RunQuery(c *fiber.Ctx) error { var body struct { ConxDbID uint `json:"conx_db_id"` Database string `json:"database"` SQL string `json:"sql"` } if err := c.BodyParser(&body); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cuerpo inválido"}) } if strings.TrimSpace(body.SQL) == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "La consulta está vacía"}) } conx, err := loadConxDb(strconv.Itoa(int(body.ConxDbID))) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } result := services.ExecuteSQL(conx, body.Database, body.SQL) return c.JSON(result) } // TestConnection verifica que la conexión funciona. // GET /app/query-runner/test?conx_db_id=1 func TestConnection(c *fiber.Ctx) error { conx, err := loadConxDb(c.Query("conx_db_id", "0")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } if err := services.TestDBConnection(conx); err != nil { return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"ok": false, "error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } // GetHistory devuelve el historial de una conexión. // GET /app/query-runner/history?conx_db_id=1&page=1 func GetHistory(c *fiber.Ctx) error { conxIDStr := c.Query("conx_db_id", "0") conxID, _ := strconv.ParseUint(conxIDStr, 10, 32) page, _ := strconv.Atoi(c.Query("page", "1")) if page < 1 { page = 1 } limit := 50 offset := (page - 1) * limit items, total, err := models.GetQueryHistory(uint(conxID), limit, offset) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } totalPages := int(math.Ceil(float64(total) / float64(limit))) return c.JSON(fiber.Map{ "data": items, "total": total, "totalPages": totalPages, "page": page, }) } // ClearHistory borra el historial de una conexión. // DELETE /app/query-runner/history?conx_db_id=1 func ClearHistory(c *fiber.Ctx) error { conxIDStr := c.Query("conx_db_id", "0") conxID, _ := strconv.ParseUint(conxIDStr, 10, 32) if err := models.DeleteQueryHistory(uint(conxID)); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } // ExportCSV exporta el resultado de una consulta como CSV. // POST /app/query-runner/export/csv func ExportCSV(c *fiber.Ctx) error { var body struct { ConxDbID uint `json:"conx_db_id"` Database string `json:"database"` SQL string `json:"sql"` } if err := c.BodyParser(&body); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cuerpo inválido"}) } conx, err := loadConxDb(strconv.Itoa(int(body.ConxDbID))) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } result := services.ExecuteSQL(conx, body.Database, body.SQL) if result.Error != "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": result.Error}) } var buf bytes.Buffer w := csv.NewWriter(&buf) w.Write(result.Columns) for _, row := range result.Rows { rec := make([]string, len(result.Columns)) for i, col := range result.Columns { v := row[col] if v == nil { rec[i] = "" } else { rec[i] = fmt.Sprintf("%v", v) } } w.Write(rec) } w.Flush() filename := fmt.Sprintf("query_%s.csv", time.Now().Format("20060102_150405")) c.Set("Content-Disposition", "attachment; filename="+filename) c.Set("Content-Type", "text/csv; charset=utf-8") return c.SendStream(bytes.NewReader(buf.Bytes()), buf.Len()) } // ExportJSON exporta el resultado como JSON. // POST /app/query-runner/export/json func ExportJSON(c *fiber.Ctx) error { var body struct { ConxDbID uint `json:"conx_db_id"` Database string `json:"database"` SQL string `json:"sql"` } if err := c.BodyParser(&body); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cuerpo inválido"}) } conx, err := loadConxDb(strconv.Itoa(int(body.ConxDbID))) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } result := services.ExecuteSQL(conx, body.Database, body.SQL) if result.Error != "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": result.Error}) } data, _ := json.MarshalIndent(result.Rows, "", " ") filename := fmt.Sprintf("query_%s.json", time.Now().Format("20060102_150405")) c.Set("Content-Disposition", "attachment; filename="+filename) c.Set("Content-Type", "application/json; charset=utf-8") return c.SendStream(bytes.NewReader(data), len(data)) } // ── helper ─────────────────────────────────────────────────────────────────── func loadConxDb(idStr string) (models.ConxDb, error) { id, err := strconv.ParseUint(idStr, 10, 32) if err != nil || id == 0 { return models.ConxDb{}, fmt.Errorf("conx_db_id inválido") } var conx models.ConxDb if err := app.Http.Database.DB.Preload("TipoDb").Preload("Servidor").First(&conx, id).Error; err != nil { return models.ConxDb{}, fmt.Errorf("conexión no encontrada: %w", err) } return conx, nil }