diff --git a/cmd/server/tests/cross_library_move_test.go b/cmd/server/tests/cross_library_move_test.go index c8e562f6..05c19340 100644 --- a/cmd/server/tests/cross_library_move_test.go +++ b/cmd/server/tests/cross_library_move_test.go @@ -12,6 +12,7 @@ import ( "bookhoard/internal/database" "bookhoard/internal/services" + wsync "bookhoard/internal/sync" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" @@ -160,11 +161,13 @@ func TestCrossLibraryMovePreservesHistory(t *testing.T) { TotalPages: pgtype.Int4{Int32: 300, Valid: true}, }) require.NoError(t, err) - _, err = setup.DB.CreateMediaHighlight(ctx, database.CreateMediaHighlightParams{ + _, err = setup.AnnotationService.SaveHighlight(ctx, wsync.SaveHighlightRequest{ MediaItemID: original.ID, UserID: adminPgID, SelectionText: "a line worth keeping", - Color: pgtype.Text{String: "#ffff00", Valid: true}, + Color: "#ffff00", + Source: "web", + ModifiedAt: time.Now(), }) require.NoError(t, err) @@ -419,11 +422,13 @@ func archiveAndIsolate(t *testing.T, setup *TestServerSetup, adminID, adminPgID TotalPages: pgtype.Int4{Int32: 200, Valid: true}, }) require.NoError(t, err) - _, err = setup.DB.CreateMediaHighlight(ctx, database.CreateMediaHighlightParams{ + _, err = setup.AnnotationService.SaveHighlight(ctx, wsync.SaveHighlightRequest{ MediaItemID: item.ID, UserID: adminPgID, SelectionText: "annotation on the old copy", - Color: pgtype.Text{String: "#00ff00", Valid: true}, + Color: "#00ff00", + Source: "web", + ModifiedAt: time.Now(), }) require.NoError(t, err) diff --git a/cmd/server/tests/test_helpers_test.go b/cmd/server/tests/test_helpers_test.go index dfff6800..7836c149 100644 --- a/cmd/server/tests/test_helpers_test.go +++ b/cmd/server/tests/test_helpers_test.go @@ -710,18 +710,19 @@ func setupTestServer(t *testing.T) *TestServerSetup { // Create TestServerSetup struct with all resources setup := &TestServerSetup{ - Server: ts, - DB: queries, - DBPool: dbPool, - Config: cfg, - ConnManager: connManager, - QueueProcessor: queueProcessor, - ProgressService: progressService, - CleanupCancel: cleanupCancel, - QueueCtx: queueCtx, - QueueCancel: queueCancel, - Token: adminToken, - RegularToken: regularToken, + Server: ts, + DB: queries, + DBPool: dbPool, + Config: cfg, + ConnManager: connManager, + QueueProcessor: queueProcessor, + ProgressService: progressService, + AnnotationService: annotationService, + CleanupCancel: cleanupCancel, + QueueCtx: queueCtx, + QueueCancel: queueCancel, + Token: adminToken, + RegularToken: regularToken, } // Register cleanup function to run automatically when test completes diff --git a/internal/handlers/cards.go b/internal/handlers/cards.go index 32d1b779..df4a8be5 100644 --- a/internal/handlers/cards.go +++ b/internal/handlers/cards.go @@ -114,7 +114,7 @@ func BookCardsFromMediaItems(rows []database.MediaItems, libTypes map[string]str cards = append(cards, buildBookCard(cardSource{ ID: uuid.UUID(r.ID.Bytes), Title: r.Title, - Author: textToString(r.Author), + Author: utils.PgTextString(r.Author), LibraryID: r.LibraryID, CoverPath: r.CoverImagePath, FormatGroup: r.FormatGroup, @@ -134,7 +134,7 @@ func BookCardsFromCollectionDashRows(rows []database.GetCollectionItemsForDashbo cards = append(cards, buildBookCard(cardSource{ ID: uuid.UUID(r.ID.Bytes), Title: r.Title, - Author: textToString(r.Author), + Author: utils.PgTextString(r.Author), LibraryID: r.LibraryID, CoverPath: r.CoverImagePath, FormatGroup: r.FormatGroup, @@ -154,7 +154,7 @@ func BookCardsFromCollectionItemRows(rows []database.GetCollectionItemsRow, libT cards = append(cards, buildBookCard(cardSource{ ID: uuid.UUID(r.MediaItemID.Bytes), Title: r.Title, - Author: textToString(r.Author), + Author: utils.PgTextString(r.Author), LibraryID: r.LibraryID, CoverPath: r.CoverImagePath, FormatGroup: r.FormatGroup, @@ -173,7 +173,7 @@ func BookCardsFromUnified(rows []database.SearchMediaItemsUnifiedRow) []BookInfo cards = append(cards, buildBookCard(cardSource{ ID: uuid.UUID(r.ID.Bytes), Title: r.Title, - Author: textToString(r.Author), + Author: utils.PgTextString(r.Author), LibraryID: r.LibraryID, CoverPath: r.CoverImagePath, FormatGroup: r.FormatGroup, diff --git a/internal/handlers/collections.go b/internal/handlers/collections.go index 71d2f8e1..3a220031 100644 --- a/internal/handlers/collections.go +++ b/internal/handlers/collections.go @@ -116,9 +116,9 @@ func (h *CollectionHandler) CreateCollection(c *echo.Context) error { "id": uuid.UUID(collection.ID.Bytes).String(), "user_id": uuid.UUID(collection.UserID.Bytes).String(), "name": collection.Name, - "description": textToString(collection.Description), - "color": textToString(collection.Color), - "icon": textToString(collection.Icon), + "description": utils.PgTextString(collection.Description), + "color": utils.PgTextString(collection.Color), + "icon": utils.PgTextString(collection.Icon), "collection_type": map[bool]string{true: "smart", false: "static"}[isSmart], "auto_assign_rules": collection.AutoAssignRules, "view_settings": collection.ViewSettings, @@ -204,9 +204,9 @@ func (h *CollectionHandler) GetCollections(c *echo.Context) error { response = append(response, CollectionResponse{ ID: col.ID.Bytes, Name: col.Name, - Description: textToString(col.Description), - Color: textToString(col.Color), - Icon: textToString(col.Icon), + Description: utils.PgTextString(col.Description), + Color: utils.PgTextString(col.Color), + Icon: utils.PgTextString(col.Icon), AutoAssignRules: col.AutoAssignRules, CreatedAt: col.CreatedAt.Time.String(), BookCount: bookCount, @@ -325,9 +325,9 @@ func (h *CollectionHandler) GetCollection(c *echo.Context) error { "id": uuid.UUID(collection.ID.Bytes).String(), "user_id": uuid.UUID(collection.UserID.Bytes).String(), "name": collection.Name, - "description": textToString(collection.Description), - "color": textToString(collection.Color), - "icon": textToString(collection.Icon), + "description": utils.PgTextString(collection.Description), + "color": utils.PgTextString(collection.Color), + "icon": utils.PgTextString(collection.Icon), "auto_assign_rules": json.RawMessage(collection.AutoAssignRules), "view_settings": viewSettings, "books": bookList, @@ -369,9 +369,9 @@ func (h *CollectionHandler) UpdateCollection(c *echo.Context) error { "id": uuid.UUID(collection.ID.Bytes).String(), "user_id": uuid.UUID(collection.UserID.Bytes).String(), "name": collection.Name, - "description": textToString(collection.Description), - "color": textToString(collection.Color), - "icon": textToString(collection.Icon), + "description": utils.PgTextString(collection.Description), + "color": utils.PgTextString(collection.Color), + "icon": utils.PgTextString(collection.Icon), "auto_assign_rules": json.RawMessage(collection.AutoAssignRules), "view_settings": json.RawMessage(collection.ViewSettings), "created_at": collection.CreatedAt.Time.String(), @@ -580,8 +580,8 @@ func (h *CollectionHandler) GetDeviceMappings(c *echo.Context) error { ID: m.ID.Bytes, CollectionID: m.CollectionID.Bytes, CollectionName: m.CollectionName, - DeviceShelfName: textToString(m.DeviceShelfName), - SyncDirection: textToString(m.SyncDirection), + DeviceShelfName: utils.PgTextString(m.DeviceShelfName), + SyncDirection: utils.PgTextString(m.SyncDirection), CreatedAt: m.CreatedAt.Time.String(), }) } @@ -627,8 +627,8 @@ func (h *CollectionHandler) CreateDeviceMapping(c *echo.Context) error { "id": uuid.UUID(mapping.ID.Bytes).String(), "collection_id": uuid.UUID(mapping.CollectionID.Bytes).String(), "device_id": uuid.UUID(mapping.DeviceID.Bytes).String(), - "device_shelf_name": textToString(mapping.DeviceShelfName), - "sync_direction": textToString(mapping.SyncDirection), + "device_shelf_name": utils.PgTextString(mapping.DeviceShelfName), + "sync_direction": utils.PgTextString(mapping.SyncDirection), "created_at": mapping.CreatedAt.Time.String(), }) } @@ -661,8 +661,8 @@ func (h *CollectionHandler) UpdateDeviceMapping(c *echo.Context) error { "id": uuid.UUID(mapping.ID.Bytes).String(), "collection_id": uuid.UUID(mapping.CollectionID.Bytes).String(), "device_id": uuid.UUID(mapping.DeviceID.Bytes).String(), - "device_shelf_name": textToString(mapping.DeviceShelfName), - "sync_direction": textToString(mapping.SyncDirection), + "device_shelf_name": utils.PgTextString(mapping.DeviceShelfName), + "sync_direction": utils.PgTextString(mapping.SyncDirection), "created_at": mapping.CreatedAt.Time.String(), }) } @@ -705,9 +705,9 @@ func (h *CollectionHandler) GetBookCollections(c *echo.Context) error { response = append(response, CollectionResponse{ ID: col.ID.Bytes, Name: col.Name, - Description: textToString(col.Description), - Color: textToString(col.Color), - Icon: textToString(col.Icon), + Description: utils.PgTextString(col.Description), + Color: utils.PgTextString(col.Color), + Icon: utils.PgTextString(col.Icon), }) } @@ -718,13 +718,6 @@ func (h *CollectionHandler) GetBookCollections(c *echo.Context) error { }) } -func textToString(t pgtype.Text) string { - if t.Valid { - return t.String - } - return "" -} - func (h *CollectionHandler) GetCollectionsData(c *echo.Context) ([]database.Collections, error) { user := c.Get("user").(database.Users) userUUID := uuid.UUID(user.ID.Bytes) diff --git a/internal/handlers/collections_test.go b/internal/handlers/collections_test.go index 5cf2b22b..bb761afa 100644 --- a/internal/handlers/collections_test.go +++ b/internal/handlers/collections_test.go @@ -4,6 +4,7 @@ import ( "testing" "bookhoard/internal/database" + "bookhoard/internal/utils" "github.com/jackc/pgx/v5/pgtype" "github.com/stretchr/testify/assert" @@ -103,7 +104,7 @@ func TestTextToString(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := textToString(tt.input) + result := utils.PgTextString(tt.input) assert.Equal(t, tt.expected, result) }) } diff --git a/internal/handlers/dashboard_test.go b/internal/handlers/dashboard_test.go index 394e3017..e6cc640d 100644 --- a/internal/handlers/dashboard_test.go +++ b/internal/handlers/dashboard_test.go @@ -5,6 +5,7 @@ import ( "bookhoard/internal/database" "bookhoard/internal/services" + "bookhoard/internal/utils" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" @@ -128,7 +129,7 @@ func TestTextToString_Valid(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := textToString(tt.input) + result := utils.PgTextString(tt.input) assert.Equal(t, tt.expected, result, "textToString result mismatch") }) } diff --git a/internal/handlers/media.go b/internal/handlers/media.go index aea610c4..a8812387 100644 --- a/internal/handlers/media.go +++ b/internal/handlers/media.go @@ -815,44 +815,44 @@ func (mh *MediaHandler) ListMediaItems(c *echo.Context) error { "id": uuid.UUID(item.ID.Bytes).String(), "library_id": uuid.UUID(item.LibraryID.Bytes).String(), "title": item.Title, - "author": textToString(item.Author), - "isbn": textToString(item.Isbn), - "description": textToString(item.Description), + "author": utils.PgTextString(item.Author), + "isbn": utils.PgTextString(item.Isbn), + "description": utils.PgTextString(item.Description), "file_path": utils.ResolveMediaURL(item.LibraryID, pgtype.Text{String: item.FilePath, Valid: item.FilePath != ""}), "file_size": item.FileSize, - "mime_type": textToString(item.MimeType), + "mime_type": utils.PgTextString(item.MimeType), "cover_image_path": utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath), - "series": textToString(item.Series), + "series": utils.PgTextString(item.Series), "series_number": item.SeriesNumber, "tags": item.Tags, - "asin": textToString(item.Asin), + "asin": utils.PgTextString(item.Asin), "date_published": item.DatePublished.Time.Format("2006-01-02"), - "publisher": textToString(item.Publisher), + "publisher": utils.PgTextString(item.Publisher), "contributors": item.Contributors, - "language": textToString(item.Language), - "edition": textToString(item.Edition), + "language": utils.PgTextString(item.Language), + "edition": utils.PgTextString(item.Edition), "page_count": item.PageCount, - "genre": textToString(item.Genre), + "genre": utils.PgTextString(item.Genre), "created_at": item.CreatedAt.Time.Format("2006-01-02T15:04:05Z07:00"), "updated_at": item.UpdatedAt.Time.Format("2006-01-02T15:04:05Z07:00"), "format_group": item.FormatGroup, // already string // NEW: Reading direction fields - "manga_type": textToString(item.MangaType), - "reading_direction": textToString(item.ReadingDirection), + "manga_type": utils.PgTextString(item.MangaType), + "reading_direction": utils.PgTextString(item.ReadingDirection), // NEW: Universal metadata fields "series_count": item.SeriesCount, "volume": item.Volume, - "imprint": textToString(item.Imprint), - "age_rating": textToString(item.AgeRating), - "web_url": textToString(item.WebUrl), - "metadata_notes": textToString(item.MetadataNotes), + "imprint": utils.PgTextString(item.Imprint), + "age_rating": utils.PgTextString(item.AgeRating), + "web_url": utils.PgTextString(item.WebUrl), + "metadata_notes": utils.PgTextString(item.MetadataNotes), "community_rating": numericToFloat(item.CommunityRating), // NEW: Comic-specific fields - "story_arc": textToString(item.StoryArc), + "story_arc": utils.PgTextString(item.StoryArc), "is_black_and_white": item.IsBlackAndWhite, "alternate_info": jsonBytesToMap(item.AlternateInfo), - "scan_information": textToString(item.ScanInformation), - "summary": textToString(item.Summary), + "scan_information": utils.PgTextString(item.ScanInformation), + "summary": utils.PgTextString(item.Summary), } } return c.JSON(http.StatusOK, map[string]interface{}{"data": resolvedItems}) @@ -887,47 +887,47 @@ func (mh *MediaHandler) GetMediaItem(c *echo.Context) error { "id": uuid.UUID(item.ID.Bytes).String(), "library_id": uuid.UUID(item.LibraryID.Bytes).String(), "title": item.Title, - "author": textToString(item.Author), - "isbn": textToString(item.Isbn), - "description": textToString(item.Description), + "author": utils.PgTextString(item.Author), + "isbn": utils.PgTextString(item.Isbn), + "description": utils.PgTextString(item.Description), "file_path": utils.ResolveMediaURL(item.LibraryID, pgtype.Text{String: item.FilePath, Valid: item.FilePath != ""}), "file_size": item.FileSize, - "mime_type": textToString(item.MimeType), + "mime_type": utils.PgTextString(item.MimeType), "cover_image_path": utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath), - "series": textToString(item.Series), + "series": utils.PgTextString(item.Series), "series_number": item.SeriesNumber, "tags": item.Tags, - "asin": textToString(item.Asin), + "asin": utils.PgTextString(item.Asin), "date_published": item.DatePublished.Time.Format("2006-01-02"), - "publisher": textToString(item.Publisher), + "publisher": utils.PgTextString(item.Publisher), "contributors": item.Contributors, - "language": textToString(item.Language), - "edition": textToString(item.Edition), + "language": utils.PgTextString(item.Language), + "edition": utils.PgTextString(item.Edition), "page_count": item.PageCount, - "genre": textToString(item.Genre), + "genre": utils.PgTextString(item.Genre), "created_at": item.CreatedAt.Time.Format("2006-01-02T15:04:05Z07:00"), "updated_at": item.UpdatedAt.Time.Format("2006-01-02T15:04:05Z07:00"), "format_group": item.FormatGroup, - "format_mimetype": textToString(item.FormatMimetype), + "format_mimetype": utils.PgTextString(item.FormatMimetype), "narrators": item.Narrators, "abridged": item.Abridged, // NEW: Reading direction fields - "manga_type": textToString(item.MangaType), - "reading_direction": textToString(item.ReadingDirection), + "manga_type": utils.PgTextString(item.MangaType), + "reading_direction": utils.PgTextString(item.ReadingDirection), // NEW: Universal metadata fields "series_count": item.SeriesCount, "volume": item.Volume, - "imprint": textToString(item.Imprint), - "age_rating": textToString(item.AgeRating), - "web_url": textToString(item.WebUrl), - "metadata_notes": textToString(item.MetadataNotes), + "imprint": utils.PgTextString(item.Imprint), + "age_rating": utils.PgTextString(item.AgeRating), + "web_url": utils.PgTextString(item.WebUrl), + "metadata_notes": utils.PgTextString(item.MetadataNotes), "community_rating": numericToFloat(item.CommunityRating), // NEW: Comic-specific fields - "story_arc": textToString(item.StoryArc), + "story_arc": utils.PgTextString(item.StoryArc), "is_black_and_white": item.IsBlackAndWhite, "alternate_info": jsonBytesToMap(item.AlternateInfo), - "scan_information": textToString(item.ScanInformation), - "summary": textToString(item.Summary), + "scan_information": utils.PgTextString(item.ScanInformation), + "summary": utils.PgTextString(item.Summary), "library_type_name": textPtrToString(item.LibraryTypeName), }) } diff --git a/internal/handlers/reader.go b/internal/handlers/reader.go index 7af83dbc..df9784b9 100644 --- a/internal/handlers/reader.go +++ b/internal/handlers/reader.go @@ -3,6 +3,7 @@ package handlers import ( "bookhoard/internal/database" "bookhoard/internal/services" + "bookhoard/internal/utils" "context" "errors" "fmt" @@ -124,10 +125,10 @@ func (h *ReaderHandler) ShowReader(c *echo.Context) error { return c.JSON(http.StatusOK, map[string]interface{}{ "media_item_id": mediaItemID, "title": mediaItem.Title, - "author": textToString(mediaItem.Author), - "cover_image_path": textToString(mediaItem.CoverImagePath), + "author": utils.PgTextString(mediaItem.Author), + "cover_image_path": utils.PgTextString(mediaItem.CoverImagePath), "library_type": mediaItem.FormatGroup, - "mime_type": textToString(mediaItem.MimeType), + "mime_type": utils.PgTextString(mediaItem.MimeType), "file_path": mediaItem.FilePath, "total_pages": mediaItem.PageCount, "chapter_count": mediaItem.ChapterCount, diff --git a/internal/handlers/series_test.go b/internal/handlers/series_test.go index 96ee8a5f..0e49b9c1 100644 --- a/internal/handlers/series_test.go +++ b/internal/handlers/series_test.go @@ -3,6 +3,8 @@ package handlers import ( "testing" + "bookhoard/internal/utils" + "github.com/jackc/pgx/v5/pgtype" "github.com/stretchr/testify/assert" ) @@ -38,7 +40,7 @@ func TestSeriesHandler_TextToStringConversion(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := textToString(tt.input) + result := utils.PgTextString(tt.input) assert.Equal(t, tt.expected, result) }) } diff --git a/internal/router/admin_library.go b/internal/router/admin_library.go index 4d4fe9eb..01529803 100644 --- a/internal/router/admin_library.go +++ b/internal/router/admin_library.go @@ -3,6 +3,7 @@ package router import ( "bookhoard/internal/database" "bookhoard/internal/handlers" + "bookhoard/internal/utils" "bookhoard/templates" "bytes" "context" @@ -213,7 +214,7 @@ func renderLibraryList(c *echo.Context, cfg *Config) error { libData[i] = templates.LibraryData{ ID: libUUID.String(), Name: lib.Name, - Description: getText(lib.Description), + Description: utils.PgTextString(lib.Description), TypeName: lib.TypeName, TypeValue: lib.TypeName, FolderCount: folderCount, @@ -262,7 +263,7 @@ func renderLibraryPanelWithError(c *echo.Context, cfg *Config, libraryID pgtype. libData := templates.LibraryData{ ID: libraryIDStr, Name: lib.Name, - Description: getText(lib.Description), + Description: utils.PgTextString(lib.Description), TypeName: lib.TypeName, TypeValue: lib.TypeName, } diff --git a/internal/router/frontend.go b/internal/router/frontend.go index 5ef48022..52d6bee0 100644 --- a/internal/router/frontend.go +++ b/internal/router/frontend.go @@ -552,9 +552,9 @@ func registerFrontendRoutes(cfg *Config) { colData[i] = templates.CollectionData{ ID: uuid.UUID(col.ID.Bytes).String(), Name: col.Name, - Description: getText(col.Description), - Color: getText(col.Color), - Icon: getText(col.Icon), + Description: utils.PgTextString(col.Description), + Color: utils.PgTextString(col.Color), + Icon: utils.PgTextString(col.Icon), IsSystem: col.IsSystemCollection.Bool, IsSmart: isSmart, BookCount: bookCount, @@ -598,9 +598,9 @@ func registerFrontendRoutes(cfg *Config) { colData := templates.CollectionData{ ID: uuid.UUID(collection.ID.Bytes).String(), Name: collection.Name, - Description: getText(collection.Description), - Color: getText(collection.Color), - Icon: getText(collection.Icon), + Description: utils.PgTextString(collection.Description), + Color: utils.PgTextString(collection.Color), + Icon: utils.PgTextString(collection.Icon), } // Ensure default color if empty @@ -652,9 +652,9 @@ func registerFrontendRoutes(cfg *Config) { colData := templates.CollectionData{ ID: collectionID, Name: collection.Name, - Description: getText(collection.Description), - Color: getText(collection.Color), - Icon: getText(collection.Icon), + Description: utils.PgTextString(collection.Description), + Color: utils.PgTextString(collection.Color), + Icon: utils.PgTextString(collection.Icon), IsSystem: collection.IsSystemCollection.Bool, IsSmart: cfg.CollectionHandler.CollectionService().IsSmartCollection(collection.AutoAssignRules), } @@ -861,7 +861,7 @@ func registerFrontendRoutes(cfg *Config) { libData[i] = templates.LibraryData{ ID: libUUID.String(), Name: lib.Name, - Description: getText(lib.Description), + Description: utils.PgTextString(lib.Description), TypeName: lib.TypeName, } } @@ -1056,7 +1056,7 @@ func registerFrontendRoutes(cfg *Config) { item := templates.ArchivedItem{ ID: itemUUID.String(), Title: row.Title, - Author: getText(row.Author), + Author: utils.PgTextString(row.Author), LibraryName: row.LibraryName, FilePath: row.FilePath, MissingScans: row.MissingScanCount, @@ -1064,7 +1064,7 @@ func registerFrontendRoutes(cfg *Config) { if row.MatchID.Valid { item.MatchID = uuid.UUID(row.MatchID.Bytes).String() item.MatchTitle = row.MatchTitle - item.MatchLibraryName = getText(row.MatchLibraryName) + item.MatchLibraryName = utils.PgTextString(row.MatchLibraryName) } if row.ArchivedAt.Valid { archived := row.ArchivedAt.Time @@ -1111,7 +1111,7 @@ func registerFrontendRoutes(cfg *Config) { libData[i] = templates.LibraryData{ ID: libUUID.String(), Name: lib.Name, - Description: getText(lib.Description), + Description: utils.PgTextString(lib.Description), TypeName: lib.TypeName, TypeValue: lib.TypeName, FolderCount: len(folders), @@ -1292,9 +1292,9 @@ func registerFrontendRoutes(cfg *Config) { Username: u.Username, Email: u.Email, Role: u.Role, - Theme: getText(u.Theme), - FirstName: getText(u.FirstName), - LastName: getText(u.LastName), + Theme: utils.PgTextString(u.Theme), + FirstName: utils.PgTextString(u.FirstName), + LastName: utils.PgTextString(u.LastName), CreatedAt: u.CreatedAt.Time, } } @@ -1401,13 +1401,13 @@ func registerFrontendRoutes(cfg *Config) { Username: targetUser.Username, Email: targetUser.Email, Role: targetUser.Role, - Theme: getText(targetUser.Theme), - FirstName: getText(targetUser.FirstName), - LastName: getText(targetUser.LastName), + Theme: utils.PgTextString(targetUser.Theme), + FirstName: utils.PgTextString(targetUser.FirstName), + LastName: utils.PgTextString(targetUser.LastName), CreatedAt: targetUser.CreatedAt.Time, // Without this the timezone select rendered UTC selected for // EVERY user — saves landed, the modal just never showed them. - Timezone: getText(targetUser.Timezone), + Timezone: utils.PgTextString(targetUser.Timezone), } // Render modal diff --git a/internal/router/helpers.go b/internal/router/helpers.go index 78022324..c4eeb247 100644 --- a/internal/router/helpers.go +++ b/internal/router/helpers.go @@ -10,6 +10,7 @@ import ( "time" "bookhoard/internal/database" + "bookhoard/internal/utils" "bookhoard/templates" "github.com/google/uuid" @@ -60,8 +61,8 @@ func getTemplateUserWithTheme(c *echo.Context, cfg *Config) (templates.User, err Theme: userTheme, Token: token, Timezone: userTimezone, - FirstName: getText(userDB.FirstName), - LastName: getText(userDB.LastName), + FirstName: utils.PgTextString(userDB.FirstName), + LastName: utils.PgTextString(userDB.LastName), CreatedAt: userDB.CreatedAt.Time, }, nil } @@ -135,13 +136,6 @@ func folderReachableCached(path string) bool { return exists } -func getText(t pgtype.Text) string { - if t.Valid { - return t.String - } - return "" -} - func resolveLibrary(c *echo.Context, cfg *Config, userUUID string) LibraryResolution { res := LibraryResolution{} @@ -195,7 +189,7 @@ func resolveLibrary(c *echo.Context, cfg *Config, userUUID string) LibraryResolu res.Libraries[i] = templates.LibraryData{ ID: libUUID.String(), Name: lib.Name, - Description: getText(lib.Description), + Description: utils.PgTextString(lib.Description), TypeName: lib.TypeName, TypeValue: lib.TypeName, MediaCount: countMap[libUUID.String()], diff --git a/internal/router/reader.go b/internal/router/reader.go index 78f113ee..9d3577d5 100644 --- a/internal/router/reader.go +++ b/internal/router/reader.go @@ -80,21 +80,21 @@ func registerReaderRoutes(cfg *Config) { mediaUUID, _ := uuid.FromBytes(mediaItem.ID.Bytes[0:16]) libUUID, _ := uuid.FromBytes(mediaItem.LibraryID.Bytes[0:16]) metadata := templates.ReaderMetadata{ - MediaItemID: mediaUUID.String(), - Title: mediaItem.Title, - Author: textToString(mediaItem.Author), + MediaItemID: mediaUUID.String(), + Title: mediaItem.Title, + Author: utils.PgTextString(mediaItem.Author), // Sidecar covers store a raw library-relative path; resolve to // the cookie-authed /uploads URL (idempotent for items whose // stored value is already a servable path). CoverImagePath: utils.ResolveMediaURL(mediaItem.LibraryID, mediaItem.CoverImagePath), LibraryType: mediaItem.FormatGroup, - MimeType: textToString(mediaItem.MimeType), + MimeType: utils.PgTextString(mediaItem.MimeType), FilePath: mediaItem.FilePath, TotalPages: int(mediaItem.PageCount.Int32), ChapterCount: int(mediaItem.ChapterCount.Int32), FormatGroup: mediaItem.FormatGroup, - MangaType: textToString(mediaItem.MangaType), - ReadingDirection: textToString(mediaItem.ReadingDirection), + MangaType: utils.PgTextString(mediaItem.MangaType), + ReadingDirection: utils.PgTextString(mediaItem.ReadingDirection), LibraryID: libUUID.String(), FileURL: utils.ResolveMediaURL(mediaItem.LibraryID, pgtype.Text{String: mediaItem.FilePath, Valid: true}), TotalCharacters: mediaItem.TotalCharacters.Int64, diff --git a/internal/router/search.go b/internal/router/search.go index 1fcaac0c..5a54a78c 100644 --- a/internal/router/search.go +++ b/internal/router/search.go @@ -116,11 +116,3 @@ func handleSearchHTML(c *echo.Context, cfg *Config) error { } return c.HTML(http.StatusOK, buf.String()) } - -// textToString converts pgtype.Text to string -func textToString(t pgtype.Text) string { - if t.Valid { - return t.String - } - return "" -} diff --git a/internal/utils/mediaurl.go b/internal/utils/mediaurl.go index e03900a3..3879e37e 100644 --- a/internal/utils/mediaurl.go +++ b/internal/utils/mediaurl.go @@ -33,3 +33,13 @@ func ResolveMediaURL(libraryID pgtype.UUID, relativePath pgtype.Text) string { encodedPath := strings.Join(segments, "/") return fmt.Sprintf("/uploads/library-%s/%s", libraryIDStr, encodedPath) } + +// PgTextString unwraps a nullable text column to a plain string ("" +// when NULL). The former textToString/getText/normText copies in +// handlers, router, and utils all did exactly this. +func PgTextString(t pgtype.Text) string { + if t.Valid { + return t.String + } + return "" +}