package infra import ( "database/sql" "fmt" "net/http" "strings" "time" ) // CRUDUpdateHandler retorna un http.HandlerFunc que hace partial update por id. // Solo actualiza los campos presentes en el body JSON. Valida los campos enviados // contra las reglas del recurso. 404 si no existe (o soft-deleted), 400 si falla validacion. func CRUDUpdateHandler(res CRUDResource, db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if id == "" { HTTPErrorResponse(w, HTTPError{Status: http.StatusBadRequest, Code: "missing_id", Message: "id path parameter is required"}) return } body := map[string]any{} if err := HTTPParseBody(r, &body, 1<<20); err != nil { HTTPErrorResponse(w, HTTPError{Status: http.StatusBadRequest, Code: "invalid_body", Message: err.Error()}) return } // Validar que existe existsSQL := fmt.Sprintf("SELECT 1 FROM %s WHERE id = ?", res.Table) if res.SoftDelete { existsSQL += " AND deleted_at IS NULL" } var dummy int if err := db.QueryRow(existsSQL, id).Scan(&dummy); err != nil { if err == sql.ErrNoRows { HTTPErrorResponse(w, HTTPError{Status: http.StatusNotFound, Code: "not_found", Message: fmt.Sprintf("%s %q not found", res.Name, id)}) return } HTTPErrorResponse(w, HTTPError{Status: http.StatusInternalServerError, Code: "db_error", Message: err.Error()}) return } // Validar los campos presentes setCols := []string{} args := []any{} for _, f := range res.Fields { val, present := body[f.Name] if !present { continue } if err := crudValidateField(f, val); err != nil { HTTPErrorResponse(w, HTTPError{Status: http.StatusBadRequest, Code: "validation_error", Message: err.Error()}) return } setCols = append(setCols, fmt.Sprintf("%s = ?", f.Name)) args = append(args, val) } now := time.Now().UTC().Format(time.RFC3339Nano) setCols = append(setCols, "updated_at = ?") args = append(args, now) args = append(args, id) updateSQL := fmt.Sprintf("UPDATE %s SET %s WHERE id = ?", res.Table, strings.Join(setCols, ", ")) if _, err := db.Exec(updateSQL, args...); err != nil { if strings.Contains(strings.ToLower(err.Error()), "unique") { HTTPErrorResponse(w, HTTPError{Status: http.StatusConflict, Code: "unique_violation", Message: err.Error()}) return } HTTPErrorResponse(w, HTTPError{Status: http.StatusInternalServerError, Code: "db_error", Message: err.Error()}) return } // Leer de vuelta rows, err := db.Query(fmt.Sprintf("SELECT * FROM %s WHERE id = ?", res.Table), id) if err != nil { HTTPErrorResponse(w, HTTPError{Status: http.StatusInternalServerError, Code: "db_error", Message: err.Error()}) return } defer rows.Close() cols, err := rows.Columns() if err != nil { HTTPErrorResponse(w, HTTPError{Status: http.StatusInternalServerError, Code: "db_error", Message: err.Error()}) return } if !rows.Next() { HTTPErrorResponse(w, HTTPError{Status: http.StatusNotFound, Code: "not_found", Message: fmt.Sprintf("%s %q not found after update", res.Name, id)}) return } row, err := crudScanRow(rows, cols) if err != nil { HTTPErrorResponse(w, HTTPError{Status: http.StatusInternalServerError, Code: "db_error", Message: err.Error()}) return } HTTPJSONResponse(w, http.StatusOK, row) } }