feat: implement notes and highlights API endpoints

- Add complete CRUD API for media items notes (/api/media-items/:id/notes/*)
- Add complete CRUD API for media highlights (/api/media-items/:id/highlights/*)
- Add backward compatibility endpoints for ebooks (/api/ebooks/:id/notes/*, /api/ebooks/:id/highlights/*)
- Implement proper validation for request payloads and UUIDs
- Support hex color codes for highlights with default yellow (#ffff00)
- Support position tracking (page:offset or CFI formats)
- Support optional note association with highlights
This commit is contained in:
2026-01-28 15:42:52 -05:00
parent 29669e2fa3
commit ee4c4faff7
+626
View File
@@ -60,6 +60,20 @@ func SetupRoutes(g *echo.Group, db *database.Queries) {
g.DELETE("/ebooks/:id/rating", h.DeleteEbookRating)
g.GET("/ebooks/:id/ratings", h.GetEbookRatings)
// Ebook notes routes (backward compatibility)
g.GET("/ebooks/:id/notes", h.GetEbookNotes)
g.POST("/ebooks/:id/notes", h.CreateEbookNote)
g.GET("/ebooks/:id/notes/:noteId", h.GetEbookNote)
g.PUT("/ebooks/:id/notes/:noteId", h.UpdateEbookNote)
g.DELETE("/ebooks/:id/notes/:noteId", h.DeleteEbookNote)
// Ebook highlights routes (backward compatibility)
g.GET("/ebooks/:id/highlights", h.GetEbookHighlights)
g.POST("/ebooks/:id/highlights", h.CreateEbookHighlight)
g.GET("/ebooks/:id/highlights/:highlightId", h.GetEbookHighlight)
g.PUT("/ebooks/:id/highlights/:highlightId", h.UpdateEbookHighlight)
g.DELETE("/ebooks/:id/highlights/:highlightId", h.DeleteEbookHighlight)
// Media item routes (new library system)
g.GET("/media-items", h.ListMediaItems)
g.GET("/media-items/:id", h.GetMediaItem)
@@ -71,6 +85,20 @@ func SetupRoutes(g *echo.Group, db *database.Queries) {
g.PUT("/media-items/:id/progress", h.UpdateMediaReadingProgress)
g.DELETE("/media-items/:id/progress", h.DeleteMediaReadingProgress)
// Notes routes
g.GET("/media-items/:id/notes", h.GetMediaNotes)
g.POST("/media-items/:id/notes", h.CreateMediaNote)
g.GET("/media-items/:id/notes/:noteId", h.GetMediaNote)
g.PUT("/media-items/:id/notes/:noteId", h.UpdateMediaNote)
g.DELETE("/media-items/:id/notes/:noteId", h.DeleteMediaNote)
// Highlights routes
g.GET("/media-items/:id/highlights", h.GetMediaHighlights)
g.POST("/media-items/:id/highlights", h.CreateMediaHighlight)
g.GET("/media-items/:id/highlights/:highlightId", h.GetMediaHighlight)
g.PUT("/media-items/:id/highlights/:highlightId", h.UpdateMediaHighlight)
g.DELETE("/media-items/:id/highlights/:highlightId", h.DeleteMediaHighlight)
// Admin-only routes
admin := g.Group("", AdminMiddleware)
admin.POST("/ebooks", h.CreateEbook)
@@ -793,3 +821,601 @@ func (h *Handler) DeleteMediaReadingProgress(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "reading progress deleted"})
}
// Media Notes handlers
// GetMediaNotes handles GET /api/media-items/:id/notes
func (h *Handler) GetMediaNotes(c echo.Context) error {
userID := c.Get("user_id").(string)
userUUID, err := uuid.Parse(userID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
}
mediaID := c.Param("id")
mediaUUID, err := uuid.Parse(mediaID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
}
notes, err := h.db.GetMediaNotes(c.Request().Context(), database.GetMediaNotesParams{
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, notes)
}
// CreateMediaNoteRequest represents the request for creating a media note
type CreateMediaNoteRequest struct {
Content string `json:"content" validate:"required,min=1,max=10000"`
Position string `json:"position"`
}
// CreateMediaNote handles POST /api/media-items/:id/notes
func (h *Handler) CreateMediaNote(c echo.Context) error {
userID := c.Get("user_id").(string)
userUUID, err := uuid.Parse(userID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
}
mediaID := c.Param("id")
mediaUUID, err := uuid.Parse(mediaID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
}
var req CreateMediaNoteRequest
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()})
}
note, err := h.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
Content: req.Content,
Position: pgtype.Text{String: req.Position, Valid: req.Position != ""},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusCreated, note)
}
// GetMediaNote handles GET /api/media-items/:id/notes/:noteId
func (h *Handler) GetMediaNote(c echo.Context) error {
noteID := c.Param("noteId")
noteUUID, err := uuid.Parse(noteID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
}
note, err := h.db.GetMediaNote(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true})
if err != nil {
if err == pgx.ErrNoRows {
return c.JSON(http.StatusNotFound, map[string]string{"error": "note not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, note)
}
// UpdateMediaNoteRequest represents the request for updating a media note
type UpdateMediaNoteRequest struct {
Content string `json:"content" validate:"required,min=1,max=10000"`
Position string `json:"position"`
}
// UpdateMediaNote handles PUT /api/media-items/:id/notes/:noteId
func (h *Handler) UpdateMediaNote(c echo.Context) error {
noteID := c.Param("noteId")
noteUUID, err := uuid.Parse(noteID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
}
var req UpdateMediaNoteRequest
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()})
}
note, err := h.db.UpdateMediaNote(c.Request().Context(), database.UpdateMediaNoteParams{
ID: pgtype.UUID{Bytes: noteUUID, Valid: true},
Content: req.Content,
Position: pgtype.Text{String: req.Position, Valid: req.Position != ""},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, note)
}
// DeleteMediaNote handles DELETE /api/media-items/:id/notes/:noteId
func (h *Handler) DeleteMediaNote(c echo.Context) error {
noteID := c.Param("noteId")
noteUUID, err := uuid.Parse(noteID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
}
err = h.db.DeleteMediaNote(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.NoContent(http.StatusNoContent)
}
// Media Highlights handlers
// GetMediaHighlights handles GET /api/media-items/:id/highlights
func (h *Handler) GetMediaHighlights(c echo.Context) error {
userID := c.Get("user_id").(string)
userUUID, err := uuid.Parse(userID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
}
mediaID := c.Param("id")
mediaUUID, err := uuid.Parse(mediaID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
}
highlights, err := h.db.GetMediaHighlights(c.Request().Context(), database.GetMediaHighlightsParams{
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, highlights)
}
// CreateMediaHighlightRequest represents the request for creating a media highlight
type CreateMediaHighlightRequest struct {
SelectionText string `json:"selection_text" validate:"required,min=1,max=5000"`
StartPosition string `json:"start_position" validate:"required,max=100"`
EndPosition string `json:"end_position" validate:"required,max=100"`
Color string `json:"color" validate:"omitempty,len=7"` // hex color
NoteID string `json:"note_id"`
}
// CreateMediaHighlight handles POST /api/media-items/:id/highlights
func (h *Handler) CreateMediaHighlight(c echo.Context) error {
userID := c.Get("user_id").(string)
userUUID, err := uuid.Parse(userID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
}
mediaID := c.Param("id")
mediaUUID, err := uuid.Parse(mediaID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
}
var req CreateMediaHighlightRequest
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()})
}
var noteUUID pgtype.UUID
if req.NoteID != "" {
if noteID, err := uuid.Parse(req.NoteID); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
} else {
noteUUID = pgtype.UUID{Bytes: noteID, Valid: true}
}
}
color := "#ffff00" // default yellow
if req.Color != "" {
color = req.Color
}
highlight, err := h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
SelectionText: req.SelectionText,
StartPosition: pgtype.Text{String: req.StartPosition, Valid: true},
EndPosition: pgtype.Text{String: req.EndPosition, Valid: true},
Color: pgtype.Text{String: color, Valid: true},
NoteID: noteUUID,
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusCreated, highlight)
}
// GetMediaHighlight handles GET /api/media-items/:id/highlights/:highlightId
func (h *Handler) GetMediaHighlight(c echo.Context) error {
highlightID := c.Param("highlightId")
highlightUUID, err := uuid.Parse(highlightID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid highlight id"})
}
highlight, err := h.db.GetMediaHighlight(c.Request().Context(), pgtype.UUID{Bytes: highlightUUID, Valid: true})
if err != nil {
if err == pgx.ErrNoRows {
return c.JSON(http.StatusNotFound, map[string]string{"error": "highlight not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, highlight)
}
// UpdateMediaHighlightRequest represents the request for updating a media highlight
type UpdateMediaHighlightRequest struct {
SelectionText string `json:"selection_text" validate:"required,min=1,max=5000"`
StartPosition string `json:"start_position" validate:"required,max=100"`
EndPosition string `json:"end_position" validate:"required,max=100"`
Color string `json:"color" validate:"omitempty,len=7"` // hex color
NoteID string `json:"note_id"`
}
// UpdateMediaHighlight handles PUT /api/media-items/:id/highlights/:highlightId
func (h *Handler) UpdateMediaHighlight(c echo.Context) error {
highlightID := c.Param("highlightId")
highlightUUID, err := uuid.Parse(highlightID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid highlight id"})
}
var req UpdateMediaHighlightRequest
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()})
}
var noteUUID pgtype.UUID
if req.NoteID != "" {
if noteID, err := uuid.Parse(req.NoteID); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
} else {
noteUUID = pgtype.UUID{Bytes: noteID, Valid: true}
}
}
color := "#ffff00" // default yellow
if req.Color != "" {
color = req.Color
}
highlight, err := h.db.UpdateMediaHighlight(c.Request().Context(), database.UpdateMediaHighlightParams{
ID: pgtype.UUID{Bytes: highlightUUID, Valid: true},
SelectionText: req.SelectionText,
StartPosition: pgtype.Text{String: req.StartPosition, Valid: true},
EndPosition: pgtype.Text{String: req.EndPosition, Valid: true},
Color: pgtype.Text{String: color, Valid: true},
NoteID: noteUUID,
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, highlight)
}
// DeleteMediaHighlight handles DELETE /api/media-items/:id/highlights/:highlightId
func (h *Handler) DeleteMediaHighlight(c echo.Context) error {
highlightID := c.Param("highlightId")
highlightUUID, err := uuid.Parse(highlightID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid highlight id"})
}
err = h.db.DeleteMediaHighlight(c.Request().Context(), pgtype.UUID{Bytes: highlightUUID, Valid: true})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.NoContent(http.StatusNoContent)
}
// Ebook notes handlers (backward compatibility using views)
// GetEbookNotes handles GET /api/ebooks/:id/notes
func (h *Handler) GetEbookNotes(c echo.Context) error {
userID := c.Get("user_id").(string)
userUUID, err := uuid.Parse(userID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
}
ebookID := c.Param("id")
ebookUUID, err := uuid.Parse(ebookID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid ebook id"})
}
notes, err := h.db.GetEbookNotes(c.Request().Context(), database.GetEbookNotesParams{
EbookID: pgtype.UUID{Bytes: ebookUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, notes)
}
// CreateEbookNote handles POST /api/ebooks/:id/notes
func (h *Handler) CreateEbookNote(c echo.Context) error {
userID := c.Get("user_id").(string)
userUUID, err := uuid.Parse(userID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
}
ebookID := c.Param("id")
ebookUUID, err := uuid.Parse(ebookID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid ebook id"})
}
var req CreateMediaNoteRequest // reuse: same request struct
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()})
}
note, err := h.db.CreateEbookNote(c.Request().Context(), database.CreateEbookNoteParams{
MediaItemID: pgtype.UUID{Bytes: ebookUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
Content: req.Content,
Position: pgtype.Text{String: req.Position, Valid: req.Position != ""},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusCreated, note)
}
// GetEbookNote handles GET /api/ebooks/:id/notes/:noteId
func (h *Handler) GetEbookNote(c echo.Context) error {
noteID := c.Param("noteId")
noteUUID, err := uuid.Parse(noteID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
}
note, err := h.db.GetEbookNote(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true})
if err != nil {
if err == pgx.ErrNoRows {
return c.JSON(http.StatusNotFound, map[string]string{"error": "note not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, note)
}
// UpdateEbookNote handles PUT /api/ebooks/:id/notes/:noteId
func (h *Handler) UpdateEbookNote(c echo.Context) error {
noteID := c.Param("noteId")
noteUUID, err := uuid.Parse(noteID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
}
var req UpdateMediaNoteRequest // reuse: same request struct
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()})
}
note, err := h.db.UpdateEbookNote(c.Request().Context(), database.UpdateEbookNoteParams{
ID: pgtype.UUID{Bytes: noteUUID, Valid: true},
Content: req.Content,
Position: pgtype.Text{String: req.Position, Valid: req.Position != ""},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, note)
}
// DeleteEbookNote handles DELETE /api/ebooks/:id/notes/:noteId
func (h *Handler) DeleteEbookNote(c echo.Context) error {
noteID := c.Param("noteId")
noteUUID, err := uuid.Parse(noteID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
}
err = h.db.DeleteEbookNote(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.NoContent(http.StatusNoContent)
}
// Ebook highlights handlers (backward compatibility using views)
// GetEbookHighlights handles GET /api/ebooks/:id/highlights
func (h *Handler) GetEbookHighlights(c echo.Context) error {
userID := c.Get("user_id").(string)
userUUID, err := uuid.Parse(userID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
}
ebookID := c.Param("id")
ebookUUID, err := uuid.Parse(ebookID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid ebook id"})
}
highlights, err := h.db.GetEbookHighlights(c.Request().Context(), database.GetEbookHighlightsParams{
EbookID: pgtype.UUID{Bytes: ebookUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, highlights)
}
// CreateEbookHighlight handles POST /api/ebooks/:id/highlights
func (h *Handler) CreateEbookHighlight(c echo.Context) error {
userID := c.Get("user_id").(string)
userUUID, err := uuid.Parse(userID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
}
ebookID := c.Param("id")
ebookUUID, err := uuid.Parse(ebookID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid ebook id"})
}
var req CreateMediaHighlightRequest // reuse: same request struct
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()})
}
var noteUUID pgtype.UUID
if req.NoteID != "" {
if noteID, err := uuid.Parse(req.NoteID); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
} else {
noteUUID = pgtype.UUID{Bytes: noteID, Valid: true}
}
}
color := "#ffff00" // default yellow
if req.Color != "" {
color = req.Color
}
highlight, err := h.db.CreateEbookHighlight(c.Request().Context(), database.CreateEbookHighlightParams{
MediaItemID: pgtype.UUID{Bytes: ebookUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
SelectionText: req.SelectionText,
StartPosition: pgtype.Text{String: req.StartPosition, Valid: true},
EndPosition: pgtype.Text{String: req.EndPosition, Valid: true},
Color: pgtype.Text{String: color, Valid: true},
NoteID: noteUUID,
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusCreated, highlight)
}
// GetEbookHighlight handles GET /api/ebooks/:id/highlights/:highlightId
func (h *Handler) GetEbookHighlight(c echo.Context) error {
highlightID := c.Param("highlightId")
highlightUUID, err := uuid.Parse(highlightID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid highlight id"})
}
highlight, err := h.db.GetEbookHighlight(c.Request().Context(), pgtype.UUID{Bytes: highlightUUID, Valid: true})
if err != nil {
if err == pgx.ErrNoRows {
return c.JSON(http.StatusNotFound, map[string]string{"error": "highlight not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, highlight)
}
// UpdateEbookHighlight handles PUT /api/ebooks/:id/highlights/:highlightId
func (h *Handler) UpdateEbookHighlight(c echo.Context) error {
highlightID := c.Param("highlightId")
highlightUUID, err := uuid.Parse(highlightID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid highlight id"})
}
var req UpdateMediaHighlightRequest // reuse: same request struct
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()})
}
var noteUUID pgtype.UUID
if req.NoteID != "" {
if noteID, err := uuid.Parse(req.NoteID); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
} else {
noteUUID = pgtype.UUID{Bytes: noteID, Valid: true}
}
}
color := "#ffff00" // default yellow
if req.Color != "" {
color = req.Color
}
highlight, err := h.db.UpdateEbookHighlight(c.Request().Context(), database.UpdateEbookHighlightParams{
ID: pgtype.UUID{Bytes: highlightUUID, Valid: true},
SelectionText: req.SelectionText,
StartPosition: pgtype.Text{String: req.StartPosition, Valid: true},
EndPosition: pgtype.Text{String: req.EndPosition, Valid: true},
Color: pgtype.Text{String: color, Valid: true},
NoteID: noteUUID,
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, highlight)
}
// DeleteEbookHighlight handles DELETE /api/ebooks/:id/highlights/:highlightId
func (h *Handler) DeleteEbookHighlight(c echo.Context) error {
highlightID := c.Param("highlightId")
highlightUUID, err := uuid.Parse(highlightID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid highlight id"})
}
err = h.db.DeleteEbookHighlight(c.Request().Context(), pgtype.UUID{Bytes: highlightUUID, Valid: true})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.NoContent(http.StatusNoContent)
}