From 8d50adce47961f7abcc562ba38ef0b65c366d5b8 Mon Sep 17 00:00:00 2001 From: dal Date: Tue, 4 Mar 2025 07:47:53 -0700 Subject: [PATCH] send up some changes to handlers --- api/.cursor/rules/global.mdc | 1 - api/.cursor/rules/handlers.mdc | 5 +- .../src/chats/delete_chats_handler.rs | 106 ++++++++++++++++++ api/libs/handlers/src/chats/mod.rs | 4 + .../src/chats/update_chats_handler.rs | 99 ++++++++++++++++ .../routes/rest/routes/chats/delete_chats.rs | 23 ++++ api/src/routes/rest/routes/chats/mod.rs | 6 +- .../routes/rest/routes/chats/update_chats.rs | 22 ++++ 8 files changed, 261 insertions(+), 5 deletions(-) create mode 100644 api/libs/handlers/src/chats/delete_chats_handler.rs create mode 100644 api/libs/handlers/src/chats/update_chats_handler.rs create mode 100644 api/src/routes/rest/routes/chats/delete_chats.rs create mode 100644 api/src/routes/rest/routes/chats/update_chats.rs diff --git a/api/.cursor/rules/global.mdc b/api/.cursor/rules/global.mdc index 564ee1353..6a99128ab 100644 --- a/api/.cursor/rules/global.mdc +++ b/api/.cursor/rules/global.mdc @@ -43,7 +43,6 @@ let mut conn = get_pg_pool().get().await?; ### Concurrency Guidelines - Prioritize concurrent operations, especially for: - API requests - - Database transactions - File operations - Optimize database connection usage: - Batch operations where possible diff --git a/api/.cursor/rules/handlers.mdc b/api/.cursor/rules/handlers.mdc index 87411a0ac..1025d94b2 100644 --- a/api/.cursor/rules/handlers.mdc +++ b/api/.cursor/rules/handlers.mdc @@ -1,6 +1,6 @@ --- -description: This is helpul docs for buildng hanlders in the project. -globs: ibs/handlers/**/*.rs +description: This is helpul docs for buildng hanlders in the project.l +globs: libs/handlers/**/*.rs alwaysApply: false --- # Handler Rules and Best Practices @@ -59,7 +59,6 @@ match operation() { ### Database Operations - Use the connection pool: `get_pg_pool().get().await?` - Run concurrent operations when possible -- Use transactions for related operations - Handle database-specific errors appropriately - Example: ```rust diff --git a/api/libs/handlers/src/chats/delete_chats_handler.rs b/api/libs/handlers/src/chats/delete_chats_handler.rs new file mode 100644 index 000000000..49822c3b1 --- /dev/null +++ b/api/libs/handlers/src/chats/delete_chats_handler.rs @@ -0,0 +1,106 @@ +use anyhow::Result; +use chrono::Utc; +use database::models::{Chat, User}; +use database::pool::get_pg_pool; +use diesel::prelude::*; +use diesel_async::RunQueryDsl; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use uuid::Uuid; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ChatDeleteResult { + pub id: Uuid, + pub success: bool, + pub error: Option, +} + +/// Bulk delete chats (soft delete by setting deleted_at) +/// +/// This function efficiently soft deletes multiple chats using a bulk update operation. +/// It validates that the user has permission to delete each chat (they must be the creator). +/// +/// Returns a list of results indicating success or failure for each chat. +pub async fn delete_chats_handler( + chat_ids: Vec, + user_id: &Uuid, +) -> Result> { + use database::schema::chats; + + // If no chat IDs provided, return empty result + if chat_ids.is_empty() { + return Ok(Vec::new()); + } + + let pool = get_pg_pool(); + let mut conn = pool.get().await?; + + // Find all chats that the user has permission to delete in one query + let user_chats: Vec = chats::table + .filter(chats::id.eq_any(chat_ids.clone())) + .filter(chats::created_by.eq(user_id)) + .filter(chats::deleted_at.is_null()) + .load::(&mut conn) + .await?; + + // Create a set of authorized chat IDs for quick lookup + let authorized_chat_ids: HashSet = + user_chats.iter().map(|c| c.id).collect(); + + // Prepare results for unauthorized chats + let mut delete_results: Vec = chat_ids + .iter() + .filter(|id| !authorized_chat_ids.contains(id)) + .map(|id| ChatDeleteResult { + id: *id, + success: false, + error: Some("Chat not found or you don't have permission to delete it".to_string()), + }) + .collect(); + + // If we have authorized deletes, perform them in bulk + if !authorized_chat_ids.is_empty() { + let authorized_ids: Vec = authorized_chat_ids.into_iter().collect(); + + // Perform a bulk update for all authorized chats at once + let result = diesel::update(chats::table) + .filter(chats::id.eq_any(authorized_ids.clone())) + .set(( + chats::deleted_at.eq(Some(Utc::now())), + chats::updated_at.eq(Utc::now()), + )) + .execute(&mut conn) + .await; + + match result { + Ok(_) => { + // Add success results for all authorized chats + let success_results: Vec = authorized_ids + .into_iter() + .map(|id| ChatDeleteResult { + id, + success: true, + error: None, + }) + .collect(); + + delete_results.extend(success_results); + }, + Err(e) => { + // Add error results for all authorized chats + let error_results: Vec = authorized_ids + .into_iter() + .map(|id| ChatDeleteResult { + id, + success: false, + error: Some(format!("Failed to delete chat: {}", e)), + }) + .collect(); + + delete_results.extend(error_results); + } + } + } + + Ok(delete_results) +} \ No newline at end of file diff --git a/api/libs/handlers/src/chats/mod.rs b/api/libs/handlers/src/chats/mod.rs index dcf36ba6f..3563b5b12 100644 --- a/api/libs/handlers/src/chats/mod.rs +++ b/api/libs/handlers/src/chats/mod.rs @@ -1,9 +1,13 @@ pub mod get_chat_handler; pub mod post_chat_handler; +pub mod update_chats_handler; +pub mod delete_chats_handler; pub mod types; pub mod streaming_parser; pub use get_chat_handler::get_chat_handler; pub use post_chat_handler::post_chat_handler; +pub use update_chats_handler::update_chats_handler; +pub use delete_chats_handler::delete_chats_handler; pub use types::*; pub use streaming_parser::StreamingParser; \ No newline at end of file diff --git a/api/libs/handlers/src/chats/update_chats_handler.rs b/api/libs/handlers/src/chats/update_chats_handler.rs new file mode 100644 index 000000000..69c4b5e8a --- /dev/null +++ b/api/libs/handlers/src/chats/update_chats_handler.rs @@ -0,0 +1,99 @@ +use anyhow::{anyhow, Result}; +use chrono::Utc; +use database::models::{Chat, User}; +use database::pool::get_pg_pool; +use diesel::prelude::*; +use diesel::pg::expression::dsl::any; +use diesel_async::RunQueryDsl; +use futures::future::try_join_all; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use uuid::Uuid; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ChatUpdate { + pub id: Uuid, + pub title: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ChatUpdateResult { + pub id: Uuid, + pub success: bool, + pub error: Option, +} + +/// Bulk update chat titles +/// +/// This function efficiently updates the titles of multiple chats. +/// It validates that the user has permission to update each chat (they must be the creator) +/// in a single database query, then performs individual updates for each chat. +/// +/// Returns a list of results indicating success or failure for each chat. +pub async fn update_chats_handler( + updates: Vec, + user_id: &Uuid, +) -> Result> { + use database::schema::chats; + + // If no updates provided, return empty result + if updates.is_empty() { + return Ok(Vec::new()); + } + + let pool = get_pg_pool(); + let mut conn = pool.get().await?; + + // Extract all chat IDs + let chat_ids: Vec = updates.iter().map(|u| u.id).collect(); + + // Find all chats that the user has permission to update in one query + let user_chats: Vec = chats::table + .filter(chats::id.eq(any(chat_ids.clone()))) + .filter(chats::created_by.eq(user_id)) + .filter(chats::deleted_at.is_null()) + .load::(&mut conn) + .await?; + + // Create a set of authorized chat IDs for quick lookup + let authorized_chat_ids: HashSet = + user_chats.iter().map(|c| c.id).collect(); + + let mut update_results = Vec::with_capacity(updates.len()); + + // Process each update + for update in updates { + if authorized_chat_ids.contains(&update.id) { + // Update the chat title + let result = diesel::update(chats::table) + .filter(chats::id.eq(update.id)) + .set(( + chats::title.eq(update.title.clone()), + chats::updated_at.eq(Utc::now()), + )) + .execute(&mut conn) + .await; + + match result { + Ok(_) => update_results.push(ChatUpdateResult { + id: update.id, + success: true, + error: None, + }), + Err(e) => update_results.push(ChatUpdateResult { + id: update.id, + success: false, + error: Some(format!("Failed to update chat: {}", e)), + }), + } + } else { + update_results.push(ChatUpdateResult { + id: update.id, + success: false, + error: Some("Chat not found or you don't have permission to update it".to_string()), + }); + } + } + + Ok(update_results) +} \ No newline at end of file diff --git a/api/src/routes/rest/routes/chats/delete_chats.rs b/api/src/routes/rest/routes/chats/delete_chats.rs new file mode 100644 index 000000000..6af2b4f12 --- /dev/null +++ b/api/src/routes/rest/routes/chats/delete_chats.rs @@ -0,0 +1,23 @@ +use anyhow::Result; +use axum::http::StatusCode; +use axum::Extension; +use axum::Json; +use database::models::User; +use handlers::chats::delete_chats_handler::{ChatDeleteResult}; +use handlers::chats::delete_chats_handler; +use uuid::Uuid; + +use crate::routes::rest::ApiResponse; + +pub async fn delete_chats_route( + Extension(user): Extension, + Json(chat_ids): Json>, +) -> Result>, (StatusCode, &'static str)> { + match delete_chats_handler(chat_ids, &user.id).await { + Ok(results) => Ok(ApiResponse::JsonData(results)), + Err(e) => { + tracing::error!("Error deleting chats: {}", e); + Err((StatusCode::INTERNAL_SERVER_ERROR, "Failed to delete chats")) + } + } +} \ No newline at end of file diff --git a/api/src/routes/rest/routes/chats/mod.rs b/api/src/routes/rest/routes/chats/mod.rs index a4ef22528..e12840b6a 100644 --- a/api/src/routes/rest/routes/chats/mod.rs +++ b/api/src/routes/rest/routes/chats/mod.rs @@ -1,13 +1,17 @@ use axum::{ - routing::{get, post}, + routing::{get, post, put, delete}, Router, }; mod get_chat; mod post_chat; +mod update_chats; +mod delete_chats; pub fn router() -> Router { Router::new() .route("/", post(post_chat::post_chat_route)) + .route("/", put(update_chats::update_chats_route)) + .route("/", delete(delete_chats::delete_chats_route)) .route("/:id", get(get_chat::get_chat_route)) } diff --git a/api/src/routes/rest/routes/chats/update_chats.rs b/api/src/routes/rest/routes/chats/update_chats.rs new file mode 100644 index 000000000..bbbdb4137 --- /dev/null +++ b/api/src/routes/rest/routes/chats/update_chats.rs @@ -0,0 +1,22 @@ +use anyhow::Result; +use axum::http::StatusCode; +use axum::Extension; +use axum::Json; +use database::models::User; +use handlers::chats::update_chats_handler::{ChatUpdate, ChatUpdateResult}; +use handlers::chats::update_chats_handler; + +use crate::routes::rest::ApiResponse; + +pub async fn update_chats_route( + Extension(user): Extension, + Json(updates): Json>, +) -> Result>, (StatusCode, &'static str)> { + match update_chats_handler(updates, &user.id).await { + Ok(results) => Ok(ApiResponse::JsonData(results)), + Err(e) => { + tracing::error!("Error updating chats: {}", e); + Err((StatusCode::INTERNAL_SERVER_ERROR, "Failed to update chats")) + } + } +} \ No newline at end of file