feat(calendar): generalize personal filter to an id-set restriction

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Quick
2026-05-29 13:49:34 -04:00
co-authored by Claude Opus 4.8
parent f113b32de0
commit 62b3861da0
2 changed files with 80 additions and 37 deletions
+36 -37
View File
@@ -32,13 +32,17 @@ type CalendarEvent struct {
type CalendarFilter struct {
Start time.Time
End time.Time
Filter string // "all" | "favorites" | "watchlist"
LibraryID *int
AllowedLibraryIDs []int
DisabledLibraryIDs []int
MaxContentRating string
UserID int
ProfileID string
// RestrictByIDs limits results to items whose movie content_id (movies) or
// series_id (episodes / season premieres) is in RestrictToIDs. When
// RestrictByIDs is true and RestrictToIDs is empty, no rows match. Callers
// resolve the id-set (Following / Popular / Trending) before querying.
RestrictByIDs bool
RestrictToIDs []string
}
// CalendarRepository provides calendar queries across movies, episodes, and seasons.
@@ -102,9 +106,18 @@ func (r *CalendarRepository) buildListEventsQuery(f CalendarFilter) (string, []a
args = append(args, f.End)
argIdx++
movieBranch := r.buildMovieBranch(startArg, endArg, f, &args, &argIdx)
filteredEpisodes := r.buildFilteredEpisodesCTE(startArg, endArg, f, &args, &argIdx)
filteredSeasons := r.buildFilteredSeasonsCTE(startArg, endArg, f, &args, &argIdx)
// Optional id-set restriction, appended once and shared by all branches.
// restrictArg stays 0 when the set is empty so branches short-circuit.
restrictArg := 0
if f.RestrictByIDs && len(f.RestrictToIDs) > 0 {
restrictArg = argIdx
args = append(args, f.RestrictToIDs)
argIdx++
}
movieBranch := r.buildMovieBranch(startArg, endArg, restrictArg, f, &args, &argIdx)
filteredEpisodes := r.buildFilteredEpisodesCTE(startArg, endArg, restrictArg, f, &args, &argIdx)
filteredSeasons := r.buildFilteredSeasonsCTE(startArg, endArg, restrictArg, f, &args, &argIdx)
episodeBranch := r.buildEpisodeBranch()
seasonBranch := r.buildSeasonBranch()
@@ -139,14 +152,14 @@ FROM (
return query, args
}
func (r *CalendarRepository) buildMovieBranch(startArg, endArg int, f CalendarFilter, args *[]any, argIdx *int) string {
func (r *CalendarRepository) buildMovieBranch(startArg, endArg, restrictArg int, f CalendarFilter, args *[]any, argIdx *int) string {
conditions := []string{
"mi.type = 'movie'",
fmt.Sprintf("mi.release_date BETWEEN $%d::date AND $%d::date", startArg, endArg),
}
r.appendLibraryExistsClauses("mi.content_id", f, &conditions, args, argIdx)
r.appendContentRatingClause("mi", f, &conditions, args, argIdx)
r.appendPersonalFilterClause("mi.content_id", f, &conditions, args, argIdx)
r.appendRestrictClause("mi.content_id", restrictArg, f, &conditions)
return fmt.Sprintf(`SELECT mi.content_id, 'movie'::text AS type,
mi.title, NULL::text AS episode_title, NULL::text AS series_id,
@@ -158,14 +171,14 @@ FROM media_items mi
WHERE %s`, strings.Join(conditions, " AND "))
}
func (r *CalendarRepository) buildFilteredEpisodesCTE(startArg, endArg int, f CalendarFilter, args *[]any, argIdx *int) string {
func (r *CalendarRepository) buildFilteredEpisodesCTE(startArg, endArg, restrictArg int, f CalendarFilter, args *[]any, argIdx *int) string {
conditions := []string{
fmt.Sprintf("e.air_date BETWEEN $%d::date AND $%d::date", startArg, endArg),
"e.season_number > 0", // exclude specials
}
r.appendLibraryExistsClauses("e.series_id", f, &conditions, args, argIdx)
r.appendContentRatingClause("mi", f, &conditions, args, argIdx)
r.appendPersonalFilterClause("e.series_id", f, &conditions, args, argIdx)
r.appendRestrictClause("e.series_id", restrictArg, f, &conditions)
return fmt.Sprintf(`SELECT e.content_id, e.series_id, e.season_number,
e.episode_number, e.title AS episode_title, e.air_date,
@@ -188,14 +201,14 @@ FROM filtered_episodes fe
LEFT JOIN season_finales sf ON sf.series_id = fe.series_id AND sf.season_number = fe.season_number`
}
func (r *CalendarRepository) buildFilteredSeasonsCTE(startArg, endArg int, f CalendarFilter, args *[]any, argIdx *int) string {
func (r *CalendarRepository) buildFilteredSeasonsCTE(startArg, endArg, restrictArg int, f CalendarFilter, args *[]any, argIdx *int) string {
conditions := []string{
fmt.Sprintf("s.air_date BETWEEN $%d::date AND $%d::date", startArg, endArg),
"s.season_number > 0", // exclude specials
}
r.appendLibraryExistsClauses("s.series_id", f, &conditions, args, argIdx)
r.appendContentRatingClause("mi", f, &conditions, args, argIdx)
r.appendPersonalFilterClause("s.series_id", f, &conditions, args, argIdx)
r.appendRestrictClause("s.series_id", restrictArg, f, &conditions)
return fmt.Sprintf(`SELECT s.content_id, s.series_id, s.season_number,
s.title AS episode_title, s.air_date, mi.title AS title, mi.air_time, mi.air_timezone,
@@ -282,30 +295,16 @@ func (r *CalendarRepository) appendLibraryExistsClauses(contentIDExpr string, f
*argIdx++
}
// appendPersonalFilterClause adds favorites/watchlist EXISTS subqueries.
func (r *CalendarRepository) appendPersonalFilterClause(itemIDExpr string, f CalendarFilter, conditions *[]string, args *[]any, argIdx *int) {
switch f.Filter {
case "favorites":
userArg := *argIdx
*args = append(*args, f.UserID)
*argIdx++
profileArg := *argIdx
*args = append(*args, f.ProfileID)
*argIdx++
*conditions = append(*conditions, fmt.Sprintf(
"EXISTS (SELECT 1 FROM user_favorites uf WHERE uf.user_id = $%d AND uf.profile_id = $%d AND uf.media_item_id = %s)",
userArg, profileArg, itemIDExpr,
))
case "watchlist":
userArg := *argIdx
*args = append(*args, f.UserID)
*argIdx++
profileArg := *argIdx
*args = append(*args, f.ProfileID)
*argIdx++
*conditions = append(*conditions, fmt.Sprintf(
"EXISTS (SELECT 1 FROM user_watchlist uw WHERE uw.user_id = $%d AND uw.profile_id = $%d AND uw.media_item_id = %s)",
userArg, profileArg, itemIDExpr,
))
// appendRestrictClause limits a branch to the id-set in CalendarFilter. restrictArg
// is the positional parameter holding the id array (0 when the set is empty). An
// empty restriction matches nothing.
func (r *CalendarRepository) appendRestrictClause(itemIDExpr string, restrictArg int, f CalendarFilter, conditions *[]string) {
if !f.RestrictByIDs {
return
}
if restrictArg == 0 {
*conditions = append(*conditions, "1 = 0")
return
}
*conditions = append(*conditions, fmt.Sprintf("%s = ANY($%d)", itemIDExpr, restrictArg))
}
+44
View File
@@ -102,3 +102,47 @@ func TestBuildListEventsQuery_RejectsExplicitLibraryOutsideAllowedScope(t *testi
t.Fatalf("expected season branch to short-circuit inaccessible library selection, got:\n%s", query)
}
}
func TestBuildListEventsQuery_AppliesIDRestriction(t *testing.T) {
t.Parallel()
repo := &CalendarRepository{}
query, args := repo.buildListEventsQuery(CalendarFilter{
Start: time.Date(2026, time.April, 6, 0, 0, 0, 0, time.UTC),
End: time.Date(2026, time.April, 12, 0, 0, 0, 0, time.UTC),
RestrictByIDs: true,
RestrictToIDs: []string{"series-1", "movie-2"},
})
for _, fragment := range []string{
"mi.content_id = ANY($3)",
"e.series_id = ANY($3)",
"s.series_id = ANY($3)",
} {
if !strings.Contains(query, fragment) {
t.Fatalf("expected query to contain %q, got:\n%s", fragment, query)
}
}
if len(args) != 3 {
t.Fatalf("expected start/end/ids args, got %d", len(args))
}
}
func TestBuildListEventsQuery_EmptyRestrictionMatchesNothing(t *testing.T) {
t.Parallel()
repo := &CalendarRepository{}
query, args := repo.buildListEventsQuery(CalendarFilter{
Start: time.Date(2026, time.April, 6, 0, 0, 0, 0, time.UTC),
End: time.Date(2026, time.April, 12, 0, 0, 0, 0, time.UTC),
RestrictByIDs: true,
RestrictToIDs: nil,
})
if strings.Count(query, "1 = 0") != 3 {
t.Fatalf("expected each branch to short-circuit with 1 = 0, got:\n%s", query)
}
if len(args) != 2 {
t.Fatalf("expected only start/end args, got %d", len(args))
}
}