refactor: migrate sqlx queries from macros to the .bind() method

This commit is contained in:
2026-02-25 16:23:37 -03:00
parent 5b3fc800c7
commit f36c53aed1
7 changed files with 235 additions and 254 deletions
+15 -15
View File
@@ -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<Uuid>,
) -> Result<StatusCode, (StatusCode, String)> {
// 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()))?;
@@ -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<PgPool>,
Path((lesson_id, prerequisite_id)): Path<(Uuid, Uuid)>,
) -> Result<StatusCode, StatusCode> {
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<PgPool>,
Path(lesson_id): Path<Uuid>,
) -> Result<Json<Vec<LessonDependency>>, 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<LessonDependency> = 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)?;
+47 -57
View File
@@ -24,21 +24,20 @@ pub async fn create_library_block(
State(pool): State<PgPool>,
Json(payload): Json<CreateLibraryBlockPayload>,
) -> Result<Json<LibraryBlock>, (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<PgPool>,
Path(block_id): Path<Uuid>,
) -> Result<Json<LibraryBlock>, (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<LibraryBlock> = 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<UpdateLibraryBlockPayload>,
) -> Result<Json<LibraryBlock>, (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<PgPool>,
Path(block_id): Path<Uuid>,
) -> Result<StatusCode, (StatusCode, String)> {
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<PgPool>,
Path(block_id): Path<Uuid>,
) -> Result<StatusCode, (StatusCode, String)> {
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()));
+117 -137
View File
@@ -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<Uuid>,
Json(payload): Json<CreateRubricPayload>,
) -> Result<Json<Rubric>, (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<PgPool>,
Path(course_id): Path<Uuid>,
) -> Result<Json<Vec<Rubric>>, (StatusCode, String)> {
let rubrics = sqlx::query_as!(
Rubric,
let rubrics: Vec<Rubric> = 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<Uuid>,
) -> Result<Json<RubricWithDetails>, (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<RubricCriterion> = 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<RubricLevel> = 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<Uuid>,
Json(payload): Json<UpdateRubricPayload>,
) -> Result<Json<Rubric>, (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<PgPool>,
Path(rubric_id): Path<Uuid>,
) -> Result<StatusCode, (StatusCode, String)> {
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<CreateCriterionPayload>,
) -> Result<Json<RubricCriterion>, (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<Uuid>,
Json(payload): Json<UpdateCriterionPayload>,
) -> Result<Json<RubricCriterion>, (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<Uuid>,
) -> Result<StatusCode, (StatusCode, String)> {
// 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<CreateLevelPayload>,
) -> Result<Json<RubricLevel>, (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<Uuid>,
Json(payload): Json<UpdateLevelPayload>,
) -> Result<Json<RubricLevel>, (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<PgPool>,
Path(level_id): Path<Uuid>,
) -> Result<StatusCode, (StatusCode, String)> {
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<PgPool>,
Path((lesson_id, rubric_id)): Path<(Uuid, Uuid)>,
) -> Result<Json<LessonRubric>, (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<PgPool>,
Path((lesson_id, rubric_id)): Path<(Uuid, Uuid)>,
) -> Result<StatusCode, (StatusCode, String)> {
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<PgPool>,
Path(lesson_id): Path<Uuid>,
) -> Result<Json<Vec<Rubric>>, (StatusCode, String)> {
let rubrics = sqlx::query_as!(
Rubric,
let rubrics: Vec<Rubric> = 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()))?;
+36 -23
View File
@@ -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<Cat> = 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<f32>,
cohort_name: Option<String>,
average_score: Option<f32>,
}
let students: Vec<StudentRow> = 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<Uuid>,
avg_score: Option<f32>,
}
let detailed_grades = sqlx::query_as!(
UserCategoryGrade,
let detailed_grades: Vec<UserCategoryGrade> = 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<LessonDependency> = 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<f32>
}
let unmet_dependencies: Vec<UnmetDep> = 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| {
@@ -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());
}
}
@@ -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(