docs(09-04): complete deterministic admin list queries plan
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
---
|
||||
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*
|
||||
Reference in New Issue
Block a user