refactor(utils): one PgTextString for the four pgtype.Text unwrappers

handlers textToString, router textToString, router getText, and utils
normText were four copies of the same NULL-to-empty unwrap (99 call
sites). One exported utils.PgTextString replaces them all; the private
normText (comparison normalizer) stays but is now the odd one out.

Harness fix found on the way: setupTestServer built a full wiring
literal for route registration and then RETURNED a second, sparser
literal that never copied AnnotationService — setup.AnnotationService
was always nil on the struct every test receives. Nobody noticed until
cross_library_move_test became the first caller (it previously used
the now-retired plain CreateMediaHighlight query); it now exercises
the real SaveHighlight path through the wired service.
This commit is contained in:
John O'Keefe committed 2026-10-17 16:36:58 -04:00
1 parent 00305ffc7b
commit 188ae076ab
15 files changed
+140 -139

No files matched your search

+3 -2
View File
@@ -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,
}
+20 -20
View File
@@ -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
+4 -10
View File
@@ -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()],
+6 -6
View File
@@ -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,
-8
View File
@@ -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 ""
}