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 { 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 { 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) }