Backend: Bucket PATCH + DELETE operations #57

Closed
opened 2026-06-26 00:04:45 +02:00 by myrmidex · 1 comment
Owner

Context

Bucket currently exposes only GetCollection / Get / Post. The old Laravel app had inline bucket editing (partial PATCH) and deletion (PLATFORM.md "Inline Editing Components"). To port that UI we need the API operations first. This backend ticket unblocks the edit/delete frontend ticket.

Scope

  • Add Patch to Bucket's #[ApiResource] operations — partial update of name, type, allocation type/value, priority, sortOrder, buffer multiplier.
  • Add Delete to Bucket's operations.
  • Ownership scoping MUST extend to the new ops. BucketOwnerExtension's applyToItem already scopes item resolution, so PATCH/DELETE on a non-owned bucket SHOULD 404 — but VERIFY with tests (per the PLATFORM.md "FUTURE (when Patch/Put/Delete land)" note; don't assume).
  • Re-validate invariants on PATCH: the custom validators (single overflow per scenario, compatible allocation type, buffer-only-for-fixed-limit, unique priority) must still fire on update, not just create.

Testing (TDD)

  • PATCH happy path (partial) → 200; PATCH non-owned → 404; PATCH violating an invariant → 422.
  • DELETE owned → 204; DELETE non-owned → 404.
  • Anon → 401 on both.

Notes

  • This is the FIRST mutating-op-beyond-Post in the project — confirm the CSRF/content-type story holds for PATCH (application/merge-patch+json is AP's PATCH content-type; check it doesn't bypass the ld+json-enforced CSRF guard, or document the difference).

Out of scope

  • Scenario / Stream PATCH/DELETE (separate, if needed).
## Context Bucket currently exposes only `GetCollection / Get / Post`. The old Laravel app had inline bucket editing (partial PATCH) and deletion (PLATFORM.md "Inline Editing Components"). To port that UI we need the API operations first. This backend ticket unblocks the edit/delete frontend ticket. ## Scope - Add `Patch` to Bucket's `#[ApiResource]` operations — partial update of name, type, allocation type/value, priority, sortOrder, buffer multiplier. - Add `Delete` to Bucket's operations. - **Ownership scoping MUST extend to the new ops.** `BucketOwnerExtension`'s `applyToItem` already scopes item resolution, so PATCH/DELETE on a non-owned bucket SHOULD 404 — but VERIFY with tests (per the PLATFORM.md "FUTURE (when Patch/Put/Delete land)" note; don't assume). - Re-validate invariants on PATCH: the custom validators (single overflow per scenario, compatible allocation type, buffer-only-for-fixed-limit, unique priority) must still fire on update, not just create. ## Testing (TDD) - PATCH happy path (partial) → 200; PATCH non-owned → 404; PATCH violating an invariant → 422. - DELETE owned → 204; DELETE non-owned → 404. - Anon → 401 on both. ## Notes - This is the FIRST mutating-op-beyond-Post in the project — confirm the CSRF/content-type story holds for PATCH (`application/merge-patch+json` is AP's PATCH content-type; check it doesn't bypass the `ld+json`-enforced CSRF guard, or document the difference). ## Out of scope - Scenario / Stream PATCH/DELETE (separate, if needed).
myrmidex added this to the v0.3.0 milestone 2026-06-26 00:04:45 +02:00
myrmidex added the
enhancement
label 2026-06-26 00:04:45 +02:00
myrmidex self-assigned this 2026-06-26 00:04:45 +02:00
myrmidex added a new dependency 2026-06-26 00:11:26 +02:00
Author
Owner

Resolved on release/v0.3.0: b932228 (PATCH + priority-uniqueness validator), cb4af69 (DELETE), + a doc-tighten follow-up.

Adding new Patch() / new Delete() to Bucket's operations was the entire change for the verbs — everything else inherited from #50/#31:

  • Auth 401 ← resource-level is_granted('ROLE_USER').
  • Ownership 404 (not 403)BucketOwnerExtension::applyToItem is operation-agnostic; verified for PATCH and DELETE (no mutation/deletion leaks).
  • Invariant validators fire on PATCH ← class-level constraints, no groups.
  • PATCH content-type = application/merge-patch+json (AP 4.3 default); wrong type → 415. Re-parent-via-PATCH into a foreign scenario IRI → 400 (locked decision, same as foreign-IRI POST).
  • DELETE → 204; cascade SET-NULLs stream.bucket_id (referencing stream survives, bucket=null).

Real bug found + fixed: priority uniqueness was enforced only by the DB unique_scenario_priority constraint, so a colliding priority PATCH/POST 500'd (unhandled UniqueConstraintViolationException at flush). Added a custom UniquePriorityPerScenario validator (+ validator class + BucketRepository::countBucketsWithPriorityForScenario, mirrors SingleOverflowPerScenario, self-excludes by id) → clean 422 at priority. Guarded by a new Bucket::hasPriority() since priority is the only validated property without a default (a POST omitting it would otherwise fatal on the uninitialized typed property).

The stream-orphan-on-delete behaviour got a characterization test folded in here (no separate follow-up needed).

217 tests green, PHPStan level 6 + php-cs-fixer clean, zero deprecations/notices. pr-reviewer (full-ticket diff): Approve, 0 must-fix.

Resolved on `release/v0.3.0`: `b932228` (PATCH + priority-uniqueness validator), `cb4af69` (DELETE), + a doc-tighten follow-up. Adding `new Patch()` / `new Delete()` to Bucket's operations was the entire change for the verbs — everything else inherited from #50/#31: - **Auth 401** ← resource-level `is_granted('ROLE_USER')`. - **Ownership 404 (not 403)** ← `BucketOwnerExtension::applyToItem` is operation-agnostic; verified for PATCH and DELETE (no mutation/deletion leaks). - **Invariant validators fire on PATCH** ← class-level constraints, no groups. - **PATCH content-type = `application/merge-patch+json`** (AP 4.3 default); wrong type → 415. Re-parent-via-PATCH into a foreign scenario IRI → 400 (locked decision, same as foreign-IRI POST). - **DELETE** → 204; cascade SET-NULLs `stream.bucket_id` (referencing stream survives, bucket=null). **Real bug found + fixed:** priority uniqueness was enforced *only* by the DB `unique_scenario_priority` constraint, so a colliding `priority` PATCH/POST 500'd (unhandled `UniqueConstraintViolationException` at flush). Added a custom `UniquePriorityPerScenario` validator (+ validator class + `BucketRepository::countBucketsWithPriorityForScenario`, mirrors `SingleOverflowPerScenario`, self-excludes by id) → clean 422 at `priority`. Guarded by a new `Bucket::hasPriority()` since `priority` is the only validated property without a default (a POST omitting it would otherwise fatal on the uninitialized typed property). The stream-orphan-on-delete behaviour got a characterization test folded in here (no separate follow-up needed). 217 tests green, PHPStan level 6 + php-cs-fixer clean, zero deprecations/notices. pr-reviewer (full-ticket diff): Approve, 0 must-fix.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference: lvl0/buckets#57
No description provided.