refactor(collections): rule evaluators take the real row; both 45-field converter twins deleted

checkRulesAgainstBook/evaluateRule and the service's EvaluateRules
existed in two parallel universes: one over database.MediaItems (what
every query actually returns) and one over ListMediaItemsRow (what two
hand-written 45-field mediaItemsToListMediaItemsRow converters
produced — verbatim twins in the handler and the dashboard service,
with a URL-resolution side effect baked into a "converter"). The rule
fields (genre/series/author/language/publisher/copyright_year/tags)
all exist on MediaItems, so the evaluators now take the real row and
the converters, their three call-site loops, and the dead
ApplyAutoAssignRules entry point (zero callers) are gone.

The rules surface is wire-covered by the collections rules scenarios;
handlers+services suites and the full pin/H1/H3 battery green.
This commit is contained in:
John O'Keefe committed 2026-10-17 16:48:39 -04:00
1 parent b47a2b86a7
commit 18727fef93
7 files changed
+54 -193

No files matched your search

+4 -59
View File
@@ -946,13 +946,8 @@ func (h *CollectionHandler) TestRules(c *echo.Context) error {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to load media items"})
}
listRows := make([]database.ListMediaItemsRow, len(mediaItems))
for i, item := range mediaItems {
listRows[i] = mediaItemsToListMediaItemsRow(item)
}
var matches []BookMatch
for _, item := range listRows {
for _, item := range mediaItems {
matchReason := h.checkRulesAgainstBook(item, req.Rules)
if matchReason != "" {
author := ""
@@ -976,7 +971,7 @@ func (h *CollectionHandler) TestRules(c *echo.Context) error {
})
}
func (h *CollectionHandler) checkRulesAgainstBook(item database.ListMediaItemsRow, rules []map[string]interface{}) string {
func (h *CollectionHandler) checkRulesAgainstBook(item database.MediaItems, rules []map[string]interface{}) string {
for _, rule := range rules {
field, _ := rule["field"].(string)
operator, _ := rule["operator"].(string)
@@ -989,7 +984,7 @@ func (h *CollectionHandler) checkRulesAgainstBook(item database.ListMediaItemsRo
return ""
}
func (h *CollectionHandler) evaluateRule(item database.ListMediaItemsRow, field, operator, value string) bool {
func (h *CollectionHandler) evaluateRule(item database.MediaItems, field, operator, value string) bool {
var itemValue string
switch field {
@@ -1209,8 +1204,7 @@ func (h *CollectionHandler) PreviewCollection(c *echo.Context) error {
var matchedItems []database.MediaItems
for _, item := range allItems {
listItem := mediaItemsToListMediaItemsRow(item)
evaluations := h.collectionService.EvaluateRules(listItem, req.Rules)
evaluations := h.collectionService.EvaluateRules(item, req.Rules)
for _, eval := range evaluations {
if eval.Matches {
matchedItems = append(matchedItems, item)
@@ -1255,52 +1249,3 @@ func (h *CollectionHandler) PreviewCollection(c *echo.Context) error {
return c.JSON(http.StatusOK, map[string]interface{}{"items": bookCards})
}
func mediaItemsToListMediaItemsRow(item database.MediaItems) database.ListMediaItemsRow {
return database.ListMediaItemsRow{
ID: item.ID,
LibraryID: item.LibraryID,
Title: item.Title,
Author: item.Author,
Isbn: item.Isbn,
Description: item.Description,
FilePath: utils.ResolveMediaURL(item.LibraryID, pgtype.Text{String: item.FilePath, Valid: true}),
FileSize: item.FileSize,
MimeType: item.MimeType,
CoverImagePath: pgtype.Text{String: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath), Valid: true},
Series: item.Series,
SeriesNumber: item.SeriesNumber,
Tags: item.Tags,
Asin: item.Asin,
DatePublished: item.DatePublished,
Publisher: item.Publisher,
Contributors: item.Contributors,
Language: item.Language,
Edition: item.Edition,
PageCount: item.PageCount,
Genre: item.Genre,
CopyrightYear: item.CopyrightYear,
GoodreadsID: item.GoodreadsID,
OpenlibraryID: item.OpenlibraryID,
GoogleBooksID: item.GoogleBooksID,
AddedByAdminID: item.AddedByAdminID,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
FormatGroup: item.FormatGroup,
FormatMimetype: item.FormatMimetype,
IsReflowable: item.IsReflowable,
HasFixedLayout: item.HasFixedLayout,
TotalCharacters: item.TotalCharacters,
ChapterCount: item.ChapterCount,
EntitlementID: item.EntitlementID,
RevisionNumber: item.RevisionNumber,
KoboContentID: item.KoboContentID,
KoboMetadata: item.KoboMetadata,
TagsSearch: item.TagsSearch,
ContributorsSearch: item.ContributorsSearch,
FileSha256: item.FileSha256,
OpfIdentifier: item.OpfIdentifier,
OpfUuid: item.OpfUuid,
HashConfidence: item.HashConfidence,
}
}
+22 -22
View File
@@ -13,7 +13,7 @@ import (
func TestEvaluateRule_Equals_StringMatch(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
Genre: pgtype.Text{String: "Science Fiction", Valid: true},
}
@@ -23,7 +23,7 @@ func TestEvaluateRule_Equals_StringMatch(t *testing.T) {
func TestEvaluateRule_Equals_NoMatch(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
Genre: pgtype.Text{String: "Fantasy", Valid: true},
}
@@ -33,7 +33,7 @@ func TestEvaluateRule_Equals_NoMatch(t *testing.T) {
func TestEvaluateRule_NotEquals_Matches(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
Genre: pgtype.Text{String: "Fantasy", Valid: true},
}
@@ -43,7 +43,7 @@ func TestEvaluateRule_NotEquals_Matches(t *testing.T) {
func TestEvaluateRule_NotEquals_NoMatch(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
Genre: pgtype.Text{String: "Science Fiction", Valid: true},
}
@@ -53,7 +53,7 @@ func TestEvaluateRule_NotEquals_NoMatch(t *testing.T) {
func TestEvaluateRule_Contains_Matches(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
Author: pgtype.Text{String: "Isaac Asimov", Valid: true},
}
@@ -63,7 +63,7 @@ func TestEvaluateRule_Contains_Matches(t *testing.T) {
func TestEvaluateRule_Contains_NoMatch(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
Author: pgtype.Text{String: "Isaac Asimov", Valid: true},
}
@@ -73,7 +73,7 @@ func TestEvaluateRule_Contains_NoMatch(t *testing.T) {
func TestEvaluateRule_NotContains_Matches(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
Author: pgtype.Text{String: "Isaac Asimov", Valid: true},
}
@@ -83,7 +83,7 @@ func TestEvaluateRule_NotContains_Matches(t *testing.T) {
func TestEvaluateRule_StartsWith_Matches(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
Series: pgtype.Text{String: "Foundation", Valid: true},
}
@@ -93,7 +93,7 @@ func TestEvaluateRule_StartsWith_Matches(t *testing.T) {
func TestEvaluateRule_StartsWith_NoMatch(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
Series: pgtype.Text{String: "Foundation", Valid: true},
}
@@ -103,7 +103,7 @@ func TestEvaluateRule_StartsWith_NoMatch(t *testing.T) {
func TestEvaluateRule_EndsWith_Matches(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
Series: pgtype.Text{String: "Foundation", Valid: true},
}
@@ -113,7 +113,7 @@ func TestEvaluateRule_EndsWith_Matches(t *testing.T) {
func TestEvaluateRule_GreaterThan_Matches(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
CopyrightYear: pgtype.Int4{Int32: 2021, Valid: true},
}
@@ -123,7 +123,7 @@ func TestEvaluateRule_GreaterThan_Matches(t *testing.T) {
func TestEvaluateRule_GreaterThan_NoMatch(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
CopyrightYear: pgtype.Int4{Int32: 1999, Valid: true},
}
@@ -133,7 +133,7 @@ func TestEvaluateRule_GreaterThan_NoMatch(t *testing.T) {
func TestEvaluateRule_LessThan_Matches(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
CopyrightYear: pgtype.Int4{Int32: 1999, Valid: true},
}
@@ -143,7 +143,7 @@ func TestEvaluateRule_LessThan_Matches(t *testing.T) {
func TestEvaluateRule_LessThan_NoMatch(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
CopyrightYear: pgtype.Int4{Int32: 2021, Valid: true},
}
@@ -153,7 +153,7 @@ func TestEvaluateRule_LessThan_NoMatch(t *testing.T) {
func TestEvaluateRule_NullField(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
Genre: pgtype.Text{Valid: false}, // NULL
}
@@ -195,7 +195,7 @@ func TestCompareValues_NumericEdgeCases(t *testing.T) {
func TestCheckRulesAgainstBook_MatchesFirstRule(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
Genre: pgtype.Text{String: "Science Fiction", Valid: true},
}
@@ -219,7 +219,7 @@ func TestCheckRulesAgainstBook_MatchesFirstRule(t *testing.T) {
func TestCheckRulesAgainstBook_MatchesSecondRule(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
Genre: pgtype.Text{String: "Fantasy", Valid: true},
Author: pgtype.Text{String: "Isaac Asimov", Valid: true},
CopyrightYear: pgtype.Int4{Int32: 2021, Valid: true},
@@ -245,7 +245,7 @@ func TestCheckRulesAgainstBook_MatchesSecondRule(t *testing.T) {
func TestCheckRulesAgainstBook_NoMatches(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
Genre: pgtype.Text{String: "Mystery", Valid: true},
Author: pgtype.Text{String: "Agatha Christie", Valid: true},
CopyrightYear: pgtype.Int4{Int32: 1950, Valid: true},
@@ -270,7 +270,7 @@ func TestCheckRulesAgainstBook_NoMatches(t *testing.T) {
func TestCheckRulesAgainstBook_EmptyRules(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
Genre: pgtype.Text{String: "Science Fiction", Valid: true},
}
@@ -286,7 +286,7 @@ func TestComplexRule_MultipleConditions(t *testing.T) {
handler := &CollectionHandler{}
// Book: "Foundation" by Isaac Asimov, Sci-Fi, 1951
item := database.ListMediaItemsRow{
item := database.MediaItems{
Title: "Foundation",
Author: pgtype.Text{String: "Isaac Asimov", Valid: true},
Genre: pgtype.Text{String: "Science Fiction", Valid: true},
@@ -371,7 +371,7 @@ func TestComplexRule_MultipleConditions(t *testing.T) {
func TestEvaluateRule_InvalidOperator(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
Genre: pgtype.Text{String: "Science Fiction", Valid: true},
}
@@ -381,7 +381,7 @@ func TestEvaluateRule_InvalidOperator(t *testing.T) {
func TestEvaluateRule_NonExistentField(t *testing.T) {
handler := &CollectionHandler{}
item := database.ListMediaItemsRow{
item := database.MediaItems{
Genre: pgtype.Text{String: "Science Fiction", Valid: true},
}
+3 -3
View File
@@ -51,7 +51,7 @@ func TestCompareValues_NotEquals(t *testing.T) {
}
func TestEvaluateRule_Genre(t *testing.T) {
item := database.ListMediaItemsRow{
item := database.MediaItems{
Genre: pgtype.Text{String: "Science Fiction", Valid: true},
}
@@ -65,7 +65,7 @@ func TestEvaluateRule_Genre(t *testing.T) {
}
func TestEvaluateRule_Author(t *testing.T) {
item := database.ListMediaItemsRow{
item := database.MediaItems{
Author: pgtype.Text{String: "Isaac Asimov", Valid: true},
}
@@ -79,7 +79,7 @@ func TestEvaluateRule_Author(t *testing.T) {
}
func TestEvaluateRule_CopyrightYear(t *testing.T) {
item := database.ListMediaItemsRow{
item := database.MediaItems{
CopyrightYear: pgtype.Int4{Int32: 2021, Valid: true},
}