298 lines
13 KiB
Markdown
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*
|