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.
116 lines
3.8 KiB
Go
116 lines
3.8 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
)
|
|
|
|
// WorkoutKind is a user-editable classification rule ("Easy", "Tempo", ...).
|
|
// RuleJSON holds the condition tree evaluated by internal/classify.
|
|
type WorkoutKind struct {
|
|
ID int64
|
|
Name string
|
|
Description string
|
|
Color string
|
|
RuleJSON string
|
|
Priority int
|
|
IsActive bool
|
|
CreatedAt string
|
|
UpdatedAt string
|
|
}
|
|
|
|
func scanWorkoutKind(row interface{ Scan(...any) error }) (WorkoutKind, error) {
|
|
var k WorkoutKind
|
|
err := row.Scan(&k.ID, &k.Name, &k.Description, &k.Color, &k.RuleJSON, &k.Priority, &k.IsActive, &k.CreatedAt, &k.UpdatedAt)
|
|
return k, err
|
|
}
|
|
|
|
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, userID int64, k WorkoutKind) (int64, error) {
|
|
res, err := db.ExecContext(ctx, `
|
|
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 for user %d: %w", k.Name, userID, err)
|
|
}
|
|
return res.LastInsertId()
|
|
}
|
|
|
|
// UpdateWorkoutKind updates an existing workout kind's editable fields.
|
|
func (db *DB) UpdateWorkoutKind(ctx context.Context, k WorkoutKind) error {
|
|
_, err := db.ExecContext(ctx, `
|
|
UPDATE workout_kinds SET name=?, description=?, color=?, rule_json=?, priority=?, is_active=?, updated_at=datetime('now')
|
|
WHERE id=?`,
|
|
k.Name, k.Description, k.Color, k.RuleJSON, k.Priority, k.IsActive, k.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("update workout kind %d: %w", k.ID, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetWorkoutKind fetches one workout kind by id.
|
|
func (db *DB) GetWorkoutKind(ctx context.Context, id int64) (WorkoutKind, bool, error) {
|
|
row := db.QueryRowContext(ctx, `SELECT `+workoutKindColumns+` FROM workout_kinds WHERE id = ?`, id)
|
|
k, err := scanWorkoutKind(row)
|
|
if err == sql.ErrNoRows {
|
|
return WorkoutKind{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return WorkoutKind{}, false, fmt.Errorf("get workout kind %d: %w", id, err)
|
|
}
|
|
return k, true, nil
|
|
}
|
|
|
|
// GetWorkoutKindByName fetches one workout kind by its unique name.
|
|
func (db *DB) GetWorkoutKindByName(ctx context.Context, name string) (WorkoutKind, bool, error) {
|
|
row := db.QueryRowContext(ctx, `SELECT `+workoutKindColumns+` FROM workout_kinds WHERE name = ?`, name)
|
|
k, err := scanWorkoutKind(row)
|
|
if err == sql.ErrNoRows {
|
|
return WorkoutKind{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return WorkoutKind{}, false, fmt.Errorf("get workout kind %q: %w", name, err)
|
|
}
|
|
return k, true, nil
|
|
}
|
|
|
|
// ListWorkoutKinds returns workout kinds for userID. If activeOnly, soft-deleted
|
|
// (is_active=0) kinds are excluded.
|
|
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 += ` AND is_active = 1`
|
|
}
|
|
query += ` ORDER BY priority DESC, name`
|
|
|
|
rows, err := db.QueryContext(ctx, query, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list workout kinds for user %d: %w", userID, err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
kinds := []WorkoutKind{}
|
|
for rows.Next() {
|
|
k, err := scanWorkoutKind(rows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("scan workout kind row: %w", err)
|
|
}
|
|
kinds = append(kinds, k)
|
|
}
|
|
return kinds, rows.Err()
|
|
}
|
|
|
|
// SoftDeleteWorkoutKind sets is_active=0, keeping history (kind_assignments
|
|
// referencing it) intact.
|
|
func (db *DB) SoftDeleteWorkoutKind(ctx context.Context, id int64) error {
|
|
_, err := db.ExecContext(ctx, `UPDATE workout_kinds SET is_active=0, updated_at=datetime('now') WHERE id=?`, id)
|
|
if err != nil {
|
|
return fmt.Errorf("soft delete workout kind %d: %w", id, err)
|
|
}
|
|
return nil
|
|
}
|