Ver Fonte

Add position to each exercise for ordering

Lee Morgan há 1 mês atrás
pai
commit
f55f6603be

+ 5 - 0
docs/components/schemas/Exercise.yaml

@@ -4,6 +4,7 @@ required:
   - workout
   - name
   - exercise_type
+  - position
 properties:
   id:
     type: string
@@ -24,3 +25,7 @@ properties:
     example: Keep elbows tucked
   exercise_type:
     $ref: './ExerciseType.yaml'
+  position:
+    type: integer
+    description: 1-based position of the exercise within its workout.
+    example: 1

+ 7 - 0
docs/paths/workout-id.yaml

@@ -41,6 +41,7 @@ get:
                     - id
                     - name
                     - exercise_type
+                    - position
                   properties:
                     id:
                       type: string
@@ -55,6 +56,10 @@ get:
                       example: Keep elbows tucked
                     exercise_type:
                       $ref: '../components/schemas/ExerciseType.yaml'
+                    position:
+                      type: integer
+                      description: 1-based position of the exercise within its workout.
+                      example: 1
           example:
             id: 0191a23b-dead-7000-beef-000000000002
             name: Push Day
@@ -63,10 +68,12 @@ get:
                 name: Bench Press
                 notes: null
                 exercise_type: WeightedReps
+                position: 1
               - id: 0191a23b-dead-7000-beef-000000000004
                 name: Push-ups
                 notes: null
                 exercise_type: BodyweightReps
+                position: 2
     '401':
       description: Missing or invalid auth token.
       content:

+ 62 - 0
docs/paths/workout.yaml

@@ -1,3 +1,57 @@
+get:
+  summary: List all workouts for the current user
+  operationId: listWorkouts
+  tags:
+    - Workouts
+  security:
+    - cookieAuth: []
+  responses:
+    '200':
+      description: Array of workouts belonging to the authenticated user.
+      content:
+        application/json:
+          schema:
+            type: array
+            items:
+              type: object
+              required:
+                - id
+                - name
+              properties:
+                id:
+                  type: string
+                  format: uuid
+                  example: 0191a23b-dead-7000-beef-000000000002
+                name:
+                  type: string
+                  example: Push Day
+          example:
+            - id: 0191a23b-dead-7000-beef-000000000002
+              name: Push Day
+            - id: 0191a23b-dead-7000-beef-000000000005
+              name: Pull Day
+    '401':
+      description: Missing or invalid auth token.
+      content:
+        application/json:
+          schema:
+            $ref: '../components/schemas/ErrorResponse.yaml'
+          examples:
+            missing:
+              summary: No cookie present
+              value:
+                error: Missing auth token
+            invalid:
+              summary: Token invalid or expired
+              value:
+                error: Invalid auth token
+    '500':
+      description: Internal server error.
+      content:
+        application/json:
+          schema:
+            $ref: '../components/schemas/ErrorResponse.yaml'
+
 post:
   summary: Create a new workout
   operationId: createWorkout
@@ -25,21 +79,29 @@ post:
                 required:
                   - name
                   - exercise_type
+                  - position
                 properties:
                   name:
                     type: string
                     example: Bench Press
                   exercise_type:
                     $ref: '../components/schemas/ExerciseType.yaml'
+                  position:
+                    type: integer
+                    description: 1-based position of the exercise within the workout.
+                    example: 1
         example:
           name: Push Day
           exercises:
             - name: Bench Press
               exercise_type: WeightedReps
+              position: 1
             - name: Push-ups
               exercise_type: BodyweightReps
+              position: 2
             - name: Treadmill
               exercise_type: Cardio
+              position: 3
   responses:
     '201':
       description: Workout created. Returns the generated workout ID.

+ 2 - 0
init.sql

@@ -5,3 +5,5 @@ CREATE DATABASE torus;
 \i migrations/0001_create_users.sql
 \i migrations/0002_create_workouts.sql
 \i migrations/0003_create_exercises.sql
+\i migrations/0004_add_user_to_workouts.sql
+\i migrations/0005_add_position_to_exercises.sql

+ 3 - 0
instructions.txt

@@ -0,0 +1,3 @@
+Create the following routes:
+
+

+ 4 - 0
migrations/0004_add_user_to_workouts.sql

