2025-10-15 10:43:58 -04:00

380 lines
9.3 KiB
Go

package db
import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
"github.com/tuusuario/go-sync-service/internal/config"
"github.com/tuusuario/go-sync-service/internal/domain/dto"
"github.com/tuusuario/go-sync-service/internal/domain/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type GormDatabase struct {
db *gorm.DB
}
func NewGormDatabase(db *gorm.DB) *GormDatabase {
return &GormDatabase{db: db}
}
func (g *GormDatabase) SyncRows(persistencia dto.Persistencia, rawData *[]map[string]interface{}, company_db string) error {
logPrefix := fmt.Sprintf("[🧹 Tabla: %s] ", persistencia.Table)
config.Log.Println(logPrefix + " ✅ Inicializacion Syncing data...")
now := time.Now()
batchSize := persistencia.BatchSize
if batchSize <= 0 {
batchSize = 100
}
batch := make([]map[string]interface{}, 0, batchSize)
for i, item := range *rawData {
record := map[string]interface{}{
persistencia.CampoSync: now,
}
// Copia de campos mapeados (con soporte a "a.b.c", "array[0].campo", "array[-1].campo", "array[].campo")
for column, jsonPath := range persistencia.Fields {
val := getNestedValue(item, jsonPath)
record[column] = val
}
// Campos estáticos
for k, v := range persistencia.StaticFields {
record[k] = v
}
// Forzar columnas a string si fue configurado
normalizeTypes(record, persistencia.StringifyFields)
// ========== NUEVO: construir PK compuesta si está configurada ==========
if persistencia.PrimaryKeyName != "" && len(persistencia.PrimaryKeyConcat) > 0 {
if key, ok := buildCompositeKey(record, company_db, persistencia.PrimaryKeyConcat, persistencia.PrimaryKeySeparator); ok {
record[persistencia.PrimaryKeyName] = key
}
}
batch = append(batch, record)
// Procesar lote
if len(batch) == batchSize || i == len(*rawData)-1 {
config.Log.Debugf(logPrefix+" Procesando batch de %d registros", len(batch))
if len(persistencia.UpdateBy) > 0 {
// Updates con múltiples campos
for _, row := range batch {
var whereParts []string
var whereValues []interface{}
for campoTabla, campoServicio := range persistencia.UpdateBy {
val, ok := row[campoServicio]
if !ok || val == nil {
continue
}
whereParts = append(whereParts, fmt.Sprintf("%s = ?", campoTabla))
whereValues = append(whereValues, val)
}
if len(whereParts) < len(persistencia.UpdateBy) {
config.Log.Warnf("⚠️ Registro incompleto para update (faltan claves): %+v", row)
continue
}
// Copia sin campos clave
updateData := make(map[string]interface{})
for k, v := range row {
skip := false
for campoTabla := range persistencia.UpdateBy {
if k == campoTabla {
skip = true
break
}
}
if !skip {
updateData[k] = v
}
}
res := g.db.Table(persistencia.Table).
Where(strings.Join(whereParts, " AND "), whereValues...).
Updates(updateData)
if res.Error != nil {
config.Log.Errorf("%s ❌ Error en update: %v", logPrefix, res.Error)
return res.Error
}
if res.RowsAffected == 0 {
config.Log.Warnf("%s ⚠️ Ninguna fila afectada con campos: %v valores: %v",
logPrefix, strings.Join(whereParts, " AND "), printWhereValues(whereValues))
}
}
} else {
// Inserts con conflicto por PK (UPSERT)
cols := unionColumnList(batch) // union de columnas del lote
// Elegir PK: si hay compuesta (calculada), úsala; si no, usa PrimaryKey
pkName := persistencia.PrimaryKey
if persistencia.PrimaryKeyName != "" && len(persistencia.PrimaryKeyConcat) > 0 {
pkName = persistencia.PrimaryKeyName
}
err := g.db.Table(persistencia.Table).
Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: pkName}},
DoUpdates: clause.AssignmentColumns(cols),
}).
Create(&batch).Error
if err != nil {
config.Log.Errorf("%s ❌ Error en batch insert: %v", logPrefix, err)
return err
}
}
batch = batch[:0]
}
}
if persistencia.Eliminacion.Enabled {
err := g.db.Table(persistencia.Table).
Where(persistencia.Eliminacion.Field+" < ?", now).
Delete(nil).Error
if err != nil {
config.Log.Printf(logPrefix+"❌ Error eliminando obsoletos: %+v", err)
return err
}
}
config.Log.Println(logPrefix + " ✅ Finalizacion Syncing data...")
return nil
}
func unionColumnList(batch []map[string]interface{}) []string {
set := map[string]struct{}{}
for _, r := range batch {
for k := range r {
set[k] = struct{}{}
}
}
cols := make([]string, 0, len(set))
for k := range set {
cols = append(cols, k)
}
return cols
}
func (g *GormDatabase) GetCredencialesFromTemplate(whereTemplate string, variables map[string]interface{}) (*model.CredencialesSAP, error) {
var cred model.CredencialesSAP
query := whereTemplate
var args []interface{}
config.Log.Debugf("🔎 Variables recibidas:")
for k, v := range variables {
placeholder := "@" + k
query = strings.ReplaceAll(query, placeholder, "?")
args = append(args, v)
config.Log.Debugf(" %s = %v", k, v)
}
config.Log.Debugf("📝 Consulta final construida:")
config.Log.Debugf(" Query: %s", query)
config.Log.Debugf(" Args: %v", args)
err := g.db.Where(query, args...).First(&cred).Error
return &cred, err
}
func printWhereValues(whereValues []interface{}) string {
if len(whereValues) == 0 {
return ""
}
var b strings.Builder
b.WriteString(fmt.Sprintf("%v", whereValues[0]))
for i := 1; i < len(whereValues); i++ {
b.WriteString(", ")
b.WriteString(fmt.Sprintf("%v", whereValues[i]))
}
return b.String()
}
// ===== Soporte de índices [n], [-1] y [] en rutas =====
var idxRe = regexp.MustCompile(`^([^\[\]]+)(?:\[(\-?\d*)\])?$`)
// toIfaceSlice intenta ver el valor como slice de interfaces
func toIfaceSlice(v interface{}) ([]interface{}, bool) {
switch s := v.(type) {
case []interface{}:
return s, true
case []map[string]interface{}:
out := make([]interface{}, len(s))
for i := range s {
out[i] = s[i]
}
return out, true
default:
return nil, false
}
}
// getNestedValue con soporte para "contacts[0].field", "contacts[-1].field", "contacts[].field"
func getNestedValue(data map[string]interface{}, path string) interface{} {
if path == "" {
return nil
}
parts := strings.Split(path, ".")
var current interface{} = data
for _, part := range parts {
// parsea nombre + índice opcional
m := idxRe.FindStringSubmatch(part)
if len(m) == 0 {
return nil
}
key := m[1]
idxStr := m[2] // "", "0", "1", "-1", ...
// paso de mapa (key)
mp, ok := current.(map[string]interface{})
if !ok {
return nil
}
var exists bool
current, exists = mp[key]
if !exists {
return nil
}
// si hay índice (o "[]"), tratar como slice
if idxStr != "" || strings.HasSuffix(part, "[]") {
arr, ok := toIfaceSlice(current)
if !ok || len(arr) == 0 {
return nil
}
var idx int
if idxStr == "" {
// "[]" -> primero
idx = 0
} else {
n, err := strconv.Atoi(idxStr)
if err != nil {
return nil
}
if n < 0 {
n = len(arr) + n // -1 => último
}
idx = n
}
if idx < 0 || idx >= len(arr) {
return nil
}
current = arr[idx]
}
}
return current
}
// ===== NUEVO: helper para clave compuesta =====
func buildCompositeKey(row map[string]interface{}, companyDB string, concat []string, sep string) (string, bool) {
if len(concat) == 0 {
return "", false
}
if sep == "" {
sep = ":"
}
parts := make([]string, 0, len(concat))
for _, item := range concat {
if strings.HasPrefix(item, "@") { // tokens especiales
token := strings.TrimPrefix(item, "@")
switch {
case token == "company_db":
parts = append(parts, companyDB)
case strings.HasPrefix(token, "literal="):
parts = append(parts, strings.TrimPrefix(token, "literal="))
default:
// Token desconocido -> vacío (o puedes return false para forzar)
parts = append(parts, "")
}
continue
}
// tomar del row (columna ya mapeada en record)
parts = append(parts, toString(row[item]))
}
return strings.Join(parts, sep), true
}
// ===== Normalización de tipos =====
func normalizeTypes(m map[string]interface{}, stringifyFields []string) {
if len(stringifyFields) == 0 {
return
}
sf := make(map[string]struct{}, len(stringifyFields))
for _, f := range stringifyFields {
sf[f] = struct{}{}
}
for k, v := range m {
if v == nil {
continue
}
if _, ok := sf[k]; ok {
m[k] = toString(v)
}
}
}
// ===== Casteo genérico a string =====
func toString(v interface{}) string {
switch t := v.(type) {
case string:
return t
case fmt.Stringer:
return t.String()
case float64:
if t == float64(int64(t)) {
return fmt.Sprintf("%d", int64(t))
}
return fmt.Sprintf("%v", t)
case float32:
if t == float32(int64(t)) {
return fmt.Sprintf("%d", int64(t))
}
return fmt.Sprintf("%v", t)
case int:
return fmt.Sprintf("%d", t)
case int8:
return fmt.Sprintf("%d", t)
case int16:
return fmt.Sprintf("%d", t)
case int32:
return fmt.Sprintf("%d", t)
case int64:
return fmt.Sprintf("%d", t)
case uint:
return fmt.Sprintf("%d", t)
case uint8:
return fmt.Sprintf("%d", t)
case uint16:
return fmt.Sprintf("%d", t)
case uint32:
return fmt.Sprintf("%d", t)
case uint64:
return fmt.Sprintf("%d", t)
case bool:
if t {
return "true"
}
return "false"
case time.Time:
return t.Format(time.RFC3339)
default:
return fmt.Sprintf("%v", t)
}
}