feat: note rest handlers

This commit is contained in:
ae 2025-04-01 18:48:00 +03:00
parent 91daec42de
commit 10bcdf88c7
Signed by: ae
GPG Key ID: 995EFD5C1B532B3E

View File

@ -1,19 +1,262 @@
package service
import (
"context"
"encoding/json"
"net/http"
"strconv"
"git.umbrella.haus/ae/notatest/pkg/data"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
type noteCtxKey struct{}
// Mockable database operations interface
type NoteStore interface {
// TODO: implement
CreateNote(ctx context.Context, userID uuid.UUID) (data.Note, error)
DeleteNote(ctx context.Context, arg data.DeleteNoteParams) error
GetNote(ctx context.Context, arg data.GetNoteParams) (data.Note, error)
ListNotes(ctx context.Context, arg data.ListNotesParams) ([]data.Note, error)
CreateNoteVersion(ctx context.Context, arg data.CreateNoteVersionParams) (data.NoteVersion, error)
FindDuplicateContent(ctx context.Context, arg data.FindDuplicateContentParams) (bool, error)
GetNoteVersion(ctx context.Context, arg data.GetNoteVersionParams) (data.NoteVersion, error)
GetNoteVersions(ctx context.Context, arg data.GetNoteVersionsParams) ([]data.NoteVersion, error)
}
type notesResource struct {
Notes NoteStore
JWTSecret string
Notes NoteStore
}
func (rs notesResource) Routes() chi.Router {
r := chi.NewRouter()
r.Group(func(r chi.Router) {
r.Use(requireAccessToken(rs.JWTSecret))
r.Post("/", rs.CreateNote) // POST /notes - note creation
r.Get("/", rs.ListNotes) // GET /notes - get all notes
r.Route("/{id}", func(r chi.Router) {
r.Use(noteCtx(rs.Notes))
r.Get("/", rs.GetNote) // GET /notes/{id} - get specific note
r.Delete("/", rs.DeleteNote) // DELETE /notes/{id} - delete specific note
r.Route("/versions", func(r chi.Router) {
r.Post("/", rs.CreateNoteVersion) // POST /notes/{id}/versions - create new version
r.Get("/", rs.ListNoteVersions) // GET /notes/{id}/versions - get all existing versions
r.Get("/{version}", rs.GetNoteVersion) // GET /notes/{id}/versions/{version} - get specific version
})
})
})
return r
}
func (rs *notesResource) CreateNote(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(userCtxKey{}).(*userClaims)
if !ok {
respondError(w, http.StatusUnauthorized, "Unauthorized")
return
}
userID, err := uuid.Parse(user.Subject)
if err != nil {
respondError(w, http.StatusInternalServerError, "Invalid user ID")
return
}
note, err := rs.Notes.CreateNote(r.Context(), userID)
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to create note")
return
}
respondJSON(w, http.StatusCreated, note)
}
func (rs *notesResource) ListNotes(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(userCtxKey{}).(*userClaims)
if !ok {
respondError(w, http.StatusUnauthorized, "Unauthorized")
return
}
userID, err := uuid.Parse(user.Subject)
if err != nil {
respondError(w, http.StatusInternalServerError, "Invalid user ID")
return
}
limit, offset := getPaginationParams(r)
notes, err := rs.Notes.ListNotes(r.Context(), data.ListNotesParams{
UserID: userID,
Limit: limit,
Offset: offset,
})
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to retrieve notes")
return
}
respondJSON(w, http.StatusOK, notes)
}
func (rs *notesResource) GetNote(w http.ResponseWriter, r *http.Request) {
note, ok := r.Context().Value(noteCtxKey{}).(data.Note)
if !ok {
respondError(w, http.StatusNotFound, "Note not found")
return
}
respondJSON(w, http.StatusOK, note)
}
func (rs *notesResource) DeleteNote(w http.ResponseWriter, r *http.Request) {
note, ok := r.Context().Value(noteCtxKey{}).(data.Note)
if !ok {
respondError(w, http.StatusNotFound, "Note not found")
return
}
user, ok := r.Context().Value(userCtxKey{}).(*userClaims)
if !ok {
respondError(w, http.StatusUnauthorized, "Unauthorized")
return
}
userID, err := uuid.Parse(user.Subject)
if err != nil {
respondError(w, http.StatusInternalServerError, "Invalid user ID")
return
}
err = rs.Notes.DeleteNote(r.Context(), data.DeleteNoteParams{
ID: note.ID,
UserID: userID,
})
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to delete note")
return
}
w.WriteHeader(http.StatusNoContent)
}
func (rs *notesResource) CreateNoteVersion(w http.ResponseWriter, r *http.Request) {
note, ok := r.Context().Value(noteCtxKey{}).(data.Note)
if !ok {
respondError(w, http.StatusNotFound, "Note not found")
return
}
var req struct {
Title string `json:"title"`
Content string `json:"content"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
// De-duplication check
duplicate, err := rs.Notes.FindDuplicateContent(r.Context(), data.FindDuplicateContentParams{
NoteID: note.ID,
Column2: []byte(req.Title),
Column3: []byte(req.Content),
})
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to check for duplicate content")
return
}
if duplicate {
respondError(w, http.StatusConflict, "Duplicate content detected")
return
}
version, err := rs.Notes.CreateNoteVersion(r.Context(), data.CreateNoteVersionParams{
NoteID: note.ID,
Title: req.Title,
Content: req.Content,
})
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to create note version")
return
}
respondJSON(w, http.StatusCreated, version)
}
func (rs *notesResource) ListNoteVersions(w http.ResponseWriter, r *http.Request) {
note, ok := r.Context().Value(noteCtxKey{}).(data.Note)
if !ok {
respondError(w, http.StatusNotFound, "Note not found")
return
}
limit, offset := getPaginationParams(r)
versions, err := rs.Notes.GetNoteVersions(r.Context(), data.GetNoteVersionsParams{
NoteID: note.ID,
Limit: limit,
Offset: offset,
})
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to retrieve versions")
return
}
respondJSON(w, http.StatusOK, versions)
}
func (rs *notesResource) GetNoteVersion(w http.ResponseWriter, r *http.Request) {
note, ok := r.Context().Value(noteCtxKey{}).(data.Note)
if !ok {
respondError(w, http.StatusNotFound, "Note not found")
return
}
versionStr := chi.URLParam(r, "version")
versionNumber, err := strconv.ParseInt(versionStr, 10, 32)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid version number")
return
}
version, err := rs.Notes.GetNoteVersion(r.Context(), data.GetNoteVersionParams{
NoteID: note.ID,
VersionNumber: int32(versionNumber),
})
if err != nil {
respondError(w, http.StatusNotFound, "Version not found")
return
}
respondJSON(w, http.StatusOK, version)
}
func getPaginationParams(r *http.Request) (limit int32, offset int32) {
defaultLimit := 50
defaultOffset := 0
limitStr := r.URL.Query().Get("limit")
if limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
defaultLimit = l
}
}
offsetStr := r.URL.Query().Get("offset")
if offsetStr != "" {
if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
defaultOffset = o
}
}
return int32(defaultLimit), int32(defaultOffset)
}