diff --git a/internal/httpapi/routes_crud.go b/internal/httpapi/routes_crud.go index f818c5b..de38fd4 100644 --- a/internal/httpapi/routes_crud.go +++ b/internal/httpapi/routes_crud.go @@ -105,11 +105,39 @@ func (s *Server) handlePatchModule(w http.ResponseWriter, r *http.Request) { if !ok || !s.requireAtLeast(w, a, "editor") { return } + rawBody, err := io.ReadAll(r.Body) + if err != nil { + writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid body") + return + } var body store.ModulePatch - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + if err := json.Unmarshal(rawBody, &body); err != nil { writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json") return } + // NOTE: + // In Go, unmarshalling JSON `null` into pointer fields results in nil, + // which is indistinguishable from "field omitted". For PATCH we need to + // distinguish these cases so clients can explicitly clear nullable fields. + var raw map[string]json.RawMessage + if err := json.Unmarshal(rawBody, &raw); err == nil { + if v, ok := raw["default_community_id"]; ok && string(v) == "null" { + empty := "" + body.DefaultCommunityID = &empty + } + if v, ok := raw["doh_profile_id"]; ok && string(v) == "null" { + empty := "" + body.DohProfileID = &empty + } + if v, ok := raw["cron_expr"]; ok && string(v) == "null" { + empty := "" + body.CronExpr = &empty + } + if v, ok := raw["refresh_interval_sec"]; ok && string(v) == "null" { + zero := 0 + body.RefreshIntervalSec = &zero + } + } mod, err := s.store.UpdateModule(a.TenantID, r.PathValue("module_id"), &body) if err != nil { writeStoreErr(w, err)