feat(podcasts): smart episode collections — ALL/ANY match modes + episode rule fields (item 2.4)
The daily briefing needs AND: today's rules engine was OR-only. The
match semantics become a per-collection column
(collections.rule_match_mode, 'any' legacy default | 'all'), flipped on
its own endpoint (PUT /api/collections/:id/match-mode, owner-only —
nothing else on the row changes) and surfaced in the rules page as an
ANY/ALL toggle with matching copy.
Episode rule fields join the vocabulary (plan item 2.4): feed (the
multi-select rides the new `in` operator — comma-separated feed ids),
played (the canonical finished predicate — viewer progress ≥ 0.999,
same as hide_finished), downloaded, published_within (N days), plus
title/description contains. The rules candidate query now carries the
viewer's progress (LEFT JOIN reading_progress) so played/unplayed is
per-user, and TestRules/preview speak the SAME evaluator: the handler's
duplicated field switch is deleted and delegates to
CollectionService.FieldMatches — one vocabulary, one semantics.
Legitimate semantics note: in ANY mode `played:false` matches unplayed
BOOKS too (everything with no progress row) — correct per-rule
behavior, and exactly why the briefing mode exists.
Backward compatibility: every legacy row defaults to 'any' (the old
OR), UpdateCollection keeps the stored mode when the form doesn't send
one, and the evaluator treats absent data honestly (no date → no
match; no progress → unplayed).
Pinned by TestPodcastSmartCollections: the AND-mode briefing
(feed∈{A} ∧ unplayed ∧ <7d = exactly the new episode), OR-mode
widening, owner-only flips, the preview honoring the mode, and
downloaded/contains rules. Also fixes a latent bug the new query
surfaced: UpdateCollectionViewSettings referenced a nonexistent
updated_at column (never exercised until now — the new sibling query
was written without it).
This commit is contained in:
1 parent
f68fb19afb
commit
abb7c84833
17 files changed
+694
-204
No files matched your search
@@ -1342,6 +1342,11 @@ CREATE TABLE IF NOT EXISTS collections (
|
||||
color VARCHAR(7),
|
||||
icon VARCHAR(50),
|
||||
auto_assign_rules JSONB,
|
||||
-- Smart-collection match semantics (plan item 2.4): 'any' = OR
|
||||
-- (the original behavior — every legacy row's default), 'all' =
|
||||
-- AND (the daily briefing needs feed∈{…} ∧ unplayed ∧ <7d).
|
||||
-- Applied only when rules exist; static collections ignore it.
|
||||
rule_match_mode TEXT NOT NULL DEFAULT 'any',
|
||||
view_settings JSONB,
|
||||
show_on_dashboard BOOLEAN DEFAULT false,
|
||||
query_type TEXT DEFAULT 'filter',
|
||||
@@ -1357,6 +1362,11 @@ CREATE TABLE IF NOT EXISTS collections (
|
||||
-- the row id (and everything referencing it) never churn.
|
||||
ALTER TABLE collections ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||
|
||||
-- Smart-collection match mode (plan item 2.4): 'any' (OR, legacy
|
||||
-- default) | 'all' (AND — the daily briefing). Idempotent; NOT NULL
|
||||
-- DEFAULT covers existing rows.
|
||||
ALTER TABLE collections ADD COLUMN IF NOT EXISTS rule_match_mode TEXT NOT NULL DEFAULT 'any';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_collections_user_id ON collections(user_id);
|
||||
|
||||
-- Index for dashboard queries
|
||||
|
||||
Reference in new issue
Block a user