package handlers import ( "bookmann/internal/database" "bookmann/internal/services" "encoding/json" "net/http" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v4" ) type CollectionHandler struct { db *database.Queries collectionService *services.CollectionService } func NewCollectionHandler(db *database.Queries) *CollectionHandler { return &CollectionHandler{ db: db, collectionService: services.NewCollectionService(db), } } type CreateCollectionRequest struct { Name string `json:"name" validate:"required"` Description string `json:"description"` Color string `json:"color"` Icon string `json:"icon"` AutoAssignRules []services.Rule `json:"auto_assign_rules"` ViewSettings map[string]interface{} `json:"view_settings"` } type UpdateCollectionRequest struct { Name string `json:"name" validate:"required"` Description string `json:"description"` Color string `json:"color"` Icon string `json:"icon"` AutoAssignRules []services.Rule `json:"auto_assign_rules"` ViewSettings map[string]interface{} `json:"view_settings"` } type AddBooksRequest struct { BookIDs []string `json:"book_ids" validate:"required"` } type CreateDeviceMappingRequest struct { CollectionID string `json:"collection_id" validate:"required"` DeviceShelfName string `json:"device_shelf_name" validate:"required"` SyncDirection string `json:"sync_direction" validate:"required,oneof=bidirectional book_to_device device_to_book none"` } type UpdateDeviceMappingRequest struct { DeviceShelfName string `json:"device_shelf_name" validate:"required"` SyncDirection string `json:"sync_direction" validate:"required,oneof=bidirectional book_to_device device_to_book none"` } func (h *CollectionHandler) CreateCollection(c echo.Context) error { user := c.Get("user").(database.Users) userUUID := uuid.UUID(user.ID.Bytes) var req CreateCollectionRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) } if err := c.Validate(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) } collection, err := h.collectionService.CreateCollection( c.Request().Context(), userUUID, req.Name, req.Description, req.Color, req.Icon, req.AutoAssignRules, req.ViewSettings, ) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } bookCount := int32(0) return c.JSON(http.StatusCreated, map[string]interface{}{ "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), "auto_assign_rules": collection.AutoAssignRules, "view_settings": collection.ViewSettings, "book_count": bookCount, "created_at": collection.CreatedAt.Time.String(), }) } func (h *CollectionHandler) GetCollections(c echo.Context) error { includeAuto := c.QueryParam("include_auto") == "true" sortBy := c.QueryParam("sort_by") collections, err := h.GetCollectionsData(c) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } type CollectionResponse struct { ID uuid.UUID `json:"id"` Name string `json:"name"` Description string `json:"description"` Color string `json:"color"` Icon string `json:"icon"` AutoAssignRules json.RawMessage `json:"auto_assign_rules"` CreatedAt string `json:"created_at"` } response := make([]CollectionResponse, 0, len(collections)) for _, col := range collections { if !includeAuto && len(col.AutoAssignRules) > 0 { continue } response = append(response, CollectionResponse{ ID: uuid.UUID(col.ID.Bytes), Name: col.Name, Description: textToString(col.Description), Color: textToString(col.Color), Icon: textToString(col.Icon), AutoAssignRules: json.RawMessage(col.AutoAssignRules), CreatedAt: col.CreatedAt.Time.String(), }) } if sortBy == "name" { for i := 0; i < len(response); i++ { for j := i + 1; j < len(response); j++ { if response[i].Name > response[j].Name { response[i], response[j] = response[j], response[i] } } } } return c.JSON(http.StatusOK, map[string]interface{}{ "collections": response, "total": len(response), }) } func (h *CollectionHandler) GetCollection(c echo.Context) error { collectionID, err := uuid.Parse(c.Param("id")) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid collection id"}) } collection, err := h.GetCollectionData(c, collectionID) if err != nil { return c.JSON(http.StatusNotFound, map[string]string{"error": "collection not found"}) } books, err := h.GetCollectionBooksData(c, collectionID) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } type BookInfo struct { MediaItemID string `json:"media_item_id"` Title string `json:"title"` Author string `json:"author"` CoverImagePath string `json:"cover_image_path"` } bookList := make([]BookInfo, 0, len(books)) for _, book := range books { bookList = append(bookList, BookInfo{ MediaItemID: uuid.UUID(book.MediaItemID.Bytes).String(), Title: book.Title, Author: textToString(book.Author), CoverImagePath: textToString(book.CoverImagePath), }) } var viewSettings map[string]interface{} if len(collection.ViewSettings) > 0 { json.Unmarshal(collection.ViewSettings, &viewSettings) } return c.JSON(http.StatusOK, map[string]interface{}{ "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), "auto_assign_rules": json.RawMessage(collection.AutoAssignRules), "view_settings": viewSettings, "books": bookList, "book_count": len(bookList), "created_at": collection.CreatedAt.Time.String(), }) } func (h *CollectionHandler) UpdateCollection(c echo.Context) error { collectionID, err := uuid.Parse(c.Param("id")) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid collection id"}) } var req UpdateCollectionRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) } if err := c.Validate(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) } collection, err := h.collectionService.UpdateCollection( c.Request().Context(), collectionID, req.Name, req.Description, req.Color, req.Icon, req.AutoAssignRules, req.ViewSettings, ) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, map[string]interface{}{ "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), "auto_assign_rules": json.RawMessage(collection.AutoAssignRules), "view_settings": json.RawMessage(collection.ViewSettings), "created_at": collection.CreatedAt.Time.String(), }) } func (h *CollectionHandler) DeleteCollection(c echo.Context) error { collectionID, err := uuid.Parse(c.Param("id")) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid collection id"}) } err = h.collectionService.DeleteCollection(c.Request().Context(), collectionID) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.NoContent(http.StatusNoContent) } func (h *CollectionHandler) AddBooks(c echo.Context) error { collectionID, err := uuid.Parse(c.Param("id")) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid collection id"}) } user := c.Get("user").(database.Users) userUUID := uuid.UUID(user.ID.Bytes) var req AddBooksRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) } if err := c.Validate(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) } for _, bookIDStr := range req.BookIDs { bookID, err := uuid.Parse(bookIDStr) if err != nil { continue } h.collectionService.AddBookToCollection(c.Request().Context(), collectionID, bookID, userUUID) } return c.NoContent(http.StatusNoContent) } func (h *CollectionHandler) RemoveBook(c echo.Context) error { collectionID, err := uuid.Parse(c.Param("id")) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid collection id"}) } bookID, err := uuid.Parse(c.Param("bookId")) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid book id"}) } err = h.collectionService.RemoveBookFromCollection(c.Request().Context(), collectionID, bookID) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.NoContent(http.StatusNoContent) } func (h *CollectionHandler) GetDeviceMappings(c echo.Context) error { deviceID, err := uuid.Parse(c.Param("id")) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid device id"}) } mappings, err := h.collectionService.GetDeviceShelfMappings(c.Request().Context(), deviceID) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } type MappingResponse struct { ID uuid.UUID `json:"id"` CollectionID uuid.UUID `json:"collection_id"` CollectionName string `json:"collection_name"` DeviceShelfName string `json:"device_shelf_name"` SyncDirection string `json:"sync_direction"` CreatedAt string `json:"created_at"` } response := make([]MappingResponse, 0, len(mappings)) for _, m := range mappings { response = append(response, MappingResponse{ ID: uuid.UUID(m.ID.Bytes), CollectionID: uuid.UUID(m.CollectionID.Bytes), CollectionName: m.CollectionName, DeviceShelfName: textToString(m.DeviceShelfName), SyncDirection: textToString(m.SyncDirection), CreatedAt: m.CreatedAt.Time.String(), }) } return c.JSON(http.StatusOK, map[string]interface{}{ "device_id": deviceID.String(), "mappings": response, "total": len(response), }) } func (h *CollectionHandler) CreateDeviceMapping(c echo.Context) error { deviceID, err := uuid.Parse(c.Param("id")) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid device id"}) } var req CreateDeviceMappingRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) } if err := c.Validate(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) } collectionID, err := uuid.Parse(req.CollectionID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid collection id"}) } mapping, err := h.collectionService.CreateDeviceShelfMapping( c.Request().Context(), collectionID, deviceID, req.DeviceShelfName, req.SyncDirection, ) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusCreated, map[string]interface{}{ "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), "created_at": mapping.CreatedAt.Time.String(), }) } func (h *CollectionHandler) UpdateDeviceMapping(c echo.Context) error { mappingID, err := uuid.Parse(c.Param("mappingId")) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid mapping id"}) } var req UpdateDeviceMappingRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) } if err := c.Validate(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) } mapping, err := h.collectionService.UpdateDeviceShelfMapping( c.Request().Context(), mappingID, req.DeviceShelfName, req.SyncDirection, ) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, map[string]interface{}{ "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), "created_at": mapping.CreatedAt.Time.String(), }) } func (h *CollectionHandler) DeleteDeviceMapping(c echo.Context) error { mappingID, err := uuid.Parse(c.Param("mappingId")) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid mapping id"}) } err = h.collectionService.DeleteDeviceShelfMapping(c.Request().Context(), mappingID) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.NoContent(http.StatusNoContent) } func (h *CollectionHandler) GetBookCollections(c echo.Context) error { bookID, err := uuid.Parse(c.Param("id")) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid book id"}) } collections, err := h.collectionService.GetBookCollections(c.Request().Context(), bookID) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } type CollectionResponse struct { ID uuid.UUID `json:"id"` Name string `json:"name"` Description string `json:"description"` Color string `json:"color"` Icon string `json:"icon"` } response := make([]CollectionResponse, 0, len(collections)) for _, col := range collections { response = append(response, CollectionResponse{ ID: uuid.UUID(col.ID.Bytes), Name: col.Name, Description: textToString(col.Description), Color: textToString(col.Color), Icon: textToString(col.Icon), }) } return c.JSON(http.StatusOK, map[string]interface{}{ "book_id": bookID.String(), "collections": response, "total": len(response), }) } 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) return h.collectionService.GetUserCollections(c.Request().Context(), userUUID) } func (h *CollectionHandler) GetCollectionData(c echo.Context, collectionID uuid.UUID) (database.Collections, error) { return h.collectionService.GetCollection(c.Request().Context(), collectionID) } func (h *CollectionHandler) GetCollectionBooksData(c echo.Context, collectionID uuid.UUID) ([]database.GetCollectionItemsRow, error) { return h.collectionService.GetCollectionBooks(c.Request().Context(), collectionID) } func (h *CollectionHandler) GetDeviceMappingsData(c echo.Context, deviceID uuid.UUID) ([]database.GetDeviceShelfMappingsRow, error) { return h.collectionService.GetDeviceShelfMappings(c.Request().Context(), deviceID) } func (h *CollectionHandler) GetUserCollectionsList(c echo.Context) ([]database.Collections, error) { user := c.Get("user").(database.Users) userUUID := uuid.UUID(user.ID.Bytes) return h.collectionService.GetUserCollections(c.Request().Context(), userUUID) }