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.
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.
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
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:Servicestruct (holds store) + constructor. Conventions to establish:func (s *Service) X(ctx, actorUserID int64, args...);requireGardenRole(ctx, actor, gardenID, minRole)helper with rolesviewer < 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 →ErrVersionConflictorErrNotFound), Delete (owner only).internal/api:GET,POST /api/v1/gardens,GET,PATCH,DELETE /api/v1/gardens/:id. PATCH body must includeversion.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.Out of scope
/fullendpoint (#10), shares (#16), any frontend (#8).Key files & patterns
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
go test ./internal/service/...green.