use chrono::{DateTime, Utc}; use serde::Serialize; use sqlx::PgPool; use uuid::Uuid; #[derive(Debug, sqlx::FromRow)] pub struct Comment { pub id: Uuid, pub upload_id: Uuid, pub user_id: Uuid, pub body: String, pub created_at: DateTime, pub deleted_at: Option>, } #[derive(Debug, Serialize, sqlx::FromRow)] pub struct CommentDto { pub id: Uuid, pub upload_id: Uuid, pub user_id: Uuid, pub uploader_name: String, pub body: String, pub created_at: DateTime, } impl Comment { pub async fn create( pool: &PgPool, upload_id: Uuid, user_id: Uuid, body: &str, ) -> Result { sqlx::query_as::<_, Self>( "INSERT INTO comment (upload_id, user_id, body) VALUES ($1, $2, $3) RETURNING *", ) .bind(upload_id) .bind(user_id) .bind(body) .fetch_one(pool) .await } /// Paginated comment listing — returns up to `limit` rows in chronological /// order (oldest first). If `before` is set, only comments older than that /// timestamp are returned, enabling backward cursor pagination ("load /// earlier"). Without the LIMIT a hot post with thousands of comments could /// OOM the server on a single GET. pub async fn list_for_upload( pool: &PgPool, upload_id: Uuid, before: Option>, limit: i64, ) -> Result, sqlx::Error> { // Two-step: pick the newest `limit` rows older than `before`, then flip // them back into ascending order so the caller can render top-to-bottom. sqlx::query_as::<_, CommentDto>( "SELECT * FROM ( SELECT c.id, c.upload_id, c.user_id, u.display_name AS uploader_name, c.body, c.created_at FROM comment c JOIN \"user\" u ON u.id = c.user_id WHERE c.upload_id = $1 AND c.deleted_at IS NULL AND ($2::timestamptz IS NULL OR c.created_at < $2) ORDER BY c.created_at DESC LIMIT $3 ) page ORDER BY created_at ASC", ) .bind(upload_id) .bind(before) .bind(limit) .fetch_all(pool) .await } pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result, sqlx::Error> { sqlx::query_as::<_, Self>( "SELECT * FROM comment WHERE id = $1 AND deleted_at IS NULL", ) .bind(id) .fetch_optional(pool) .await } pub async fn soft_delete(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> { sqlx::query("UPDATE comment SET deleted_at = NOW() WHERE id = $1") .bind(id) .execute(pool) .await?; Ok(()) } /// Event-scoped variant of [`Self::soft_delete`]. Returns `false` if the /// comment doesn't exist or belongs to a different event. pub async fn soft_delete_in_event( pool: &PgPool, id: Uuid, event_id: Uuid, ) -> Result { let result = sqlx::query( "UPDATE comment SET deleted_at = NOW() WHERE id = $1 AND deleted_at IS NULL AND upload_id IN (SELECT id FROM upload WHERE event_id = $2)", ) .bind(id) .bind(event_id) .execute(pool) .await?; Ok(result.rows_affected() > 0) } }