--- phase: 09-backend-admin-authentication-and-schema-pipeline plan: 05 subsystem: admin tags: [cabana, crud, bulk-delete, gorm, mass-assignment, lifecycle] requires: - phase: 09-backend-admin-authentication-and-schema-pipeline provides: compiled form schema, backend permission gate, and D-10 envelopes provides: - Schema-projected create and update through Fill then Validate - Permissioned show, create, update, and delete with scoped lookup - Transactional bulk delete with duplicate, empty, retry, and concurrency rules affects: [09-backend-admin-authentication-and-schema-pipeline, admin-api, phase-10-spa] actuals: tokens: 17783 tasks: 3 commits: 6 tech-stack: added: [] patterns: - "Writable fields are bound to gorm columns at activation and projected by exact key" - "Record mutations run Fill, BeforeValidate, Validate, then controller and model hooks in one transaction" - "Bulk delete locks the scoped set FOR UPDATE and commits every selected row or none" key-files: created: - cabana/crud.go - cabana/crud_test.go - cabana/crud_lifecycle_test.go - cabana/bulk_test.go modified: - cabana/http.go - cabana/registry.go - cabana/contracts.go - pact/capabilities.go key-decisions: - "Writable admin fields are bound to gorm columns at activation; id, timestamps, scope, and system flags are never fillable" - "Show and update use one not-found body for missing and out-of-scope rows; delete of an absent row is deleted 0 and does not run hooks" - "A bulk selection that matches no scoped row is a no-op; a mixed present and absent selection is a 409 and rolls back" - "Controller hook failures return an opaque lifecycle error and do not echo the hook text" patterns-established: - "Pattern: ProjectWritableFields copies only activation bindings, so request key casing and nesting cannot reach Fill" - "Pattern: bulk ids are parsed, deduped, and sorted before a single locked transaction" requirements-completed: [ADMIN-04] coverage: - id: D1 description: Create and update project only schema-writable fields, call Fill then Validate, and return D-10 422 field errors. requirement: ADMIN-04 verification: - kind: integration ref: cabana/crud_test.go#TestCRUDFillValidate status: pass human_judgment: false - id: D2 description: Unknown, cased, nested, and protected keys never change id, timestamps, scope, or system flags. requirement: ADMIN-04 verification: - kind: unit ref: cabana/crud_test.go#TestCRUDWritableProjection status: pass - kind: integration ref: cabana/crud_test.go#TestCRUDRejectsProtectedFields status: pass human_judgment: false - id: D3 description: A model missing Fillable or Rules, or a Validate provider error, fails closed with the controller id and persists nothing. requirement: ADMIN-04 verification: - kind: integration ref: cabana/crud_test.go#TestCRUDCapabilityFailure status: pass human_judgment: false - id: D4 description: Show, create, update, and delete are mounted behind the backend permission check and use D-10 envelopes. requirement: ADMIN-04 verification: - kind: integration ref: cabana/crud_lifecycle_test.go#TestCRUDRecordRoutes status: pass - kind: integration ref: cabana/crud_lifecycle_test.go#TestCRUDPermissions status: pass human_judgment: false - id: D5 description: Missing and out-of-scope records share one not-found or deleted-zero body, and in-scope rows stay unchanged. requirement: ADMIN-04 verification: - kind: integration ref: cabana/crud_lifecycle_test.go#TestCRUDScope status: pass human_judgment: false - id: D6 description: Create, update, and delete run Before and After hooks once, in order, inside the transaction. requirement: ADMIN-04 verification: - kind: integration ref: cabana/crud_lifecycle_test.go#TestCRUDHooks status: pass human_judgment: false - id: D7 description: A failing create, update, or delete hook rolls the transaction back and the HTTP body stays opaque. requirement: ADMIN-04 verification: - kind: integration ref: cabana/crud_lifecycle_test.go#TestCRUDRollback status: pass human_judgment: false - id: D8 description: Bulk delete rejects an empty selection, collapses duplicates, and deletes in ascending primary-key order. requirement: ADMIN-04 verification: - kind: integration ref: cabana/bulk_test.go#TestBulkDeleteEmpty status: pass - kind: integration ref: cabana/bulk_test.go#TestBulkDeleteDuplicates status: pass - kind: integration ref: cabana/bulk_test.go#TestBulkDeleteOrder status: pass human_judgment: false - id: D9 description: Repeating a completed bulk delete, or naming only out-of-scope rows, returns deleted 0 and does not run hooks. requirement: ADMIN-04 verification: - kind: integration ref: cabana/bulk_test.go#TestBulkDeleteIdempotent status: pass human_judgment: false - id: D10 description: A mixed selection, hook error, or cancellation rolls the whole batch back, and two concurrent deletes remove each row once. requirement: ADMIN-04 verification: - kind: integration ref: cabana/bulk_test.go#TestBulkDeleteRollback status: pass - kind: integration ref: cabana/bulk_test.go#TestBulkDeleteConcurrent status: pass human_judgment: false duration: 25min completed: 2026-09-24 status: complete plan_head_before: 040f3ef81c199c82beec1ee8d4626aa4f85fe19a plan_head_after: 50754808f61071e23746f3f36dde8a292a18360b --- # Phase 9 Plan 05: Schema-projected CRUD and atomic bulk delete Summary **Admin create and update fill only activation-bound fields, and bulk delete locks the scoped set so every selected row commits or none do.** ## Performance - **Duration:** 25 min - **Started:** 2026-09-24T17:20:57Z - **Completed:** 2026-09-24T17:45:40Z - **Tasks:** 3 - **Files modified:** 8 ## Accomplishments - `ProjectWritableFields` keeps only schema fields bound to gorm columns at activation. `id`, timestamps, `scope_id`, ownership ids, and system flags never reach `lagoon.Fill`, even when the JSON key is cased or nested. - Create and update run Fill, `BeforeValidate`, then `lagoon.Validate` (YAML `required` merged onto `Rules()`) before persistence. Validation errors are D-10 `validation_failed` with field messages. A missing Fillable/Rules method or a Validate provider error does not insert. - `GET/POST/PUT/DELETE /_admin/api/v1/{vendor}/{plugin}/{controller}` record routes sit behind the backend group and `protect`, so a forbidden caller is 403 before the id or body is read. Show and update of a missing or out-of-scope id share one `not_found` body. Delete of an absent id returns `deleted: 0` and does not run hooks. - Create, update, and delete call the matching `FormBefore*` / `FormAfter*` hook once around the GORM callbacks, inside one transaction. A hook error rolls back and the response is `Server error` without the hook text. - `POST .../bulk-delete` rejects an empty `ids` list with 422, dedupes, sorts by primary key, and locks the scoped rows `FOR UPDATE`. A wholly absent selection returns `deleted: 0`. A mixed present/absent selection is 409 and rolls back. Concurrent identical requests delete each row once. ## TDD Gate Compliance Each task has a `test(09-05)` commit that failed on the named assertion, then a `feat(09-05)` commit. `gsd_run check tdd-red-evidence` returned `RED_EVIDENCE_OK` for `TestCRUDFillValidate`, `TestCRUDRecordRoutes`, and `TestBulkDeleteEmpty` before the matching implementation. No refactor commit was needed. | Task | RED | GREEN | REFACTOR | |------|-----|-------|----------| | 1 Fill/Validate | 1e14da7 | 578bdc8 | — | | 2 Record lifecycle | 94814d9 | e3e1c25 | — | | 3 Bulk delete | 247c323 | 5075480 | — | ## Task Commits Each task was committed atomically. `commits: 6` is `git rev-list --count` from `040f3ef81c199c82beec1ee8d4626aa4f85fe19a` to `50754808f61071e23746f3f36dde8a292a18360b`. 1. **Task 1: Project writable fields and enforce Fill/Validate (RED)** - `1e14da7` (test) 2. **Task 1: Project writable fields and enforce Fill/Validate (GREEN)** - `578bdc8` (feat) 3. **Task 2: Wire scoped record CRUD with mandatory lifecycle hooks (RED)** - `94814d9` (test) 4. **Task 2: Wire scoped record CRUD with mandatory lifecycle hooks (GREEN)** - `e3e1c25` (feat) 5. **Task 3: Make bulk deletion deterministic, retry-safe, and atomic (RED)** - `247c323` (test) 6. **Task 3: Make bulk deletion deterministic, retry-safe, and atomic (GREEN)** - `5075480` (feat) **Plan metadata:** included in the docs commit for this summary ## Files Created/Modified - `cabana/crud.go` - CRUDService, writable projection, record lifecycle, and locked bulk delete - `cabana/http.go` - show, create, update, delete, and bulk-delete routes after the permission check - `cabana/registry.go` - binds writable fields while compiling a controller - `cabana/contracts.go` - `WritableField` on the compiled controller - `cabana/crud_test.go` - Fill/Validate, projection, protected fields, and capability tests - `cabana/crud_lifecycle_test.go` - routes, permissions, scope, hooks, and rollback - `cabana/bulk_test.go` - empty, duplicate, order, retry, rollback, and concurrency - `pact/capabilities.go` - FormAfterCreate, FormAfterUpdate, FormBeforeDelete, FormAfterDelete ## Decisions Made - Activation binds a scalar form field to the gorm column of the same name. Protected columns are omitted from that list rather than copied and then dropped. - YAML `required: true` is appended to model `Rules()` and never replaces a stricter rule. Validate reads the model after `BeforeValidate`, so a hook can still clear a value and fail required. - Show and update answer missing and out-of-scope ids with the same 404. Delete answers both with `deleted: 0` so a retry of a completed delete does not rerun hooks and does not reveal scope. - Bulk delete uses `ListExtendQuery`. If the locked row count is zero, the result is `deleted: 0`. If it is greater than zero but short of the normalized ids, the response is `conflict` and the transaction rolls back. - Hook and database errors become `cabana: controller failed`. The HTTP body stays `Server error`. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 2 - Missing Critical] Added the After and delete controller hooks** - **Found during:** Task 2 (scoped record lifecycle) - **Issue:** D-13 named `FormBeforeCreate` and `FormBeforeUpdate` only. The plan also requires Before/After hooks for create, update, and delete, once each. - **Fix:** Added `FormAfterCreate`, `FormAfterUpdate`, `FormBeforeDelete`, and `FormAfterDelete` next to the existing pact hooks and call each implemented hook inside the transaction. - **Files modified:** `pact/capabilities.go`, `cabana/crud.go` - **Verification:** `TestCRUDHooks` and `TestCRUDRollback` - **Committed in:** `94814d9` (interfaces) and `e3e1c25` (calls) **2. [Rule 2 - Missing Critical] Stored the writable binding on the compiled controller** - **Found during:** Task 1 (Fill/Validate) - **Issue:** The plan's file list did not include `contracts.go`, but the binding has to survive activation and be readable without reflecting over request keys. - **Fix:** Added `Writable []WritableField` and fill it from `BindWritableFields` during `compileRegistry`. - **Files modified:** `cabana/contracts.go`, `cabana/registry.go` - **Verification:** `TestCRUDWritableProjection` - **Committed in:** `1e14da7` and `578bdc8` --- **Total deviations:** 2 auto-fixed (2 missing critical) **Impact on plan:** Both were required to enforce the mass-assignment and lifecycle contracts. No new route family or dependency. ## Issues Encountered None. ## User Setup Required None - no external service configuration required. ## Next Phase Readiness Ready for 09-06. Record and bulk routes are in place for every compiled controller that exposes `NewRecord`, `Fillable`, and `Rules`. Relation link/unlink and settings upsert are still later plans. `ADMIN-04` stays pending in `REQUIREMENTS.md` because 09-06, 09-07, 09-08, 09-09, 09-10, and 09-12 also declare it. `go test ./cabana -run '^(TestCRUD|TestBulkDelete)' -count=1` passed. ## Self-Check: PASSED - FOUND: cabana/crud.go - FOUND: cabana/bulk_test.go - FOUND: cabana/crud_test.go - FOUND: cabana/crud_lifecycle_test.go - FOUND: 1e14da7 - FOUND: 578bdc8 - FOUND: 94814d9 - FOUND: e3e1c25 - FOUND: 247c323 - FOUND: 5075480 --- *Phase: 09-backend-admin-authentication-and-schema-pipeline* *Completed: 2026-09-24*