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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user