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) } }