This commit is contained in:
Lizandro Guarnizo
2026-05-01 17:01:42 -05:00
parent f8e4ee5b05
commit 5583314d0a
3 changed files with 408 additions and 1 deletions
+386 -1
View File
@@ -3,7 +3,9 @@ package services
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"regexp"
"strings"
"time"
@@ -11,6 +13,9 @@ import (
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
_ "github.com/microsoft/go-mssqldb"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
@@ -76,6 +81,9 @@ func openDynamicDB(c models.ConxDb) (*sql.DB, error) {
// ExecuteSQL ejecuta SQL arbitrario contra la conexión y devuelve QueryResult.
// También guarda en query_history.
func ExecuteSQL(conx models.ConxDb, database, sqlText string) QueryResult {
if isMongoDriver(strings.ToLower(conx.TipoDb.Nombre)) {
return mongoExecuteSQL(conx, database, sqlText)
}
start := time.Now()
db, err := openDynamicDB(conx)
@@ -161,13 +169,16 @@ func ExecuteSQL(conx models.ConxDb, database, sqlText string) QueryResult {
// ListDatabases devuelve la lista de bases de datos del servidor.
func ListDatabases(conx models.ConxDb) ([]string, error) {
driver := strings.ToLower(conx.TipoDb.Nombre)
if isMongoDriver(driver) {
return mongoListDatabases(conx)
}
db, err := openDynamicDB(conx)
if err != nil {
return nil, err
}
defer db.Close()
driver := strings.ToLower(conx.TipoDb.Nombre)
var query string
switch {
case strings.Contains(driver, "postgres"):
@@ -200,6 +211,9 @@ func ListDatabases(conx models.ConxDb) ([]string, error) {
// ListTables devuelve las tablas de una base de datos.
func ListTables(conx models.ConxDb, database string) ([]string, error) {
driver := strings.ToLower(conx.TipoDb.Nombre)
if isMongoDriver(driver) {
return mongoListCollections(conx, database)
}
var db *sql.DB
var err error
@@ -246,6 +260,9 @@ func ListTables(conx models.ConxDb, database string) ([]string, error) {
// TestConnection verifica si la conexión es válida.
func TestDBConnection(conx models.ConxDb) error {
if isMongoDriver(strings.ToLower(conx.TipoDb.Nombre)) {
return mongoTestConnection(conx)
}
db, err := openDynamicDB(conx)
if err != nil {
return err
@@ -315,3 +332,371 @@ func saveHistory(conxID uint, sqlText, status, errMsg string, rows, durationMs i
ExecutedAt: time.Now(),
})
}
// ── MongoDB ───────────────────────────────────────────────────────────────────
func isMongoDriver(driver string) bool {
return strings.Contains(driver, "mongo")
}
func mongoURI(c models.ConxDb) string {
host := c.Servidor.IpServidor
port := c.Puerto
user := c.Usuario
pass := c.Password
if user != "" && pass != "" {
return fmt.Sprintf("mongodb://%s:%s@%s:%s/?directConnection=true&serverSelectionTimeoutMS=8000", user, pass, host, port)
}
return fmt.Sprintf("mongodb://%s:%s/?directConnection=true&serverSelectionTimeoutMS=8000", host, port)
}
func mongoConnect(c models.ConxDb) (*mongo.Client, error) {
ctx, cancel := context.WithTimeout(context.Background(), connTimeout)
defer cancel()
client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI(c)))
if err != nil {
return nil, err
}
if err := client.Ping(ctx, nil); err != nil {
client.Disconnect(context.Background()) //nolint
return nil, fmt.Errorf("no se pudo conectar a MongoDB: %w", err)
}
return client, nil
}
func mongoTestConnection(c models.ConxDb) error {
client, err := mongoConnect(c)
if err != nil {
return err
}
client.Disconnect(context.Background()) //nolint
return nil
}
func mongoListDatabases(c models.ConxDb) ([]string, error) {
client, err := mongoConnect(c)
if err != nil {
return nil, err
}
defer client.Disconnect(context.Background()) //nolint
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
return client.ListDatabaseNames(ctx, bson.M{})
}
func mongoListCollections(c models.ConxDb, database string) ([]string, error) {
client, err := mongoConnect(c)
if err != nil {
return nil, err
}
defer client.Disconnect(context.Background()) //nolint
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
return client.Database(database).ListCollectionNames(ctx, bson.M{})
}
var mongoQueryRe = regexp.MustCompile(`(?s)^db\.(\w[\w\d_]*)\.([\w]+)\((.*)\)\s*$`)
func mongoExecuteSQL(conx models.ConxDb, database, queryText string) QueryResult {
start := time.Now()
client, err := mongoConnect(conx)
if err != nil {
saveHistory(conx.ID, queryText, "error", err.Error(), 0, time.Since(start).Milliseconds())
return QueryResult{Error: err.Error()}
}
defer client.Disconnect(context.Background()) //nolint
db := client.Database(database)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
result, err := mongoRunQuery(ctx, db, strings.TrimSpace(queryText))
elapsed := time.Since(start).Milliseconds()
if err != nil {
saveHistory(conx.ID, queryText, "error", err.Error(), 0, elapsed)
return QueryResult{Error: err.Error()}
}
result.DurationMs = elapsed
saveHistory(conx.ID, queryText, "ok", "", int64(result.RowCount)+result.AffectedRows, elapsed)
return *result
}
func mongoRunQuery(ctx context.Context, db *mongo.Database, query string) (*QueryResult, error) {
m := mongoQueryRe.FindStringSubmatch(query)
if m == nil {
return nil, fmt.Errorf("sintaxis no reconocida. Use: db.coleccion.metodo({...})")
}
collName, method, argsRaw := m[1], strings.ToLower(m[2]), strings.TrimSpace(m[3])
if collName == "runCommand" {
return mongoRunCommand(ctx, db, argsRaw)
}
coll := db.Collection(collName)
args := splitTopLevelArgs(argsRaw)
switch method {
case "find":
return mongoFind(ctx, coll, args, false)
case "findone":
return mongoFind(ctx, coll, args, true)
case "insertone":
return mongoInsertOne(ctx, coll, args)
case "insertmany":
return mongoInsertMany(ctx, coll, args)
case "updateone":
return mongoUpdate(ctx, coll, args, false)
case "updatemany":
return mongoUpdate(ctx, coll, args, true)
case "deleteone":
return mongoDelete(ctx, coll, args, false)
case "deletemany":
return mongoDelete(ctx, coll, args, true)
case "countdocuments":
return mongoCount(ctx, coll, args)
case "aggregate":
return mongoAggregate(ctx, coll, argsRaw)
case "drop":
if err := coll.Drop(ctx); err != nil {
return nil, err
}
return &QueryResult{IsSelect: false}, nil
default:
return nil, fmt.Errorf("método MongoDB no soportado: %s", m[2])
}
}
func mongoFind(ctx context.Context, coll *mongo.Collection, args []string, one bool) (*QueryResult, error) {
var filter bson.M
if len(args) > 0 && args[0] != "" {
if err := bson.UnmarshalExtJSON([]byte(args[0]), true, &filter); err != nil {
return nil, fmt.Errorf("filtro inválido: %w", err)
}
} else {
filter = bson.M{}
}
var docs []bson.M
if one {
var doc bson.M
if err := coll.FindOne(ctx, filter).Decode(&doc); err != nil {
if err == mongo.ErrNoDocuments {
return &QueryResult{IsSelect: true, Columns: []string{}, Rows: []map[string]any{}}, nil
}
return nil, err
}
docs = []bson.M{doc}
} else {
cursor, err := coll.Find(ctx, filter)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
if err := cursor.All(ctx, &docs); err != nil {
return nil, err
}
}
return bsonDocsToResult(docs), nil
}
func mongoInsertOne(ctx context.Context, coll *mongo.Collection, args []string) (*QueryResult, error) {
if len(args) == 0 || args[0] == "" {
return nil, fmt.Errorf("insertOne requiere un documento")
}
var doc bson.M
if err := bson.UnmarshalExtJSON([]byte(args[0]), true, &doc); err != nil {
return nil, fmt.Errorf("documento inválido: %w", err)
}
res, err := coll.InsertOne(ctx, doc)
if err != nil {
return nil, err
}
return &QueryResult{
IsSelect: true, AffectedRows: 1,
Columns: []string{"insertedId"},
Rows: []map[string]any{{"insertedId": fmt.Sprintf("%v", res.InsertedID)}},
RowCount: 1,
}, nil
}
func mongoInsertMany(ctx context.Context, coll *mongo.Collection, args []string) (*QueryResult, error) {
if len(args) == 0 || args[0] == "" {
return nil, fmt.Errorf("insertMany requiere un array de documentos")
}
var arr bson.A
if err := bson.UnmarshalExtJSON([]byte(args[0]), true, &arr); err != nil {
return nil, fmt.Errorf("documentos inválidos: %w", err)
}
docs := make([]interface{}, len(arr))
copy(docs, arr)
res, err := coll.InsertMany(ctx, docs)
if err != nil {
return nil, err
}
return &QueryResult{IsSelect: false, AffectedRows: int64(len(res.InsertedIDs))}, nil
}
func mongoUpdate(ctx context.Context, coll *mongo.Collection, args []string, many bool) (*QueryResult, error) {
if len(args) < 2 {
return nil, fmt.Errorf("update requiere filtro y documento de actualización")
}
var filter, update bson.M
if err := bson.UnmarshalExtJSON([]byte(args[0]), true, &filter); err != nil {
return nil, fmt.Errorf("filtro inválido: %w", err)
}
if err := bson.UnmarshalExtJSON([]byte(args[1]), true, &update); err != nil {
return nil, fmt.Errorf("update inválido: %w", err)
}
var affected int64
if many {
res, err := coll.UpdateMany(ctx, filter, update)
if err != nil {
return nil, err
}
affected = res.ModifiedCount
} else {
res, err := coll.UpdateOne(ctx, filter, update)
if err != nil {
return nil, err
}
affected = res.ModifiedCount
}
return &QueryResult{IsSelect: false, AffectedRows: affected}, nil
}
func mongoDelete(ctx context.Context, coll *mongo.Collection, args []string, many bool) (*QueryResult, error) {
var filter bson.M
if len(args) > 0 && args[0] != "" {
if err := bson.UnmarshalExtJSON([]byte(args[0]), true, &filter); err != nil {
return nil, fmt.Errorf("filtro inválido: %w", err)
}
} else {
filter = bson.M{}
}
var deleted int64
if many {
res, err := coll.DeleteMany(ctx, filter)
if err != nil {
return nil, err
}
deleted = res.DeletedCount
} else {
res, err := coll.DeleteOne(ctx, filter)
if err != nil {
return nil, err
}
deleted = res.DeletedCount
}
return &QueryResult{IsSelect: false, AffectedRows: deleted}, nil
}
func mongoCount(ctx context.Context, coll *mongo.Collection, args []string) (*QueryResult, error) {
var filter bson.M
if len(args) > 0 && args[0] != "" {
if err := bson.UnmarshalExtJSON([]byte(args[0]), true, &filter); err != nil {
return nil, fmt.Errorf("filtro inválido: %w", err)
}
} else {
filter = bson.M{}
}
count, err := coll.CountDocuments(ctx, filter)
if err != nil {
return nil, err
}
return &QueryResult{
IsSelect: true,
Columns: []string{"count"},
Rows: []map[string]any{{"count": count}},
RowCount: 1,
}, nil
}
func mongoAggregate(ctx context.Context, coll *mongo.Collection, pipelineStr string) (*QueryResult, error) {
var pipeline bson.A
if err := bson.UnmarshalExtJSON([]byte(pipelineStr), true, &pipeline); err != nil {
return nil, fmt.Errorf("pipeline inválido: %w", err)
}
cursor, err := coll.Aggregate(ctx, pipeline)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var docs []bson.M
if err := cursor.All(ctx, &docs); err != nil {
return nil, err
}
return bsonDocsToResult(docs), nil
}
func mongoRunCommand(ctx context.Context, db *mongo.Database, cmdStr string) (*QueryResult, error) {
var cmd bson.D
if err := bson.UnmarshalExtJSON([]byte(cmdStr), true, &cmd); err != nil {
return nil, fmt.Errorf("comando inválido: %w", err)
}
var result bson.M
if err := db.RunCommand(ctx, cmd).Decode(&result); err != nil {
return nil, err
}
return bsonDocsToResult([]bson.M{result}), nil
}
func bsonDocsToResult(docs []bson.M) *QueryResult {
if len(docs) == 0 {
return &QueryResult{IsSelect: true, Columns: []string{}, Rows: []map[string]any{}}
}
keyIdx := make(map[string]int)
for _, doc := range docs {
for k := range doc {
if _, seen := keyIdx[k]; !seen {
keyIdx[k] = len(keyIdx)
}
}
}
cols := make([]string, len(keyIdx))
for k, i := range keyIdx {
cols[i] = k
}
rows := make([]map[string]any, len(docs))
for i, doc := range docs {
row := make(map[string]any, len(doc))
for k, v := range doc {
switch v.(type) {
case bson.M, bson.A, bson.D:
b, _ := json.Marshal(v)
row[k] = string(b)
default:
row[k] = fmt.Sprintf("%v", v)
}
}
rows[i] = row
}
return &QueryResult{IsSelect: true, Columns: cols, Rows: rows, RowCount: len(rows)}
}
func splitTopLevelArgs(s string) []string {
var args []string
depth := 0
inStr := false
start := 0
s = strings.TrimSpace(s)
for i := 0; i < len(s); i++ {
ch := s[i]
switch ch {
case '"':
if i == 0 || s[i-1] != '\\' {
inStr = !inStr
}
case '{', '[', '(':
if !inStr {
depth++
}
case '}', ']', ')':
if !inStr {
depth--
}
case ',':
if !inStr && depth == 0 {
args = append(args, strings.TrimSpace(s[start:i]))
start = i + 1
}
}
}
if last := strings.TrimSpace(s[start:]); last != "" {
args = append(args, last)
}
return args
}