Copy a garden (deep duplicate) from the gardens list #46

Merged
steve merged 2 commits from feat/copy-garden into main 2026-07-21 03:58:40 +00:00
2 Commits
Author SHA1 Message Date
steveandClaude Opus 4.8 e1ec93ddff Address Gadfly review on the garden copy
Build image / build-and-push (push) Successful in 8s
* api: detect an absent copy body by io.EOF from the decoder rather than
  Content-Length. A chunked request reports -1, not 0, so the length test
  read an empty chunked body as malformed and 400'd instead of taking the
  default-name path. Covered by a new unknown-length test.

* store: hoist the garden_objects/plantings INSERTs into shared
  objectInsert/plantingInsert statements with matching *InsertArgs
  helpers. CopyGarden had its own copies of both column lists, so a
  column added to the table could be wired into Create* and silently
  dropped from a copy.

* store: move the queryer interface to sqlite.go, next to the other
  shared query plumbing, instead of users.go.

* web: derive a copy's prefilled name via defaultCopyName, mirroring the
  server's copyName including its 200-BYTE cap. The inline
  `${name} (copy)` both duplicated the suffix and, for a garden whose
  name was already at the cap, prefilled an over-long name that the
  server rejected with a 400. Unit-tested, including multi-byte
  truncation on a code-point boundary.

* test: drop a throwaway `_ = kept`.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-20 23:57:44 -04:00
steveandClaude Opus 4.8 5dd35c3800 Copy a garden (deep duplicate) from the gardens list
Build image / build-and-push (push) Successful in 23s
Gadfly review (reusable) / review (pull_request) Successful in 6m30s
Adversarial Review (Gadfly) / review (pull_request) Successful in 6m30s
Adds POST /gardens/:id/copy: duplicates a garden the actor owns, along
with its objects and their currently-planted plops, into a new garden
owned by the actor. A blank name derives "<source> (copy)".

Deliberately not carried over:
  * public_token — the share link is a capability granted to the
    original; a copy must not silently inherit a live public URL
  * garden_shares — a copy is private to whoever made it
  * removed plantings — the copy is a fresh layout, not a history

Owner-only for now: a copy's plops keep pointing at the SOURCE's
plant_ids, so letting a viewer copy someone else's garden would hand
them plants outside their catalog and pin the original owner's plants
against deletion (plantings.plant_id is ON DELETE RESTRICT). Copying a
shared garden needs a plant-cloning policy first.

The whole copy runs in one transaction, so a failure part-way leaves no
half-populated garden. The object/planting list scans are factored into
queryObjects/queryPlantings so the same code serves a plain read and a
read inside that transaction.

UI: a Copy action on the owner's garden card opens a modal prefilled
with the derived name, then lands in the new garden.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-20 23:49:50 -04:00