From f36c53aed1bef5c46df49dedbbd12412a03b000c Mon Sep 17 00:00:00 2001 From: Nurfog Date: Wed, 25 Feb 2026 16:23:37 -0300 Subject: [PATCH] refactor: migrate sqlx queries from macros to the `.bind()` method --- services/cms-service/src/handlers_assets.rs | 30 +-- .../cms-service/src/handlers_dependencies.rs | 32 ++- services/cms-service/src/handlers_library.rs | 104 ++++--- services/cms-service/src/handlers_rubrics.rs | 254 ++++++++---------- services/lms-service/src/handlers.rs | 59 ++-- .../lms-service/src/handlers_announcements.rs | 8 +- .../lms-service/src/handlers_peer_review.rs | 2 +- 7 files changed, 235 insertions(+), 254 deletions(-) diff --git a/services/cms-service/src/handlers_assets.rs b/services/cms-service/src/handlers_assets.rs index 2e74b6c..f824b4d 100644 --- a/services/cms-service/src/handlers_assets.rs +++ b/services/cms-service/src/handlers_assets.rs @@ -95,20 +95,20 @@ pub async fn upload_asset( .unwrap_or(0); // Record in DB - sqlx::query!( + sqlx::query( r#" INSERT INTO assets (id, organization_id, uploaded_by, course_id, filename, storage_path, mimetype, size_bytes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) "#, - asset_id, - org_ctx.id, - claims.sub, - course_id, - filename, - storage_path, - mimetype, - size_bytes ) + .bind(asset_id) + .bind(org_ctx.id) + .bind(claims.sub) + .bind(course_id) + .bind(&filename) + .bind(&storage_path) + .bind(&mimetype) + .bind(size_bytes) .execute(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -182,19 +182,19 @@ pub async fn delete_asset( Path(id): Path, ) -> Result { // 1. Get asset metadata to find file path - let asset = sqlx::query_as!( - Asset, - "SELECT * FROM assets WHERE id = $1 AND organization_id = $2", - id, - org_ctx.id + let asset: Asset = sqlx::query_as( + "SELECT * FROM assets WHERE id = $1 AND organization_id = $2" ) + .bind(id) + .bind(org_ctx.id) .fetch_optional(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? .ok_or((StatusCode::NOT_FOUND, "Asset not found".to_string()))?; // 2. Delete from DB - sqlx::query!("DELETE FROM assets WHERE id = $1", id) + sqlx::query("DELETE FROM assets WHERE id = $1") + .bind(id) .execute(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; diff --git a/services/cms-service/src/handlers_dependencies.rs b/services/cms-service/src/handlers_dependencies.rs index de43314..1e59f6f 100644 --- a/services/cms-service/src/handlers_dependencies.rs +++ b/services/cms-service/src/handlers_dependencies.rs @@ -37,20 +37,19 @@ pub async fn assign_dependency( } // 2. Insertar la dependencia - let dependency = sqlx::query_as!( - LessonDependency, + let dependency: LessonDependency = sqlx::query_as( r#" INSERT INTO lesson_dependencies (organization_id, lesson_id, prerequisite_lesson_id, min_score_percentage) VALUES ($1, $2, $3, $4) ON CONFLICT (lesson_id, prerequisite_lesson_id) DO UPDATE SET min_score_percentage = EXCLUDED.min_score_percentage RETURNING id, organization_id, lesson_id, prerequisite_lesson_id, min_score_percentage, created_at - "#, - org_ctx.id, - lesson_id, - payload.prerequisite_lesson_id, - payload.min_score_percentage + "# ) + .bind(org_ctx.id) + .bind(lesson_id) + .bind(payload.prerequisite_lesson_id) + .bind(payload.min_score_percentage) .fetch_one(&pool) .await .map_err(|e| { @@ -66,12 +65,12 @@ pub async fn remove_dependency( State(pool): State, Path((lesson_id, prerequisite_id)): Path<(Uuid, Uuid)>, ) -> Result { - let result = sqlx::query!( - "DELETE FROM lesson_dependencies WHERE lesson_id = $1 AND prerequisite_lesson_id = $2 AND organization_id = $3", - lesson_id, - prerequisite_id, - org_ctx.id + let result = sqlx::query( + "DELETE FROM lesson_dependencies WHERE lesson_id = $1 AND prerequisite_lesson_id = $2 AND organization_id = $3" ) + .bind(lesson_id) + .bind(prerequisite_id) + .bind(org_ctx.id) .execute(&pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; @@ -88,12 +87,11 @@ pub async fn list_lesson_dependencies( State(pool): State, Path(lesson_id): Path, ) -> Result>, StatusCode> { - let dependencies = sqlx::query_as!( - LessonDependency, - "SELECT * FROM lesson_dependencies WHERE lesson_id = $1 AND organization_id = $2", - lesson_id, - org_ctx.id + let dependencies: Vec = sqlx::query_as( + "SELECT * FROM lesson_dependencies WHERE lesson_id = $1 AND organization_id = $2" ) + .bind(lesson_id) + .bind(org_ctx.id) .fetch_all(&pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; diff --git a/services/cms-service/src/handlers_library.rs b/services/cms-service/src/handlers_library.rs index 06eb922..a8cedf9 100644 --- a/services/cms-service/src/handlers_library.rs +++ b/services/cms-service/src/handlers_library.rs @@ -24,21 +24,20 @@ pub async fn create_library_block( State(pool): State, Json(payload): Json, ) -> Result, (StatusCode, String)> { - let block = sqlx::query_as!( - LibraryBlock, + let block: LibraryBlock = sqlx::query_as( r#" INSERT INTO library_blocks (organization_id, created_by, name, description, block_type, block_data, tags) VALUES ($1, $2, $3, $4, $5, $6, $7) - RETURNING id, organization_id, created_by, name, description, block_type, block_data, tags, usage_count as "usage_count!", created_at, updated_at - "#, - org_ctx.id, - claims.sub, - payload.name, - payload.description, - payload.block_type, - payload.block_data, - payload.tags.as_deref() + RETURNING id, organization_id, created_by, name, description, block_type, block_data, tags, usage_count, created_at, updated_at + "# ) + .bind(org_ctx.id) + .bind(claims.sub) + .bind(&payload.name) + .bind(&payload.description) + .bind(&payload.block_type) + .bind(&payload.block_data) + .bind(payload.tags.as_deref()) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -110,12 +109,11 @@ pub async fn get_library_block( State(pool): State, Path(block_id): Path, ) -> Result, (StatusCode, String)> { - let block = sqlx::query_as!( - LibraryBlock, - r#"SELECT id, organization_id, created_by, name, description, block_type, block_data, tags, usage_count as "usage_count!", created_at, updated_at FROM library_blocks WHERE id = $1 AND organization_id = $2"#, - block_id, - org_ctx.id + let block: Option = sqlx::query_as( + r#"SELECT id, organization_id, created_by, name, description, block_type, block_data, tags, usage_count, created_at, updated_at FROM library_blocks WHERE id = $1 AND organization_id = $2"# ) + .bind(block_id) + .bind(org_ctx.id) .fetch_optional(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -134,14 +132,12 @@ pub async fn update_library_block( Json(payload): Json, ) -> Result, (StatusCode, String)> { // Verificar que el bloque existe y pertenece a la org - let existing = sqlx::query!( - "SELECT id FROM library_blocks WHERE id = $1 AND organization_id = $2", - block_id, - org_ctx.id - ) - .fetch_optional(&pool) - .await - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + let existing = sqlx::query("SELECT id FROM library_blocks WHERE id = $1 AND organization_id = $2") + .bind(block_id) + .bind(org_ctx.id) + .fetch_optional(&pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; if existing.is_none() { return Err((StatusCode::NOT_FOUND, "Block not found".to_string())); @@ -149,8 +145,7 @@ pub async fn update_library_block( // Update dinĂ¡mico basado en campos provistos let updated = if let Some(name) = &payload.name { - sqlx::query_as!( - LibraryBlock, + sqlx::query_as( r#" UPDATE library_blocks SET name = COALESCE($1, name), @@ -158,33 +153,32 @@ pub async fn update_library_block( tags = COALESCE($3, tags), updated_at = NOW() WHERE id = $4 AND organization_id = $5 - RETURNING id, organization_id, created_by, name, description, block_type, block_data, tags, usage_count as "usage_count!", created_at, updated_at - "#, - Some(name), - payload.description, - payload.tags.as_deref(), - block_id, - org_ctx.id + RETURNING id, organization_id, created_by, name, description, block_type, block_data, tags, usage_count, created_at, updated_at + "# ) + .bind(Some(name)) + .bind(payload.description) + .bind(payload.tags.as_deref()) + .bind(block_id) + .bind(org_ctx.id) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? } else { - sqlx::query_as!( - LibraryBlock, + sqlx::query_as( r#" UPDATE library_blocks SET description = COALESCE($1, description), tags = COALESCE($2, tags), updated_at = NOW() WHERE id = $3 AND organization_id = $4 - RETURNING id, organization_id, created_by, name, description, block_type, block_data, tags, usage_count as "usage_count!", created_at, updated_at - "#, - payload.description, - payload.tags.as_deref(), - block_id, - org_ctx.id + RETURNING id, organization_id, created_by, name, description, block_type, block_data, tags, usage_count, created_at, updated_at + "# ) + .bind(payload.description) + .bind(payload.tags.as_deref()) + .bind(block_id) + .bind(org_ctx.id) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? @@ -199,14 +193,12 @@ pub async fn delete_library_block( State(pool): State, Path(block_id): Path, ) -> Result { - let result = sqlx::query!( - "DELETE FROM library_blocks WHERE id = $1 AND organization_id = $2", - block_id, - org_ctx.id - ) - .execute(&pool) - .await - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + let result = sqlx::query("DELETE FROM library_blocks WHERE id = $1 AND organization_id = $2") + .bind(block_id) + .bind(org_ctx.id) + .execute(&pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; if result.rows_affected() == 0 { return Err((StatusCode::NOT_FOUND, "Block not found".to_string())); @@ -221,14 +213,12 @@ pub async fn increment_block_usage( State(pool): State, Path(block_id): Path, ) -> Result { - let result = sqlx::query!( - "UPDATE library_blocks SET usage_count = usage_count + 1 WHERE id = $1 AND organization_id = $2", - block_id, - org_ctx.id - ) - .execute(&pool) - .await - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + let result = sqlx::query("UPDATE library_blocks SET usage_count = usage_count + 1 WHERE id = $1 AND organization_id = $2") + .bind(block_id) + .bind(org_ctx.id) + .execute(&pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; if result.rows_affected() == 0 { return Err((StatusCode::NOT_FOUND, "Block not found".to_string())); diff --git a/services/cms-service/src/handlers_rubrics.rs b/services/cms-service/src/handlers_rubrics.rs index 679f34f..c941316 100644 --- a/services/cms-service/src/handlers_rubrics.rs +++ b/services/cms-service/src/handlers_rubrics.rs @@ -7,7 +7,7 @@ use common::auth::Claims; use common::middleware::Org; use common::models::{LessonRubric, Rubric, RubricCriterion, RubricLevel}; use serde::{Deserialize, Serialize}; -use sqlx::PgPool; +use sqlx::{PgPool, Row}; use uuid::Uuid; // ==================== Payload Structs ==================== @@ -107,19 +107,18 @@ pub async fn create_rubric( Path(course_id): Path, Json(payload): Json, ) -> Result, (StatusCode, String)> { - let rubric = sqlx::query_as!( - Rubric, + let rubric: Rubric = sqlx::query_as( r#" INSERT INTO rubrics (organization_id, course_id, created_by, name, description) VALUES ($1, $2, $3, $4, $5) RETURNING id, organization_id, course_id, created_by, name, description, total_points, created_at, updated_at - "#, - org_ctx.id, - payload.course_id.or(Some(course_id)), - claims.sub, - payload.name, - payload.description + "# ) + .bind(org_ctx.id) + .bind(payload.course_id.or(Some(course_id))) + .bind(claims.sub) + .bind(&payload.name) + .bind(&payload.description) .fetch_one(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -133,17 +132,16 @@ pub async fn list_course_rubrics( State(pool): State, Path(course_id): Path, ) -> Result>, (StatusCode, String)> { - let rubrics = sqlx::query_as!( - Rubric, + let rubrics: Vec = sqlx::query_as( r#" SELECT id, organization_id, course_id, created_by, name, description, total_points, created_at, updated_at FROM rubrics WHERE organization_id = $1 AND (course_id = $2 OR course_id IS NULL) ORDER BY created_at DESC - "#, - org_ctx.id, - course_id + "# ) + .bind(org_ctx.id) + .bind(course_id) .fetch_all(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -158,32 +156,30 @@ pub async fn get_rubric_with_details( Path(rubric_id): Path, ) -> Result, (StatusCode, String)> { // Get rubric - let rubric = sqlx::query_as!( - Rubric, + let rubric: Rubric = sqlx::query_as( r#" SELECT id, organization_id, course_id, created_by, name, description, total_points, created_at, updated_at FROM rubrics WHERE id = $1 AND organization_id = $2 - "#, - rubric_id, - org_ctx.id + "# ) + .bind(rubric_id) + .bind(org_ctx.id) .fetch_optional(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? .ok_or((StatusCode::NOT_FOUND, "Rubric not found".to_string()))?; // Get criteria - let criteria = sqlx::query_as!( - RubricCriterion, + let criteria: Vec = sqlx::query_as( r#" SELECT id, rubric_id, name, description, max_points, position, created_at FROM rubric_criteria WHERE rubric_id = $1 ORDER BY position ASC - "#, - rubric_id + "# ) + .bind(rubric_id) .fetch_all(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -191,16 +187,15 @@ pub async fn get_rubric_with_details( // Get levels for each criterion let mut criteria_with_levels = Vec::new(); for criterion in criteria { - let levels = sqlx::query_as!( - RubricLevel, + let levels: Vec = sqlx::query_as( r#" SELECT id, criterion_id, name, description, points, position, created_at FROM rubric_levels WHERE criterion_id = $1 ORDER BY position ASC - "#, - criterion.id + "# ) + .bind(criterion.id) .fetch_all(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -221,8 +216,7 @@ pub async fn update_rubric( Path(rubric_id): Path, Json(payload): Json, ) -> Result, (StatusCode, String)> { - let rubric = sqlx::query_as!( - Rubric, + let rubric: Rubric = sqlx::query_as( r#" UPDATE rubrics SET name = COALESCE($1, name), @@ -230,12 +224,12 @@ pub async fn update_rubric( updated_at = NOW() WHERE id = $3 AND organization_id = $4 RETURNING id, organization_id, course_id, created_by, name, description, total_points, created_at, updated_at - "#, - payload.name, - payload.description, - rubric_id, - org_ctx.id + "# ) + .bind(payload.name) + .bind(payload.description) + .bind(rubric_id) + .bind(org_ctx.id) .fetch_optional(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? @@ -250,14 +244,12 @@ pub async fn delete_rubric( State(pool): State, Path(rubric_id): Path, ) -> Result { - let result = sqlx::query!( - "DELETE FROM rubrics WHERE id = $1 AND organization_id = $2", - rubric_id, - org_ctx.id - ) - .execute(&pool) - .await - .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + let result = sqlx::query("DELETE FROM rubrics WHERE id = $1 AND organization_id = $2") + .bind(rubric_id) + .bind(org_ctx.id) + .execute(&pool) + .await + .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; if result.rows_affected() == 0 { return Err((StatusCode::NOT_FOUND, "Rubric not found".to_string())); @@ -276,45 +268,42 @@ pub async fn create_criterion( Json(payload): Json, ) -> Result, (StatusCode, String)> { // Verify rubric exists and belongs to org - let _rubric = sqlx::query!( - "SELECT id FROM rubrics WHERE id = $1 AND organization_id = $2", - rubric_id, - org_ctx.id - ) - .fetch_optional(&pool) - .await - .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? - .ok_or((StatusCode::NOT_FOUND, "Rubric not found".to_string()))?; + let _rubric = sqlx::query("SELECT id FROM rubrics WHERE id = $1 AND organization_id = $2") + .bind(rubric_id) + .bind(org_ctx.id) + .fetch_optional(&pool) + .await + .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? + .ok_or((StatusCode::NOT_FOUND, "Rubric not found".to_string()))?; let position = payload.position.unwrap_or(0); - let criterion = sqlx::query_as!( - RubricCriterion, + let criterion: RubricCriterion = sqlx::query_as( r#" INSERT INTO rubric_criteria (rubric_id, name, description, max_points, position) VALUES ($1, $2, $3, $4, $5) RETURNING id, rubric_id, name, description, max_points, position, created_at - "#, - rubric_id, - payload.name, - payload.description, - payload.max_points, - position + "# ) + .bind(rubric_id) + .bind(&payload.name) + .bind(&payload.description) + .bind(payload.max_points) + .bind(position) .fetch_one(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; // Update rubric total_points - let _= sqlx::query!( + let _= sqlx::query( r#" UPDATE rubrics SET total_points = (SELECT COALESCE(SUM(max_points), 0) FROM rubric_criteria WHERE rubric_id = $1), updated_at = NOW() WHERE id = $1 - "#, - rubric_id + "# ) + .bind(rubric_id) .execute(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -329,8 +318,7 @@ pub async fn update_criterion( Path(criterion_id): Path, Json(payload): Json, ) -> Result, (StatusCode, String)> { - let criterion = sqlx::query_as!( - RubricCriterion, + let criterion: RubricCriterion = sqlx::query_as( r#" UPDATE rubric_criteria SET name = COALESCE($1, name), @@ -340,14 +328,14 @@ pub async fn update_criterion( WHERE id = $5 AND rubric_id IN (SELECT id FROM rubrics WHERE organization_id = $6) RETURNING id, rubric_id, name, description, max_points, position, created_at - "#, - payload.name, - payload.description, - payload.max_points, - payload.position, - criterion_id, - org_ctx.id + "# ) + .bind(payload.name) + .bind(payload.description) + .bind(payload.max_points) + .bind(payload.position) + .bind(criterion_id) + .bind(org_ctx.id) .fetch_optional(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? @@ -355,15 +343,15 @@ pub async fn update_criterion( // Update rubric total_points if max_points changed if payload.max_points.is_some() { - let _ = sqlx::query!( + let _ = sqlx::query( r#" UPDATE rubrics SET total_points = (SELECT COALESCE(SUM(max_points), 0) FROM rubric_criteria WHERE rubric_id = $1), updated_at = NOW() WHERE id = $1 - "#, - criterion.rubric_id + "# ) + .bind(criterion.rubric_id) .execute(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -379,24 +367,24 @@ pub async fn delete_criterion( Path(criterion_id): Path, ) -> Result { // Get rubric_id before deleting - let criterion = sqlx::query!( - "SELECT rubric_id FROM rubric_criteria WHERE id = $1", - criterion_id - ) - .fetch_optional(&pool) - .await - .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? - .ok_or((StatusCode::NOT_FOUND, "Criterion not found".to_string()))?; + let criterion_row = sqlx::query("SELECT rubric_id FROM rubric_criteria WHERE id = $1") + .bind(criterion_id) + .fetch_optional(&pool) + .await + .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? + .ok_or((StatusCode::NOT_FOUND, "Criterion not found".to_string()))?; + + let rubric_id: Uuid = criterion_row.get("rubric_id"); - let result = sqlx::query!( + let result = sqlx::query( r#" DELETE FROM rubric_criteria WHERE id = $1 AND rubric_id IN (SELECT id FROM rubrics WHERE organization_id = $2) - "#, - criterion_id, - org_ctx.id + "# ) + .bind(criterion_id) + .bind(org_ctx.id) .execute(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -406,15 +394,15 @@ pub async fn delete_criterion( } // Update rubric total_points - let _ = sqlx::query!( + let _ = sqlx::query( r#" UPDATE rubrics SET total_points = (SELECT COALESCE(SUM(max_points), 0) FROM rubric_criteria WHERE rubric_id = $1), updated_at = NOW() WHERE id = $1 - "#, - criterion.rubric_id + "# ) + .bind(rubric_id) .execute(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -432,31 +420,28 @@ pub async fn create_level( Json(payload): Json, ) -> Result, (StatusCode, String)> { // Verify criterion exists and belongs to org - let _criterion = sqlx::query!( - "SELECT id FROM rubric_criteria WHERE id = $1 AND rubric_id IN (SELECT id FROM rubrics WHERE organization_id = $2)", - criterion_id, - org_ctx.id - ) - .fetch_optional(&pool) - .await - .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? - .ok_or((StatusCode::NOT_FOUND, "Criterion not found".to_string()))?; + let _criterion = sqlx::query("SELECT id FROM rubric_criteria WHERE id = $1 AND rubric_id IN (SELECT id FROM rubrics WHERE organization_id = $2)") + .bind(criterion_id) + .bind(org_ctx.id) + .fetch_optional(&pool) + .await + .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? + .ok_or((StatusCode::NOT_FOUND, "Criterion not found".to_string()))?; let position = payload.position.unwrap_or(0); - let level = sqlx::query_as!( - RubricLevel, + let level: RubricLevel = sqlx::query_as( r#" INSERT INTO rubric_levels (criterion_id, name, description, points, position) VALUES ($1, $2, $3, $4, $5) RETURNING id, criterion_id, name, description, points, position, created_at - "#, - criterion_id, - payload.name, - payload.description, - payload.points, - position + "# ) + .bind(criterion_id) + .bind(&payload.name) + .bind(&payload.description) + .bind(payload.points) + .bind(position) .fetch_one(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -471,8 +456,7 @@ pub async fn update_level( Path(level_id): Path, Json(payload): Json, ) -> Result, (StatusCode, String)> { - let level = sqlx::query_as!( - RubricLevel, + let level: RubricLevel = sqlx::query_as( r#" UPDATE rubric_levels SET name = COALESCE($1, name), @@ -485,14 +469,14 @@ pub async fn update_level( WHERE rubric_id IN (SELECT id FROM rubrics WHERE organization_id = $6) ) RETURNING id, criterion_id, name, description, points, position, created_at - "#, - payload.name, - payload.description, - payload.points, - payload.position, - level_id, - org_ctx.id + "# ) + .bind(payload.name) + .bind(payload.description) + .bind(payload.points) + .bind(payload.position) + .bind(level_id) + .bind(org_ctx.id) .fetch_optional(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? @@ -507,7 +491,7 @@ pub async fn delete_level( State(pool): State, Path(level_id): Path, ) -> Result { - let result = sqlx::query!( + let result = sqlx::query( r#" DELETE FROM rubric_levels WHERE id = $1 @@ -515,10 +499,10 @@ pub async fn delete_level( SELECT id FROM rubric_criteria WHERE rubric_id IN (SELECT id FROM rubrics WHERE organization_id = $2) ) - "#, - level_id, - org_ctx.id + "# ) + .bind(level_id) + .bind(org_ctx.id) .execute(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -538,17 +522,16 @@ pub async fn assign_rubric_to_lesson( State(pool): State, Path((lesson_id, rubric_id)): Path<(Uuid, Uuid)>, ) -> Result, (StatusCode, String)> { - let lesson_rubric = sqlx::query_as!( - LessonRubric, + let lesson_rubric: LessonRubric = sqlx::query_as( r#" INSERT INTO lesson_rubrics (lesson_id, rubric_id, is_active) VALUES ($1, $2, true) ON CONFLICT (lesson_id, rubric_id) DO UPDATE SET is_active = true RETURNING id, lesson_id, rubric_id, is_active, assigned_at - "#, - lesson_id, - rubric_id + "# ) + .bind(lesson_id) + .bind(rubric_id) .fetch_one(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -562,14 +545,12 @@ pub async fn unassign_rubric_from_lesson( State(pool): State, Path((lesson_id, rubric_id)): Path<(Uuid, Uuid)>, ) -> Result { - let result = sqlx::query!( - "DELETE FROM lesson_rubrics WHERE lesson_id = $1 AND rubric_id = $2", - lesson_id, - rubric_id - ) - .execute(&pool) - .await - .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + let result = sqlx::query("DELETE FROM lesson_rubrics WHERE lesson_id = $1 AND rubric_id = $2") + .bind(lesson_id) + .bind(rubric_id) + .execute(&pool) + .await + .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; if result.rows_affected() == 0 { return Err((StatusCode::NOT_FOUND, "Lesson rubric not found".to_string())); @@ -584,18 +565,17 @@ pub async fn get_lesson_rubrics( State(pool): State, Path(lesson_id): Path, ) -> Result>, (StatusCode, String)> { - let rubrics = sqlx::query_as!( - Rubric, + let rubrics: Vec = sqlx::query_as( r#" SELECT r.id, r.organization_id, r.course_id, r.created_by, r.name, r.description, r.total_points, r.created_at, r.updated_at FROM rubrics r INNER JOIN lesson_rubrics lr ON lr.rubric_id = r.id WHERE lr.lesson_id = $1 AND lr.is_active = true AND r.organization_id = $2 ORDER BY lr.assigned_at DESC - "#, - lesson_id, - org_ctx.id + "# ) + .bind(lesson_id) + .bind(org_ctx.id) .fetch_all(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; diff --git a/services/lms-service/src/handlers.rs b/services/lms-service/src/handlers.rs index 5c65fa5..79d2f9f 100644 --- a/services/lms-service/src/handlers.rs +++ b/services/lms-service/src/handlers.rs @@ -164,16 +164,27 @@ pub async fn export_course_grades( } // 1. Get Categories - let categories = sqlx::query!( - "SELECT id, name FROM grading_categories WHERE course_id = $1 ORDER BY name", - course_id + #[derive(sqlx::FromRow)] + struct Cat { id: Uuid, name: String } + let categories: Vec = sqlx::query_as( + "SELECT id, name FROM grading_categories WHERE course_id = $1 ORDER BY name" ) + .bind(course_id) .fetch_all(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; // 2. Get Student general data - let students = sqlx::query!( + #[derive(sqlx::FromRow)] + struct StudentRow { + id: Uuid, + full_name: String, + email: String, + progress: Option, + cohort_name: Option, + average_score: Option, + } + let students: Vec = sqlx::query_as( r#" SELECT u.id, @@ -188,23 +199,23 @@ pub async fn export_course_grades( WHERE e.organization_id = $2 GROUP BY u.id, u.full_name, u.email ORDER BY u.full_name - "#, - course_id, - org_ctx.id + "# ) + .bind(course_id) + .bind(org_ctx.id) .fetch_all(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; // 3. Get detailed grades per user/category + #[derive(sqlx::FromRow)] struct UserCategoryGrade { user_id: Uuid, grading_category_id: Option, avg_score: Option, } - let detailed_grades = sqlx::query_as!( - UserCategoryGrade, + let detailed_grades: Vec = sqlx::query_as( r#" SELECT g.user_id, @@ -214,9 +225,9 @@ pub async fn export_course_grades( JOIN lessons l ON g.lesson_id = l.id WHERE g.course_id = $1 GROUP BY g.user_id, l.grading_category_id - "#, - course_id + "# ) + .bind(course_id) .fetch_all(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -906,17 +917,16 @@ pub async fn get_course_outline( } // 6. Fetch all dependencies for this course - let dependencies = sqlx::query_as!( - LessonDependency, + let dependencies: Vec = sqlx::query_as( r#" SELECT ld.* FROM lesson_dependencies ld JOIN lessons l ON ld.lesson_id = l.id JOIN modules m ON l.module_id = m.id WHERE m.course_id = $1 - "#, - id + "# ) + .bind(id) .fetch_all(&pool) .await .map_err(|e: sqlx::Error| { @@ -1011,12 +1021,15 @@ pub async fn get_lesson_content( return Ok(Json(lesson)); } // We check if there are any prerequisites that the user hasn't completed yet. - // A prerequisite is completed if: - // a) It's graded and the user has a grade >= min_score_percentage (default 0) - // b) It's not graded and the user has a 'complete' interaction - let unmet_dependencies = sqlx::query!( + #[derive(sqlx::FromRow)] + struct UnmetDep { + prerequisite_lesson_id: Uuid, + prereq_title: String, + min_score_percentage: Option + } + let unmet_dependencies: Vec = sqlx::query_as( r#" - SELECT ld.prerequisite_lesson_id, p.title as prereq_title, ld.min_score_percentage + SELECT ld.prerequisite_lesson_id, p.title as prereq_title, ld.min_score_percentage::float4 as min_score_percentage FROM lesson_dependencies ld JOIN lessons p ON ld.prerequisite_lesson_id = p.id LEFT JOIN user_grades ug ON ld.prerequisite_lesson_id = ug.lesson_id AND ug.user_id = $2 @@ -1028,10 +1041,10 @@ pub async fn get_lesson_content( OR (p.is_graded = false AND li.id IS NULL) ) - "#, - id, - claims.sub + "# ) + .bind(id) + .bind(claims.sub) .fetch_all(&pool) .await .map_err(|e: sqlx::Error| { diff --git a/services/lms-service/src/handlers_announcements.rs b/services/lms-service/src/handlers_announcements.rs index c603829..05ba8e2 100644 --- a/services/lms-service/src/handlers_announcements.rs +++ b/services/lms-service/src/handlers_announcements.rs @@ -52,16 +52,16 @@ pub async fn list_announcements( // Attach cohort_ids to each announcement for a in &mut announcements { - let cohorts = sqlx::query!( - "SELECT cohort_id FROM announcement_cohorts WHERE announcement_id = $1", - a.id + let cohorts: Vec<(Uuid,)> = sqlx::query_as( + "SELECT cohort_id FROM announcement_cohorts WHERE announcement_id = $1" ) + .bind(a.id) .fetch_all(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; if !cohorts.is_empty() { - a.cohort_ids = Some(cohorts.into_iter().map(|c| c.cohort_id).collect()); + a.cohort_ids = Some(cohorts.into_iter().map(|c| c.0).collect()); } } diff --git a/services/lms-service/src/handlers_peer_review.rs b/services/lms-service/src/handlers_peer_review.rs index f2f1a12..32cd4e2 100644 --- a/services/lms-service/src/handlers_peer_review.rs +++ b/services/lms-service/src/handlers_peer_review.rs @@ -8,7 +8,7 @@ use common::models::{ SubmitPeerReviewPayload, }; use common::{auth::Claims, middleware::Org}; -use sqlx::PgPool; +use sqlx::{PgPool, Row}; use uuid::Uuid; pub async fn submit_assignment(