Files
summercms/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-04-SUMMARY.md

298 lines
13 KiB
Markdown

---
phase: 09-backend-admin-authentication-and-schema-pipeline
plan: 04
subsystem: admin
tags: [cabana, list-schema, gorm, pagination, filters, phrasebook]
requires:
- phase: 09-backend-admin-authentication-and-schema-pipeline
provides: cabana list compiler, form localizer, and backend guard
provides:
- Ordered Winter list schema with columns, actions, default sort, and page sizes
- Switch, date-range, and registered model-scope filters without raw SQL
- Allowlisted list query execution with D-11 envelopes and primary-key tie-break
affects: [09-backend-admin-authentication-and-schema-pipeline, admin-api, phase-10-spa]
actuals:
tokens: 22349
tasks: 3
commits: 7
tech-stack:
added: []
patterns:
- "List IR caches phrase keys; Localize copies labels per request"
- "Query identifiers resolve only through compiled columns, filters, and FilterScopes"
- "lagoon.Paginate supplies last-page math; the admin envelope keeps D-11's page key"
key-files:
created:
- cabana/list_schema.go
- cabana/filter_schema.go
- cabana/query.go
- cabana/testdata/list/all_columns.yaml
- cabana/testdata/list/all_filters.yaml
modified:
- cabana/schema.go
- cabana/schema_types.go
- cabana/http.go
- cabana/contracts.go
- pact/capabilities.go
key-decisions:
- "Omitted sortable defaults to true, except relation columns, which stay unsortable unless columns.yaml sets sortable"
- "perPageOptions keep YAML order and must include recordsPerPage"
- "list_toolbar, recordUrl, and showCheckboxes compile to create, update, and delete"
- "A conditions key is a boot error; a model scope must be listed by FilterScopes()"
- "Admin list meta uses D-11 page, while lagoon.Paginate still computes last_page"
patterns-established:
- "Pattern: list compilation walks declaration order and rejects unknown keys before routes are served"
- "Pattern: search, sort, and filter values are bound; identifiers come only from the compiled schema"
requirements-completed: [ADMIN-02]
coverage:
- id: D1
description: Every locked list column, action, default sort, search term, page size, and showSetup switch compiles to stable Winter JSON.
requirement: ADMIN-02
verification:
- kind: unit
ref: cabana/list_schema_test.go#TestListSchemaCompile
status: pass
human_judgment: false
- id: D2
description: Empty and single list declarations stay arrays, and column plus perPageOptions order is stable across compiles.
requirement: ADMIN-02
verification:
- kind: unit
ref: cabana/list_schema_test.go#TestListSchemaEmpty
status: pass
- kind: unit
ref: cabana/list_schema_test.go#TestListSchemaSingle
status: pass
- kind: unit
ref: cabana/list_schema_test.go#TestListSchemaOrdering
status: pass
human_judgment: false
- id: D3
description: Duplicate columns, unknown keys, bad defaults, path escape, a mismatched modelClass, and unsupported actions fail with plugin, controller, and file context.
requirement: ADMIN-02
verification:
- kind: unit
ref: cabana/list_schema_test.go#TestListSchemaRejects
status: pass
human_judgment: false
- id: D4
description: Switch, date-range, and model-scope filters compile in source order with typed switch values and no condition string.
requirement: ADMIN-02
verification:
- kind: unit
ref: cabana/list_schema_test.go#TestListSchemaFilter
status: pass
human_judgment: false
- id: D5
description: Model scopes resolve only through FilterScopes, and raw conditions, unknown columns, and arbitrary method names fail activation.
requirement: ADMIN-02
verification:
- kind: unit
ref: cabana/list_schema_test.go#TestListSchemaScope
status: pass
- kind: unit
ref: cabana/list_schema_test.go#TestListSchemaRejectsRawCondition
status: pass
human_judgment: false
- id: D6
description: Filter and option labels localize per request while column, scope, and option values stay unchanged on the cached schema.
requirement: ADMIN-02
verification:
- kind: unit
ref: cabana/list_schema_test.go#TestListSchemaFilter/labels_localize
status: pass
human_judgment: false
- id: D7
description: Search, sort, relation search, empty, single, and adjacent pages return the D-11 envelope with a primary-key tie-break.
requirement: ADMIN-02
verification:
- kind: integration
ref: cabana/query_test.go#TestListQueryContract
status: pass
- kind: integration
ref: cabana/query_test.go#TestListQueryEmpty
status: pass
- kind: integration
ref: cabana/query_test.go#TestListQuerySingle
status: pass
- kind: integration
ref: cabana/query_test.go#TestListQueryAdjacent
status: pass
human_judgment: false
- id: D8
description: Switch, date-range, and model-scope filters narrow rows through bound values and the registered scope name.
requirement: ADMIN-02
verification:
- kind: integration
ref: cabana/query_test.go#TestListQueryFilters
status: pass
human_judgment: false
- id: D9
description: Unknown or case-changed identifiers, oversized pages, and injected search text fail closed, and permission denial still runs before the query.
requirement: ADMIN-02
verification:
- kind: integration
ref: cabana/query_test.go#TestListQueryRejectsInjection
status: pass
human_judgment: false
duration: 36min
completed: 2026-09-24
status: complete
plan_head_before: db3d7222e9cf733b9926e29921c5e74d6abab579
plan_head_after: 3efdcc1c59a94533f28427495bfe0a646aa3fd4e
---
# Phase 9 Plan 04: Deterministic admin list queries Summary
**Winter list YAML now compiles to ordered columns and typed filters, and the admin index runs that schema through bound, tie-broken queries.**
## Performance
- **Duration:** 36 min
- **Started:** 2026-09-24T16:29:24Z
- **Completed:** 2026-09-24T17:05:17Z
- **Tasks:** 3
- **Files modified:** 13
## Accomplishments
- `config_list.yaml` and `columns.yaml` compile to ordered columns, default sort, `searchTerm: "search"`, page-size choices, toolbar create, row update, and bulk delete. Empty collections marshal as `[]`.
- `config_filter.yaml` accepts only switch, daterange, and a model scope named by `FilterScopes()`. A `conditions` key fails activation. Option values keep their JSON types.
- `GET /_admin/api/v1/{vendor}/{plugin}/{controller}` searches, sorts, filters, and pages through those selectors. Equal sort values break ties on the primary key. Unknown identifiers return `validation_failed` after the permission check.
- The 09-01 genre list still compiles, including a controller with no `config_form.yaml`.
## Task Commits
Each task was committed atomically. `commits: 7` is `git rev-list --count` from the plan ledger in summercms.go. The tracer expectation lives in fonoteka.go.
1. **Task 1: Ordered list columns and actions (RED)** - `fd591ed` (test)
2. **Task 1: Ordered list columns and actions (GREEN)** - `aab4398` (feat)
3. **Task 2: Typed filters (RED)** - `cb832eb` (test)
4. **Task 2: Typed filters (GREEN)** - `d3a9307` (feat)
5. **Task 3: Deterministic list queries (RED)** - `7f451c3` (test)
6. **Task 3: Deterministic list queries (GREEN)** - `6a57f63` (feat)
7. **Relation sort default (fix)** - `3efdcc1` (fix)
**App tracer:** `d8fb9ac` in fonoteka.go (test)
**Plan metadata:** included in the docs commit for this summary
## Files Created/Modified
- `cabana/list_schema.go` - strict list compiler, model column check, and request localizer
- `cabana/filter_schema.go` - switch, daterange, and scope compilation
- `cabana/query.go` - allowlisted search, sort, filter, scope, and pagination
- `cabana/schema_types.go` - list, filter, and action JSON types
- `cabana/schema.go` - shared YAML helpers; list compilation moved out
- `cabana/http.go` - schema response and index handler use the compiled list
- `cabana/contracts.go` - D-10 error details for validation
- `pact/capabilities.go` - `FilterScopes()` catalog on `FilterScope`
- `cabana/testdata/list/all_columns.yaml` - column fixture
- `cabana/testdata/list/all_filters.yaml` - filter fixture
- `fonoteka.go/plugins/golem15/fonoteka/admin_tracer_test.go` - unknown sort expects 422
## Decisions Made
- Omitted `sortable` means true, matching Winter, except a relation column, which is not a local column and stays unsortable unless YAML sets `sortable`. An explicit true orders the joined `select` column, then the primary key.
- `perPageOptions` keep source order. `recordsPerPage` must be one of them. The query rejects any other `per_page`.
- `toolbar.buttons: list_toolbar` is the create action. `recordUrl` is the update row action. `showCheckboxes` is bulk delete. Other button names fail activation.
- The search query parameter is always `search`. YAML cannot rename it.
- `conditions` is rejected with the column-or-scope rule. Scope names must appear in `FilterScope.FilterScopes()` exactly.
- D-11 meta is `page`, `per_page`, `total`, `last_page`. `lagoon.Paginate` still computes `last_page`; its `current_page` is not the admin key.
- `ADMIN-02` stays shared with later plans in this phase, so REQUIREMENTS.md is not marked complete by this plan alone.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 2 - Missing Critical] Served the full list schema and validation details**
- **Found during:** Task 1 and Task 3
- **Issue:** The schema handler copied a subset of `ListSchema`, so new slices would marshal as null, and `WriteError` could not attach field messages.
- **Fix:** The schema handler returns the localized schema. `WriteErrorDetails` writes D-10 `validation_failed` details.
- **Files modified:** `cabana/http.go`, `cabana/contracts.go`
- **Verification:** `TestListSchemaEmpty` and `TestListQueryRejectsInjection` passed
- **Committed in:** `aab4398` and `6a57f63`
**2. [Rule 2 - Missing Critical] Added FilterScopes() to the model capability**
- **Found during:** Task 2
- **Issue:** `FilterScope` could execute a name but could not declare the finite set, so an arbitrary method could not be rejected without reflection.
- **Fix:** `FilterScopes() []string` is now part of `pact.FilterScope`. Compilation accepts only those names.
- **Files modified:** `pact/capabilities.go`, `cabana/filter_schema.go`
- **Verification:** `TestListSchemaScope` and `TestListSchemaRejectsRawCondition` passed
- **Committed in:** `d3a9307`
**3. [Rule 1 - Bug] Stopped treating relation columns as sortable by default**
- **Found during:** Task 3
- **Issue:** Omitted `sortable` became true for relation columns, while a sort on that key is not a local column.
- **Fix:** Relation columns default to not sortable. Explicit `sortable: true` orders the joined select column and then the primary key.
- **Files modified:** `cabana/list_schema.go`, `cabana/query.go`
- **Verification:** `go test ./cabana -run '^(TestListSchema|TestListQuery)' -count=1` passed
- **Committed in:** `3efdcc1`
**4. [Rule 1 - Bug] Updated the genre tracer for unknown sorts**
- **Found during:** Task 3
- **Issue:** `TestAdminTracerGenreList` expected `sort=users.email` to return 200. The new contract rejects that identifier.
- **Fix:** The persisted genre is still read without a caller sort. The unknown sort expects `validation_failed` and the body does not echo the identifier.
- **Files modified:** `fonoteka.go/plugins/golem15/fonoteka/admin_tracer_test.go`
- **Verification:** `go test ./plugins/golem15/fonoteka -run '^TestAdminTracerGenreList$' -count=1` passed
- **Committed in:** `d8fb9ac` (fonoteka.go)
---
**Total deviations:** 4 auto-fixed (2 missing critical, 2 bug)
**Impact on plan:** The extra capability and response fields are the locked list contract. No new dependency and no change to the frontend API.
## TDD Gate Compliance
| Gate | Commit | Result |
|------|--------|--------|
| RED Task 1 | `fd591ed` | `TestListSchemaCompile` failed because `perPageOptions` was unknown. `TestListSchemaEmpty` compared the old four-key JSON. |
| GREEN Task 1 | `aab4398` | `TestListSchema(Compile\|Empty\|Single\|Ordering\|Rejects)` passed |
| RED Task 2 | `cb832eb` | `TestListSchemaFilter` failed on unknown field `filter` |
| GREEN Task 2 | `d3a9307` | `TestListSchema(Filter\|Scope\|RejectsRawCondition)` passed |
| RED Task 3 | `7f451c3` | `TestListQueryContract` returned every row for `search=Other` |
| GREEN Task 3 | `6a57f63` | `TestListQuery(Contract\|Empty\|Single\|Adjacent\|Filters\|RejectsInjection)` passed |
## Issues Encountered
None.
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
Ready for 09-05. List compilation and the index query are reusable by the later controller plans. `ADMIN-02` remains pending until every plan that declares it has a summary.
## Self-Check: PASSED
- FOUND: cabana/list_schema.go
- FOUND: cabana/filter_schema.go
- FOUND: cabana/query.go
- FOUND: cabana/testdata/list/all_columns.yaml
- FOUND: cabana/testdata/list/all_filters.yaml
- FOUND: fd591ed
- FOUND: aab4398
- FOUND: cb832eb
- FOUND: d3a9307
- FOUND: 7f451c3
- FOUND: 6a57f63
- FOUND: 3efdcc1
- FOUND: d8fb9ac
RED evidence for `TestListSchemaCompile`, `TestListSchemaFilter`, and `TestListQueryContract` was checked with `gsd_run check tdd-red-evidence` and returned `RED_EVIDENCE_OK` before each implementation commit.
---
*Phase: 09-backend-admin-authentication-and-schema-pipeline*
*Completed: 2026-09-24*