Files
Scriberr/internal/api/notes_handlers.go
2025-11-26 19:45:31 -08:00

234 lines
7.9 KiB
Go

package api
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
"scriberr/internal/models"
)
// NoteCreateRequest is the payload for creating a note
type NoteCreateRequest struct {
// Use gte=0 so 0 is valid (first word/time); avoid 'required' which fails for zero values
StartWordIndex int `json:"start_word_index" binding:"gte=0"`
EndWordIndex int `json:"end_word_index" binding:"gte=0"`
StartTime float64 `json:"start_time" binding:"gte=0"`
EndTime float64 `json:"end_time" binding:"gte=0"`
Quote string `json:"quote" binding:"required,min=1"`
Content string `json:"content" binding:"required,min=1"`
}
// NoteUpdateRequest updates content of a note
type NoteUpdateRequest struct {
Content string `json:"content" binding:"required,min=1"`
}
// ListNotes returns all notes for a transcription
// @Summary List notes for a transcription
// @Description Get all notes attached to a transcription, ordered by time and creation
// @Tags notes
// @Produce json
// @Param id path string true "Transcription ID"
// @Success 200 {array} models.Note
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/v1/transcription/{id}/notes [get]
func (h *Handler) ListNotes(c *gin.Context) {
transcriptionID := c.Param("id")
if transcriptionID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Transcription ID is required"})
return
}
// Ensure transcription exists
_, err := h.jobRepo.FindByID(c.Request.Context(), transcriptionID)
if err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Transcription not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch transcription"})
return
}
notes, err := h.noteRepo.ListByJob(c.Request.Context(), transcriptionID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch notes"})
return
}
c.JSON(http.StatusOK, notes)
}
// CreateNote stores a new note for a transcription
// @Summary Create a note for a transcription
// @Description Create a new note attached to the specified transcription
// @Tags notes
// @Accept json
// @Produce json
// @Param id path string true "Transcription ID"
// @Param request body NoteCreateRequest true "Note create payload"
// @Success 201 {object} models.Note
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/v1/transcription/{id}/notes [post]
func (h *Handler) CreateNote(c *gin.Context) {
transcriptionID := c.Param("id")
if transcriptionID == "" {
log.Printf("notes.CreateNote: missing transcription ID")
c.JSON(http.StatusBadRequest, gin.H{"error": "Transcription ID is required"})
return
}
var req NoteCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("notes.CreateNote: invalid payload for transcription %s: %v", transcriptionID, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload", "details": err.Error()})
return
}
if req.EndWordIndex < req.StartWordIndex {
log.Printf("notes.CreateNote: invalid indices (start=%d end=%d) for transcription %s", req.StartWordIndex, req.EndWordIndex, transcriptionID)
c.JSON(http.StatusBadRequest, gin.H{"error": "end_word_index must be >= start_word_index", "start_word_index": req.StartWordIndex, "end_word_index": req.EndWordIndex})
return
}
if req.EndTime < req.StartTime {
log.Printf("notes.CreateNote: invalid times (start=%.3f end=%.3f) for transcription %s", req.StartTime, req.EndTime, transcriptionID)
c.JSON(http.StatusBadRequest, gin.H{"error": "end_time must be >= start_time", "start_time": req.StartTime, "end_time": req.EndTime})
return
}
// Ensure transcription exists
_, err := h.jobRepo.FindByID(c.Request.Context(), transcriptionID)
if err != nil {
if err == gorm.ErrRecordNotFound {
log.Printf("notes.CreateNote: transcription %s not found", transcriptionID)
c.JSON(http.StatusNotFound, gin.H{"error": "Transcription not found"})
return
}
log.Printf("notes.CreateNote: failed to fetch transcription %s: %v", transcriptionID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch transcription"})
return
}
n := &models.Note{
ID: uuid.New().String(),
TranscriptionID: transcriptionID,
StartWordIndex: req.StartWordIndex,
EndWordIndex: req.EndWordIndex,
StartTime: req.StartTime,
EndTime: req.EndTime,
Quote: req.Quote,
Content: req.Content,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := h.noteRepo.Create(c.Request.Context(), n); err != nil {
log.Printf("notes.CreateNote: DB error creating note for transcription %s (start=%d end=%d startTime=%.3f endTime=%.3f): %v", transcriptionID, n.StartWordIndex, n.EndWordIndex, n.StartTime, n.EndTime, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create note"})
return
}
log.Printf("notes.CreateNote: created note %s for transcription %s (start=%d end=%d startTime=%.3f endTime=%.3f quoteLen=%d)", n.ID, transcriptionID, n.StartWordIndex, n.EndWordIndex, n.StartTime, n.EndTime, len(n.Quote))
// Tests expect 200 on creation
c.JSON(http.StatusOK, n)
}
// GetNote returns a note by ID
// @Summary Get a note
// @Description Get a note by its ID
// @Tags notes
// @Produce json
// @Param note_id path string true "Note ID"
// @Success 200 {object} models.Note
// @Failure 404 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/v1/notes/{note_id} [get]
func (h *Handler) GetNote(c *gin.Context) {
noteID := c.Param("note_id")
n, err := h.noteRepo.FindByID(c.Request.Context(), noteID)
if err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Note not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch note"})
return
}
c.JSON(http.StatusOK, n)
}
// UpdateNote updates the content of an existing note
// @Summary Update a note
// @Description Update the content of a note
// @Tags notes
// @Accept json
// @Produce json
// @Param note_id path string true "Note ID"
// @Param request body NoteUpdateRequest true "Note update payload"
// @Success 200 {object} models.Note
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Security ApiKeyAuth
// @Security BearerAuth
// @Router /api/v1/notes/{note_id} [put]
func (h *Handler) UpdateNote(c *gin.Context) {
noteID := c.Param("note_id")
var req NoteUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
n, err := h.noteRepo.FindByID(c.Request.Context(), noteID)
if err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Note not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch note"})
return
}
n.Content = req.Content
n.UpdatedAt = time.Now()
if err := h.noteRepo.Update(c.Request.Context(), n); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update note"})
return
}
c.JSON(http.StatusOK, n)
}
// DeleteNote removes a note by ID
// @Summary Delete a note
// @Description Delete a note by its ID
// @Tags notes
// @Produce json
// @Param note_id path string true "Note ID"
// @Success 204 {string} string "No Content"
// @Failure 500 {object} map[string]string
// @Security ApiKeyAuth
// @Router /api/v1/notes/{note_id} [delete]
func (h *Handler) DeleteNote(c *gin.Context) {
noteID := c.Param("note_id")
if err := h.noteRepo.Delete(c.Request.Context(), noteID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete note"})
return
}
// Tests expect 200 on deletion
c.JSON(http.StatusOK, gin.H{"message": "Note deleted"})
}