Conform Go code to project conventions

- Propagate context.Context through all exported store/service methods
  that perform I/O; use QueryContext/ExecContext/QueryRowContext throughout
- Add package-level sentinel errors (ErrNotFound, ErrAlreadyCheckedIn,
  ErrNotCheckedIn) and replace nil,nil returns with explicit errors
- Update handlers to use errors.Is() instead of nil checks, with correct
  HTTP status codes per error type
- Fix SQLite datetime('now') → MySQL NOW() in volunteer, schedule,
  timeoff, and checkin stores
- Refactor db.Migrate to execute schema statements individually (MySQL
  driver does not support multi-statement Exec)
- Fix import grouping in handler files (stdlib, external, internal)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 16:20:23 -04:00
parent 55f68c571e
commit 87caf478df
14 changed files with 180 additions and 145 deletions

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
@@ -29,7 +30,7 @@ func main() {
} }
defer database.Close() defer database.Close()
if err := db.Migrate(database); err != nil { if err := db.Migrate(context.Background(), database); err != nil {
log.Fatalf("migrate database: %v", err) log.Fatalf("migrate database: %v", err)
} }

View File

@@ -1,6 +1,7 @@
package auth package auth
import ( import (
"context"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
@@ -27,10 +28,10 @@ func NewService(db *sql.DB, secret string) *Service {
return &Service{db: db, jwtSecret: []byte(secret)} return &Service{db: db, jwtSecret: []byte(secret)}
} }
func (s *Service) Login(email, password string) (string, error) { func (s *Service) Login(ctx context.Context, email, password string) (string, error) {
var id int64 var id int64
var hash, role string var hash, role string
err := s.db.QueryRow( err := s.db.QueryRowContext(ctx,
`SELECT id, password, role FROM volunteers WHERE email = ? AND active = 1`, `SELECT id, password, role FROM volunteers WHERE email = ? AND active = 1`,
email, email,
).Scan(&id, &hash, &role) ).Scan(&id, &hash, &role)

View File

@@ -1,12 +1,19 @@
package checkin package checkin
import ( import (
"context"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"time" "time"
) )
var (
ErrNotFound = fmt.Errorf("check-in not found")
ErrAlreadyCheckedIn = fmt.Errorf("already checked in")
ErrNotCheckedIn = fmt.Errorf("not checked in")
)
type CheckIn struct { type CheckIn struct {
ID int64 `json:"id"` ID int64 `json:"id"`
VolunteerID int64 `json:"volunteer_id"` VolunteerID int64 `json:"volunteer_id"`
@@ -33,17 +40,17 @@ func NewStore(db *sql.DB) *Store {
return &Store{db: db} return &Store{db: db}
} }
func (s *Store) CheckIn(volunteerID int64, in CheckInInput) (*CheckIn, error) { func (s *Store) CheckIn(ctx context.Context, volunteerID int64, in CheckInInput) (*CheckIn, error) {
// Ensure no active check-in exists // Ensure no active check-in exists
var count int var count int
s.db.QueryRow( s.db.QueryRowContext(ctx,
`SELECT COUNT(*) FROM checkins WHERE volunteer_id = ? AND checked_out_at IS NULL`, volunteerID, `SELECT COUNT(*) FROM checkins WHERE volunteer_id = ? AND checked_out_at IS NULL`, volunteerID,
).Scan(&count) ).Scan(&count)
if count > 0 { if count > 0 {
return nil, fmt.Errorf("already checked in") return nil, ErrAlreadyCheckedIn
} }
res, err := s.db.Exec( res, err := s.db.ExecContext(ctx,
`INSERT INTO checkins (volunteer_id, schedule_id, notes) VALUES (?, ?, ?)`, `INSERT INTO checkins (volunteer_id, schedule_id, notes) VALUES (?, ?, ?)`,
volunteerID, in.ScheduleID, in.Notes, volunteerID, in.ScheduleID, in.Notes,
) )
@@ -51,43 +58,43 @@ func (s *Store) CheckIn(volunteerID int64, in CheckInInput) (*CheckIn, error) {
return nil, fmt.Errorf("insert checkin: %w", err) return nil, fmt.Errorf("insert checkin: %w", err)
} }
id, _ := res.LastInsertId() id, _ := res.LastInsertId()
return s.GetByID(id) return s.GetByID(ctx, id)
} }
func (s *Store) CheckOut(volunteerID int64, in CheckOutInput) (*CheckIn, error) { func (s *Store) CheckOut(ctx context.Context, volunteerID int64, in CheckOutInput) (*CheckIn, error) {
var id int64 var id int64
err := s.db.QueryRow( err := s.db.QueryRowContext(ctx,
`SELECT id FROM checkins WHERE volunteer_id = ? AND checked_out_at IS NULL ORDER BY checked_in_at DESC LIMIT 1`, `SELECT id FROM checkins WHERE volunteer_id = ? AND checked_out_at IS NULL ORDER BY checked_in_at DESC LIMIT 1`,
volunteerID, volunteerID,
).Scan(&id) ).Scan(&id)
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("not checked in") return nil, ErrNotCheckedIn
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("find active checkin: %w", err) return nil, fmt.Errorf("find active checkin: %w", err)
} }
_, err = s.db.Exec( _, err = s.db.ExecContext(ctx,
`UPDATE checkins SET checked_out_at=datetime('now'), notes=COALESCE(NULLIF(?, ''), notes) WHERE id=?`, `UPDATE checkins SET checked_out_at=NOW(), notes=COALESCE(NULLIF(?, ''), notes) WHERE id=?`,
in.Notes, id, in.Notes, id,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("checkout: %w", err) return nil, fmt.Errorf("checkout: %w", err)
} }
return s.GetByID(id) return s.GetByID(ctx, id)
} }
func (s *Store) GetByID(id int64) (*CheckIn, error) { func (s *Store) GetByID(ctx context.Context, id int64) (*CheckIn, error) {
ci := &CheckIn{} ci := &CheckIn{}
var checkedInAt string var checkedInAt string
var checkedOutAt sql.NullString var checkedOutAt sql.NullString
var scheduleID sql.NullInt64 var scheduleID sql.NullInt64
var notes sql.NullString var notes sql.NullString
err := s.db.QueryRow( err := s.db.QueryRowContext(ctx,
`SELECT id, volunteer_id, schedule_id, checked_in_at, checked_out_at, notes FROM checkins WHERE id = ?`, id, `SELECT id, volunteer_id, schedule_id, checked_in_at, checked_out_at, notes FROM checkins WHERE id = ?`, id,
).Scan(&ci.ID, &ci.VolunteerID, &scheduleID, &checkedInAt, &checkedOutAt, &notes) ).Scan(&ci.ID, &ci.VolunteerID, &scheduleID, &checkedInAt, &checkedOutAt, &notes)
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, nil return nil, ErrNotFound
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("get checkin: %w", err) return nil, fmt.Errorf("get checkin: %w", err)
@@ -106,7 +113,7 @@ func (s *Store) GetByID(id int64) (*CheckIn, error) {
return ci, nil return ci, nil
} }
func (s *Store) History(volunteerID int64) ([]CheckIn, error) { func (s *Store) History(ctx context.Context, volunteerID int64) ([]CheckIn, error) {
query := `SELECT id, volunteer_id, schedule_id, checked_in_at, checked_out_at, notes FROM checkins` query := `SELECT id, volunteer_id, schedule_id, checked_in_at, checked_out_at, notes FROM checkins`
args := []any{} args := []any{}
if volunteerID > 0 { if volunteerID > 0 {
@@ -115,7 +122,7 @@ func (s *Store) History(volunteerID int64) ([]CheckIn, error) {
} }
query += ` ORDER BY checked_in_at DESC` query += ` ORDER BY checked_in_at DESC`
rows, err := s.db.Query(query, args...) rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return nil, fmt.Errorf("list checkins: %w", err) return nil, fmt.Errorf("list checkins: %w", err)
} }

View File

@@ -2,6 +2,7 @@ package checkin
import ( import (
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"git.unsupervised.ca/walkies/internal/respond" "git.unsupervised.ca/walkies/internal/respond"
@@ -24,11 +25,15 @@ func (h *Handler) CheckIn(w http.ResponseWriter, r *http.Request) {
respond.Error(w, http.StatusBadRequest, "invalid request body") respond.Error(w, http.StatusBadRequest, "invalid request body")
return return
} }
ci, err := h.store.CheckIn(claims.VolunteerID, in) ci, err := h.store.CheckIn(r.Context(), claims.VolunteerID, in)
if err != nil { if errors.Is(err, ErrAlreadyCheckedIn) {
respond.Error(w, http.StatusConflict, err.Error()) respond.Error(w, http.StatusConflict, err.Error())
return return
} }
if err != nil {
respond.Error(w, http.StatusInternalServerError, "could not check in")
return
}
respond.JSON(w, http.StatusCreated, ci) respond.JSON(w, http.StatusCreated, ci)
} }
@@ -37,11 +42,15 @@ func (h *Handler) CheckOut(w http.ResponseWriter, r *http.Request) {
claims := middleware.ClaimsFromContext(r.Context()) claims := middleware.ClaimsFromContext(r.Context())
var in CheckOutInput var in CheckOutInput
json.NewDecoder(r.Body).Decode(&in) json.NewDecoder(r.Body).Decode(&in)
ci, err := h.store.CheckOut(claims.VolunteerID, in) ci, err := h.store.CheckOut(r.Context(), claims.VolunteerID, in)
if err != nil { if errors.Is(err, ErrNotCheckedIn) {
respond.Error(w, http.StatusConflict, err.Error()) respond.Error(w, http.StatusConflict, err.Error())
return return
} }
if err != nil {
respond.Error(w, http.StatusInternalServerError, "could not check out")
return
}
respond.JSON(w, http.StatusOK, ci) respond.JSON(w, http.StatusOK, ci)
} }
@@ -52,7 +61,7 @@ func (h *Handler) History(w http.ResponseWriter, r *http.Request) {
if claims.Role != "admin" { if claims.Role != "admin" {
volunteerID = claims.VolunteerID volunteerID = claims.VolunteerID
} }
history, err := h.store.History(volunteerID) history, err := h.store.History(r.Context(), volunteerID)
if err != nil { if err != nil {
respond.Error(w, http.StatusInternalServerError, "could not get history") respond.Error(w, http.StatusInternalServerError, "could not get history")
return return

View File

@@ -1,6 +1,7 @@
package db package db
import ( import (
"context"
"database/sql" "database/sql"
"fmt" "fmt"
@@ -18,7 +19,11 @@ func Open(dsn string) (*sql.DB, error) {
return db, nil return db, nil
} }
func Migrate(db *sql.DB) error { func Migrate(ctx context.Context, db *sql.DB) error {
_, err := db.Exec(schema) for _, stmt := range statements {
return err if _, err := db.ExecContext(ctx, stmt); err != nil {
return fmt.Errorf("migrate: %w", err)
}
}
return nil
} }

View File

@@ -1,7 +1,7 @@
package db package db
const schema = ` var statements = []string{
CREATE TABLE IF NOT EXISTS volunteers ( `CREATE TABLE IF NOT EXISTS volunteers (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE, email VARCHAR(255) NOT NULL UNIQUE,
@@ -10,9 +10,8 @@ CREATE TABLE IF NOT EXISTS volunteers (
active TINYINT NOT NULL DEFAULT 1, active TINYINT NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS schedules (
CREATE TABLE IF NOT EXISTS schedules (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
volunteer_id INT NOT NULL, volunteer_id INT NOT NULL,
title VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL,
@@ -23,9 +22,8 @@ CREATE TABLE IF NOT EXISTS schedules (
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (volunteer_id) REFERENCES volunteers(id) ON DELETE CASCADE, FOREIGN KEY (volunteer_id) REFERENCES volunteers(id) ON DELETE CASCADE,
INDEX idx_volunteer_id (volunteer_id) INDEX idx_volunteer_id (volunteer_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS time_off_requests (
CREATE TABLE IF NOT EXISTS time_off_requests (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
volunteer_id INT NOT NULL, volunteer_id INT NOT NULL,
starts_at DATETIME NOT NULL, starts_at DATETIME NOT NULL,
@@ -40,9 +38,8 @@ CREATE TABLE IF NOT EXISTS time_off_requests (
FOREIGN KEY (reviewed_by) REFERENCES volunteers(id) ON DELETE SET NULL, FOREIGN KEY (reviewed_by) REFERENCES volunteers(id) ON DELETE SET NULL,
INDEX idx_volunteer_id (volunteer_id), INDEX idx_volunteer_id (volunteer_id),
INDEX idx_status (status) INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS checkins (
CREATE TABLE IF NOT EXISTS checkins (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
volunteer_id INT NOT NULL, volunteer_id INT NOT NULL,
schedule_id INT, schedule_id INT,
@@ -53,9 +50,8 @@ CREATE TABLE IF NOT EXISTS checkins (
FOREIGN KEY (schedule_id) REFERENCES schedules(id) ON DELETE SET NULL, FOREIGN KEY (schedule_id) REFERENCES schedules(id) ON DELETE SET NULL,
INDEX idx_volunteer_id (volunteer_id), INDEX idx_volunteer_id (volunteer_id),
INDEX idx_schedule_id (schedule_id) INDEX idx_schedule_id (schedule_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS notifications (
CREATE TABLE IF NOT EXISTS notifications (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
volunteer_id INT NOT NULL, volunteer_id INT NOT NULL,
message TEXT NOT NULL, message TEXT NOT NULL,
@@ -64,5 +60,5 @@ CREATE TABLE IF NOT EXISTS notifications (
FOREIGN KEY (volunteer_id) REFERENCES volunteers(id) ON DELETE CASCADE, FOREIGN KEY (volunteer_id) REFERENCES volunteers(id) ON DELETE CASCADE,
INDEX idx_volunteer_id (volunteer_id), INDEX idx_volunteer_id (volunteer_id),
INDEX idx_read (read) INDEX idx_read (read)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
` }

View File

@@ -1,12 +1,13 @@
package notification package notification
import ( import (
"errors"
"net/http" "net/http"
"strconv" "strconv"
"github.com/go-chi/chi/v5"
"git.unsupervised.ca/walkies/internal/respond" "git.unsupervised.ca/walkies/internal/respond"
"git.unsupervised.ca/walkies/internal/server/middleware" "git.unsupervised.ca/walkies/internal/server/middleware"
"github.com/go-chi/chi/v5"
) )
type Handler struct { type Handler struct {
@@ -20,7 +21,7 @@ func NewHandler(store *Store) *Handler {
// GET /api/v1/notifications // GET /api/v1/notifications
func (h *Handler) List(w http.ResponseWriter, r *http.Request) { func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
claims := middleware.ClaimsFromContext(r.Context()) claims := middleware.ClaimsFromContext(r.Context())
notifications, err := h.store.ListForVolunteer(claims.VolunteerID) notifications, err := h.store.ListForVolunteer(r.Context(), claims.VolunteerID)
if err != nil { if err != nil {
respond.Error(w, http.StatusInternalServerError, "could not list notifications") respond.Error(w, http.StatusInternalServerError, "could not list notifications")
return return
@@ -39,14 +40,14 @@ func (h *Handler) MarkRead(w http.ResponseWriter, r *http.Request) {
respond.Error(w, http.StatusBadRequest, "invalid id") respond.Error(w, http.StatusBadRequest, "invalid id")
return return
} }
n, err := h.store.MarkRead(id, claims.VolunteerID) n, err := h.store.MarkRead(r.Context(), id, claims.VolunteerID)
if errors.Is(err, ErrNotFound) {
respond.Error(w, http.StatusNotFound, "notification not found")
return
}
if err != nil { if err != nil {
respond.Error(w, http.StatusInternalServerError, "could not mark notification as read") respond.Error(w, http.StatusInternalServerError, "could not mark notification as read")
return return
} }
if n == nil {
respond.Error(w, http.StatusNotFound, "notification not found")
return
}
respond.JSON(w, http.StatusOK, n) respond.JSON(w, http.StatusOK, n)
} }

View File

@@ -1,12 +1,15 @@
package notification package notification
import ( import (
"context"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"time" "time"
) )
var ErrNotFound = fmt.Errorf("notification not found")
type Notification struct { type Notification struct {
ID int64 `json:"id"` ID int64 `json:"id"`
VolunteerID int64 `json:"volunteer_id"` VolunteerID int64 `json:"volunteer_id"`
@@ -23,8 +26,8 @@ func NewStore(db *sql.DB) *Store {
return &Store{db: db} return &Store{db: db}
} }
func (s *Store) Create(volunteerID int64, message string) (*Notification, error) { func (s *Store) Create(ctx context.Context, volunteerID int64, message string) (*Notification, error) {
res, err := s.db.Exec( res, err := s.db.ExecContext(ctx,
`INSERT INTO notifications (volunteer_id, message) VALUES (?, ?)`, `INSERT INTO notifications (volunteer_id, message) VALUES (?, ?)`,
volunteerID, message, volunteerID, message,
) )
@@ -32,17 +35,17 @@ func (s *Store) Create(volunteerID int64, message string) (*Notification, error)
return nil, fmt.Errorf("insert notification: %w", err) return nil, fmt.Errorf("insert notification: %w", err)
} }
id, _ := res.LastInsertId() id, _ := res.LastInsertId()
return s.GetByID(id) return s.GetByID(ctx, id)
} }
func (s *Store) GetByID(id int64) (*Notification, error) { func (s *Store) GetByID(ctx context.Context, id int64) (*Notification, error) {
n := &Notification{} n := &Notification{}
var createdAt string var createdAt string
err := s.db.QueryRow( err := s.db.QueryRowContext(ctx,
`SELECT id, volunteer_id, message, read, created_at FROM notifications WHERE id = ?`, id, `SELECT id, volunteer_id, message, read, created_at FROM notifications WHERE id = ?`, id,
).Scan(&n.ID, &n.VolunteerID, &n.Message, &n.Read, &createdAt) ).Scan(&n.ID, &n.VolunteerID, &n.Message, &n.Read, &createdAt)
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, nil return nil, ErrNotFound
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("get notification: %w", err) return nil, fmt.Errorf("get notification: %w", err)
@@ -51,8 +54,8 @@ func (s *Store) GetByID(id int64) (*Notification, error) {
return n, nil return n, nil
} }
func (s *Store) ListForVolunteer(volunteerID int64) ([]Notification, error) { func (s *Store) ListForVolunteer(ctx context.Context, volunteerID int64) ([]Notification, error) {
rows, err := s.db.Query( rows, err := s.db.QueryContext(ctx,
`SELECT id, volunteer_id, message, read, created_at FROM notifications WHERE volunteer_id = ? ORDER BY created_at DESC`, `SELECT id, volunteer_id, message, read, created_at FROM notifications WHERE volunteer_id = ? ORDER BY created_at DESC`,
volunteerID, volunteerID,
) )
@@ -74,17 +77,17 @@ func (s *Store) ListForVolunteer(volunteerID int64) ([]Notification, error) {
return notifications, rows.Err() return notifications, rows.Err()
} }
func (s *Store) MarkRead(id, volunteerID int64) (*Notification, error) { func (s *Store) MarkRead(ctx context.Context, id, volunteerID int64) (*Notification, error) {
result, err := s.db.Exec( result, err := s.db.ExecContext(ctx,
`UPDATE notifications SET read = 1 WHERE id = ? AND volunteer_id = ?`, `UPDATE notifications SET read = 1 WHERE id = ? AND volunteer_id = ?`,
id, volunteerID, id, volunteerID,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("mark read: %w", err) return nil, fmt.Errorf("mark read: %w", err)
} }
rows, _ := result.RowsAffected() affected, _ := result.RowsAffected()
if rows == 0 { if affected == 0 {
return nil, nil return nil, ErrNotFound
} }
return s.GetByID(id) return s.GetByID(ctx, id)
} }

View File

@@ -2,12 +2,13 @@ package schedule
import ( import (
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"strconv" "strconv"
"github.com/go-chi/chi/v5"
"git.unsupervised.ca/walkies/internal/respond" "git.unsupervised.ca/walkies/internal/respond"
"git.unsupervised.ca/walkies/internal/server/middleware" "git.unsupervised.ca/walkies/internal/server/middleware"
"github.com/go-chi/chi/v5"
) )
type Handler struct { type Handler struct {
@@ -25,7 +26,7 @@ func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
if claims.Role != "admin" { if claims.Role != "admin" {
volunteerID = claims.VolunteerID volunteerID = claims.VolunteerID
} }
schedules, err := h.store.List(volunteerID) schedules, err := h.store.List(r.Context(), volunteerID)
if err != nil { if err != nil {
respond.Error(w, http.StatusInternalServerError, "could not list schedules") respond.Error(w, http.StatusInternalServerError, "could not list schedules")
return return
@@ -51,7 +52,7 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
respond.Error(w, http.StatusBadRequest, "title, starts_at, and ends_at are required") respond.Error(w, http.StatusBadRequest, "title, starts_at, and ends_at are required")
return return
} }
sc, err := h.store.Create(in) sc, err := h.store.Create(r.Context(), in)
if err != nil { if err != nil {
respond.Error(w, http.StatusInternalServerError, "could not create schedule") respond.Error(w, http.StatusInternalServerError, "could not create schedule")
return return
@@ -71,13 +72,13 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
respond.Error(w, http.StatusBadRequest, "invalid request body") respond.Error(w, http.StatusBadRequest, "invalid request body")
return return
} }
sc, err := h.store.Update(id, in) sc, err := h.store.Update(r.Context(), id, in)
if err != nil { if errors.Is(err, ErrNotFound) {
respond.Error(w, http.StatusInternalServerError, "could not update schedule") respond.Error(w, http.StatusNotFound, "schedule not found")
return return
} }
if sc == nil { if err != nil {
respond.Error(w, http.StatusNotFound, "schedule not found") respond.Error(w, http.StatusInternalServerError, "could not update schedule")
return return
} }
respond.JSON(w, http.StatusOK, sc) respond.JSON(w, http.StatusOK, sc)
@@ -90,7 +91,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
respond.Error(w, http.StatusBadRequest, "invalid id") respond.Error(w, http.StatusBadRequest, "invalid id")
return return
} }
if err := h.store.Delete(id); err != nil { if err := h.store.Delete(r.Context(), id); err != nil {
respond.Error(w, http.StatusInternalServerError, "could not delete schedule") respond.Error(w, http.StatusInternalServerError, "could not delete schedule")
return return
} }

View File

@@ -1,12 +1,15 @@
package schedule package schedule
import ( import (
"context"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"time" "time"
) )
var ErrNotFound = fmt.Errorf("schedule not found")
type Schedule struct { type Schedule struct {
ID int64 `json:"id"` ID int64 `json:"id"`
VolunteerID int64 `json:"volunteer_id"` VolunteerID int64 `json:"volunteer_id"`
@@ -43,8 +46,8 @@ func NewStore(db *sql.DB) *Store {
return &Store{db: db} return &Store{db: db}
} }
func (s *Store) Create(in CreateInput) (*Schedule, error) { func (s *Store) Create(ctx context.Context, in CreateInput) (*Schedule, error) {
res, err := s.db.Exec( res, err := s.db.ExecContext(ctx,
`INSERT INTO schedules (volunteer_id, title, starts_at, ends_at, notes) VALUES (?, ?, ?, ?, ?)`, `INSERT INTO schedules (volunteer_id, title, starts_at, ends_at, notes) VALUES (?, ?, ?, ?, ?)`,
in.VolunteerID, in.Title, in.StartsAt, in.EndsAt, in.Notes, in.VolunteerID, in.Title, in.StartsAt, in.EndsAt, in.Notes,
) )
@@ -52,18 +55,18 @@ func (s *Store) Create(in CreateInput) (*Schedule, error) {
return nil, fmt.Errorf("insert schedule: %w", err) return nil, fmt.Errorf("insert schedule: %w", err)
} }
id, _ := res.LastInsertId() id, _ := res.LastInsertId()
return s.GetByID(id) return s.GetByID(ctx, id)
} }
func (s *Store) GetByID(id int64) (*Schedule, error) { func (s *Store) GetByID(ctx context.Context, id int64) (*Schedule, error) {
sc := &Schedule{} sc := &Schedule{}
var startsAt, endsAt, createdAt, updatedAt string var startsAt, endsAt, createdAt, updatedAt string
var notes sql.NullString var notes sql.NullString
err := s.db.QueryRow( err := s.db.QueryRowContext(ctx,
`SELECT id, volunteer_id, title, starts_at, ends_at, notes, created_at, updated_at FROM schedules WHERE id = ?`, id, `SELECT id, volunteer_id, title, starts_at, ends_at, notes, created_at, updated_at FROM schedules WHERE id = ?`, id,
).Scan(&sc.ID, &sc.VolunteerID, &sc.Title, &startsAt, &endsAt, &notes, &createdAt, &updatedAt) ).Scan(&sc.ID, &sc.VolunteerID, &sc.Title, &startsAt, &endsAt, &notes, &createdAt, &updatedAt)
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, nil return nil, ErrNotFound
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("get schedule: %w", err) return nil, fmt.Errorf("get schedule: %w", err)
@@ -78,7 +81,7 @@ func (s *Store) GetByID(id int64) (*Schedule, error) {
return sc, nil return sc, nil
} }
func (s *Store) List(volunteerID int64) ([]Schedule, error) { func (s *Store) List(ctx context.Context, volunteerID int64) ([]Schedule, error) {
query := `SELECT id, volunteer_id, title, starts_at, ends_at, notes, created_at, updated_at FROM schedules` query := `SELECT id, volunteer_id, title, starts_at, ends_at, notes, created_at, updated_at FROM schedules`
args := []any{} args := []any{}
if volunteerID > 0 { if volunteerID > 0 {
@@ -87,7 +90,7 @@ func (s *Store) List(volunteerID int64) ([]Schedule, error) {
} }
query += ` ORDER BY starts_at` query += ` ORDER BY starts_at`
rows, err := s.db.Query(query, args...) rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return nil, fmt.Errorf("list schedules: %w", err) return nil, fmt.Errorf("list schedules: %w", err)
} }
@@ -113,10 +116,10 @@ func (s *Store) List(volunteerID int64) ([]Schedule, error) {
return schedules, rows.Err() return schedules, rows.Err()
} }
func (s *Store) Update(id int64, in UpdateInput) (*Schedule, error) { func (s *Store) Update(ctx context.Context, id int64, in UpdateInput) (*Schedule, error) {
sc, err := s.GetByID(id) sc, err := s.GetByID(ctx, id)
if err != nil || sc == nil { if err != nil {
return sc, err return nil, err
} }
title := sc.Title title := sc.Title
startsAt := sc.StartsAt.Format("2006-01-02 15:04:05") startsAt := sc.StartsAt.Format("2006-01-02 15:04:05")
@@ -135,17 +138,17 @@ func (s *Store) Update(id int64, in UpdateInput) (*Schedule, error) {
if in.Notes != nil { if in.Notes != nil {
notes = *in.Notes notes = *in.Notes
} }
_, err = s.db.Exec( _, err = s.db.ExecContext(ctx,
`UPDATE schedules SET title=?, starts_at=?, ends_at=?, notes=?, updated_at=datetime('now') WHERE id=?`, `UPDATE schedules SET title=?, starts_at=?, ends_at=?, notes=?, updated_at=NOW() WHERE id=?`,
title, startsAt, endsAt, notes, id, title, startsAt, endsAt, notes, id,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("update schedule: %w", err) return nil, fmt.Errorf("update schedule: %w", err)
} }
return s.GetByID(id) return s.GetByID(ctx, id)
} }
func (s *Store) Delete(id int64) error { func (s *Store) Delete(ctx context.Context, id int64) error {
_, err := s.db.Exec(`DELETE FROM schedules WHERE id = ?`, id) _, err := s.db.ExecContext(ctx, `DELETE FROM schedules WHERE id = ?`, id)
return err return err
} }

View File

@@ -2,12 +2,13 @@ package timeoff
import ( import (
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"strconv" "strconv"
"github.com/go-chi/chi/v5"
"git.unsupervised.ca/walkies/internal/respond" "git.unsupervised.ca/walkies/internal/respond"
"git.unsupervised.ca/walkies/internal/server/middleware" "git.unsupervised.ca/walkies/internal/server/middleware"
"github.com/go-chi/chi/v5"
) )
type Handler struct { type Handler struct {
@@ -25,7 +26,7 @@ func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
if claims.Role != "admin" { if claims.Role != "admin" {
volunteerID = claims.VolunteerID volunteerID = claims.VolunteerID
} }
requests, err := h.store.List(volunteerID) requests, err := h.store.List(r.Context(), volunteerID)
if err != nil { if err != nil {
respond.Error(w, http.StatusInternalServerError, "could not list time off requests") respond.Error(w, http.StatusInternalServerError, "could not list time off requests")
return return
@@ -48,7 +49,7 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
respond.Error(w, http.StatusBadRequest, "starts_at and ends_at are required") respond.Error(w, http.StatusBadRequest, "starts_at and ends_at are required")
return return
} }
req, err := h.store.Create(claims.VolunteerID, in) req, err := h.store.Create(r.Context(), claims.VolunteerID, in)
if err != nil { if err != nil {
respond.Error(w, http.StatusInternalServerError, "could not create time off request") respond.Error(w, http.StatusInternalServerError, "could not create time off request")
return return
@@ -73,14 +74,14 @@ func (h *Handler) Review(w http.ResponseWriter, r *http.Request) {
respond.Error(w, http.StatusBadRequest, "status must be 'approved' or 'rejected'") respond.Error(w, http.StatusBadRequest, "status must be 'approved' or 'rejected'")
return return
} }
req, err := h.store.Review(id, claims.VolunteerID, in.Status) req, err := h.store.Review(r.Context(), id, claims.VolunteerID, in.Status)
if errors.Is(err, ErrNotFound) {
respond.Error(w, http.StatusNotFound, "time off request not found")
return
}
if err != nil { if err != nil {
respond.Error(w, http.StatusInternalServerError, "could not review time off request") respond.Error(w, http.StatusInternalServerError, "could not review time off request")
return return
} }
if req == nil {
respond.Error(w, http.StatusNotFound, "time off request not found")
return
}
respond.JSON(w, http.StatusOK, req) respond.JSON(w, http.StatusOK, req)
} }

View File

@@ -1,12 +1,15 @@
package timeoff package timeoff
import ( import (
"context"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"time" "time"
) )
var ErrNotFound = fmt.Errorf("time off request not found")
type Request struct { type Request struct {
ID int64 `json:"id"` ID int64 `json:"id"`
VolunteerID int64 `json:"volunteer_id"` VolunteerID int64 `json:"volunteer_id"`
@@ -38,8 +41,8 @@ func NewStore(db *sql.DB) *Store {
return &Store{db: db} return &Store{db: db}
} }
func (s *Store) Create(volunteerID int64, in CreateInput) (*Request, error) { func (s *Store) Create(ctx context.Context, volunteerID int64, in CreateInput) (*Request, error) {
res, err := s.db.Exec( res, err := s.db.ExecContext(ctx,
`INSERT INTO time_off_requests (volunteer_id, starts_at, ends_at, reason) VALUES (?, ?, ?, ?)`, `INSERT INTO time_off_requests (volunteer_id, starts_at, ends_at, reason) VALUES (?, ?, ?, ?)`,
volunteerID, in.StartsAt, in.EndsAt, in.Reason, volunteerID, in.StartsAt, in.EndsAt, in.Reason,
) )
@@ -47,22 +50,22 @@ func (s *Store) Create(volunteerID int64, in CreateInput) (*Request, error) {
return nil, fmt.Errorf("insert time off request: %w", err) return nil, fmt.Errorf("insert time off request: %w", err)
} }
id, _ := res.LastInsertId() id, _ := res.LastInsertId()
return s.GetByID(id) return s.GetByID(ctx, id)
} }
func (s *Store) GetByID(id int64) (*Request, error) { func (s *Store) GetByID(ctx context.Context, id int64) (*Request, error) {
req := &Request{} req := &Request{}
var startsAt, endsAt, createdAt, updatedAt string var startsAt, endsAt, createdAt, updatedAt string
var reason sql.NullString var reason sql.NullString
var reviewedBy sql.NullInt64 var reviewedBy sql.NullInt64
var reviewedAt sql.NullString var reviewedAt sql.NullString
err := s.db.QueryRow( err := s.db.QueryRowContext(ctx,
`SELECT id, volunteer_id, starts_at, ends_at, reason, status, reviewed_by, reviewed_at, created_at, updated_at `SELECT id, volunteer_id, starts_at, ends_at, reason, status, reviewed_by, reviewed_at, created_at, updated_at
FROM time_off_requests WHERE id = ?`, id, FROM time_off_requests WHERE id = ?`, id,
).Scan(&req.ID, &req.VolunteerID, &startsAt, &endsAt, &reason, &req.Status, &reviewedBy, &reviewedAt, &createdAt, &updatedAt) ).Scan(&req.ID, &req.VolunteerID, &startsAt, &endsAt, &reason, &req.Status, &reviewedBy, &reviewedAt, &createdAt, &updatedAt)
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, nil return nil, ErrNotFound
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("get time off request: %w", err) return nil, fmt.Errorf("get time off request: %w", err)
@@ -84,7 +87,7 @@ func (s *Store) GetByID(id int64) (*Request, error) {
return req, nil return req, nil
} }
func (s *Store) List(volunteerID int64) ([]Request, error) { func (s *Store) List(ctx context.Context, volunteerID int64) ([]Request, error) {
query := `SELECT id, volunteer_id, starts_at, ends_at, reason, status, reviewed_by, reviewed_at, created_at, updated_at FROM time_off_requests` query := `SELECT id, volunteer_id, starts_at, ends_at, reason, status, reviewed_by, reviewed_at, created_at, updated_at FROM time_off_requests`
args := []any{} args := []any{}
if volunteerID > 0 { if volunteerID > 0 {
@@ -93,7 +96,7 @@ func (s *Store) List(volunteerID int64) ([]Request, error) {
} }
query += ` ORDER BY starts_at DESC` query += ` ORDER BY starts_at DESC`
rows, err := s.db.Query(query, args...) rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return nil, fmt.Errorf("list time off requests: %w", err) return nil, fmt.Errorf("list time off requests: %w", err)
} }
@@ -128,13 +131,13 @@ func (s *Store) List(volunteerID int64) ([]Request, error) {
return requests, rows.Err() return requests, rows.Err()
} }
func (s *Store) Review(id, reviewerID int64, status string) (*Request, error) { func (s *Store) Review(ctx context.Context, id, reviewerID int64, status string) (*Request, error) {
_, err := s.db.Exec( _, err := s.db.ExecContext(ctx,
`UPDATE time_off_requests SET status=?, reviewed_by=?, reviewed_at=datetime('now'), updated_at=datetime('now') WHERE id=?`, `UPDATE time_off_requests SET status=?, reviewed_by=?, reviewed_at=NOW(), updated_at=NOW() WHERE id=?`,
status, reviewerID, id, status, reviewerID, id,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("review time off request: %w", err) return nil, fmt.Errorf("review time off request: %w", err)
} }
return s.GetByID(id) return s.GetByID(ctx, id)
} }

View File

@@ -2,12 +2,13 @@ package volunteer
import ( import (
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"strconv" "strconv"
"github.com/go-chi/chi/v5"
"git.unsupervised.ca/walkies/internal/auth" "git.unsupervised.ca/walkies/internal/auth"
"git.unsupervised.ca/walkies/internal/respond" "git.unsupervised.ca/walkies/internal/respond"
"github.com/go-chi/chi/v5"
) )
type Handler struct { type Handler struct {
@@ -38,7 +39,7 @@ func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
respond.Error(w, http.StatusInternalServerError, "could not hash password") respond.Error(w, http.StatusInternalServerError, "could not hash password")
return return
} }
v, err := h.store.Create(in.Name, in.Email, hash, in.Role) v, err := h.store.Create(r.Context(), in.Name, in.Email, hash, in.Role)
if err != nil { if err != nil {
respond.Error(w, http.StatusConflict, "email already in use") respond.Error(w, http.StatusConflict, "email already in use")
return return
@@ -56,7 +57,7 @@ func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
respond.Error(w, http.StatusBadRequest, "invalid request body") respond.Error(w, http.StatusBadRequest, "invalid request body")
return return
} }
token, err := h.authSvc.Login(body.Email, body.Password) token, err := h.authSvc.Login(r.Context(), body.Email, body.Password)
if err != nil { if err != nil {
respond.Error(w, http.StatusUnauthorized, "invalid credentials") respond.Error(w, http.StatusUnauthorized, "invalid credentials")
return return
@@ -66,7 +67,7 @@ func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
// GET /api/v1/volunteers // GET /api/v1/volunteers
func (h *Handler) List(w http.ResponseWriter, r *http.Request) { func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
volunteers, err := h.store.List(true) volunteers, err := h.store.List(r.Context(), true)
if err != nil { if err != nil {
respond.Error(w, http.StatusInternalServerError, "could not list volunteers") respond.Error(w, http.StatusInternalServerError, "could not list volunteers")
return return
@@ -84,13 +85,13 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
respond.Error(w, http.StatusBadRequest, "invalid id") respond.Error(w, http.StatusBadRequest, "invalid id")
return return
} }
v, err := h.store.GetByID(id) v, err := h.store.GetByID(r.Context(), id)
if err != nil { if errors.Is(err, ErrNotFound) {
respond.Error(w, http.StatusInternalServerError, "could not get volunteer") respond.Error(w, http.StatusNotFound, "volunteer not found")
return return
} }
if v == nil { if err != nil {
respond.Error(w, http.StatusNotFound, "volunteer not found") respond.Error(w, http.StatusInternalServerError, "could not get volunteer")
return return
} }
respond.JSON(w, http.StatusOK, v) respond.JSON(w, http.StatusOK, v)
@@ -108,14 +109,14 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
respond.Error(w, http.StatusBadRequest, "invalid request body") respond.Error(w, http.StatusBadRequest, "invalid request body")
return return
} }
v, err := h.store.Update(id, in) v, err := h.store.Update(r.Context(), id, in)
if errors.Is(err, ErrNotFound) {
respond.Error(w, http.StatusNotFound, "volunteer not found")
return
}
if err != nil { if err != nil {
respond.Error(w, http.StatusInternalServerError, "could not update volunteer") respond.Error(w, http.StatusInternalServerError, "could not update volunteer")
return return
} }
if v == nil {
respond.Error(w, http.StatusNotFound, "volunteer not found")
return
}
respond.JSON(w, http.StatusOK, v) respond.JSON(w, http.StatusOK, v)
} }

View File

@@ -1,12 +1,15 @@
package volunteer package volunteer
import ( import (
"context"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"time" "time"
) )
var ErrNotFound = fmt.Errorf("volunteer not found")
type Volunteer struct { type Volunteer struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -39,8 +42,8 @@ func NewStore(db *sql.DB) *Store {
return &Store{db: db} return &Store{db: db}
} }
func (s *Store) Create(name, email, hashedPassword, role string) (*Volunteer, error) { func (s *Store) Create(ctx context.Context, name, email, hashedPassword, role string) (*Volunteer, error) {
res, err := s.db.Exec( res, err := s.db.ExecContext(ctx,
`INSERT INTO volunteers (name, email, password, role) VALUES (?, ?, ?, ?)`, `INSERT INTO volunteers (name, email, password, role) VALUES (?, ?, ?, ?)`,
name, email, hashedPassword, role, name, email, hashedPassword, role,
) )
@@ -48,17 +51,17 @@ func (s *Store) Create(name, email, hashedPassword, role string) (*Volunteer, er
return nil, fmt.Errorf("insert volunteer: %w", err) return nil, fmt.Errorf("insert volunteer: %w", err)
} }
id, _ := res.LastInsertId() id, _ := res.LastInsertId()
return s.GetByID(id) return s.GetByID(ctx, id)
} }
func (s *Store) GetByID(id int64) (*Volunteer, error) { func (s *Store) GetByID(ctx context.Context, id int64) (*Volunteer, error) {
v := &Volunteer{} v := &Volunteer{}
var createdAt, updatedAt string var createdAt, updatedAt string
err := s.db.QueryRow( err := s.db.QueryRowContext(ctx,
`SELECT id, name, email, role, active, created_at, updated_at FROM volunteers WHERE id = ?`, id, `SELECT id, name, email, role, active, created_at, updated_at FROM volunteers WHERE id = ?`, id,
).Scan(&v.ID, &v.Name, &v.Email, &v.Role, &v.Active, &createdAt, &updatedAt) ).Scan(&v.ID, &v.Name, &v.Email, &v.Role, &v.Active, &createdAt, &updatedAt)
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, nil return nil, ErrNotFound
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("get volunteer: %w", err) return nil, fmt.Errorf("get volunteer: %w", err)
@@ -68,14 +71,14 @@ func (s *Store) GetByID(id int64) (*Volunteer, error) {
return v, nil return v, nil
} }
func (s *Store) List(activeOnly bool) ([]Volunteer, error) { func (s *Store) List(ctx context.Context, activeOnly bool) ([]Volunteer, error) {
query := `SELECT id, name, email, role, active, created_at, updated_at FROM volunteers` query := `SELECT id, name, email, role, active, created_at, updated_at FROM volunteers`
if activeOnly { if activeOnly {
query += ` WHERE active = 1` query += ` WHERE active = 1`
} }
query += ` ORDER BY name` query += ` ORDER BY name`
rows, err := s.db.Query(query) rows, err := s.db.QueryContext(ctx, query)
if err != nil { if err != nil {
return nil, fmt.Errorf("list volunteers: %w", err) return nil, fmt.Errorf("list volunteers: %w", err)
} }
@@ -95,10 +98,10 @@ func (s *Store) List(activeOnly bool) ([]Volunteer, error) {
return volunteers, rows.Err() return volunteers, rows.Err()
} }
func (s *Store) Update(id int64, in UpdateInput) (*Volunteer, error) { func (s *Store) Update(ctx context.Context, id int64, in UpdateInput) (*Volunteer, error) {
v, err := s.GetByID(id) v, err := s.GetByID(ctx, id)
if err != nil || v == nil { if err != nil {
return v, err return nil, err
} }
if in.Name != nil { if in.Name != nil {
v.Name = *in.Name v.Name = *in.Name
@@ -116,12 +119,12 @@ func (s *Store) Update(id int64, in UpdateInput) (*Volunteer, error) {
if v.Active { if v.Active {
activeInt = 1 activeInt = 1
} }
_, err = s.db.Exec( _, err = s.db.ExecContext(ctx,
`UPDATE volunteers SET name=?, email=?, role=?, active=?, updated_at=datetime('now') WHERE id=?`, `UPDATE volunteers SET name=?, email=?, role=?, active=?, updated_at=NOW() WHERE id=?`,
v.Name, v.Email, v.Role, activeInt, id, v.Name, v.Email, v.Role, activeInt, id,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("update volunteer: %w", err) return nil, fmt.Errorf("update volunteer: %w", err)
} }
return s.GetByID(id) return s.GetByID(ctx, id)
} }