From 040f3ef81c199c82beec1ee8d4626aa4f85fe19a Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Thu, 24 Sep 2026 19:07:01 +0200 Subject: [PATCH] docs(09-04): complete deterministic admin list queries plan --- .planning/ROADMAP.md | 6 +- .planning/STATE.md | 20 +- .../09-04-SUMMARY.md | 297 ++++++++++++++++++ 3 files changed, 313 insertions(+), 10 deletions(-) create mode 100644 .planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-04-SUMMARY.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index a3fd931..6d6da6b 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -375,7 +375,7 @@ Plans: 4. The relation-manager schema supports search/link/unlink/manage-or-view lists for Collections' editors tab, replacing the `partial` field entirely. 5. Admin CRUD endpoints expose `listExtendQuery`/`formExtendQuery`/`formBeforeCreate`/`formBeforeUpdate`/`relationExtendManageQuery` hooks, bulk delete runs each record's lifecycle hooks, and the Settings model binds to a settings screen through the same schema pipeline. -**Plans**: 3/12 plans executed +**Plans**: 4/12 plans executed **Research flag:** yes Plans: @@ -390,7 +390,7 @@ Plans: - [x] 09-03-PLAN.md — Compile the typed form-schema contract and Winter scaffolding **Wave 4** *(blocked on Wave 3 completion)* -- [ ] 09-04-PLAN.md — Compile the list contract and the allowlisted query engine +- [x] 09-04-PLAN.md — Compile the list contract and the allowlisted query engine **Wave 5** *(blocked on Wave 4 completion)* - [ ] 09-05-PLAN.md — Deliver schema-projected CRUD and transactional bulk deletion @@ -529,7 +529,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → | 6. HTTP routing, auth groups and rate limiting | 14/14 | Complete | 2026-09-21 | | 7. User plugin and authentication | 8/8 | Complete | 2026-09-23 | | 8. OAuth2.1 authorization server | 10/10 | Complete | 2026-09-23 | -| 9. Backend admin authentication and schema pipeline | 3/12 | In Progress| | +| 9. Backend admin authentication and schema pipeline | 4/12 | In Progress| | | 10. Admin Vue SPA | 0/TBD | Not started | - | | 11. Jobs, realtime and search infrastructure | 0/TBD | Not started | - | | 12. Płytarium API — Collections and Albums | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 1651266..e91928f 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,16 +4,16 @@ milestone: v1.0 current_phase: 09 current_phase_name: Backend admin authentication and schema pipeline status: executing -stopped_at: Completed 09-03-PLAN.md -last_updated: "2026-09-24T16:27:09.874Z" +stopped_at: Completed 09-04-PLAN.md +last_updated: "2026-09-24T17:06:46.487Z" last_activity: 2026-09-24 last_activity_desc: Phase 09 execution started -state_head: 68715fc2603f129f0e9f05d8d1db23f5d7594e12 +state_head: 3efdcc1c59a94533f28427495bfe0a646aa3fd4e progress: total_phases: 15 completed_phases: 8 total_plans: 67 - completed_plans: 58 + completed_plans: 59 milestone_name: milestone --- @@ -29,7 +29,7 @@ See: .planning/PROJECT.md (updated 2026-09-16) ## Current Position Phase: 09 (Backend admin authentication and schema pipeline) — EXECUTING -Plan: 4 of 12 +Plan: 5 of 12 Status: Ready to execute Last activity: 2026-09-24 — Phase 09 execution started @@ -112,6 +112,7 @@ Progress: [██████████] 100% | Phase 09 P01 | 26min | 2 tasks | 26 files | | Phase 09 P02 | 22 min | 3 tasks | 14 files | | Phase 09 P03 | 25min | 3 tasks | 10 files | +| Phase 09 P04 | 36min | 3 tasks | 13 files | ## Accumulated Context @@ -281,6 +282,11 @@ Recent decisions affecting current work: - [Phase 09]: Absent config_form.yaml does not fail activation, so the 09-01 Genre list controller still boots - [Phase 09]: YAML option keys keep their JSON scalar type; method options call DropdownOptions with the exact field name - [Phase 09]: make:admin-controller writes controllers/name config_form and config_list pointing at models/name fields and columns +- [Phase 09]: Omitted sortable defaults to true, except relation columns, which stay unsortable unless columns.yaml sets sortable +- [Phase 09]: perPageOptions keep YAML order and must include recordsPerPage +- [Phase 09]: list_toolbar, recordUrl, and showCheckboxes compile to create, update, and delete +- [Phase 09]: A conditions key is a boot error; a model scope must be listed by FilterScopes() +- [Phase 09]: Admin list meta uses D-11 page, while lagoon.Paginate still computes last_page ### Pending Todos @@ -303,6 +309,6 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-09-24T16:27:09.536Z -Stopped at: Completed 09-03-PLAN.md +Last session: 2026-09-24T17:06:46.102Z +Stopped at: Completed 09-04-PLAN.md Resume file: None diff --git a/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-04-SUMMARY.md b/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-04-SUMMARY.md new file mode 100644 index 0000000..6d77da3 --- /dev/null +++ b/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-04-SUMMARY.md @@ -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*