diff --git a/internal/api/ops.go b/internal/api/ops.go index 97e0cba..f212055 100644 --- a/internal/api/ops.go +++ b/internal/api/ops.go @@ -37,14 +37,14 @@ func (r fillRect) degenerate() bool { return r.MaxX <= r.MinX || r.MaxY <= r.MinY } -// fillRequest is the body for POST /objects/:id/fill. +// objectFillRequest is the body for POST /objects/:id/fill. // // A region is given EITHER by compass name ("ne", "south half", "all") or as an // explicit rect in the object's local frame. The named form is what a person // means and what the agent uses; the rect is for a future drag-a-box affordance. // Exactly one must be supplied — accepting both and silently preferring one // would make a client bug look like a geometry bug. -type fillRequest struct { +type objectFillRequest struct { PlantID int64 `json:"plantId" binding:"required"` Region string `json:"region"` Rect *fillRect `json:"rect"` @@ -58,7 +58,7 @@ func (h *handlers) fillObject(c *gin.Context) { if !ok { return } - var req fillRequest + var req objectFillRequest if err := c.ShouldBindJSON(&req); err != nil { writeAPIError(c, http.StatusBadRequest, "INVALID_INPUT", "a plantId and a region are required") return diff --git a/internal/api/ops_test.go b/internal/api/ops_test.go index 2bcd06f..bf317de 100644 --- a/internal/api/ops_test.go +++ b/internal/api/ops_test.go @@ -10,11 +10,10 @@ import ( func fillPath(id int64) string { return objectPath(id) + "/fill" } func clearPath(id int64) string { return objectPath(id) + "/clear" } -// makeFillPlant creates a custom plant and returns its id. -// -// Deliberately named apart from the seed-lot tests' helper: that one arrives on -// a separate branch, and two files declaring the same helper would collide on -// whichever merged second. +// makeFillPlant creates a custom plant and returns its id. (A near-identical +// createPlantAPI landed alongside the seed-lot tests; consolidating the two into +// one shared helper is a fine follow-up, kept separate here only to avoid a +// merge collision on the shared symbol.) func makeFillPlant(t *testing.T, r *gin.Engine, cookie *http.Cookie, name string, spacing float64) int64 { t.Helper() w := doJSON(t, r, http.MethodPost, "/api/v1/plants", map[string]any{ @@ -221,4 +220,7 @@ func TestFillClearPermissionsAPI(t *testing.T) { if w := doJSON(t, r, http.MethodPost, fillPath(objID), fillBody, nil); w.Code != http.StatusUnauthorized { t.Errorf("anonymous fill = %d, want 401", w.Code) } + if w := doJSON(t, r, http.MethodPost, clearPath(objID), nil, nil); w.Code != http.StatusUnauthorized { + t.Errorf("anonymous clear = %d, want 401", w.Code) + } } diff --git a/web/src/lib/objects.ts b/web/src/lib/objects.ts index 21cf433..44dbf30 100644 --- a/web/src/lib/objects.ts +++ b/web/src/lib/objects.ts @@ -328,6 +328,8 @@ export function useUpdatePlanting(gardenId: number) { }) } +const clearResultSchema = z.object({ cleared: z.number() }) + /** Clear a bed: soft-remove every active plop in an object (#82). * * ONE request, and so ONE change set. This used to be a loop of PATCHes, which @@ -341,8 +343,10 @@ export function useClearObject(gardenId: number) { const qc = useQueryClient() return useMutation({ mutationFn: async (objectId: number): Promise => { - const res = (await api.post(`/objects/${objectId}/clear`, {})) as { cleared?: number } - return res?.cleared ?? 0 + // No body — clear takes none; passing undefined sends none rather than an + // empty {}. The response is just a count; validate it rather than cast. + const res = clearResultSchema.parse(await api.post(`/objects/${objectId}/clear`)) + return res.cleared }, onSettled: () => qc.invalidateQueries({ queryKey: fullKey(gardenId) }), onError: (err) => toast.error(objectErrorMessage(err, 'Could not clear the bed.')),