Garden objects are the things placed on the field: beds, grow bags, containers, in-ground plots, trees, paths, structures. They live in one polymorphic table (kind column + props JSON for kind-specific extras) since placement/geometry behavior is identical across kinds. This issue implements their CRUD plus GET /gardens/:id/full — the one-shot payload the editor loads.
Scope
internal/store/objects.go + internal/service/objects.go following the #7 conventions (actor param, requireGardenRole(editor) for mutations, version guard).
Validation: kind in the DESIGN.md enum; shape rect|circle (reject polygon for now with a clear "not yet supported"); positive dimensions; object roughly within garden bounds (loose — allow partial overhang); rotation_deg normalized to [0,360); plantable defaults by kind (bed/grow_bag/container/in_ground → true; tree/path/structure → false) but is overridable.
POST /api/v1/gardens/:id/objects, PATCH /api/v1/objects/:id (partial: any of name/x/y/width/height/rotation/z_index/color/props/notes/plantable + required version), DELETE /api/v1/objects/:id (cascades plantings — FK ON DELETE CASCADE from #1's schema).
GET /api/v1/gardens/:id/full (any role): {garden, objects[], plantings[], plants[]} — plantings filtered to removed_at IS NULL, plants = referenced ones only. Arrays will be empty until #12/#14; define the response shape completely now so the frontend types once.
Service tests: CRUD, validation rejects, version conflict, viewer-role mutation denial (owner-only until #16 — test with a second user).
curl: create a bed, move it (PATCH x/y), rotate it, delete it; stale version → 409 + current row.
/full returns the documented shape with coherent data; a non-owner (pre-#16) gets 404.
go test ./internal/service/... green including validation cases.
Part of epic #20 · Design: [DESIGN.md](https://gitea.stevedudenhoeffer.com/steve/pansy/src/branch/main/DESIGN.md)
## Context
Garden objects are the things placed on the field: beds, grow bags, containers, in-ground plots, trees, paths, structures. They live in **one polymorphic table** (`kind` column + `props` JSON for kind-specific extras) since placement/geometry behavior is identical across kinds. This issue implements their CRUD plus `GET /gardens/:id/full` — the one-shot payload the editor loads.
## Scope
- [ ] `internal/store/objects.go` + `internal/service/objects.go` following the #7 conventions (actor param, `requireGardenRole(editor)` for mutations, version guard).
- [ ] Validation: `kind` in the DESIGN.md enum; `shape` rect|circle (reject polygon for now with a clear "not yet supported"); positive dimensions; object roughly within garden bounds (loose — allow partial overhang); `rotation_deg` normalized to [0,360); `plantable` defaults by kind (bed/grow_bag/container/in_ground → true; tree/path/structure → false) but is overridable.
- [ ] `POST /api/v1/gardens/:id/objects`, `PATCH /api/v1/objects/:id` (partial: any of name/x/y/width/height/rotation/z_index/color/props/notes/plantable + required `version`), `DELETE /api/v1/objects/:id` (cascades plantings — FK ON DELETE CASCADE from #1's schema).
- [ ] `GET /api/v1/gardens/:id/full` (any role): `{garden, objects[], plantings[], plants[]}` — plantings filtered to `removed_at IS NULL`, plants = referenced ones only. Arrays will be empty until #12/#14; define the response shape completely now so the frontend types once.
- [ ] Service tests: CRUD, validation rejects, version conflict, viewer-role mutation denial (owner-only until #16 — test with a second user).
## Out of scope
- Plantings CRUD (#14). Frontend (#11). Polygon shapes (post-v1).
## Key files & patterns
- Table/columns: `DESIGN.md` § Domain model (`garden_objects`); conventions from #7.
## Dependencies
Blocked by #7.
## Acceptance criteria
- curl: create a bed, move it (PATCH x/y), rotate it, delete it; stale version → 409 + current row.
- `/full` returns the documented shape with coherent data; a non-owner (pre-#16) gets 404.
- `go test ./internal/service/...` green including validation cases.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Part of epic #20 · Design: DESIGN.md
Context
Garden objects are the things placed on the field: beds, grow bags, containers, in-ground plots, trees, paths, structures. They live in one polymorphic table (
kindcolumn +propsJSON for kind-specific extras) since placement/geometry behavior is identical across kinds. This issue implements their CRUD plusGET /gardens/:id/full— the one-shot payload the editor loads.Scope
internal/store/objects.go+internal/service/objects.gofollowing the #7 conventions (actor param,requireGardenRole(editor)for mutations, version guard).kindin the DESIGN.md enum;shaperect|circle (reject polygon for now with a clear "not yet supported"); positive dimensions; object roughly within garden bounds (loose — allow partial overhang);rotation_degnormalized to [0,360);plantabledefaults by kind (bed/grow_bag/container/in_ground → true; tree/path/structure → false) but is overridable.POST /api/v1/gardens/:id/objects,PATCH /api/v1/objects/:id(partial: any of name/x/y/width/height/rotation/z_index/color/props/notes/plantable + requiredversion),DELETE /api/v1/objects/:id(cascades plantings — FK ON DELETE CASCADE from #1's schema).GET /api/v1/gardens/:id/full(any role):{garden, objects[], plantings[], plants[]}— plantings filtered toremoved_at IS NULL, plants = referenced ones only. Arrays will be empty until #12/#14; define the response shape completely now so the frontend types once.Out of scope
Key files & patterns
DESIGN.md§ Domain model (garden_objects); conventions from #7.Dependencies
Blocked by #7.
Acceptance criteria
/fullreturns the documented shape with coherent data; a non-owner (pre-#16) gets 404.go test ./internal/service/...green including validation cases.