380 lines
9.3 KiB
Go
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)
|
|
}
|
|
}
|