feat: Add REST API handlers for exercises, plans, and sessions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
44
backend/cmd/server/main.go
Normal file
44
backend/cmd/server/main.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"training-tracker/internal/api"
|
||||||
|
"training-tracker/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||||
|
slog.SetDefault(logger)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
db, err := storage.NewDB(ctx)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to connect to database", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
if err := db.Migrate(ctx); err != nil {
|
||||||
|
slog.Error("failed to run migrations", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
slog.Info("database migrations completed")
|
||||||
|
|
||||||
|
router := api.NewRouter(db)
|
||||||
|
|
||||||
|
port := os.Getenv("PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("server starting", "port", port)
|
||||||
|
if err := http.ListenAndServe(":"+port, router); err != nil {
|
||||||
|
slog.Error("server failed", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
135
backend/internal/api/exercises.go
Normal file
135
backend/internal/api/exercises.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"training-tracker/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExerciseHandler struct {
|
||||||
|
repo ExerciseRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExerciseHandler(repo ExerciseRepository) *ExerciseHandler {
|
||||||
|
return &ExerciseHandler{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ExerciseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, _ := parseID(r, "/api/exercises/")
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
if id > 0 {
|
||||||
|
h.getByID(w, r, id)
|
||||||
|
} else {
|
||||||
|
h.list(w, r)
|
||||||
|
}
|
||||||
|
case http.MethodPost:
|
||||||
|
h.create(w, r)
|
||||||
|
case http.MethodPut:
|
||||||
|
if id > 0 {
|
||||||
|
h.update(w, r, id)
|
||||||
|
} else {
|
||||||
|
respondError(w, http.StatusBadRequest, "exercise ID required")
|
||||||
|
}
|
||||||
|
case http.MethodDelete:
|
||||||
|
if id > 0 {
|
||||||
|
h.delete(w, r, id)
|
||||||
|
} else {
|
||||||
|
respondError(w, http.StatusBadRequest, "exercise ID required")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
respondError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ExerciseHandler) list(w http.ResponseWriter, r *http.Request) {
|
||||||
|
exercises, err := h.repo.List(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, "failed to list exercises")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if exercises == nil {
|
||||||
|
exercises = []models.Exercise{}
|
||||||
|
}
|
||||||
|
respondJSON(w, http.StatusOK, exercises)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ExerciseHandler) getByID(w http.ResponseWriter, r *http.Request, id int64) {
|
||||||
|
exercise, err := h.repo.GetByID(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, "failed to get exercise")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if exercise == nil {
|
||||||
|
respondError(w, http.StatusNotFound, "exercise not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondJSON(w, http.StatusOK, exercise)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ExerciseHandler) create(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req models.CreateExerciseRequest
|
||||||
|
if err := decodeJSON(r, &req); err != nil {
|
||||||
|
respondError(w, http.StatusBadRequest, "invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name == "" {
|
||||||
|
respondError(w, http.StatusBadRequest, "name is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Type != models.ExerciseTypeStrength && req.Type != models.ExerciseTypeCardio {
|
||||||
|
respondError(w, http.StatusBadRequest, "type must be 'strength' or 'cardio'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exercise, err := h.repo.Create(r.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, "failed to create exercise")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondJSON(w, http.StatusCreated, exercise)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ExerciseHandler) update(w http.ResponseWriter, r *http.Request, id int64) {
|
||||||
|
var req models.CreateExerciseRequest
|
||||||
|
if err := decodeJSON(r, &req); err != nil {
|
||||||
|
respondError(w, http.StatusBadRequest, "invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name == "" {
|
||||||
|
respondError(w, http.StatusBadRequest, "name is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Type != models.ExerciseTypeStrength && req.Type != models.ExerciseTypeCardio {
|
||||||
|
respondError(w, http.StatusBadRequest, "type must be 'strength' or 'cardio'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exercise, err := h.repo.Update(r.Context(), id, &req)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, "failed to update exercise")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if exercise == nil {
|
||||||
|
respondError(w, http.StatusNotFound, "exercise not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondJSON(w, http.StatusOK, exercise)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ExerciseHandler) delete(w http.ResponseWriter, r *http.Request, id int64) {
|
||||||
|
err := h.repo.Delete(r.Context(), id)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
respondError(w, http.StatusNotFound, "exercise not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, "failed to delete exercise")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
36
backend/internal/api/handlers.go
Normal file
36
backend/internal/api/handlers.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Response helpers
|
||||||
|
|
||||||
|
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
if data != nil {
|
||||||
|
json.NewEncoder(w).Encode(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func respondError(w http.ResponseWriter, status int, message string) {
|
||||||
|
respondJSON(w, status, map[string]string{"error": message})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseID(r *http.Request, prefix string) (int64, error) {
|
||||||
|
path := strings.TrimPrefix(r.URL.Path, prefix)
|
||||||
|
path = strings.TrimSuffix(path, "/")
|
||||||
|
parts := strings.Split(path, "/")
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return strconv.ParseInt(parts[0], 10, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeJSON(r *http.Request, v interface{}) error {
|
||||||
|
return json.NewDecoder(r.Body).Decode(v)
|
||||||
|
}
|
||||||
35
backend/internal/api/interfaces.go
Normal file
35
backend/internal/api/interfaces.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"training-tracker/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExerciseRepository defines the interface for exercise storage operations
|
||||||
|
type ExerciseRepository interface {
|
||||||
|
List(ctx context.Context) ([]models.Exercise, error)
|
||||||
|
GetByID(ctx context.Context, id int64) (*models.Exercise, error)
|
||||||
|
Create(ctx context.Context, req *models.CreateExerciseRequest) (*models.Exercise, error)
|
||||||
|
Update(ctx context.Context, id int64, req *models.CreateExerciseRequest) (*models.Exercise, error)
|
||||||
|
Delete(ctx context.Context, id int64) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlanRepository defines the interface for training plan storage operations
|
||||||
|
type PlanRepository interface {
|
||||||
|
List(ctx context.Context) ([]models.TrainingPlan, error)
|
||||||
|
GetByID(ctx context.Context, id int64) (*models.TrainingPlan, error)
|
||||||
|
Create(ctx context.Context, req *models.CreatePlanRequest) (*models.TrainingPlan, error)
|
||||||
|
Update(ctx context.Context, id int64, req *models.CreatePlanRequest) (*models.TrainingPlan, error)
|
||||||
|
Delete(ctx context.Context, id int64) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionRepository defines the interface for session storage operations
|
||||||
|
type SessionRepository interface {
|
||||||
|
List(ctx context.Context) ([]models.Session, error)
|
||||||
|
GetByID(ctx context.Context, id int64) (*models.Session, error)
|
||||||
|
Create(ctx context.Context, req *models.CreateSessionRequest) (*models.Session, error)
|
||||||
|
Update(ctx context.Context, id int64, req *models.CreateSessionRequest) (*models.Session, error)
|
||||||
|
Delete(ctx context.Context, id int64) error
|
||||||
|
AddEntry(ctx context.Context, sessionID int64, req *models.CreateSessionEntryRequest) (*models.SessionEntry, error)
|
||||||
|
}
|
||||||
127
backend/internal/api/plans.go
Normal file
127
backend/internal/api/plans.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"training-tracker/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlanHandler struct {
|
||||||
|
repo PlanRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPlanHandler(repo PlanRepository) *PlanHandler {
|
||||||
|
return &PlanHandler{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PlanHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, _ := parseID(r, "/api/plans/")
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
if id > 0 {
|
||||||
|
h.getByID(w, r, id)
|
||||||
|
} else {
|
||||||
|
h.list(w, r)
|
||||||
|
}
|
||||||
|
case http.MethodPost:
|
||||||
|
h.create(w, r)
|
||||||
|
case http.MethodPut:
|
||||||
|
if id > 0 {
|
||||||
|
h.update(w, r, id)
|
||||||
|
} else {
|
||||||
|
respondError(w, http.StatusBadRequest, "plan ID required")
|
||||||
|
}
|
||||||
|
case http.MethodDelete:
|
||||||
|
if id > 0 {
|
||||||
|
h.delete(w, r, id)
|
||||||
|
} else {
|
||||||
|
respondError(w, http.StatusBadRequest, "plan ID required")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
respondError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PlanHandler) list(w http.ResponseWriter, r *http.Request) {
|
||||||
|
plans, err := h.repo.List(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, "failed to list plans")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if plans == nil {
|
||||||
|
plans = []models.TrainingPlan{}
|
||||||
|
}
|
||||||
|
respondJSON(w, http.StatusOK, plans)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PlanHandler) getByID(w http.ResponseWriter, r *http.Request, id int64) {
|
||||||
|
plan, err := h.repo.GetByID(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, "failed to get plan")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if plan == nil {
|
||||||
|
respondError(w, http.StatusNotFound, "plan not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondJSON(w, http.StatusOK, plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PlanHandler) create(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req models.CreatePlanRequest
|
||||||
|
if err := decodeJSON(r, &req); err != nil {
|
||||||
|
respondError(w, http.StatusBadRequest, "invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name == "" {
|
||||||
|
respondError(w, http.StatusBadRequest, "name is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
plan, err := h.repo.Create(r.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, "failed to create plan")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondJSON(w, http.StatusCreated, plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PlanHandler) update(w http.ResponseWriter, r *http.Request, id int64) {
|
||||||
|
var req models.CreatePlanRequest
|
||||||
|
if err := decodeJSON(r, &req); err != nil {
|
||||||
|
respondError(w, http.StatusBadRequest, "invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name == "" {
|
||||||
|
respondError(w, http.StatusBadRequest, "name is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
plan, err := h.repo.Update(r.Context(), id, &req)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, "failed to update plan")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if plan == nil {
|
||||||
|
respondError(w, http.StatusNotFound, "plan not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondJSON(w, http.StatusOK, plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PlanHandler) delete(w http.ResponseWriter, r *http.Request, id int64) {
|
||||||
|
err := h.repo.Delete(r.Context(), id)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
respondError(w, http.StatusNotFound, "plan not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, "failed to delete plan")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
55
backend/internal/api/router.go
Normal file
55
backend/internal/api/router.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"training-tracker/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Router sets up and returns the HTTP router
|
||||||
|
type Router struct {
|
||||||
|
exerciseHandler *ExerciseHandler
|
||||||
|
planHandler *PlanHandler
|
||||||
|
sessionHandler *SessionHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRouter creates a new router with all handlers
|
||||||
|
func NewRouter(db *storage.DB) *Router {
|
||||||
|
exerciseRepo := storage.NewExerciseRepository(db)
|
||||||
|
planRepo := storage.NewPlanRepository(db)
|
||||||
|
sessionRepo := storage.NewSessionRepository(db)
|
||||||
|
|
||||||
|
return &Router{
|
||||||
|
exerciseHandler: NewExerciseHandler(exerciseRepo),
|
||||||
|
planHandler: NewPlanHandler(planRepo),
|
||||||
|
sessionHandler: NewSessionHandler(sessionRepo),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP implements http.Handler
|
||||||
|
func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Enable CORS
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||||
|
|
||||||
|
if r.Method == http.MethodOptions {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path := r.URL.Path
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(path, "/api/exercises"):
|
||||||
|
router.exerciseHandler.ServeHTTP(w, r)
|
||||||
|
case strings.HasPrefix(path, "/api/plans"):
|
||||||
|
router.planHandler.ServeHTTP(w, r)
|
||||||
|
case strings.HasPrefix(path, "/api/sessions"):
|
||||||
|
router.sessionHandler.ServeHTTP(w, r)
|
||||||
|
case path == "/api/health":
|
||||||
|
respondJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||||
|
default:
|
||||||
|
respondError(w, http.StatusNotFound, "not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
166
backend/internal/api/sessions.go
Normal file
166
backend/internal/api/sessions.go
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"training-tracker/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionHandler struct {
|
||||||
|
repo SessionRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSessionHandler(repo SessionRepository) *SessionHandler {
|
||||||
|
return &SessionHandler{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SessionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
path := strings.TrimPrefix(r.URL.Path, "/api/sessions")
|
||||||
|
path = strings.TrimPrefix(path, "/")
|
||||||
|
parts := strings.Split(path, "/")
|
||||||
|
|
||||||
|
if len(parts) >= 2 && parts[1] == "entries" {
|
||||||
|
id, err := parseID(r, "/api/sessions/")
|
||||||
|
if err != nil || id == 0 {
|
||||||
|
respondError(w, http.StatusBadRequest, "session ID required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
h.addEntry(w, r, id)
|
||||||
|
} else {
|
||||||
|
respondError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, _ := parseID(r, "/api/sessions/")
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
if id > 0 {
|
||||||
|
h.getByID(w, r, id)
|
||||||
|
} else {
|
||||||
|
h.list(w, r)
|
||||||
|
}
|
||||||
|
case http.MethodPost:
|
||||||
|
h.create(w, r)
|
||||||
|
case http.MethodPut:
|
||||||
|
if id > 0 {
|
||||||
|
h.update(w, r, id)
|
||||||
|
} else {
|
||||||
|
respondError(w, http.StatusBadRequest, "session ID required")
|
||||||
|
}
|
||||||
|
case http.MethodDelete:
|
||||||
|
if id > 0 {
|
||||||
|
h.delete(w, r, id)
|
||||||
|
} else {
|
||||||
|
respondError(w, http.StatusBadRequest, "session ID required")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
respondError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SessionHandler) list(w http.ResponseWriter, r *http.Request) {
|
||||||
|
sessions, err := h.repo.List(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, "failed to list sessions")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if sessions == nil {
|
||||||
|
sessions = []models.Session{}
|
||||||
|
}
|
||||||
|
respondJSON(w, http.StatusOK, sessions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SessionHandler) getByID(w http.ResponseWriter, r *http.Request, id int64) {
|
||||||
|
session, err := h.repo.GetByID(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, "failed to get session")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if session == nil {
|
||||||
|
respondError(w, http.StatusNotFound, "session not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondJSON(w, http.StatusOK, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SessionHandler) create(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req models.CreateSessionRequest
|
||||||
|
if err := decodeJSON(r, &req); err != nil {
|
||||||
|
respondError(w, http.StatusBadRequest, "invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Date.IsZero() {
|
||||||
|
respondError(w, http.StatusBadRequest, "date is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := h.repo.Create(r.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, "failed to create session")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondJSON(w, http.StatusCreated, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SessionHandler) update(w http.ResponseWriter, r *http.Request, id int64) {
|
||||||
|
var req models.CreateSessionRequest
|
||||||
|
if err := decodeJSON(r, &req); err != nil {
|
||||||
|
respondError(w, http.StatusBadRequest, "invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Date.IsZero() {
|
||||||
|
respondError(w, http.StatusBadRequest, "date is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := h.repo.Update(r.Context(), id, &req)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, "failed to update session")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if session == nil {
|
||||||
|
respondError(w, http.StatusNotFound, "session not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondJSON(w, http.StatusOK, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SessionHandler) delete(w http.ResponseWriter, r *http.Request, id int64) {
|
||||||
|
err := h.repo.Delete(r.Context(), id)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
respondError(w, http.StatusNotFound, "session not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, "failed to delete session")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SessionHandler) addEntry(w http.ResponseWriter, r *http.Request, sessionID int64) {
|
||||||
|
var req models.CreateSessionEntryRequest
|
||||||
|
if err := decodeJSON(r, &req); err != nil {
|
||||||
|
respondError(w, http.StatusBadRequest, "invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ExerciseID == 0 {
|
||||||
|
respondError(w, http.StatusBadRequest, "exercise_id is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, err := h.repo.AddEntry(r.Context(), sessionID, &req)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, "failed to add session entry")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondJSON(w, http.StatusCreated, entry)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user