From ba243c223da647cf2b85db851cd718448b41c2ea Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Fri, 27 Mar 2026 18:07:51 -0400 Subject: [PATCH] fix: implement proper 3-state boolean handling in backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated the backend services and handlers to properly detect and pass the has_cover parameter's validity state to the database layer. Changes: - services/search.go: Changed HasCover type from bool to pgtype.Bool to support 3-state logic (NULL, TRUE, FALSE) - handlers/media.go: Fixed 3-state detection by checking if has_cover exists in query params before setting Valid flag - router/search.go: Fixed 3-state detection to match media.go logic - router/frontend.go: Use pgtype.Bool{Valid: false} for SSR initial load to ensure no filtering occurs on first page load The key fix is detecting whether the has_cover parameter was actually sent in the request: - Parameter not sent → pgtype.Bool{Bool: false, Valid: false} - Parameter sent as "true" → pgtype.Bool{Bool: true, Valid: true} - Parameter sent as "false" → pgtype.Bool{Bool: false, Valid: true} Previously, media.go was hardcoding Valid: true, which meant it was always filtering by has_cover=false (only books without covers) when the parameter wasn't sent, causing searches to incorrectly return 0 results for queries like "1984". This ensures consistency between the JSON API endpoint (media.go) and the HTML endpoint (search.go), and fixes the critical bug where SSR was returning 0 books on initial page load. --- internal/handlers/media.go | 9 +++++++-- internal/router/frontend.go | 2 +- internal/router/search.go | 9 +++++++-- internal/services/search.go | 4 ++-- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/internal/handlers/media.go b/internal/handlers/media.go index 56ff196..6e08679 100644 --- a/internal/handlers/media.go +++ b/internal/handlers/media.go @@ -1424,7 +1424,12 @@ func (mh *MediaHandler) SearchMediaItems(c *echo.Context) error { languageFilter := c.QueryParam("language_filter") yearMin, _ := strconv.Atoi(c.QueryParam("year_min")) yearMax, _ := strconv.Atoi(c.QueryParam("year_max")) - hasCover := c.QueryParam("has_cover") == "true" + hasCover := false + hasCoverValid := false + if _, exists := c.QueryParams()["has_cover"]; exists { + hasCover = c.QueryParam("has_cover") == "true" + hasCoverValid = true + } // Extract sort parameter sortParam := c.QueryParam("sort") @@ -1444,7 +1449,7 @@ func (mh *MediaHandler) SearchMediaItems(c *echo.Context) error { LanguageFilter: languageFilter, YearMin: yearMin, YearMax: yearMax, - HasCover: hasCover, + HasCover: pgtype.Bool{Bool: hasCover, Valid: hasCoverValid}, Sort: sortParam, Limit: limit, Offset: offset, diff --git a/internal/router/frontend.go b/internal/router/frontend.go index 3accf2b..022fb14 100644 --- a/internal/router/frontend.go +++ b/internal/router/frontend.go @@ -212,7 +212,7 @@ func registerFrontendRoutes(cfg *Config) { LanguageFilter: "", YearMin: 0, YearMax: 0, - HasCover: false, + HasCover: pgtype.Bool{Valid: false}, Sort: "created_at DESC", Limit: limit, Offset: offset, diff --git a/internal/router/search.go b/internal/router/search.go index 2b3eb28..8602df5 100644 --- a/internal/router/search.go +++ b/internal/router/search.go @@ -67,7 +67,12 @@ func handleSearchHTML(c *echo.Context, cfg *Config) error { languageFilter := c.QueryParam("language_filter") yearMin, _ := strconv.Atoi(c.QueryParam("year_min")) yearMax, _ := strconv.Atoi(c.QueryParam("year_max")) - hasCover := c.QueryParam("has_cover") == "true" + hasCover := false + hasCoverValid := false + if _, exists := c.QueryParams()["has_cover"]; exists { + hasCover = c.QueryParam("has_cover") == "true" + hasCoverValid = true + } sortParam := c.QueryParam("sort") if sortParam == "" { sortParam = "title ASC" @@ -84,7 +89,7 @@ func handleSearchHTML(c *echo.Context, cfg *Config) error { LanguageFilter: languageFilter, YearMin: yearMin, YearMax: yearMax, - HasCover: hasCover, + HasCover: pgtype.Bool{Bool: hasCover, Valid: hasCoverValid}, Sort: sortParam, Limit: limit, Offset: offset, diff --git a/internal/services/search.go b/internal/services/search.go index 2e41fba..eea0afd 100644 --- a/internal/services/search.go +++ b/internal/services/search.go @@ -30,7 +30,7 @@ type SearchParams struct { LanguageFilter string YearMin int YearMax int - HasCover bool + HasCover pgtype.Bool SearchQuery string Sort string Limit int @@ -76,7 +76,7 @@ func (s *SearchService) SearchMediaItemsUnified(ctx context.Context, params Sear LanguageFilter: pgtype.Text{String: params.LanguageFilter, Valid: true}, YearMin: pgtype.Int4{Int32: int32(params.YearMin), Valid: true}, YearMax: pgtype.Int4{Int32: int32(params.YearMax), Valid: true}, - HasCover: pgtype.Bool{Bool: params.HasCover, Valid: true}, + HasCover: params.HasCover, SearchQuery: pgtype.Text{String: searchQuery, Valid: true}, IsExactSearch: pgtype.Bool{Bool: isExact, Valid: true}, // SearchPattern: pgtype.Text{String: searchPattern, Valid: isExact},