feat: Add PUT and PATCH datasets endpoints
This commit is contained in:
@@ -37,6 +37,8 @@ func (h *DatasetHandler) Register(r chi.Router) {
|
||||
r.Get("/", h.list)
|
||||
r.Post("/", h.upload)
|
||||
r.Get("/{id}", h.get)
|
||||
r.Put("/{id}", h.update)
|
||||
r.Patch("/{id}", h.patch)
|
||||
r.Get("/{id}.geojson", h.geojson)
|
||||
r.Get("/{id}.cog", h.cog)
|
||||
r.Get("/{id}/status", h.status)
|
||||
@@ -306,6 +308,150 @@ func (h *DatasetHandler) get(w http.ResponseWriter, r *http.Request) {
|
||||
httputil.WriteJSON(w, http.StatusOK, dataset)
|
||||
}
|
||||
|
||||
// datasetPutRequest is the full JSON body for PUT /datasets/{id}: every
|
||||
// editable field is required. Description and Unit are pointers so an empty
|
||||
// string is distinguishable from an absent field; a blank value clears the
|
||||
// (nullable) column. File-related and lifecycle fields are not editable here.
|
||||
type datasetPutRequest struct {
|
||||
CategoryID string `json:"category_id" validate:"required,uuid"`
|
||||
Name string `json:"name" validate:"required,max=255"`
|
||||
Description *string `json:"description" validate:"required"`
|
||||
Unit *string `json:"unit" validate:"required"`
|
||||
}
|
||||
|
||||
// update replaces a dataset's editable metadata (category, name, description,
|
||||
// unit).
|
||||
func (h *DatasetHandler) update(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := parseUUIDParam(w, r, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
req, err := httputil.DecodeJSON[datasetPutRequest](w, r)
|
||||
if err != nil {
|
||||
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
if err := h.validate.Struct(req); err != nil {
|
||||
httputil.WriteValidationErrors(w, err)
|
||||
return
|
||||
}
|
||||
categoryID, err := uuid.Parse(req.CategoryID)
|
||||
if err != nil {
|
||||
httputil.WriteError(w, http.StatusUnprocessableEntity, "category_id must be a valid UUID")
|
||||
return
|
||||
}
|
||||
|
||||
dataset, err := h.svc.Update(r.Context(), id, service.DatasetUpdateInput{
|
||||
CategoryID: categoryID,
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
Description: normalizeOptional(*req.Description),
|
||||
Unit: normalizeOptional(*req.Unit),
|
||||
})
|
||||
if err != nil {
|
||||
respondDomainError(w, err)
|
||||
return
|
||||
}
|
||||
httputil.WriteJSON(w, http.StatusOK, dataset)
|
||||
}
|
||||
|
||||
// datasetPatchFields lists the JSON keys accepted by PATCH /datasets/{id}.
|
||||
var datasetPatchFields = map[string]struct{}{
|
||||
"category_id": {}, "name": {}, "description": {}, "unit": {},
|
||||
}
|
||||
|
||||
// patch applies a sparse update to a dataset's editable metadata (category,
|
||||
// name, description, unit): only fields present in the body are changed.
|
||||
// Sending an explicit null for description/unit clears that column.
|
||||
func (h *DatasetHandler) patch(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := parseUUIDParam(w, r, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
raw, err := httputil.DecodeJSON[map[string]json.RawMessage](w, r)
|
||||
if err != nil {
|
||||
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
httputil.WriteError(w, http.StatusBadRequest, "request body must include at least one field")
|
||||
return
|
||||
}
|
||||
|
||||
var in service.DatasetPatchInput
|
||||
for key, val := range raw {
|
||||
if _, ok := datasetPatchFields[key]; !ok {
|
||||
httputil.WriteError(w, http.StatusUnprocessableEntity, "unknown field: "+key)
|
||||
return
|
||||
}
|
||||
switch key {
|
||||
case "category_id":
|
||||
var s string
|
||||
if err := json.Unmarshal(val, &s); err != nil || s == "" {
|
||||
httputil.WriteError(w, http.StatusUnprocessableEntity, "category_id must be a valid UUID")
|
||||
return
|
||||
}
|
||||
cid, err := uuid.Parse(s)
|
||||
if err != nil {
|
||||
httputil.WriteError(w, http.StatusUnprocessableEntity, "category_id must be a valid UUID")
|
||||
return
|
||||
}
|
||||
in.CategoryID = &cid
|
||||
case "name":
|
||||
var s string
|
||||
if err := json.Unmarshal(val, &s); err != nil || strings.TrimSpace(s) == "" {
|
||||
httputil.WriteError(w, http.StatusUnprocessableEntity, "name must not be blank")
|
||||
return
|
||||
}
|
||||
name := strings.TrimSpace(s)
|
||||
in.Name = &name
|
||||
case "description":
|
||||
desc, err := decodeOptionalPatchString(val)
|
||||
if err != nil {
|
||||
httputil.WriteError(w, http.StatusUnprocessableEntity, "description must be a string or null")
|
||||
return
|
||||
}
|
||||
in.Description, in.DescriptionSet = desc, true
|
||||
case "unit":
|
||||
unit, err := decodeOptionalPatchString(val)
|
||||
if err != nil {
|
||||
httputil.WriteError(w, http.StatusUnprocessableEntity, "unit must be a string or null")
|
||||
return
|
||||
}
|
||||
in.Unit, in.UnitSet = unit, true
|
||||
}
|
||||
}
|
||||
|
||||
dataset, err := h.svc.Patch(r.Context(), id, in)
|
||||
if err != nil {
|
||||
respondDomainError(w, err)
|
||||
return
|
||||
}
|
||||
httputil.WriteJSON(w, http.StatusOK, dataset)
|
||||
}
|
||||
|
||||
// normalizeOptional trims s and returns nil when blank, so an empty string
|
||||
// clears a nullable column rather than being stored verbatim.
|
||||
func normalizeOptional(s string) *string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
// decodeOptionalPatchString unmarshals a PATCH field that may be a JSON string
|
||||
// or null, returning nil (clear the column) for null or a blank string.
|
||||
func decodeOptionalPatchString(raw json.RawMessage) (*string, error) {
|
||||
if string(raw) == "null" {
|
||||
return nil, nil
|
||||
}
|
||||
var s string
|
||||
if err := json.Unmarshal(raw, &s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return normalizeOptional(s), nil
|
||||
}
|
||||
|
||||
// geojson returns the dataset's pre-assembled GeoJSON FeatureCollection (RFC
|
||||
// 7946), generated and stored at processing time. A vector_with_kato dataset's
|
||||
// collection joins the districts table on KATO code (one feature per KATO with
|
||||
|
||||
Reference in New Issue
Block a user