@@ -0,0 +1,4 @@
+TRUNCATE TABLE workouts CASCADE;
+
+ALTER TABLE workouts
+    ADD COLUMN user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE;

+ 1 - 0
migrations/0005_add_position_to_exercises.sql

@@ -0,0 +1 @@
+ALTER TABLE exercises ADD COLUMN position INTEGER NOT NULL DEFAULT 0;

+ 1 - 0
src/main.rs

@@ -55,6 +55,7 @@ async fn main() -> std::io::Result<()> {
             .service(routes::users::get::get_user)
             .service(routes::workouts::create::create_workout)
             .service(routes::workouts::get::get_workout)
+            .service(routes::workouts::list::list_workouts)
     })
     .bind((host.as_str(), port))?
     .run()

+ 5 - 2
src/models/exercise.rs

@@ -18,11 +18,12 @@ pub struct Exercise {
     pub name: String,
     pub notes: Option<String>,
     pub exercise_type: ExerciseType,
+    pub position: i32,
 }
 
 impl Exercise {
     pub async fn find_by_workout(pool: &PgPool, workout_id: Uuid) -> Result<Vec<Exercise>, sqlx::Error> {
-        sqlx::query_as::<_, Exercise>("SELECT * FROM exercises WHERE workout = $1 ORDER BY id")
+        sqlx::query_as::<_, Exercise>("SELECT * FROM exercises WHERE workout = $1 ORDER BY position")
             .bind(workout_id)
             .fetch_all(pool)
             .await
@@ -33,16 +34,18 @@ impl Exercise {
         workout_id: Uuid,
         name: &str,
         exercise_type: ExerciseType,
+        position: i32,
     ) -> Result<Exercise, sqlx::Error>
     where
         E: sqlx::Executor<'c, Database = Postgres>,
     {
         sqlx::query_as::<_, Exercise>(
-            "INSERT INTO exercises (workout, name, exercise_type) VALUES ($1, $2, $3) RETURNING *",
+            "INSERT INTO exercises (workout, name, exercise_type, position) VALUES ($1, $2, $3, $4) RETURNING *",
         )
         .bind(workout_id)
         .bind(name)
         .bind(exercise_type)
+        .bind(position)
         .fetch_one(executor)
         .await
     }

+ 18 - 5
src/models/workout.rs

@@ -5,18 +5,22 @@ use uuid::Uuid;
 #[derive(sqlx::FromRow, Serialize)]
 pub struct Workout {
     pub id: Uuid,
+    pub user_id: Uuid,
     pub name: String,
 }
 
 impl Workout {
-    pub async fn create<'c, E>(executor: E, name: &str) -> Result<Workout, sqlx::Error>
+    pub async fn create<'c, E>(executor: E, user_id: Uuid, name: &str) -> Result<Workout, sqlx::Error>
     where
         E: sqlx::Executor<'c, Database = Postgres>,
     {
-        sqlx::query_as::<_, Workout>("INSERT INTO workouts (name) VALUES ($1) RETURNING *")
-            .bind(name)
-            .fetch_one(executor)
-            .await
+        sqlx::query_as::<_, Workout>(
+            "INSERT INTO workouts (user_id, name) VALUES ($1, $2) RETURNING *",
+        )
+        .bind(user_id)
+        .bind(name)
+        .fetch_one(executor)
+        .await
     }
 
     pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Workout>, sqlx::Error> {
@@ -25,4 +29,13 @@ impl Workout {
             .fetch_optional(pool)
             .await
     }
+
+    pub async fn find_by_user(pool: &PgPool, user_id: Uuid) -> Result<Vec<Workout>, sqlx::Error> {
+        sqlx::query_as::<_, Workout>(
+            "SELECT * FROM workouts WHERE user_id = $1 ORDER BY id",
+        )
+        .bind(user_id)
+        .fetch_all(pool)
+        .await
+    }
 }

+ 11 - 9
src/routes/workouts/create.rs

@@ -11,6 +11,7 @@ use crate::models::workout::Workout;
 pub struct CreateExerciseInput {
     pub name: String,
     pub exercise_type: ExerciseType,
+    pub position: i32,
 }
 
 #[derive(Deserialize)]
@@ -22,35 +23,36 @@ pub struct CreateWorkoutInput {
 #[post("/workout")]
 pub async fn create_workout(
     pool: web::Data<PgPool>,
-    _auth: AuthUser,
+    auth: AuthUser,
     body: web::Json<CreateWorkoutInput>,
 ) -> HttpResponse {
     let mut tx = match pool.begin().await {
         Ok(t) => t,
-        Err(_) => {
+        Err(e) => {
+            eprintln!("create_workout begin tx error: {e}");
             return HttpResponse::InternalServerError().json(json!({"error": "An error occurred"}))
         }
     };
 
-    let workout = match Workout::create(&mut *tx, &body.name).await {
+    let workout = match Workout::create(&mut *tx, auth.0.id, &body.name).await {
         Ok(w) => w,
-        Err(_) => {
+        Err(e) => {
+            eprintln!("create_workout insert workout error: {e}");
             return HttpResponse::InternalServerError()
                 .json(json!({"error": "Failed to create workout"}))
         }
     };
 
     for exercise in &body.exercises {
-        if Exercise::create(&mut *tx, workout.id, &exercise.name, exercise.exercise_type.clone())
-            .await
-            .is_err()
-        {
+        if let Err(e) = Exercise::create(&mut *tx, workout.id, &exercise.name, exercise.exercise_type.clone(), exercise.position).await {
+            eprintln!("create_workout insert exercise error: {e}");
             return HttpResponse::InternalServerError()
                 .json(json!({"error": "Failed to create exercise"}));
         }
     }
 
-    if tx.commit().await.is_err() {
+    if let Err(e) = tx.commit().await {
+        eprintln!("create_workout commit error: {e}");
         return HttpResponse::InternalServerError().json(json!({"error": "An error occurred"}));
     }
 

+ 6 - 2
src/routes/workouts/get.rs

@@ -14,6 +14,7 @@ struct ExerciseResponse {
     name: String,
     notes: Option<String>,
     exercise_type: ExerciseType,
+    position: i32,
 }
 
 impl From<Exercise> for ExerciseResponse {
@@ -23,6 +24,7 @@ impl From<Exercise> for ExerciseResponse {
             name: e.name,
             notes: e.notes,
             exercise_type: e.exercise_type,
+            position: e.position,
         }
     }
 }
@@ -38,14 +40,16 @@ pub async fn get_workout(
     let workout = match Workout::find_by_id(pool.get_ref(), id).await {
         Ok(Some(w)) => w,
         Ok(None) => return HttpResponse::NotFound().json(json!({"error": "Workout not found"})),
-        Err(_) => {
+        Err(e) => {
+            eprintln!("get_workout find error: {e}");
             return HttpResponse::InternalServerError().json(json!({"error": "An error occurred"}))
         }
     };
 
     let exercises = match Exercise::find_by_workout(pool.get_ref(), id).await {
         Ok(e) => e,
-        Err(_) => {
+        Err(e) => {
+            eprintln!("get_workout exercises error: {e}");
             return HttpResponse::InternalServerError().json(json!({"error": "An error occurred"}))
         }
     };

+ 34 - 0
src/routes/workouts/list.rs

@@ -0,0 +1,34 @@
+use actix_web::{get, web, HttpResponse};
+use serde::Serialize;
+use serde_json::json;
+use sqlx::PgPool;
+use uuid::Uuid;
+
+use crate::auth::AuthUser;
+use crate::models::workout::Workout;
+
+#[derive(Serialize)]
+struct WorkoutResponse {
+    id: Uuid,
+    name: String,
+}
+
+impl From<Workout> for WorkoutResponse {
+    fn from(w: Workout) -> Self {
+        Self { id: w.id, name: w.name }
+    }
+}
+
+#[get("/workout")]
+pub async fn list_workouts(pool: web::Data<PgPool>, auth: AuthUser) -> HttpResponse {
+    match Workout::find_by_user(pool.get_ref(), auth.0.id).await {
+        Ok(workouts) => {
+            let workouts: Vec<WorkoutResponse> = workouts.into_iter().map(Into::into).collect();
+            HttpResponse::Ok().json(workouts)
+        }
+        Err(e) => {
+            eprintln!("list_workouts error: {e}");
+            HttpResponse::InternalServerError().json(json!({"error": "An error occurred"}))
+        }
+    }
+}

+ 1 - 0
src/routes/workouts/mod.rs

@@ -1,2 +1,3 @@
 pub mod create;
 pub mod get;
+pub mod list;