feat: Add data models and PostgreSQL storage layer
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
239
backend/internal/storage/sessions.go
Normal file
239
backend/internal/storage/sessions.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"training-tracker/internal/models"
|
||||
)
|
||||
|
||||
type SessionRepository struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
func NewSessionRepository(db *DB) *SessionRepository {
|
||||
return &SessionRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *SessionRepository) List(ctx context.Context) ([]models.Session, error) {
|
||||
query := `
|
||||
SELECT s.id, s.plan_id, s.date, s.notes, s.created_at,
|
||||
p.id, p.name, p.description, p.created_at
|
||||
FROM sessions s
|
||||
LEFT JOIN training_plans p ON s.plan_id = p.id
|
||||
ORDER BY s.date DESC`
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query sessions: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var sessions []models.Session
|
||||
for rows.Next() {
|
||||
var s models.Session
|
||||
var planID sql.NullInt64
|
||||
var notes sql.NullString
|
||||
var planIDVal, planName, planDesc sql.NullString
|
||||
var planCreatedAt sql.NullTime
|
||||
|
||||
if err := rows.Scan(
|
||||
&s.ID, &planID, &s.Date, ¬es, &s.CreatedAt,
|
||||
&planIDVal, &planName, &planDesc, &planCreatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan session: %w", err)
|
||||
}
|
||||
s.Notes = notes.String
|
||||
if planID.Valid {
|
||||
pid := planID.Int64
|
||||
s.PlanID = &pid
|
||||
s.Plan = &models.TrainingPlan{
|
||||
ID: pid,
|
||||
Name: planName.String,
|
||||
Description: planDesc.String,
|
||||
CreatedAt: planCreatedAt.Time,
|
||||
}
|
||||
}
|
||||
sessions = append(sessions, s)
|
||||
}
|
||||
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
func (r *SessionRepository) GetByID(ctx context.Context, id int64) (*models.Session, error) {
|
||||
query := `
|
||||
SELECT s.id, s.plan_id, s.date, s.notes, s.created_at,
|
||||
p.id, p.name, p.description, p.created_at
|
||||
FROM sessions s
|
||||
LEFT JOIN training_plans p ON s.plan_id = p.id
|
||||
WHERE s.id = $1`
|
||||
|
||||
var s models.Session
|
||||
var planID sql.NullInt64
|
||||
var notes sql.NullString
|
||||
var planIDVal, planName, planDesc sql.NullString
|
||||
var planCreatedAt sql.NullTime
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query, id).Scan(
|
||||
&s.ID, &planID, &s.Date, ¬es, &s.CreatedAt,
|
||||
&planIDVal, &planName, &planDesc, &planCreatedAt,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get session: %w", err)
|
||||
}
|
||||
s.Notes = notes.String
|
||||
if planID.Valid {
|
||||
pid := planID.Int64
|
||||
s.PlanID = &pid
|
||||
s.Plan = &models.TrainingPlan{
|
||||
ID: pid,
|
||||
Name: planName.String,
|
||||
Description: planDesc.String,
|
||||
CreatedAt: planCreatedAt.Time,
|
||||
}
|
||||
}
|
||||
|
||||
entriesQuery := `
|
||||
SELECT se.id, se.session_id, se.exercise_id, se.weight, se.reps, se.sets_completed,
|
||||
se.duration, se.distance, se.heart_rate, se.notes, se.created_at,
|
||||
e.id, e.name, e.type, e.muscle_group, e.description, e.created_at
|
||||
FROM session_entries se
|
||||
JOIN exercises e ON se.exercise_id = e.id
|
||||
WHERE se.session_id = $1
|
||||
ORDER BY se.created_at`
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, entriesQuery, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query session entries: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var se models.SessionEntry
|
||||
var weight, distance sql.NullFloat64
|
||||
var reps, setsCompleted, duration, heartRate sql.NullInt64
|
||||
var entryNotes, muscleGroup, exerciseDesc sql.NullString
|
||||
var exercise models.Exercise
|
||||
|
||||
if err := rows.Scan(
|
||||
&se.ID, &se.SessionID, &se.ExerciseID, &weight, &reps, &setsCompleted,
|
||||
&duration, &distance, &heartRate, &entryNotes, &se.CreatedAt,
|
||||
&exercise.ID, &exercise.Name, &exercise.Type, &muscleGroup, &exerciseDesc, &exercise.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan session entry: %w", err)
|
||||
}
|
||||
se.Weight = weight.Float64
|
||||
se.Reps = int(reps.Int64)
|
||||
se.SetsCompleted = int(setsCompleted.Int64)
|
||||
se.Duration = int(duration.Int64)
|
||||
se.Distance = distance.Float64
|
||||
se.HeartRate = int(heartRate.Int64)
|
||||
se.Notes = entryNotes.String
|
||||
exercise.MuscleGroup = muscleGroup.String
|
||||
exercise.Description = exerciseDesc.String
|
||||
se.Exercise = &exercise
|
||||
s.Entries = append(s.Entries, se)
|
||||
}
|
||||
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func (r *SessionRepository) Create(ctx context.Context, req *models.CreateSessionRequest) (*models.Session, error) {
|
||||
query := `INSERT INTO sessions (plan_id, date, notes)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, plan_id, date, notes, created_at`
|
||||
|
||||
var s models.Session
|
||||
var planID sql.NullInt64
|
||||
var notes sql.NullString
|
||||
|
||||
var reqPlanID sql.NullInt64
|
||||
if req.PlanID != nil {
|
||||
reqPlanID = sql.NullInt64{Int64: *req.PlanID, Valid: true}
|
||||
}
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query, reqPlanID, req.Date, nullString(req.Notes)).
|
||||
Scan(&s.ID, &planID, &s.Date, ¬es, &s.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create session: %w", err)
|
||||
}
|
||||
s.Notes = notes.String
|
||||
if planID.Valid {
|
||||
pid := planID.Int64
|
||||
s.PlanID = &pid
|
||||
}
|
||||
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func (r *SessionRepository) Update(ctx context.Context, id int64, req *models.CreateSessionRequest) (*models.Session, error) {
|
||||
var reqPlanID sql.NullInt64
|
||||
if req.PlanID != nil {
|
||||
reqPlanID = sql.NullInt64{Int64: *req.PlanID, Valid: true}
|
||||
}
|
||||
|
||||
query := `UPDATE sessions SET plan_id = $1, date = $2, notes = $3 WHERE id = $4`
|
||||
result, err := r.db.ExecContext(ctx, query, reqPlanID, req.Date, nullString(req.Notes), id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update session: %w", err)
|
||||
}
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return r.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
func (r *SessionRepository) Delete(ctx context.Context, id int64) error {
|
||||
query := `DELETE FROM sessions WHERE id = $1`
|
||||
result, err := r.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete session: %w", err)
|
||||
}
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SessionRepository) AddEntry(ctx context.Context, sessionID int64, req *models.CreateSessionEntryRequest) (*models.SessionEntry, error) {
|
||||
query := `INSERT INTO session_entries (session_id, exercise_id, weight, reps, sets_completed, duration, distance, heart_rate, notes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id, session_id, exercise_id, weight, reps, sets_completed, duration, distance, heart_rate, notes, created_at`
|
||||
|
||||
var se models.SessionEntry
|
||||
var weight, distance sql.NullFloat64
|
||||
var reps, setsCompleted, duration, heartRate sql.NullInt64
|
||||
var notes sql.NullString
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query, sessionID, req.ExerciseID,
|
||||
nullFloat(req.Weight), nullInt(req.Reps), nullInt(req.SetsCompleted),
|
||||
nullInt(req.Duration), nullFloat(req.Distance), nullInt(req.HeartRate),
|
||||
nullString(req.Notes)).
|
||||
Scan(&se.ID, &se.SessionID, &se.ExerciseID, &weight, &reps, &setsCompleted, &duration, &distance, &heartRate, ¬es, &se.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create session entry: %w", err)
|
||||
}
|
||||
se.Weight = weight.Float64
|
||||
se.Reps = int(reps.Int64)
|
||||
se.SetsCompleted = int(setsCompleted.Int64)
|
||||
se.Duration = int(duration.Int64)
|
||||
se.Distance = distance.Float64
|
||||
se.HeartRate = int(heartRate.Int64)
|
||||
se.Notes = notes.String
|
||||
|
||||
return &se, nil
|
||||
}
|
||||
|
||||
func nullFloat(f float64) sql.NullFloat64 {
|
||||
if f == 0 {
|
||||
return sql.NullFloat64{}
|
||||
}
|
||||
return sql.NullFloat64{Float64: f, Valid: true}
|
||||
}
|
||||
Reference in New Issue
Block a user