Files
summercms/.planning/phases/03-first-vertical-slice-genres-end-to-end/03-02-PLAN.md
2026-09-17 18:40:13 +02:00

11 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
03-first-vertical-slice-genres-end-to-end 02 execute 2
03-01
lagoon/order.go
../fonoteka.go/plugins/golem15/fonoteka/active_collection.go
../fonoteka.go/plugins/golem15/fonoteka/genre.go
../fonoteka.go/plugins/golem15/fonoteka/genre_handler.go
../fonoteka.go/plugins/golem15/fonoteka/plugin.go
../fonoteka.go/plugins/golem15/fonoteka/migrations.go
../fonoteka.go/parity/genre_integration_test.go
true
DATA-01
HTTP-02
QA-04
truths artifacts key_links
D-01 D-02 D-03: the genre count comes from albums accessible to the authenticated user in the active collection; owner and editor access are preserved and foreign albums never count.
D-04 D-05: `non_empty=1` keeps only positive-count genres; invalid values return PHP's 422 envelope; a Postgres test proves positive, zero and forbidden counts.
D-06: a reusable allow listed lagoon ordering helper is called at the PHP PolishOrder call site and uses the database default ICU pl-PL locale confirmed by the user.
The handler returns a dedicated DTO with id, name, slug and integer album_count under data, with [] for an empty result.
path provides
lagoon/order.go Safe database-default ordering helper
path provides
../fonoteka.go/plugins/golem15/fonoteka/active_collection.go Default active collection selection and access predicate
path provides
../fonoteka.go/plugins/golem15/fonoteka/genre_handler.go PHP-fidelity aggregate query and response
path provides
../fonoteka.go/parity/genre_integration_test.go Postgres proof unavailable from the zero-count fixture
from to via
../fonoteka.go/plugins/golem15/fonoteka/genre_handler.go ../fonoteka.go/plugins/golem15/fonoteka/active_collection.go authenticated user and active collection lookup
from to via
../fonoteka.go/plugins/golem15/fonoteka/genre_handler.go lagoon/order.go allow listed name ordering call
from to via
../fonoteka.go/parity/genre_integration_test.go ../fonoteka.go/plugins/golem15/fonoteka/genre_handler.go real HTTP request over testcontainers Postgres
**As a** Fonoteka user, **I want to** see the global genre list with album counts from my active collection, **so that** the API has the same tenant scoped behavior as PHP.

Purpose: Replace the Plan 01 zero-count placeholder with the complete PHP read path and prove the path the recorded fixture cannot exercise. Output: Active collection service, scoped aggregate query, query validation, database-default Polish order helper, and a nonzero-count integration test.

<execution_context> @/home/jin/.codex/get-shit-done/workflows/execute-plan.md @/home/jin/.codex/get-shit-done/templates/summary.md </execution_context>

