Gardens CRUD + service-layer conventions (actor, version guard, 409) #7

Closed
opened 2026-07-18 18:16:20 +00:00 by steve · 0 comments
Owner

Part of epic #20 · Design: DESIGN.md

Context

A garden is one property/site: a bounded rectangle (cm) that objects get placed into. This issue implements gardens CRUD and establishes the service-layer conventions every later backend issue copies — the actor parameter, centralized role checks, and the version-guard/409 sync protocol. Get these right here; everything else imitates them. The service layer is also the seam future majordomo/executus agent tools call directly, which is why permissions must live here and not in HTTP handlers.

Scope

  • internal/service/service.go: Service struct (holds store) + constructor. Conventions to establish:
    • every method: func (s *Service) X(ctx, actorUserID int64, args...);
    • requireGardenRole(ctx, actor, gardenID, minRole) helper with roles viewer < editor < owner. Until #16, only the owner has any role; the helper is the place shares get consulted later.
  • internal/store/gardens.go + internal/service/gardens.go: Create (defaults: 1000×1000 cm, metric), Get, ListForActor (owned only until #16), Update (name/width/height/unit_pref/notes, version guard: UPDATE ... WHERE id=? AND version=? incrementing version; 0 rows → re-read → ErrVersionConflict or ErrNotFound), Delete (owner only).
  • internal/api: GET,POST /api/v1/gardens, GET,PATCH,DELETE /api/v1/gardens/:id. PATCH body must include version.
  • Error-mapping helper used by all handlers: ErrNotFound→404, ErrForbidden→403, ErrVersionConflict409 with the current row in the body ({"error": "version_conflict", "current": {...}}) so clients can rebase. Document this envelope in a comment — it's the contract for every mutable resource.
  • Service tests: CRUD happy path, version conflict, cross-user access denied.

Out of scope

  • /full endpoint (#10), shares (#16), any frontend (#8).

Key files & patterns

  • Convention rationale: DESIGN.md § Backend layout ("the service seam is the point") and § API (sync protocol).

Dependencies

Blocked by #4 (auth middleware supplies the actor).

Acceptance criteria

  • curl: create → list → get → patch (with version) → delete, all as the owner.
  • PATCH with a stale version returns 409 + the current row; retry with the fresh version succeeds.
  • A second user gets 404/403 on someone else's garden (pick one consistently — 404 to avoid existence leaks — and document it).
  • go test ./internal/service/... green.
Part of epic #20 · Design: [DESIGN.md](https://gitea.stevedudenhoeffer.com/steve/pansy/src/branch/main/DESIGN.md) ## Context A garden is one property/site: a bounded rectangle (cm) that objects get placed into. This issue implements gardens CRUD **and establishes the service-layer conventions every later backend issue copies** — the actor parameter, centralized role checks, and the version-guard/409 sync protocol. Get these right here; everything else imitates them. The service layer is also the seam future majordomo/executus agent tools call directly, which is why permissions must live here and not in HTTP handlers. ## Scope - [ ] `internal/service/service.go`: `Service` struct (holds store) + constructor. Conventions to establish: - every method: `func (s *Service) X(ctx, actorUserID int64, args...)`; - `requireGardenRole(ctx, actor, gardenID, minRole)` helper with roles `viewer < editor < owner`. Until #16, only the owner has any role; the helper is *the* place shares get consulted later. - [ ] `internal/store/gardens.go` + `internal/service/gardens.go`: Create (defaults: 1000×1000 cm, metric), Get, ListForActor (owned only until #16), Update (name/width/height/unit_pref/notes, **version guard**: `UPDATE ... WHERE id=? AND version=?` incrementing version; 0 rows → re-read → `ErrVersionConflict` or `ErrNotFound`), Delete (owner only). - [ ] `internal/api`: `GET,POST /api/v1/gardens`, `GET,PATCH,DELETE /api/v1/gardens/:id`. PATCH body must include `version`. - [ ] Error-mapping helper used by all handlers: `ErrNotFound`→404, `ErrForbidden`→403, `ErrVersionConflict`→**409 with the current row in the body** (`{"error": "version_conflict", "current": {...}}`) so clients can rebase. Document this envelope in a comment — it's the contract for every mutable resource. - [ ] Service tests: CRUD happy path, version conflict, cross-user access denied. ## Out of scope - `/full` endpoint (#10), shares (#16), any frontend (#8). ## Key files & patterns - Convention rationale: `DESIGN.md` § Backend layout ("the service seam is the point") and § API (sync protocol). ## Dependencies Blocked by #4 (auth middleware supplies the actor). ## Acceptance criteria - curl: create → list → get → patch (with version) → delete, all as the owner. - PATCH with a stale version returns 409 + the current row; retry with the fresh version succeeds. - A second user gets 404/403 on someone else's garden (pick one consistently — 404 to avoid existence leaks — and document it). - `go test ./internal/service/...` green.
steve added the backendgardens labels 2026-07-18 18:16:20 +00:00
steve closed this issue 2026-07-18 22:40:50 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: steve/pansy#7