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:
1 parent
00305ffc7b
commit
188ae076ab
15 files changed
+140
-139
No files matched your search
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
+39
-39
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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()],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
Reference in new issue
Block a user