@.planning/phases/03-first-vertical-slice-genres-end-to-end/03-CONTEXT.md @.planning/phases/03-first-vertical-slice-genres-end-to-end/03-RESEARCH.md @.planning/phases/03-first-vertical-slice-genres-end-to-end/03-VALIDATION.md @.planning/phases/03-first-vertical-slice-genres-end-to-end/03-01-SUMMARY.md Plan 01 provides app-scoped `*gorm.DB`, authenticated user in request context, `golem15_fonoteka_genres`, minimal users/collections/albums/editor/context tables, and a mounted `GET /_fonoteka/api/v1/genres` handler. The PHP controller uses an accessible-albums subquery narrowed to the active collection, groups by `genre_id`, left joins counts to all genres, and calls `PolishOrder::apply` before optional `non_empty` filtering. The confirmed Postgres choice is a database-level ICU `pl-PL` locale, so the helper emits an ordinary ORDER BY. Task 1: Count only albums visible in the active collection ../fonoteka.go/plugins/golem15/fonoteka/active_collection.go, ../fonoteka.go/plugins/golem15/fonoteka/genre.go, ../fonoteka.go/plugins/golem15/fonoteka/genre_handler.go, ../fonoteka.go/plugins/golem15/fonoteka/plugin.go, ../fonoteka.go/plugins/golem15/fonoteka/migrations.go, ../fonoteka.go/parity/genre_integration_test.go ../fonoteka.go/plugins/golem15/fonoteka/genre.go, ../fonoteka.go/plugins/golem15/fonoteka/plugin.go, ../fonoteka.go/plugins/golem15/fonoteka/migrations.go, ../fonoteka.go/parity/synthetic_test.go, ../fonoteka.go/parity/parity_test.go, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/GenreApiController.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/classes/ActiveCollectionResolver.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/Album.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/Collection.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/UserCollectionContext.php, .planning/phases/03-first-vertical-slice-genres-end-to-end/03-CONTEXT.md Port only `ActiveCollectionResolver::resolve`'s JWT default path: read `golem15_fonoteka_user_collection_contexts` for the authenticated user, accept the selected collection only when the user owns it or appears in `golem15_fonoteka_collection_editors`, otherwise choose the lowest-ID accessible real collection (`kind='collection'`). Store the fallback context if the PHP path does; do not add token pinning, invitation handling, switching or auto-provisioning. Define the owner OR editor condition in a grouped predicate, then AND it with `collection_id = active.id` in the albums subquery. In the genre handler, use GORM to select album IDs visible through that predicate, aggregate `COUNT(*)` by `genre_id`, left join to all genres, select exactly `id`, `name`, `slug`, `COALESCE(album_count,0)`, and serialize through a separate `GenreAggregate` DTO. Keep Postgres column aliases and `ORDER BY` injection safe. If Plan 01's minimal schema lacks a column needed by this path, append a new numbered ALTER migration; never rewrite a Plan 01 migration. Add a focused testcontainers HTTP test with Alice's owned collection, an editor-shared collection, a foreign collection, positive and zero genre counts, and an active-context switch. Assert foreign albums do not enter Alice's count. go vet ./... && go test ./... && (cd ../fonoteka.go && go vet ./... && go test ./... -run 'TestGenre' -count=1) - Counts for Alice's active collection equal the inserted accessible albums for each genre; another user's albums remain excluded even when the genre IDs overlap. - An editor-shared collection is accessible, a foreign collection is not, and an invalid stored context falls back to the lowest-ID accessible real collection. - SQL keeps owner/editor as one grouped OR before collection and genre restrictions; an initialized zero-count genre remains in the list. - App and framework vet/test exit 0 at commit. The authenticated list reports real tenant scoped counts rather than a constant zero. Task 2: Match PHP filter, validation, ordering and JSON shape lagoon/order.go, ../fonoteka.go/plugins/golem15/fonoteka/genre_handler.go, ../fonoteka.go/parity/genre_integration_test.go lagoon/connection.go, ../fonoteka.go/plugins/golem15/fonoteka/genre_handler.go, ../fonoteka.go/parity/genre_integration_test.go, ../fonoteka.go/parity/fixtures/routes/get_genres_jwt.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/GenreApiController.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/classes/PolishOrder.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/traits/SerializesFonoteka.php, /media/nvme/dev/golem15/fonoteka/modules/system/lang/pl/validation.php, .planning/phases/03-first-vertical-slice-genres-end-to-end/03-CONTEXT.md, .planning/phases/03-first-vertical-slice-genres-end-to-end/03-RESEARCH.md Interpret absent or `non_empty=0` as the full list; `non_empty=1` adds `COALESCE(counts.album_count,0) > 0`; `non_empty` outside `0|1` returns status 422 with `{"error":"Validation failed","errors":{"non_empty":[...]}}` using the PHP Polish `validation.in` message from the referenced language file. Match PHP's handling of duplicate query keys by checking the isolated PHP endpoint before encoding a rule in Go. Add a reusable `lagoon` order helper that accepts only configured qualified columns and `asc|desc`, rejects every other identifier/direction, and applies normal `ORDER BY golem15_fonoteka_genres.name ASC` without `COLLATE`, relying on the confirmed database default ICU `pl-PL`. Call the helper where the PHP controller calls `PolishOrder::apply`. Keep a dedicated DTO with integer `id` and `album_count`, a `data` array (never null), `Content-Type: application/json`, and `Cache-Control: no-cache, private`. Extend the Postgres test to compare the complete 15-name sequence from the recorded fixture and a positive-count `non_empty=1` response; a database with the wrong provider/locale must fail before the query. go vet ./... && go test ./... && (cd ../fonoteka.go && go vet ./... && go test ./... -run 'TestGenre' -count=1) - `non_empty=1` returns only positive-count genres and invalid `non_empty` returns 422 with `error` and `errors.non_empty` in PHP shape. - Ordinary GORM ordering on ICU `pl-PL` database yields the exact 15-row order recorded by PHP; the handler calls the allow listed `lagoon` helper. - An empty result is `{"data":[]}`, and `id`/`album_count` remain JSON numbers rather than strings. - Root and app vet/test exit 0 at commit. The route's query, filter, ordering and envelope match the PHP behavior beyond the zero-count fixture.

<threat_model>

Trust Boundaries

Boundary Description
Authenticated user and context → SQL scope An incorrect OR/AND grouping can expose another tenant's album counts.
Query values and order identifiers → SQL Untrusted filter values and dynamic ORDER BY input can alter query behavior.

STRIDE Threat Register

Threat ID Category Severity Component Disposition Mitigation Plan
T-03-04 Information disclosure high active collection and album count query mitigate Group owner/editor OR, AND active collection, test foreign albums and invalid context.
T-03-05 Tampering medium lagoon ordering and non_empty mitigate Allow listed identifiers/directions, explicit `0
T-03-01 Tampering medium appended migrations mitigate Never edit a shipped migration; append reversible ALTER if needed.
</threat_model>
Run focused app Postgres HTTP tests for owned, editor, foreign and zero counts; invalid filter; 15-name order; and wrong-locale startup. Run both modules' vet/test after each task.

<success_criteria> The real JWT route returns the PHP-shaped, collection-scoped aggregate with tested nonzero counts, non_empty, and Polish database ordering. </success_criteria>

Create `.planning/phases/03-first-vertical-slice-genres-end-to-end/03-02-SUMMARY.md` after completion.