feat(filters): the rule builder — composed engines over the field registry (faceted plan Phase 9)
- Field registry (services/filter_registry.go): ~55 typed fields — every book column, file/lifecycle facts, the three HOLDING STRUCTURES (library, collections with excluded-aware membership, series with FULL primary+alternate_info membership), and the whole user-relative class (status, progress, last read, my rating, review, notes/highlights presence, note/highlight text, time spent, pages read, collections, added by) reading the VIEWER's rows. Operators are typed constants; negation is an operator. - Composed engines (services/filter_query.go): one Go clause builder over raw pgx feeds the search (identical SearchMediaItemsUnifiedRow shape), the field-aware suggestions, and a true-COUNT live count. Mandatory WHERE head everywhere (family lens + integrity facts). Values are always bound parameters — no user string reaches a field/operator position, ever. - Wire: the qf JSON param (validated strictly — unknown fields/operators/malformed values are 400); qf wins over flat params on hand-crafted URLs; flat deep links keep working unchanged. - Endpoints: GET /api/media-items/filter-count (live count); field-values autocomplete upgraded in place — any registry field key, qf binds the OTHER rules (symmetric under-filter suggestions, zero-match values never offered); classic six without a tree keep their static pinned contract. - UI: the drawer becomes the rule builder — the ONLY filter UI (templ markup, Alpine tree state, sanctioned island; grouped field menus, operator menus with red negations, field-aware value menus, live count, chips per rule with red negations, URL rehydration, smart serialization flat-when-simple else qf). - Tests: composed semantics (AND/OR/negation/nesting/enums/bools/ numbers/dates/empty), every user-relative field class with the my-data weld (two readers, different worlds), note-text substring pin (catharsis ≠ Carthage), family lens (hidden libraries nowhere, 403s), suggestion-under-rules exactness, qf validation + precedence, flat-vs-tree equivalence; registry completeness unit test.
This commit is contained in:
1 parent
ebb7f4a5b0
commit
4ee24e7152
18 files changed
+3788
-376
No files matched your search
@@ -301,3 +301,44 @@ cover_image_path, has_conflict, format_group, badge`.
|
||||
the standing rule forbids). The single-facet
|
||||
`/api/media-items/facets?facet=X` shape still REQUIRES `library_id`
|
||||
— that pin is unchanged.
|
||||
|
||||
## 2026-10-20 — Filter rule builder: the `qf` wire param + composed engines (faceted plan Phase 9)
|
||||
|
||||
- NEW wire param on the search endpoint family: `qf` — a JSON rule tree
|
||||
`{mode: all|any, children: [{field, op, values} | {mode, children}]}`.
|
||||
Fields/operators are registry-typed constants
|
||||
(`services/filter_registry.go` — ~55 fields incl. every book column,
|
||||
file/lifecycle facts, the three HOLDING STRUCTURES — library,
|
||||
collections (playlists included, `excluded IS NOT TRUE` membership),
|
||||
series (primary + `alternate_info` full membership) — and the whole
|
||||
user-relative class reading the VIEWER's rows). Unknown fields or
|
||||
operators, disallowed operator/field pairs, malformed values, empty
|
||||
groups, and nesting beyond 8 are a **400** (`ErrInvalidFilterTree`),
|
||||
never a silently-ignored token. Values are always bound pgx
|
||||
parameters — an injection-shaped value is just a value.
|
||||
- **Precedence (pinned)**: when a request carries BOTH `qf` and flat
|
||||
facet params, **qf wins** — the flat params are ignored by the engine.
|
||||
Flat links keep working unchanged server-side (and a tree equivalent
|
||||
to a flat link returns identical rows — pinned by test).
|
||||
- The composed engines (`services/filter_query.go`) run the mandatory
|
||||
WHERE head (visibility join + archived + missing-scan — family lens)
|
||||
for search, suggestions, and count alike; row shape is identical to
|
||||
`SearchMediaItemsUnifiedRow`. A rule naming a hidden library matches
|
||||
nothing; an explicit out-of-set `library_id` stays a 403 on every
|
||||
entry point.
|
||||
- NEW endpoint `GET /api/media-items/filter-count?qf=…[&library_id=…]`
|
||||
— the builder's live count: a true COUNT(*) of the rules + scope
|
||||
(not the page length). Family-lens 403 applies.
|
||||
- Field-values autocomplete UPGRADED in place (same route): ANY registry
|
||||
field key in its bare form (`?note_text=…`, `?collection=…`,
|
||||
`?library=…`) routes to the field-values path; with a `qf` param the
|
||||
OTHER rules bind as filters (symmetric under-filter suggestions,
|
||||
zero-match values never offered). Without a tree, the classic six
|
||||
(author/genre/tags/narrator/series/language) keep their static
|
||||
queries and response shape — pinned contract, unchanged. ID-valued
|
||||
suggestions (library/collection/added_by) add an OPTIONAL
|
||||
`display` field to result items (`value` stays the id; `omitempty`
|
||||
elsewhere).
|
||||
- The Phase-7 facet-panel partial stays removed; the all-facets JSON
|
||||
endpoint and its two shapes are unchanged (they serve flat params —
|
||||
the builder's UI does not use them).
|
||||
Reference in new issue
Block a user