From b466a061c4c62a9fb299c3168a59e715913cc89c Mon Sep 17 00:00:00 2001 From: dal Date: Wed, 5 Mar 2025 12:47:08 -0700 Subject: [PATCH] list is good for now, will adjust later. --- api/libs/handlers/Cargo.toml | 1 + .../handlers/src/chats/list_chats_handler.rs | 148 ++++++++++++++++++ api/libs/handlers/src/chats/mod.rs | 2 + .../src/chats/update_chats_handler.rs | 2 +- .../routes/rest/routes/chats/list_chats.rs | 40 +++++ api/src/routes/rest/routes/chats/mod.rs | 18 ++- 6 files changed, 205 insertions(+), 6 deletions(-) create mode 100644 api/libs/handlers/src/chats/list_chats_handler.rs create mode 100644 api/src/routes/rest/routes/chats/list_chats.rs diff --git a/api/libs/handlers/Cargo.toml b/api/libs/handlers/Cargo.toml index 9930b79c1..f009d446f 100644 --- a/api/libs/handlers/Cargo.toml +++ b/api/libs/handlers/Cargo.toml @@ -21,6 +21,7 @@ regex = { workspace = true } indexmap = { workspace = true } async-trait = { workspace = true } once_cell = { workspace = true } +base64 = { workspace = true } # Local dependencies database = { path = "../database" } diff --git a/api/libs/handlers/src/chats/list_chats_handler.rs b/api/libs/handlers/src/chats/list_chats_handler.rs new file mode 100644 index 000000000..b0bd1e52f --- /dev/null +++ b/api/libs/handlers/src/chats/list_chats_handler.rs @@ -0,0 +1,148 @@ +use anyhow::{anyhow, Result}; +use chrono::{DateTime, Utc}; +use database::pool::get_pg_pool; +use diesel::prelude::*; +use diesel::{ExpressionMethods, JoinOnDsl, QueryDsl}; +use diesel_async::RunQueryDsl; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use uuid::Uuid; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ListChatsRequest { + pub page_token: Option, + pub page_size: i32, + pub admin_view: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ChatListItem { + pub id: String, + pub title: String, + pub is_favorited: bool, + pub updated_at: String, + pub created_at: String, + pub created_by: String, + pub created_by_id: String, + pub created_by_name: String, + pub created_by_avatar: Option, + pub last_edited: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ListChatsResponse { + pub items: Vec, + pub next_page_token: Option, +} + +#[derive(Queryable)] +struct ChatWithUser { + // Chat fields + pub id: Uuid, + pub title: String, + pub created_at: DateTime, + pub updated_at: DateTime, + pub created_by: Uuid, + // User fields + pub user_name: Option, + pub user_attributes: Value, +} + +/// List chats with pagination support +/// +/// This function efficiently retrieves a list of chats with their associated user information. +/// It supports pagination using cursor-based pagination (page_token) and limits results using page_size. +/// If admin_view is true and the user has admin privileges, it shows all chats; otherwise, only the user's chats. +/// +/// Returns a list of chat items with user information and pagination details. +pub async fn list_chats_handler( + request: ListChatsRequest, + user_id: &Uuid, +) -> Result { + use database::schema::{chats, users}; + + let mut conn = get_pg_pool().get().await?; + + // Start building the query + let mut query = chats::table + .inner_join(users::table.on(chats::created_by.eq(users::id))) + .filter(chats::deleted_at.is_null()) + .into_boxed(); + + // Add user filter if not admin view + if !request.admin_view { + query = query.filter(chats::created_by.eq(user_id)); + } + + // Add cursor-based pagination if page_token is provided + if let Some(token) = request.page_token { + let decoded_token = base64::decode(&token) + .map_err(|_| anyhow!("Invalid page token"))?; + let cursor_timestamp = String::from_utf8(decoded_token) + .map_err(|_| anyhow!("Invalid page token format"))?; + let cursor_dt = DateTime::parse_from_rfc3339(&cursor_timestamp) + .map_err(|_| anyhow!("Invalid timestamp in page token"))? + .with_timezone(&Utc); + + query = query.filter(chats::created_at.lt(cursor_dt)); + } + + // Order by creation date descending and limit results + query = query + .order_by(chats::created_at.desc()) + .limit((request.page_size + 1) as i64); + + // Execute query and select required fields + let results: Vec = query + .select(( + chats::id, + chats::title, + chats::created_at, + chats::updated_at, + chats::created_by, + users::name.nullable(), + users::attributes, + )) + .load::(&mut conn) + .await?; + + // Check if there are more results + let has_more = results.len() > request.page_size as usize; + let mut items = results + .into_iter() + .take(request.page_size as usize) + .map(|chat| { + let created_by_avatar = chat.user_attributes + .get("avatar") + .and_then(|v| v.as_str()) + .map(String::from); + + ChatListItem { + id: chat.id.to_string(), + title: chat.title, + is_favorited: false, // TODO: Implement favorites feature + created_at: chat.created_at.to_rfc3339(), + updated_at: chat.updated_at.to_rfc3339(), + created_by: chat.created_by.to_string(), + created_by_id: chat.created_by.to_string(), + created_by_name: chat.user_name.unwrap_or_else(|| "Unknown".to_string()), + created_by_avatar, + last_edited: chat.updated_at.to_rfc3339(), + } + }) + .collect::>(); + + // Generate next page token if there are more results + let next_page_token = if has_more { + items + .last() + .map(|last_item| base64::encode(&last_item.created_at)) + } else { + None + }; + + Ok(ListChatsResponse { + items, + next_page_token, + }) +} \ 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 bf86d8f27..a36afa343 100644 --- a/api/libs/handlers/src/chats/mod.rs +++ b/api/libs/handlers/src/chats/mod.rs @@ -5,10 +5,12 @@ pub mod delete_chats_handler; pub mod types; pub mod streaming_parser; pub mod context_loaders; +pub mod list_chats_handler; 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 list_chats_handler::list_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 index 69c4b5e8a..04d3d7844 100644 --- a/api/libs/handlers/src/chats/update_chats_handler.rs +++ b/api/libs/handlers/src/chats/update_chats_handler.rs @@ -49,7 +49,7 @@ pub async fn update_chats_handler( // 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::id.eq_any(chat_ids.clone())) .filter(chats::created_by.eq(user_id)) .filter(chats::deleted_at.is_null()) .load::(&mut conn) diff --git a/api/src/routes/rest/routes/chats/list_chats.rs b/api/src/routes/rest/routes/chats/list_chats.rs new file mode 100644 index 000000000..274c0b3a4 --- /dev/null +++ b/api/src/routes/rest/routes/chats/list_chats.rs @@ -0,0 +1,40 @@ +use axum::{extract::Query, http::StatusCode, Extension}; +use database::models::User; +use handlers::chats::list_chats_handler::{ + list_chats_handler, ListChatsRequest, ListChatsResponse, +}; +use serde::Deserialize; + +use crate::routes::rest::ApiResponse; + +#[derive(Debug, Deserialize)] +pub struct ListChatsQuery { + pub page_token: Option, + #[serde(default = "default_page_size")] + pub page_size: i32, + #[serde(default)] + pub admin_view: bool, +} + +fn default_page_size() -> i32 { + 20 // Default to 20 items per page +} + +pub async fn list_chats_route( + Extension(user): Extension, + Query(query): Query, +) -> Result, (StatusCode, &'static str)> { + let request = ListChatsRequest { + page_token: query.page_token, + page_size: query.page_size, + admin_view: query.admin_view, + }; + + match list_chats_handler(request, &user.id).await { + Ok(response) => Ok(ApiResponse::JsonData(response)), + Err(e) => { + tracing::error!("Error listing chats: {}", e); + Err((StatusCode::INTERNAL_SERVER_ERROR, "Failed to list chats")) + } + } +} diff --git a/api/src/routes/rest/routes/chats/mod.rs b/api/src/routes/rest/routes/chats/mod.rs index e12840b6a..1bdc0074c 100644 --- a/api/src/routes/rest/routes/chats/mod.rs +++ b/api/src/routes/rest/routes/chats/mod.rs @@ -3,15 +3,23 @@ use axum::{ Router, }; +mod delete_chats; mod get_chat; +mod list_chats; mod post_chat; mod update_chats; -mod delete_chats; + +pub use delete_chats::delete_chats_route; +pub use get_chat::get_chat_route; +pub use list_chats::list_chats_route; +pub use post_chat::post_chat_route; +pub use update_chats::update_chats_route; 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)) + .route("/", get(list_chats_route)) + .route("/", post(post_chat_route)) + .route("/", put(update_chats_route)) + .route("/", delete(delete_chats_route)) + .route("/:id", get(get_chat_route)) }