feat(dashboard): implement Phase 4.5 Collections Preview Endpoint
Add preview endpoint for custom section builder and rule evaluation: Handler Implementation (internal/handlers/collections.go): - PreviewCollection method: Evaluates filter rules and returns matching items without saving * Accepts library_id, rules array, manual_book_ids array, and limit * Evaluates rules against all library items using collectionService.EvaluateRules * Adds manually selected books to results * Deduplicates manual books (avoids adding same book twice) * Applies limit (default: 20, max: 100) * Returns array of BookInfo with matching items - Helper function: mediaItemsToListMediaItemsRow * Converts database.MediaItems to database.ListMediaItemsRow * Required for EvaluateRules which expects ListMediaItemsRow type Route Registration (internal/router/collections.go): - POST /api/collections/preview - Protected by JWT middleware - Part of collections API group Why This Endpoint is Necessary: - Allows users to see what books match their filter rules BEFORE saving - Avoids creating incorrect collections - Enables testing different rule combinations quickly - Reuses existing service logic (collectionService.EvaluateRules) - Client-side preview would require downloading entire library (10,000+ books) - Would duplicate 500+ lines of rule evaluation logic in TypeScript - Would create maintenance nightmare keeping Go and TypeScript in sync Bruno Test (bruno/collections/preview-collection.bru): - Tests POST /api/collections/preview endpoint - Validates status 200 response - Validates items array in response - Example request with genre filter rule This endpoint is required for both the web UI Custom Section Builder and future mobile apps.
This commit is contained in:
@@ -825,3 +825,135 @@ func (h *CollectionHandler) HandleBulkAddBooks(c echo.Context) error {
|
||||
"failed": failedCount,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *CollectionHandler) PreviewCollection(c echo.Context) error {
|
||||
var req struct {
|
||||
LibraryID string `json:"library_id"`
|
||||
Rules []services.Rule `json:"rules"`
|
||||
ManualBookIDs []string `json:"manual_book_ids"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"})
|
||||
}
|
||||
|
||||
libUUID, err := uuid.Parse(req.LibraryID)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
|
||||
}
|
||||
|
||||
if req.Limit <= 0 || req.Limit > 100 {
|
||||
req.Limit = 20
|
||||
}
|
||||
|
||||
allItems, err := h.db.GetLibraryItems(c.Request().Context(), pgtype.UUID{Bytes: libUUID, Valid: true})
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to load library items"})
|
||||
}
|
||||
|
||||
var matchedItems []database.MediaItems
|
||||
for _, item := range allItems {
|
||||
listItem := mediaItemsToListMediaItemsRow(item)
|
||||
evaluations := h.collectionService.EvaluateRules(listItem, req.Rules)
|
||||
for _, eval := range evaluations {
|
||||
if eval.Matches {
|
||||
matchedItems = append(matchedItems, item)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, bookID := range req.ManualBookIDs {
|
||||
bookUUID, err := uuid.Parse(bookID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, item := range allItems {
|
||||
itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
|
||||
if itemUUID == bookUUID {
|
||||
alreadyAdded := false
|
||||
for _, added := range matchedItems {
|
||||
addedUUID, _ := uuid.FromBytes(added.ID.Bytes[0:16])
|
||||
if addedUUID == bookUUID {
|
||||
alreadyAdded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !alreadyAdded {
|
||||
matchedItems = append(matchedItems, item)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(matchedItems) > req.Limit {
|
||||
matchedItems = matchedItems[:req.Limit]
|
||||
}
|
||||
|
||||
bookCards := make([]BookInfo, len(matchedItems))
|
||||
for i, item := range matchedItems {
|
||||
itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
|
||||
bookCards[i] = BookInfo{
|
||||
MediaItemID: itemUUID.String(),
|
||||
Title: item.Title,
|
||||
Author: textToString(item.Author),
|
||||
CoverImagePath: textToString(item.CoverImagePath),
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"items": bookCards})
|
||||
}
|
||||
|
||||
func mediaItemsToListMediaItemsRow(item database.MediaItems) database.ListMediaItemsRow {
|
||||
return database.ListMediaItemsRow{
|
||||
ID: item.ID,
|
||||
LibraryID: item.LibraryID,
|
||||
Title: item.Title,
|
||||
Author: item.Author,
|
||||
Isbn: item.Isbn,
|
||||
Description: item.Description,
|
||||
FilePath: item.FilePath,
|
||||
FileSize: item.FileSize,
|
||||
MimeType: item.MimeType,
|
||||
CoverImagePath: item.CoverImagePath,
|
||||
Series: item.Series,
|
||||
SeriesNumber: item.SeriesNumber,
|
||||
Tags: item.Tags,
|
||||
Asin: item.Asin,
|
||||
DatePublished: item.DatePublished,
|
||||
Publisher: item.Publisher,
|
||||
Contributors: item.Contributors,
|
||||
Language: item.Language,
|
||||
Edition: item.Edition,
|
||||
PageCount: item.PageCount,
|
||||
Genre: item.Genre,
|
||||
CopyrightYear: item.CopyrightYear,
|
||||
GoodreadsID: item.GoodreadsID,
|
||||
OpenlibraryID: item.OpenlibraryID,
|
||||
GoogleBooksID: item.GoogleBooksID,
|
||||
AddedByAdminID: item.AddedByAdminID,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
FormatGroup: item.FormatGroup,
|
||||
FormatMimetype: item.FormatMimetype,
|
||||
IsReflowable: item.IsReflowable,
|
||||
HasFixedLayout: item.HasFixedLayout,
|
||||
TotalCharacters: item.TotalCharacters,
|
||||
ChapterCount: item.ChapterCount,
|
||||
EntitlementID: item.EntitlementID,
|
||||
RevisionNumber: item.RevisionNumber,
|
||||
KoboContentID: item.KoboContentID,
|
||||
KoboMetadata: item.KoboMetadata,
|
||||
TagsSearch: item.TagsSearch,
|
||||
ContributorsSearch: item.ContributorsSearch,
|
||||
FileSha256: item.FileSha256,
|
||||
OpfIdentifier: item.OpfIdentifier,
|
||||
OpfUuid: item.OpfUuid,
|
||||
HashConfidence: item.HashConfidence,
|
||||
LibraryName: "",
|
||||
LibraryTypeName: "",
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user