store: scope GetProfile/UpdateProfile to a user_id
Part of per-user profile isolation: profile rows are no longer a global singleton, so every read/write requires the caller's userID. This also requires scoping ListActivities, ListWorkoutKinds, UpsertActivity, CreateWorkoutKind, GetSyncState, UpdateSyncState, and ResetAllSyncedData to userID, plus updating all related tests in the store package.
This commit is contained in:
@@ -52,15 +52,15 @@ type Activity struct {
|
||||
// UpsertActivity inserts a new activity or updates the existing row for the
|
||||
// same garmin_activity_id (idempotent, safe to call on every sync pass), and
|
||||
// returns its internal id.
|
||||
func (db *DB) UpsertActivity(ctx context.Context, a Activity) (int64, error) {
|
||||
func (db *DB) UpsertActivity(ctx context.Context, userID int64, a Activity) (int64, error) {
|
||||
_, err := db.ExecContext(ctx, `
|
||||
INSERT INTO activities (
|
||||
garmin_activity_id, event_type_key, workout_id, start_time_utc,
|
||||
user_id, garmin_activity_id, event_type_key, workout_id, start_time_utc,
|
||||
duration_seconds, distance_meters, avg_hr, max_hr,
|
||||
avg_speed_mps, elevation_gain_m,
|
||||
aerobic_training_effect, anaerobic_training_effect, vo2max_value,
|
||||
raw_json, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?, datetime('now'))
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, datetime('now'))
|
||||
ON CONFLICT(garmin_activity_id) DO UPDATE SET
|
||||
event_type_key=excluded.event_type_key,
|
||||
workout_id=excluded.workout_id,
|
||||
@@ -77,18 +77,18 @@ func (db *DB) UpsertActivity(ctx context.Context, a Activity) (int64, error) {
|
||||
raw_json=excluded.raw_json,
|
||||
updated_at=datetime('now')
|
||||
`,
|
||||
a.GarminActivityID, a.EventTypeKey, a.WorkoutID, a.StartTimeUTC,
|
||||
userID, a.GarminActivityID, a.EventTypeKey, a.WorkoutID, a.StartTimeUTC,
|
||||
a.DurationSeconds, a.DistanceMeters, a.AvgHR, a.MaxHR,
|
||||
a.AvgSpeedMps, a.ElevationGainM,
|
||||
a.AerobicTrainingEffect, a.AnaerobicTrainingEffect, a.VO2MaxValue,
|
||||
a.RawJSON,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("upsert activity %d: %w", a.GarminActivityID, err)
|
||||
return 0, fmt.Errorf("upsert activity %d for user %d: %w", a.GarminActivityID, userID, err)
|
||||
}
|
||||
|
||||
var id int64
|
||||
if err := db.QueryRowContext(ctx, `SELECT id FROM activities WHERE garmin_activity_id = ?`, a.GarminActivityID).Scan(&id); err != nil {
|
||||
if err := db.QueryRowContext(ctx, `SELECT id FROM activities WHERE garmin_activity_id = ? AND user_id = ?`, a.GarminActivityID, userID).Scan(&id); err != nil {
|
||||
return 0, fmt.Errorf("fetch id for activity %d: %w", a.GarminActivityID, err)
|
||||
}
|
||||
return id, nil
|
||||
@@ -137,10 +137,10 @@ type ActivityFilter struct {
|
||||
Offset int
|
||||
}
|
||||
|
||||
// ListActivities returns activities newest-first, optionally filtered by date range.
|
||||
func (db *DB) ListActivities(ctx context.Context, f ActivityFilter) ([]Activity, error) {
|
||||
query := `SELECT ` + activityColumns + ` FROM activities WHERE 1=1`
|
||||
var args []any
|
||||
// ListActivities returns activities for userID newest-first, optionally filtered by date range.
|
||||
func (db *DB) ListActivities(ctx context.Context, userID int64, f ActivityFilter) ([]Activity, error) {
|
||||
query := `SELECT ` + activityColumns + ` FROM activities WHERE user_id = ?`
|
||||
args := []any{userID}
|
||||
if f.FromDate != "" {
|
||||
query += ` AND start_time_utc >= ?`
|
||||
args = append(args, f.FromDate)
|
||||
@@ -157,7 +157,7 @@ func (db *DB) ListActivities(ctx context.Context, f ActivityFilter) ([]Activity,
|
||||
|
||||
rows, err := db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list activities: %w", err)
|
||||
return nil, fmt.Errorf("list activities for user %d: %w", userID, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
|
||||
@@ -81,10 +81,10 @@ const profileColumns = `
|
||||
created_at, updated_at
|
||||
`
|
||||
|
||||
// GetProfile returns the single profile row.
|
||||
func (db *DB) GetProfile(ctx context.Context) (Profile, error) {
|
||||
// GetProfile returns the profile row for userID.
|
||||
func (db *DB) GetProfile(ctx context.Context, userID int64) (Profile, error) {
|
||||
var p Profile
|
||||
err := db.QueryRowContext(ctx, `SELECT `+profileColumns+` FROM profile WHERE id = 1`).Scan(
|
||||
err := db.QueryRowContext(ctx, `SELECT `+profileColumns+` FROM profile WHERE user_id = ?`, userID).Scan(
|
||||
&p.Name, &p.GarminEmail, &p.GarminPassword, &p.RollingWindowDays, &p.BackfillHorizonDays, &p.MaxHeartRate, &p.RestingHeartRate,
|
||||
&p.HRZone1MinPct, &p.HRZone1MaxPct, &p.HRZone2MinPct, &p.HRZone2MaxPct,
|
||||
&p.HRZone3MinPct, &p.HRZone3MaxPct, &p.HRZone4MinPct, &p.HRZone4MaxPct,
|
||||
@@ -96,15 +96,15 @@ func (db *DB) GetProfile(ctx context.Context) (Profile, error) {
|
||||
&p.CreatedAt, &p.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return Profile{}, fmt.Errorf("get profile: %w", err)
|
||||
return Profile{}, fmt.Errorf("get profile for user %d: %w", userID, err)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// UpdateProfile overwrites the single profile row. Callers should read via
|
||||
// UpdateProfile overwrites userID's profile row. Callers should read via
|
||||
// GetProfile first and modify the fields they intend to change, since this
|
||||
// replaces every column.
|
||||
func (db *DB) UpdateProfile(ctx context.Context, p Profile) error {
|
||||
func (db *DB) UpdateProfile(ctx context.Context, userID int64, p Profile) error {
|
||||
_, err := db.ExecContext(ctx, `
|
||||
UPDATE profile SET
|
||||
name=?, garmin_email=?, garmin_password=?, rolling_window_days=?, backfill_horizon_days=?, max_heart_rate=?, resting_heart_rate=?,
|
||||
@@ -116,7 +116,7 @@ func (db *DB) UpdateProfile(ctx context.Context, p Profile) error {
|
||||
pace_color=?, heart_rate_color=?, warmup_color=?, effort_color=?, recovery_color=?, cooldown_color=?,
|
||||
main_line_tint_pct=?, background_darken_pct=?, target_brighten_pct=?,
|
||||
updated_at=datetime('now')
|
||||
WHERE id = 1`,
|
||||
WHERE user_id = ?`,
|
||||
p.Name, p.GarminEmail, p.GarminPassword, p.RollingWindowDays, p.BackfillHorizonDays, p.MaxHeartRate, p.RestingHeartRate,
|
||||
p.HRZone1MinPct, p.HRZone1MaxPct, p.HRZone2MinPct, p.HRZone2MaxPct,
|
||||
p.HRZone3MinPct, p.HRZone3MaxPct, p.HRZone4MinPct, p.HRZone4MaxPct,
|
||||
@@ -125,9 +125,10 @@ func (db *DB) UpdateProfile(ctx context.Context, p Profile) error {
|
||||
p.MinRepresentativePaceSecPerKm, p.MinRepresentativeTimeSeconds,
|
||||
p.PaceColor, p.HeartRateColor, p.WarmupColor, p.EffortColor, p.RecoveryColor, p.CooldownColor,
|
||||
p.MainLineTintPct, p.BackgroundDarkenPct, p.TargetBrightenPct,
|
||||
userID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update profile: %w", err)
|
||||
return fmt.Errorf("update profile for user %d: %w", userID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,8 +8,12 @@ import (
|
||||
func TestProfile_DefaultsThenUpdate(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
ctx := context.Background()
|
||||
userID, err := db.ProvisionUser(ctx, "test-sub", "Default")
|
||||
if err != nil {
|
||||
t.Fatalf("ProvisionUser: %v", err)
|
||||
}
|
||||
|
||||
p, err := db.GetProfile(ctx)
|
||||
p, err := db.GetProfile(ctx, userID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetProfile: %v", err)
|
||||
}
|
||||
@@ -64,11 +68,11 @@ func TestProfile_DefaultsThenUpdate(t *testing.T) {
|
||||
p.MinRepresentativePaceSecPerKm = 600
|
||||
p.MinRepresentativeTimeSeconds = 5
|
||||
|
||||
if err := db.UpdateProfile(ctx, p); err != nil {
|
||||
if err := db.UpdateProfile(ctx, userID, p); err != nil {
|
||||
t.Fatalf("UpdateProfile: %v", err)
|
||||
}
|
||||
|
||||
got, err := db.GetProfile(ctx)
|
||||
got, err := db.GetProfile(ctx, userID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetProfile after update: %v", err)
|
||||
}
|
||||
|
||||
@@ -5,23 +5,23 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ResetAllSyncedData deletes every synced activity (cascading to its laps,
|
||||
// ResetAllSyncedData deletes every synced activity for userID (cascading to its laps,
|
||||
// activity_samples, and kind_assignments via ON DELETE CASCADE) and rewinds
|
||||
// the backfill watermark to its initial state, so a subsequent Backfill
|
||||
// starts a genuinely fresh pull instead of thinking history is already
|
||||
// covered. Workout kinds (the user's taxonomy) are left untouched.
|
||||
func (db *DB) ResetAllSyncedData(ctx context.Context) error {
|
||||
func (db *DB) ResetAllSyncedData(ctx context.Context, userID int64) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin reset tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if _, err := tx.ExecContext(ctx, `DELETE FROM activities`); err != nil {
|
||||
return fmt.Errorf("delete activities: %w", err)
|
||||
if _, err := tx.ExecContext(ctx, `DELETE FROM activities WHERE user_id = ?`, userID); err != nil {
|
||||
return fmt.Errorf("delete activities for user %d: %w", userID, err)
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE sync_state SET earliest_synced_date = NULL, backfill_complete = 0 WHERE id = 1`); err != nil {
|
||||
return fmt.Errorf("reset sync state: %w", err)
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE sync_state SET earliest_synced_date = NULL, backfill_complete = 0 WHERE user_id = ?`, userID); err != nil {
|
||||
return fmt.Errorf("reset sync state for user %d: %w", userID, err)
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
@@ -8,12 +8,16 @@ import (
|
||||
func TestResetAllSyncedData_DeletesActivitiesCascadesAndRewindsWatermark(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
ctx := context.Background()
|
||||
userID, err := db.ProvisionUser(ctx, "test-sub", "Test User")
|
||||
if err != nil {
|
||||
t.Fatalf("ProvisionUser: %v", err)
|
||||
}
|
||||
|
||||
activityID, err := db.UpsertActivity(ctx, Activity{GarminActivityID: 1, StartTimeUTC: "2026-07-11 06:00:00", RawJSON: "{}"})
|
||||
activityID, err := db.UpsertActivity(ctx, userID, Activity{GarminActivityID: 1, StartTimeUTC: "2026-07-11 06:00:00", RawJSON: "{}"})
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertActivity: %v", err)
|
||||
}
|
||||
kindID, err := db.CreateWorkoutKind(ctx, WorkoutKind{Name: "Test Reset Kind", RuleJSON: `{"match":"all","conditions":[]}`, IsActive: true})
|
||||
kindID, err := db.CreateWorkoutKind(ctx, userID, WorkoutKind{Name: "Test Reset Kind", RuleJSON: `{"match":"all","conditions":[]}`, IsActive: true})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateWorkoutKind: %v", err)
|
||||
}
|
||||
@@ -26,15 +30,15 @@ func TestResetAllSyncedData_DeletesActivitiesCascadesAndRewindsWatermark(t *test
|
||||
}); err != nil {
|
||||
t.Fatalf("InsertKindAssignment: %v", err)
|
||||
}
|
||||
if err := db.UpdateSyncState(ctx, "2020-01-01", true); err != nil {
|
||||
if err := db.UpdateSyncState(ctx, userID, "2020-01-01", true); err != nil {
|
||||
t.Fatalf("UpdateSyncState: %v", err)
|
||||
}
|
||||
|
||||
if err := db.ResetAllSyncedData(ctx); err != nil {
|
||||
if err := db.ResetAllSyncedData(ctx, userID); err != nil {
|
||||
t.Fatalf("ResetAllSyncedData: %v", err)
|
||||
}
|
||||
|
||||
activities, err := db.ListActivities(ctx, ActivityFilter{})
|
||||
activities, err := db.ListActivities(ctx, userID, ActivityFilter{})
|
||||
if err != nil {
|
||||
t.Fatalf("ListActivities: %v", err)
|
||||
}
|
||||
@@ -52,7 +56,7 @@ func TestResetAllSyncedData_DeletesActivitiesCascadesAndRewindsWatermark(t *test
|
||||
t.Errorf("expected kind assignment to cascade-delete, ok=%v err=%v", ok, err)
|
||||
}
|
||||
|
||||
state, err := db.GetSyncState(ctx)
|
||||
state, err := db.GetSyncState(ctx, userID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSyncState: %v", err)
|
||||
}
|
||||
|
||||
@@ -41,6 +41,10 @@ func TestMigrateIsIdempotent(t *testing.T) {
|
||||
func TestUpsertActivity_InsertThenUpdateIsIdempotent(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
ctx := context.Background()
|
||||
userID, err := db.ProvisionUser(ctx, "test-sub", "Test User")
|
||||
if err != nil {
|
||||
t.Fatalf("ProvisionUser: %v", err)
|
||||
}
|
||||
|
||||
a := Activity{
|
||||
GarminActivityID: 23554066504,
|
||||
@@ -51,13 +55,13 @@ func TestUpsertActivity_InsertThenUpdateIsIdempotent(t *testing.T) {
|
||||
RawJSON: `{"activityId":23554066504,"activityName":"Auriol - W2-5-Base Endurance","activityType":{"typeKey":"running"}}`,
|
||||
}
|
||||
|
||||
id, err := db.UpsertActivity(ctx, a)
|
||||
id, err := db.UpsertActivity(ctx, userID, a)
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertActivity (insert): %v", err)
|
||||
}
|
||||
|
||||
a.AvgHR = f(150) // simulate a re-sync with a corrected value
|
||||
id2, err := db.UpsertActivity(ctx, a)
|
||||
id2, err := db.UpsertActivity(ctx, userID, a)
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertActivity (update): %v", err)
|
||||
}
|
||||
@@ -73,7 +77,7 @@ func TestUpsertActivity_InsertThenUpdateIsIdempotent(t *testing.T) {
|
||||
t.Errorf("AvgHR = %v, want 150 (update should have applied)", *got.AvgHR)
|
||||
}
|
||||
|
||||
all, err := db.ListActivities(ctx, ActivityFilter{})
|
||||
all, err := db.ListActivities(ctx, userID, ActivityFilter{})
|
||||
if err != nil {
|
||||
t.Fatalf("ListActivities: %v", err)
|
||||
}
|
||||
@@ -85,8 +89,12 @@ func TestUpsertActivity_InsertThenUpdateIsIdempotent(t *testing.T) {
|
||||
func TestKindAssignment_AppendOnlyHistoryAndCurrentView(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
ctx := context.Background()
|
||||
userID, err := db.ProvisionUser(ctx, "test-sub", "Test User")
|
||||
if err != nil {
|
||||
t.Fatalf("ProvisionUser: %v", err)
|
||||
}
|
||||
|
||||
activityID, err := db.UpsertActivity(ctx, Activity{
|
||||
activityID, err := db.UpsertActivity(ctx, userID, Activity{
|
||||
GarminActivityID: 1,
|
||||
StartTimeUTC: "2026-07-11 05:00:00",
|
||||
RawJSON: "{}",
|
||||
@@ -95,7 +103,7 @@ func TestKindAssignment_AppendOnlyHistoryAndCurrentView(t *testing.T) {
|
||||
t.Fatalf("UpsertActivity: %v", err)
|
||||
}
|
||||
|
||||
kindID, err := db.CreateWorkoutKind(ctx, WorkoutKind{
|
||||
kindID, err := db.CreateWorkoutKind(ctx, userID, WorkoutKind{
|
||||
Name: "Test Assignment Custom",
|
||||
RuleJSON: `{"match":"all","conditions":[]}`,
|
||||
IsActive: true,
|
||||
@@ -169,8 +177,12 @@ func TestKindAssignment_AppendOnlyHistoryAndCurrentView(t *testing.T) {
|
||||
func TestReplaceLapsIsIdempotent(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
ctx := context.Background()
|
||||
userID, err := db.ProvisionUser(ctx, "test-sub", "Test User")
|
||||
if err != nil {
|
||||
t.Fatalf("ProvisionUser: %v", err)
|
||||
}
|
||||
|
||||
activityID, err := db.UpsertActivity(ctx, Activity{
|
||||
activityID, err := db.UpsertActivity(ctx, userID, Activity{
|
||||
GarminActivityID: 2,
|
||||
StartTimeUTC: "2026-07-11 05:00:00",
|
||||
RawJSON: "{}",
|
||||
@@ -245,8 +257,13 @@ func TestForeignKeyEnforcementPostMigration(t *testing.T) {
|
||||
// migrations 0023/0024/0025) is properly scoped and re-enabled after each
|
||||
// table rebuild.
|
||||
|
||||
userID, err := db.ProvisionUser(ctx, "test-sub", "Test User")
|
||||
if err != nil {
|
||||
t.Fatalf("ProvisionUser: %v", err)
|
||||
}
|
||||
|
||||
// Insert an activity that we can reference.
|
||||
activityID, err := db.UpsertActivity(ctx, Activity{
|
||||
activityID, err := db.UpsertActivity(ctx, userID, Activity{
|
||||
GarminActivityID: 1001,
|
||||
StartTimeUTC: "2026-07-11 05:00:00",
|
||||
RawJSON: "{}",
|
||||
|
||||
@@ -13,25 +13,24 @@ type SyncState struct {
|
||||
BackfillComplete bool // true once backfill has reached the configured horizon (or Garmin's own history start)
|
||||
}
|
||||
|
||||
// GetSyncState returns the current backfill watermark (the singleton row,
|
||||
// created by migration 0002).
|
||||
func (db *DB) GetSyncState(ctx context.Context) (SyncState, error) {
|
||||
// GetSyncState returns the current backfill watermark for userID.
|
||||
func (db *DB) GetSyncState(ctx context.Context, userID int64) (SyncState, error) {
|
||||
var s SyncState
|
||||
err := db.QueryRowContext(ctx, `SELECT earliest_synced_date, backfill_complete FROM sync_state WHERE id = 1`).
|
||||
err := db.QueryRowContext(ctx, `SELECT earliest_synced_date, backfill_complete FROM sync_state WHERE user_id = ?`, userID).
|
||||
Scan(&s.EarliestSyncedDate, &s.BackfillComplete)
|
||||
if err != nil {
|
||||
return SyncState{}, fmt.Errorf("get sync state: %w", err)
|
||||
return SyncState{}, fmt.Errorf("get sync state for user %d: %w", userID, err)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// UpdateSyncState records progress of a backfill run.
|
||||
func (db *DB) UpdateSyncState(ctx context.Context, earliestSyncedDate string, complete bool) error {
|
||||
// UpdateSyncState records progress of a backfill run for userID.
|
||||
func (db *DB) UpdateSyncState(ctx context.Context, userID int64, earliestSyncedDate string, complete bool) error {
|
||||
_, err := db.ExecContext(ctx, `
|
||||
UPDATE sync_state SET earliest_synced_date = ?, backfill_complete = ? WHERE id = 1`,
|
||||
earliestSyncedDate, complete)
|
||||
UPDATE sync_state SET earliest_synced_date = ?, backfill_complete = ? WHERE user_id = ?`,
|
||||
earliestSyncedDate, complete, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update sync state: %w", err)
|
||||
return fmt.Errorf("update sync state for user %d: %w", userID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,8 +8,12 @@ import (
|
||||
func TestSyncState_DefaultsAndUpdate(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
ctx := context.Background()
|
||||
userID, err := db.ProvisionUser(ctx, "test-sub", "Test User")
|
||||
if err != nil {
|
||||
t.Fatalf("ProvisionUser: %v", err)
|
||||
}
|
||||
|
||||
state, err := db.GetSyncState(ctx)
|
||||
state, err := db.GetSyncState(ctx, userID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSyncState: %v", err)
|
||||
}
|
||||
@@ -17,11 +21,11 @@ func TestSyncState_DefaultsAndUpdate(t *testing.T) {
|
||||
t.Fatalf("expected fresh DB to have no watermark, got %+v", state)
|
||||
}
|
||||
|
||||
if err := db.UpdateSyncState(ctx, "2023-01-01", true); err != nil {
|
||||
if err := db.UpdateSyncState(ctx, userID, "2023-01-01", true); err != nil {
|
||||
t.Fatalf("UpdateSyncState: %v", err)
|
||||
}
|
||||
|
||||
state, err = db.GetSyncState(ctx)
|
||||
state, err = db.GetSyncState(ctx, userID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSyncState after update: %v", err)
|
||||
}
|
||||
|
||||
@@ -29,13 +29,13 @@ func scanWorkoutKind(row interface{ Scan(...any) error }) (WorkoutKind, error) {
|
||||
const workoutKindColumns = `id, name, description, color, rule_json, priority, is_active, created_at, updated_at`
|
||||
|
||||
// CreateWorkoutKind inserts a new workout kind and returns its id.
|
||||
func (db *DB) CreateWorkoutKind(ctx context.Context, k WorkoutKind) (int64, error) {
|
||||
func (db *DB) CreateWorkoutKind(ctx context.Context, userID int64, k WorkoutKind) (int64, error) {
|
||||
res, err := db.ExecContext(ctx, `
|
||||
INSERT INTO workout_kinds (name, description, color, rule_json, priority, is_active)
|
||||
VALUES (?,?,?,?,?,?)`,
|
||||
k.Name, k.Description, k.Color, k.RuleJSON, k.Priority, k.IsActive)
|
||||
INSERT INTO workout_kinds (user_id, name, description, color, rule_json, priority, is_active)
|
||||
VALUES (?,?,?,?,?,?,?)`,
|
||||
userID, k.Name, k.Description, k.Color, k.RuleJSON, k.Priority, k.IsActive)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("create workout kind %q: %w", k.Name, err)
|
||||
return 0, fmt.Errorf("create workout kind %q for user %d: %w", k.Name, userID, err)
|
||||
}
|
||||
return res.LastInsertId()
|
||||
}
|
||||
@@ -78,18 +78,18 @@ func (db *DB) GetWorkoutKindByName(ctx context.Context, name string) (WorkoutKin
|
||||
return k, true, nil
|
||||
}
|
||||
|
||||
// ListWorkoutKinds returns workout kinds. If activeOnly, soft-deleted
|
||||
// ListWorkoutKinds returns workout kinds for userID. If activeOnly, soft-deleted
|
||||
// (is_active=0) kinds are excluded.
|
||||
func (db *DB) ListWorkoutKinds(ctx context.Context, activeOnly bool) ([]WorkoutKind, error) {
|
||||
query := `SELECT ` + workoutKindColumns + ` FROM workout_kinds`
|
||||
func (db *DB) ListWorkoutKinds(ctx context.Context, userID int64, activeOnly bool) ([]WorkoutKind, error) {
|
||||
query := `SELECT ` + workoutKindColumns + ` FROM workout_kinds WHERE user_id = ?`
|
||||
if activeOnly {
|
||||
query += ` WHERE is_active = 1`
|
||||
query += ` AND is_active = 1`
|
||||
}
|
||||
query += ` ORDER BY priority DESC, name`
|
||||
|
||||
rows, err := db.QueryContext(ctx, query)
|
||||
rows, err := db.QueryContext(ctx, query, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list workout kinds: %w", err)
|
||||
return nil, fmt.Errorf("list workout kinds for user %d: %w", userID, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
|
||||
@@ -8,8 +8,12 @@ import (
|
||||
func TestWorkoutTaxonomy_SeededWithEightFixedTypes(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
ctx := context.Background()
|
||||
userID, err := db.ProvisionUser(ctx, "test-sub", "Test User")
|
||||
if err != nil {
|
||||
t.Fatalf("ProvisionUser: %v", err)
|
||||
}
|
||||
|
||||
kinds, err := db.ListWorkoutKinds(ctx, true)
|
||||
kinds, err := db.ListWorkoutKinds(ctx, userID, true)
|
||||
if err != nil {
|
||||
t.Fatalf("ListWorkoutKinds: %v", err)
|
||||
}
|
||||
@@ -42,8 +46,12 @@ func TestWorkoutTaxonomy_SeededWithEightFixedTypes(t *testing.T) {
|
||||
func TestWorkoutTaxonomy_ListedInFixedDisplayOrder(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
ctx := context.Background()
|
||||
userID, err := db.ProvisionUser(ctx, "test-sub", "Test User")
|
||||
if err != nil {
|
||||
t.Fatalf("ProvisionUser: %v", err)
|
||||
}
|
||||
|
||||
kinds, err := db.ListWorkoutKinds(ctx, true)
|
||||
kinds, err := db.ListWorkoutKinds(ctx, userID, true)
|
||||
if err != nil {
|
||||
t.Fatalf("ListWorkoutKinds: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user