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
+126
-125
No files matched your search
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"bookhoard/internal/database"
|
"bookhoard/internal/database"
|
||||||
"bookhoard/internal/services"
|
"bookhoard/internal/services"
|
||||||
|
wsync "bookhoard/internal/sync"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
@@ -160,11 +161,13 @@ func TestCrossLibraryMovePreservesHistory(t *testing.T) {
|
|||||||
TotalPages: pgtype.Int4{Int32: 300, Valid: true},
|
TotalPages: pgtype.Int4{Int32: 300, Valid: true},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = setup.DB.CreateMediaHighlight(ctx, database.CreateMediaHighlightParams{
|
_, err = setup.AnnotationService.SaveHighlight(ctx, wsync.SaveHighlightRequest{
|
||||||
MediaItemID: original.ID,
|
MediaItemID: original.ID,
|
||||||
UserID: adminPgID,
|
UserID: adminPgID,
|
||||||
SelectionText: "a line worth keeping",
|
SelectionText: "a line worth keeping",
|
||||||
Color: pgtype.Text{String: "#ffff00", Valid: true},
|
Color: "#ffff00",
|
||||||
|
Source: "web",
|
||||||
|
ModifiedAt: time.Now(),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -419,11 +422,13 @@ func archiveAndIsolate(t *testing.T, setup *TestServerSetup, adminID, adminPgID
|
|||||||
TotalPages: pgtype.Int4{Int32: 200, Valid: true},
|
TotalPages: pgtype.Int4{Int32: 200, Valid: true},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = setup.DB.CreateMediaHighlight(ctx, database.CreateMediaHighlightParams{
|
_, err = setup.AnnotationService.SaveHighlight(ctx, wsync.SaveHighlightRequest{
|
||||||
MediaItemID: item.ID,
|
MediaItemID: item.ID,
|
||||||
UserID: adminPgID,
|
UserID: adminPgID,
|
||||||
SelectionText: "annotation on the old copy",
|
SelectionText: "annotation on the old copy",
|
||||||
Color: pgtype.Text{String: "#00ff00", Valid: true},
|
Color: "#00ff00",
|
||||||
|
Source: "web",
|
||||||
|
ModifiedAt: time.Now(),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|||||||
@@ -717,6 +717,7 @@ func setupTestServer(t *testing.T) *TestServerSetup {
|
|||||||
ConnManager: connManager,
|
ConnManager: connManager,
|
||||||
QueueProcessor: queueProcessor,
|
QueueProcessor: queueProcessor,
|
||||||
ProgressService: progressService,
|
ProgressService: progressService,
|
||||||
|
AnnotationService: annotationService,
|
||||||
CleanupCancel: cleanupCancel,
|
CleanupCancel: cleanupCancel,
|
||||||
QueueCtx: queueCtx,
|
QueueCtx: queueCtx,
|
||||||
QueueCancel: queueCancel,
|
QueueCancel: queueCancel,
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ func BookCardsFromMediaItems(rows []database.MediaItems, libTypes map[string]str
|
|||||||
cards = append(cards, buildBookCard(cardSource{
|
cards = append(cards, buildBookCard(cardSource{
|
||||||
ID: uuid.UUID(r.ID.Bytes),
|
ID: uuid.UUID(r.ID.Bytes),
|
||||||
Title: r.Title,
|
Title: r.Title,
|
||||||
Author: textToString(r.Author),
|
Author: utils.PgTextString(r.Author),
|
||||||
LibraryID: r.LibraryID,
|
LibraryID: r.LibraryID,
|
||||||
CoverPath: r.CoverImagePath,
|
CoverPath: r.CoverImagePath,
|
||||||
FormatGroup: r.FormatGroup,
|
FormatGroup: r.FormatGroup,
|
||||||
@@ -134,7 +134,7 @@ func BookCardsFromCollectionDashRows(rows []database.GetCollectionItemsForDashbo
|
|||||||
cards = append(cards, buildBookCard(cardSource{
|
cards = append(cards, buildBookCard(cardSource{
|
||||||
ID: uuid.UUID(r.ID.Bytes),
|
ID: uuid.UUID(r.ID.Bytes),
|
||||||
Title: r.Title,
|
Title: r.Title,
|
||||||
Author: textToString(r.Author),
|
Author: utils.PgTextString(r.Author),
|
||||||
LibraryID: r.LibraryID,
|
LibraryID: r.LibraryID,
|
||||||
CoverPath: r.CoverImagePath,
|
CoverPath: r.CoverImagePath,
|
||||||
FormatGroup: r.FormatGroup,
|
FormatGroup: r.FormatGroup,
|
||||||
@@ -154,7 +154,7 @@ func BookCardsFromCollectionItemRows(rows []database.GetCollectionItemsRow, libT
|
|||||||
cards = append(cards, buildBookCard(cardSource{
|
cards = append(cards, buildBookCard(cardSource{
|
||||||
ID: uuid.UUID(r.MediaItemID.Bytes),
|
ID: uuid.UUID(r.MediaItemID.Bytes),
|
||||||
Title: r.Title,
|
Title: r.Title,
|
||||||
Author: textToString(r.Author),
|
Author: utils.PgTextString(r.Author),
|
||||||
LibraryID: r.LibraryID,
|
LibraryID: r.LibraryID,
|
||||||
CoverPath: r.CoverImagePath,
|
CoverPath: r.CoverImagePath,
|
||||||
FormatGroup: r.FormatGroup,
|
FormatGroup: r.FormatGroup,
|
||||||
@@ -173,7 +173,7 @@ func BookCardsFromUnified(rows []database.SearchMediaItemsUnifiedRow) []BookInfo
|
|||||||
cards = append(cards, buildBookCard(cardSource{
|
cards = append(cards, buildBookCard(cardSource{
|
||||||
ID: uuid.UUID(r.ID.Bytes),
|
ID: uuid.UUID(r.ID.Bytes),
|
||||||
Title: r.Title,
|
Title: r.Title,
|
||||||
Author: textToString(r.Author),
|
Author: utils.PgTextString(r.Author),
|
||||||
LibraryID: r.LibraryID,
|
LibraryID: r.LibraryID,
|
||||||
CoverPath: r.CoverImagePath,
|
CoverPath: r.CoverImagePath,
|
||||||
FormatGroup: r.FormatGroup,
|
FormatGroup: r.FormatGroup,
|
||||||
|
|||||||
@@ -116,9 +116,9 @@ func (h *CollectionHandler) CreateCollection(c *echo.Context) error {
|
|||||||
"id": uuid.UUID(collection.ID.Bytes).String(),
|
"id": uuid.UUID(collection.ID.Bytes).String(),
|
||||||
"user_id": uuid.UUID(collection.UserID.Bytes).String(),
|
"user_id": uuid.UUID(collection.UserID.Bytes).String(),
|
||||||
"name": collection.Name,
|
"name": collection.Name,
|
||||||
"description": textToString(collection.Description),
|
"description": utils.PgTextString(collection.Description),
|
||||||
"color": textToString(collection.Color),
|
"color": utils.PgTextString(collection.Color),
|
||||||
"icon": textToString(collection.Icon),
|
"icon": utils.PgTextString(collection.Icon),
|
||||||
"collection_type": map[bool]string{true: "smart", false: "static"}[isSmart],
|
"collection_type": map[bool]string{true: "smart", false: "static"}[isSmart],
|
||||||
"auto_assign_rules": collection.AutoAssignRules,
|
"auto_assign_rules": collection.AutoAssignRules,
|
||||||
"view_settings": collection.ViewSettings,
|
"view_settings": collection.ViewSettings,
|
||||||
@@ -204,9 +204,9 @@ func (h *CollectionHandler) GetCollections(c *echo.Context) error {
|
|||||||
response = append(response, CollectionResponse{
|
response = append(response, CollectionResponse{
|
||||||
ID: col.ID.Bytes,
|
ID: col.ID.Bytes,
|
||||||
Name: col.Name,
|
Name: col.Name,
|
||||||
Description: textToString(col.Description),
|
Description: utils.PgTextString(col.Description),
|
||||||
Color: textToString(col.Color),
|
Color: utils.PgTextString(col.Color),
|
||||||
Icon: textToString(col.Icon),
|
Icon: utils.PgTextString(col.Icon),
|
||||||
AutoAssignRules: col.AutoAssignRules,
|
AutoAssignRules: col.AutoAssignRules,
|
||||||
CreatedAt: col.CreatedAt.Time.String(),
|
CreatedAt: col.CreatedAt.Time.String(),
|
||||||
BookCount: bookCount,
|
BookCount: bookCount,
|
||||||
@@ -325,9 +325,9 @@ func (h *CollectionHandler) GetCollection(c *echo.Context) error {
|
|||||||
"id": uuid.UUID(collection.ID.Bytes).String(),
|
"id": uuid.UUID(collection.ID.Bytes).String(),
|
||||||
"user_id": uuid.UUID(collection.UserID.Bytes).String(),
|
"user_id": uuid.UUID(collection.UserID.Bytes).String(),
|
||||||
"name": collection.Name,
|
"name": collection.Name,
|
||||||
"description": textToString(collection.Description),
|
"description": utils.PgTextString(collection.Description),
|
||||||
"color": textToString(collection.Color),
|
"color": utils.PgTextString(collection.Color),
|
||||||
"icon": textToString(collection.Icon),
|
"icon": utils.PgTextString(collection.Icon),
|
||||||
"auto_assign_rules": json.RawMessage(collection.AutoAssignRules),
|
"auto_assign_rules": json.RawMessage(collection.AutoAssignRules),
|
||||||
"view_settings": viewSettings,
|
"view_settings": viewSettings,
|
||||||
"books": bookList,
|
"books": bookList,
|
||||||
@@ -369,9 +369,9 @@ func (h *CollectionHandler) UpdateCollection(c *echo.Context) error {
|
|||||||
"id": uuid.UUID(collection.ID.Bytes).String(),
|
"id": uuid.UUID(collection.ID.Bytes).String(),
|
||||||
"user_id": uuid.UUID(collection.UserID.Bytes).String(),
|
"user_id": uuid.UUID(collection.UserID.Bytes).String(),
|
||||||
"name": collection.Name,
|
"name": collection.Name,
|
||||||
"description": textToString(collection.Description),
|
"description": utils.PgTextString(collection.Description),
|
||||||
"color": textToString(collection.Color),
|
"color": utils.PgTextString(collection.Color),
|
||||||
"icon": textToString(collection.Icon),
|
"icon": utils.PgTextString(collection.Icon),
|
||||||
"auto_assign_rules": json.RawMessage(collection.AutoAssignRules),
|
"auto_assign_rules": json.RawMessage(collection.AutoAssignRules),
|
||||||
"view_settings": json.RawMessage(collection.ViewSettings),
|
"view_settings": json.RawMessage(collection.ViewSettings),
|
||||||
"created_at": collection.CreatedAt.Time.String(),
|
"created_at": collection.CreatedAt.Time.String(),
|
||||||
@@ -580,8 +580,8 @@ func (h *CollectionHandler) GetDeviceMappings(c *echo.Context) error {
|
|||||||
ID: m.ID.Bytes,
|
ID: m.ID.Bytes,
|
||||||
CollectionID: m.CollectionID.Bytes,
|
CollectionID: m.CollectionID.Bytes,
|
||||||
CollectionName: m.CollectionName,
|
CollectionName: m.CollectionName,
|
||||||
DeviceShelfName: textToString(m.DeviceShelfName),
|
DeviceShelfName: utils.PgTextString(m.DeviceShelfName),
|
||||||
SyncDirection: textToString(m.SyncDirection),
|
SyncDirection: utils.PgTextString(m.SyncDirection),
|
||||||
CreatedAt: m.CreatedAt.Time.String(),
|
CreatedAt: m.CreatedAt.Time.String(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -627,8 +627,8 @@ func (h *CollectionHandler) CreateDeviceMapping(c *echo.Context) error {
|
|||||||
"id": uuid.UUID(mapping.ID.Bytes).String(),
|
"id": uuid.UUID(mapping.ID.Bytes).String(),
|
||||||
"collection_id": uuid.UUID(mapping.CollectionID.Bytes).String(),
|
"collection_id": uuid.UUID(mapping.CollectionID.Bytes).String(),
|
||||||
"device_id": uuid.UUID(mapping.DeviceID.Bytes).String(),
|
"device_id": uuid.UUID(mapping.DeviceID.Bytes).String(),
|
||||||
"device_shelf_name": textToString(mapping.DeviceShelfName),
|
"device_shelf_name": utils.PgTextString(mapping.DeviceShelfName),
|
||||||
"sync_direction": textToString(mapping.SyncDirection),
|
"sync_direction": utils.PgTextString(mapping.SyncDirection),
|
||||||
"created_at": mapping.CreatedAt.Time.String(),
|
"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(),
|
"id": uuid.UUID(mapping.ID.Bytes).String(),
|
||||||
"collection_id": uuid.UUID(mapping.CollectionID.Bytes).String(),
|
"collection_id": uuid.UUID(mapping.CollectionID.Bytes).String(),
|
||||||
"device_id": uuid.UUID(mapping.DeviceID.Bytes).String(),
|
"device_id": uuid.UUID(mapping.DeviceID.Bytes).String(),
|
||||||
"device_shelf_name": textToString(mapping.DeviceShelfName),
|
"device_shelf_name": utils.PgTextString(mapping.DeviceShelfName),
|
||||||
"sync_direction": textToString(mapping.SyncDirection),
|
"sync_direction": utils.PgTextString(mapping.SyncDirection),
|
||||||
"created_at": mapping.CreatedAt.Time.String(),
|
"created_at": mapping.CreatedAt.Time.String(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -705,9 +705,9 @@ func (h *CollectionHandler) GetBookCollections(c *echo.Context) error {
|
|||||||
response = append(response, CollectionResponse{
|
response = append(response, CollectionResponse{
|
||||||
ID: col.ID.Bytes,
|
ID: col.ID.Bytes,
|
||||||
Name: col.Name,
|
Name: col.Name,
|
||||||
Description: textToString(col.Description),
|
Description: utils.PgTextString(col.Description),
|
||||||
Color: textToString(col.Color),
|
Color: utils.PgTextString(col.Color),
|
||||||
Icon: textToString(col.Icon),
|
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) {
|
func (h *CollectionHandler) GetCollectionsData(c *echo.Context) ([]database.Collections, error) {
|
||||||
user := c.Get("user").(database.Users)
|
user := c.Get("user").(database.Users)
|
||||||
userUUID := uuid.UUID(user.ID.Bytes)
|
userUUID := uuid.UUID(user.ID.Bytes)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"bookhoard/internal/database"
|
"bookhoard/internal/database"
|
||||||
|
"bookhoard/internal/utils"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -103,7 +104,7 @@ func TestTextToString(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result := textToString(tt.input)
|
result := utils.PgTextString(tt.input)
|
||||||
assert.Equal(t, tt.expected, result)
|
assert.Equal(t, tt.expected, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"bookhoard/internal/database"
|
"bookhoard/internal/database"
|
||||||
"bookhoard/internal/services"
|
"bookhoard/internal/services"
|
||||||
|
"bookhoard/internal/utils"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
@@ -128,7 +129,7 @@ func TestTextToString_Valid(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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")
|
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(),
|
"id": uuid.UUID(item.ID.Bytes).String(),
|
||||||
"library_id": uuid.UUID(item.LibraryID.Bytes).String(),
|
"library_id": uuid.UUID(item.LibraryID.Bytes).String(),
|
||||||
"title": item.Title,
|
"title": item.Title,
|
||||||
"author": textToString(item.Author),
|
"author": utils.PgTextString(item.Author),
|
||||||
"isbn": textToString(item.Isbn),
|
"isbn": utils.PgTextString(item.Isbn),
|
||||||
"description": textToString(item.Description),
|
"description": utils.PgTextString(item.Description),
|
||||||
"file_path": utils.ResolveMediaURL(item.LibraryID, pgtype.Text{String: item.FilePath, Valid: item.FilePath != ""}),
|
"file_path": utils.ResolveMediaURL(item.LibraryID, pgtype.Text{String: item.FilePath, Valid: item.FilePath != ""}),
|
||||||
"file_size": item.FileSize,
|
"file_size": item.FileSize,
|
||||||
"mime_type": textToString(item.MimeType),
|
"mime_type": utils.PgTextString(item.MimeType),
|
||||||
"cover_image_path": utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
|
"cover_image_path": utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
|
||||||
"series": textToString(item.Series),
|
"series": utils.PgTextString(item.Series),
|
||||||
"series_number": item.SeriesNumber,
|
"series_number": item.SeriesNumber,
|
||||||
"tags": item.Tags,
|
"tags": item.Tags,
|
||||||
"asin": textToString(item.Asin),
|
"asin": utils.PgTextString(item.Asin),
|
||||||
"date_published": item.DatePublished.Time.Format("2006-01-02"),
|
"date_published": item.DatePublished.Time.Format("2006-01-02"),
|
||||||
"publisher": textToString(item.Publisher),
|
"publisher": utils.PgTextString(item.Publisher),
|
||||||
"contributors": item.Contributors,
|
"contributors": item.Contributors,
|
||||||
"language": textToString(item.Language),
|
"language": utils.PgTextString(item.Language),
|
||||||
"edition": textToString(item.Edition),
|
"edition": utils.PgTextString(item.Edition),
|
||||||
"page_count": item.PageCount,
|
"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"),
|
"created_at": item.CreatedAt.Time.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
"updated_at": item.UpdatedAt.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
|
"format_group": item.FormatGroup, // already string
|
||||||
// NEW: Reading direction fields
|
// NEW: Reading direction fields
|
||||||
"manga_type": textToString(item.MangaType),
|
"manga_type": utils.PgTextString(item.MangaType),
|
||||||
"reading_direction": textToString(item.ReadingDirection),
|
"reading_direction": utils.PgTextString(item.ReadingDirection),
|
||||||
// NEW: Universal metadata fields
|
// NEW: Universal metadata fields
|
||||||
"series_count": item.SeriesCount,
|
"series_count": item.SeriesCount,
|
||||||
"volume": item.Volume,
|
"volume": item.Volume,
|
||||||
"imprint": textToString(item.Imprint),
|
"imprint": utils.PgTextString(item.Imprint),
|
||||||
"age_rating": textToString(item.AgeRating),
|
"age_rating": utils.PgTextString(item.AgeRating),
|
||||||
"web_url": textToString(item.WebUrl),
|
"web_url": utils.PgTextString(item.WebUrl),
|
||||||
"metadata_notes": textToString(item.MetadataNotes),
|
"metadata_notes": utils.PgTextString(item.MetadataNotes),
|
||||||
"community_rating": numericToFloat(item.CommunityRating),
|
"community_rating": numericToFloat(item.CommunityRating),
|
||||||
// NEW: Comic-specific fields
|
// NEW: Comic-specific fields
|
||||||
"story_arc": textToString(item.StoryArc),
|
"story_arc": utils.PgTextString(item.StoryArc),
|
||||||
"is_black_and_white": item.IsBlackAndWhite,
|
"is_black_and_white": item.IsBlackAndWhite,
|
||||||
"alternate_info": jsonBytesToMap(item.AlternateInfo),
|
"alternate_info": jsonBytesToMap(item.AlternateInfo),
|
||||||
"scan_information": textToString(item.ScanInformation),
|
"scan_information": utils.PgTextString(item.ScanInformation),
|
||||||
"summary": textToString(item.Summary),
|
"summary": utils.PgTextString(item.Summary),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return c.JSON(http.StatusOK, map[string]interface{}{"data": resolvedItems})
|
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(),
|
"id": uuid.UUID(item.ID.Bytes).String(),
|
||||||
"library_id": uuid.UUID(item.LibraryID.Bytes).String(),
|
"library_id": uuid.UUID(item.LibraryID.Bytes).String(),
|
||||||
"title": item.Title,
|
"title": item.Title,
|
||||||
"author": textToString(item.Author),
|
"author": utils.PgTextString(item.Author),
|
||||||
"isbn": textToString(item.Isbn),
|
"isbn": utils.PgTextString(item.Isbn),
|
||||||
"description": textToString(item.Description),
|
"description": utils.PgTextString(item.Description),
|
||||||
"file_path": utils.ResolveMediaURL(item.LibraryID, pgtype.Text{String: item.FilePath, Valid: item.FilePath != ""}),
|
"file_path": utils.ResolveMediaURL(item.LibraryID, pgtype.Text{String: item.FilePath, Valid: item.FilePath != ""}),
|
||||||
"file_size": item.FileSize,
|
"file_size": item.FileSize,
|
||||||
"mime_type": textToString(item.MimeType),
|
"mime_type": utils.PgTextString(item.MimeType),
|
||||||
"cover_image_path": utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
|
"cover_image_path": utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
|
||||||
"series": textToString(item.Series),
|
"series": utils.PgTextString(item.Series),
|
||||||
"series_number": item.SeriesNumber,
|
"series_number": item.SeriesNumber,
|
||||||
"tags": item.Tags,
|
"tags": item.Tags,
|
||||||
"asin": textToString(item.Asin),
|
"asin": utils.PgTextString(item.Asin),
|
||||||
"date_published": item.DatePublished.Time.Format("2006-01-02"),
|
"date_published": item.DatePublished.Time.Format("2006-01-02"),
|
||||||
"publisher": textToString(item.Publisher),
|
"publisher": utils.PgTextString(item.Publisher),
|
||||||
"contributors": item.Contributors,
|
"contributors": item.Contributors,
|
||||||
"language": textToString(item.Language),
|
"language": utils.PgTextString(item.Language),
|
||||||
"edition": textToString(item.Edition),
|
"edition": utils.PgTextString(item.Edition),
|
||||||
"page_count": item.PageCount,
|
"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"),
|
"created_at": item.CreatedAt.Time.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
"updated_at": item.UpdatedAt.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_group": item.FormatGroup,
|
||||||
"format_mimetype": textToString(item.FormatMimetype),
|
"format_mimetype": utils.PgTextString(item.FormatMimetype),
|
||||||
"narrators": item.Narrators,
|
"narrators": item.Narrators,
|
||||||
"abridged": item.Abridged,
|
"abridged": item.Abridged,
|
||||||
// NEW: Reading direction fields
|
// NEW: Reading direction fields
|
||||||
"manga_type": textToString(item.MangaType),
|
"manga_type": utils.PgTextString(item.MangaType),
|
||||||
"reading_direction": textToString(item.ReadingDirection),
|
"reading_direction": utils.PgTextString(item.ReadingDirection),
|
||||||
// NEW: Universal metadata fields
|
// NEW: Universal metadata fields
|
||||||
"series_count": item.SeriesCount,
|
"series_count": item.SeriesCount,
|
||||||
"volume": item.Volume,
|
"volume": item.Volume,
|
||||||
"imprint": textToString(item.Imprint),
|
"imprint": utils.PgTextString(item.Imprint),
|
||||||
"age_rating": textToString(item.AgeRating),
|
"age_rating": utils.PgTextString(item.AgeRating),
|
||||||
"web_url": textToString(item.WebUrl),
|
"web_url": utils.PgTextString(item.WebUrl),
|
||||||
"metadata_notes": textToString(item.MetadataNotes),
|
"metadata_notes": utils.PgTextString(item.MetadataNotes),
|
||||||
"community_rating": numericToFloat(item.CommunityRating),
|
"community_rating": numericToFloat(item.CommunityRating),
|
||||||
// NEW: Comic-specific fields
|
// NEW: Comic-specific fields
|
||||||
"story_arc": textToString(item.StoryArc),
|
"story_arc": utils.PgTextString(item.StoryArc),
|
||||||
"is_black_and_white": item.IsBlackAndWhite,
|
"is_black_and_white": item.IsBlackAndWhite,
|
||||||
"alternate_info": jsonBytesToMap(item.AlternateInfo),
|
"alternate_info": jsonBytesToMap(item.AlternateInfo),
|
||||||
"scan_information": textToString(item.ScanInformation),
|
"scan_information": utils.PgTextString(item.ScanInformation),
|
||||||
"summary": textToString(item.Summary),
|
"summary": utils.PgTextString(item.Summary),
|
||||||
"library_type_name": textPtrToString(item.LibraryTypeName),
|
"library_type_name": textPtrToString(item.LibraryTypeName),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"bookhoard/internal/database"
|
"bookhoard/internal/database"
|
||||||
"bookhoard/internal/services"
|
"bookhoard/internal/services"
|
||||||
|
"bookhoard/internal/utils"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -124,10 +125,10 @@ func (h *ReaderHandler) ShowReader(c *echo.Context) error {
|
|||||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
"media_item_id": mediaItemID,
|
"media_item_id": mediaItemID,
|
||||||
"title": mediaItem.Title,
|
"title": mediaItem.Title,
|
||||||
"author": textToString(mediaItem.Author),
|
"author": utils.PgTextString(mediaItem.Author),
|
||||||
"cover_image_path": textToString(mediaItem.CoverImagePath),
|
"cover_image_path": utils.PgTextString(mediaItem.CoverImagePath),
|
||||||
"library_type": mediaItem.FormatGroup,
|
"library_type": mediaItem.FormatGroup,
|
||||||
"mime_type": textToString(mediaItem.MimeType),
|
"mime_type": utils.PgTextString(mediaItem.MimeType),
|
||||||
"file_path": mediaItem.FilePath,
|
"file_path": mediaItem.FilePath,
|
||||||
"total_pages": mediaItem.PageCount,
|
"total_pages": mediaItem.PageCount,
|
||||||
"chapter_count": mediaItem.ChapterCount,
|
"chapter_count": mediaItem.ChapterCount,
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"bookhoard/internal/utils"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -38,7 +40,7 @@ func TestSeriesHandler_TextToStringConversion(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result := textToString(tt.input)
|
result := utils.PgTextString(tt.input)
|
||||||
assert.Equal(t, tt.expected, result)
|
assert.Equal(t, tt.expected, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package router
|
|||||||
import (
|
import (
|
||||||
"bookhoard/internal/database"
|
"bookhoard/internal/database"
|
||||||
"bookhoard/internal/handlers"
|
"bookhoard/internal/handlers"
|
||||||
|
"bookhoard/internal/utils"
|
||||||
"bookhoard/templates"
|
"bookhoard/templates"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
@@ -213,7 +214,7 @@ func renderLibraryList(c *echo.Context, cfg *Config) error {
|
|||||||
libData[i] = templates.LibraryData{
|
libData[i] = templates.LibraryData{
|
||||||
ID: libUUID.String(),
|
ID: libUUID.String(),
|
||||||
Name: lib.Name,
|
Name: lib.Name,
|
||||||
Description: getText(lib.Description),
|
Description: utils.PgTextString(lib.Description),
|
||||||
TypeName: lib.TypeName,
|
TypeName: lib.TypeName,
|
||||||
TypeValue: lib.TypeName,
|
TypeValue: lib.TypeName,
|
||||||
FolderCount: folderCount,
|
FolderCount: folderCount,
|
||||||
@@ -262,7 +263,7 @@ func renderLibraryPanelWithError(c *echo.Context, cfg *Config, libraryID pgtype.
|
|||||||
libData := templates.LibraryData{
|
libData := templates.LibraryData{
|
||||||
ID: libraryIDStr,
|
ID: libraryIDStr,
|
||||||
Name: lib.Name,
|
Name: lib.Name,
|
||||||
Description: getText(lib.Description),
|
Description: utils.PgTextString(lib.Description),
|
||||||
TypeName: lib.TypeName,
|
TypeName: lib.TypeName,
|
||||||
TypeValue: lib.TypeName,
|
TypeValue: lib.TypeName,
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-20
@@ -552,9 +552,9 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
colData[i] = templates.CollectionData{
|
colData[i] = templates.CollectionData{
|
||||||
ID: uuid.UUID(col.ID.Bytes).String(),
|
ID: uuid.UUID(col.ID.Bytes).String(),
|
||||||
Name: col.Name,
|
Name: col.Name,
|
||||||
Description: getText(col.Description),
|
Description: utils.PgTextString(col.Description),
|
||||||
Color: getText(col.Color),
|
Color: utils.PgTextString(col.Color),
|
||||||
Icon: getText(col.Icon),
|
Icon: utils.PgTextString(col.Icon),
|
||||||
IsSystem: col.IsSystemCollection.Bool,
|
IsSystem: col.IsSystemCollection.Bool,
|
||||||
IsSmart: isSmart,
|
IsSmart: isSmart,
|
||||||
BookCount: bookCount,
|
BookCount: bookCount,
|
||||||
@@ -598,9 +598,9 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
colData := templates.CollectionData{
|
colData := templates.CollectionData{
|
||||||
ID: uuid.UUID(collection.ID.Bytes).String(),
|
ID: uuid.UUID(collection.ID.Bytes).String(),
|
||||||
Name: collection.Name,
|
Name: collection.Name,
|
||||||
Description: getText(collection.Description),
|
Description: utils.PgTextString(collection.Description),
|
||||||
Color: getText(collection.Color),
|
Color: utils.PgTextString(collection.Color),
|
||||||
Icon: getText(collection.Icon),
|
Icon: utils.PgTextString(collection.Icon),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure default color if empty
|
// Ensure default color if empty
|
||||||
@@ -652,9 +652,9 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
colData := templates.CollectionData{
|
colData := templates.CollectionData{
|
||||||
ID: collectionID,
|
ID: collectionID,
|
||||||
Name: collection.Name,
|
Name: collection.Name,
|
||||||
Description: getText(collection.Description),
|
Description: utils.PgTextString(collection.Description),
|
||||||
Color: getText(collection.Color),
|
Color: utils.PgTextString(collection.Color),
|
||||||
Icon: getText(collection.Icon),
|
Icon: utils.PgTextString(collection.Icon),
|
||||||
IsSystem: collection.IsSystemCollection.Bool,
|
IsSystem: collection.IsSystemCollection.Bool,
|
||||||
IsSmart: cfg.CollectionHandler.CollectionService().IsSmartCollection(collection.AutoAssignRules),
|
IsSmart: cfg.CollectionHandler.CollectionService().IsSmartCollection(collection.AutoAssignRules),
|
||||||
}
|
}
|
||||||
@@ -861,7 +861,7 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
libData[i] = templates.LibraryData{
|
libData[i] = templates.LibraryData{
|
||||||
ID: libUUID.String(),
|
ID: libUUID.String(),
|
||||||
Name: lib.Name,
|
Name: lib.Name,
|
||||||
Description: getText(lib.Description),
|
Description: utils.PgTextString(lib.Description),
|
||||||
TypeName: lib.TypeName,
|
TypeName: lib.TypeName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1056,7 +1056,7 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
item := templates.ArchivedItem{
|
item := templates.ArchivedItem{
|
||||||
ID: itemUUID.String(),
|
ID: itemUUID.String(),
|
||||||
Title: row.Title,
|
Title: row.Title,
|
||||||
Author: getText(row.Author),
|
Author: utils.PgTextString(row.Author),
|
||||||
LibraryName: row.LibraryName,
|
LibraryName: row.LibraryName,
|
||||||
FilePath: row.FilePath,
|
FilePath: row.FilePath,
|
||||||
MissingScans: row.MissingScanCount,
|
MissingScans: row.MissingScanCount,
|
||||||
@@ -1064,7 +1064,7 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
if row.MatchID.Valid {
|
if row.MatchID.Valid {
|
||||||
item.MatchID = uuid.UUID(row.MatchID.Bytes).String()
|
item.MatchID = uuid.UUID(row.MatchID.Bytes).String()
|
||||||
item.MatchTitle = row.MatchTitle
|
item.MatchTitle = row.MatchTitle
|
||||||
item.MatchLibraryName = getText(row.MatchLibraryName)
|
item.MatchLibraryName = utils.PgTextString(row.MatchLibraryName)
|
||||||
}
|
}
|
||||||
if row.ArchivedAt.Valid {
|
if row.ArchivedAt.Valid {
|
||||||
archived := row.ArchivedAt.Time
|
archived := row.ArchivedAt.Time
|
||||||
@@ -1111,7 +1111,7 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
libData[i] = templates.LibraryData{
|
libData[i] = templates.LibraryData{
|
||||||
ID: libUUID.String(),
|
ID: libUUID.String(),
|
||||||
Name: lib.Name,
|
Name: lib.Name,
|
||||||
Description: getText(lib.Description),
|
Description: utils.PgTextString(lib.Description),
|
||||||
TypeName: lib.TypeName,
|
TypeName: lib.TypeName,
|
||||||
TypeValue: lib.TypeName,
|
TypeValue: lib.TypeName,
|
||||||
FolderCount: len(folders),
|
FolderCount: len(folders),
|
||||||
@@ -1292,9 +1292,9 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
Username: u.Username,
|
Username: u.Username,
|
||||||
Email: u.Email,
|
Email: u.Email,
|
||||||
Role: u.Role,
|
Role: u.Role,
|
||||||
Theme: getText(u.Theme),
|
Theme: utils.PgTextString(u.Theme),
|
||||||
FirstName: getText(u.FirstName),
|
FirstName: utils.PgTextString(u.FirstName),
|
||||||
LastName: getText(u.LastName),
|
LastName: utils.PgTextString(u.LastName),
|
||||||
CreatedAt: u.CreatedAt.Time,
|
CreatedAt: u.CreatedAt.Time,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1401,13 +1401,13 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
Username: targetUser.Username,
|
Username: targetUser.Username,
|
||||||
Email: targetUser.Email,
|
Email: targetUser.Email,
|
||||||
Role: targetUser.Role,
|
Role: targetUser.Role,
|
||||||
Theme: getText(targetUser.Theme),
|
Theme: utils.PgTextString(targetUser.Theme),
|
||||||
FirstName: getText(targetUser.FirstName),
|
FirstName: utils.PgTextString(targetUser.FirstName),
|
||||||
LastName: getText(targetUser.LastName),
|
LastName: utils.PgTextString(targetUser.LastName),
|
||||||
CreatedAt: targetUser.CreatedAt.Time,
|
CreatedAt: targetUser.CreatedAt.Time,
|
||||||
// Without this the timezone select rendered UTC selected for
|
// Without this the timezone select rendered UTC selected for
|
||||||
// EVERY user — saves landed, the modal just never showed them.
|
// EVERY user — saves landed, the modal just never showed them.
|
||||||
Timezone: getText(targetUser.Timezone),
|
Timezone: utils.PgTextString(targetUser.Timezone),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render modal
|
// Render modal
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"bookhoard/internal/database"
|
"bookhoard/internal/database"
|
||||||
|
"bookhoard/internal/utils"
|
||||||
"bookhoard/templates"
|
"bookhoard/templates"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -60,8 +61,8 @@ func getTemplateUserWithTheme(c *echo.Context, cfg *Config) (templates.User, err
|
|||||||
Theme: userTheme,
|
Theme: userTheme,
|
||||||
Token: token,
|
Token: token,
|
||||||
Timezone: userTimezone,
|
Timezone: userTimezone,
|
||||||
FirstName: getText(userDB.FirstName),
|
FirstName: utils.PgTextString(userDB.FirstName),
|
||||||
LastName: getText(userDB.LastName),
|
LastName: utils.PgTextString(userDB.LastName),
|
||||||
CreatedAt: userDB.CreatedAt.Time,
|
CreatedAt: userDB.CreatedAt.Time,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -135,13 +136,6 @@ func folderReachableCached(path string) bool {
|
|||||||
return exists
|
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 {
|
func resolveLibrary(c *echo.Context, cfg *Config, userUUID string) LibraryResolution {
|
||||||
res := LibraryResolution{}
|
res := LibraryResolution{}
|
||||||
|
|
||||||
@@ -195,7 +189,7 @@ func resolveLibrary(c *echo.Context, cfg *Config, userUUID string) LibraryResolu
|
|||||||
res.Libraries[i] = templates.LibraryData{
|
res.Libraries[i] = templates.LibraryData{
|
||||||
ID: libUUID.String(),
|
ID: libUUID.String(),
|
||||||
Name: lib.Name,
|
Name: lib.Name,
|
||||||
Description: getText(lib.Description),
|
Description: utils.PgTextString(lib.Description),
|
||||||
TypeName: lib.TypeName,
|
TypeName: lib.TypeName,
|
||||||
TypeValue: lib.TypeName,
|
TypeValue: lib.TypeName,
|
||||||
MediaCount: countMap[libUUID.String()],
|
MediaCount: countMap[libUUID.String()],
|
||||||
|
|||||||
@@ -82,19 +82,19 @@ func registerReaderRoutes(cfg *Config) {
|
|||||||
metadata := templates.ReaderMetadata{
|
metadata := templates.ReaderMetadata{
|
||||||
MediaItemID: mediaUUID.String(),
|
MediaItemID: mediaUUID.String(),
|
||||||
Title: mediaItem.Title,
|
Title: mediaItem.Title,
|
||||||
Author: textToString(mediaItem.Author),
|
Author: utils.PgTextString(mediaItem.Author),
|
||||||
// Sidecar covers store a raw library-relative path; resolve to
|
// Sidecar covers store a raw library-relative path; resolve to
|
||||||
// the cookie-authed /uploads URL (idempotent for items whose
|
// the cookie-authed /uploads URL (idempotent for items whose
|
||||||
// stored value is already a servable path).
|
// stored value is already a servable path).
|
||||||
CoverImagePath: utils.ResolveMediaURL(mediaItem.LibraryID, mediaItem.CoverImagePath),
|
CoverImagePath: utils.ResolveMediaURL(mediaItem.LibraryID, mediaItem.CoverImagePath),
|
||||||
LibraryType: mediaItem.FormatGroup,
|
LibraryType: mediaItem.FormatGroup,
|
||||||
MimeType: textToString(mediaItem.MimeType),
|
MimeType: utils.PgTextString(mediaItem.MimeType),
|
||||||
FilePath: mediaItem.FilePath,
|
FilePath: mediaItem.FilePath,
|
||||||
TotalPages: int(mediaItem.PageCount.Int32),
|
TotalPages: int(mediaItem.PageCount.Int32),
|
||||||
ChapterCount: int(mediaItem.ChapterCount.Int32),
|
ChapterCount: int(mediaItem.ChapterCount.Int32),
|
||||||
FormatGroup: mediaItem.FormatGroup,
|
FormatGroup: mediaItem.FormatGroup,
|
||||||
MangaType: textToString(mediaItem.MangaType),
|
MangaType: utils.PgTextString(mediaItem.MangaType),
|
||||||
ReadingDirection: textToString(mediaItem.ReadingDirection),
|
ReadingDirection: utils.PgTextString(mediaItem.ReadingDirection),
|
||||||
LibraryID: libUUID.String(),
|
LibraryID: libUUID.String(),
|
||||||
FileURL: utils.ResolveMediaURL(mediaItem.LibraryID, pgtype.Text{String: mediaItem.FilePath, Valid: true}),
|
FileURL: utils.ResolveMediaURL(mediaItem.LibraryID, pgtype.Text{String: mediaItem.FilePath, Valid: true}),
|
||||||
TotalCharacters: mediaItem.TotalCharacters.Int64,
|
TotalCharacters: mediaItem.TotalCharacters.Int64,
|
||||||
|
|||||||
@@ -116,11 +116,3 @@ func handleSearchHTML(c *echo.Context, cfg *Config) error {
|
|||||||
}
|
}
|
||||||
return c.HTML(http.StatusOK, buf.String())
|
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, "/")
|
encodedPath := strings.Join(segments, "/")
|
||||||
return fmt.Sprintf("/uploads/library-%s/%s", libraryIDStr, encodedPath)
|
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