diff --git a/crates/adapters/postgres/src/engagement.rs b/crates/adapters/postgres/src/engagement.rs new file mode 100644 index 0000000..d5cb4d0 --- /dev/null +++ b/crates/adapters/postgres/src/engagement.rs @@ -0,0 +1,83 @@ +use crate::db_error::IntoDbResult; +use async_trait::async_trait; +use domain::{ + errors::DomainError, + models::feed::{EngagementStats, ViewerContext}, + ports::EngagementRepository, + value_objects::{ThoughtId, UserId}, +}; +use sqlx::PgPool; +use std::collections::HashMap; + +pub struct PgEngagementRepository { + pool: PgPool, +} + +impl PgEngagementRepository { + pub fn new(pool: PgPool) -> Self { + Self { pool } + } +} + +#[async_trait] +impl EngagementRepository for PgEngagementRepository { + async fn get_for_thoughts( + &self, + thought_ids: &[ThoughtId], + viewer_id: Option<&UserId>, + ) -> Result)>, DomainError> { + if thought_ids.is_empty() { + return Ok(HashMap::new()); + } + + #[derive(sqlx::FromRow)] + struct Row { + thought_id: uuid::Uuid, + like_count: i64, + boost_count: i64, + reply_count: i64, + liked_by_viewer: bool, + boosted_by_viewer: bool, + } + + let ids: Vec = thought_ids.iter().map(|t| t.as_uuid()).collect(); + let viewer_uuid: Option = viewer_id.map(|v| v.as_uuid()); + + let rows = sqlx::query_as::<_, Row>( + "SELECT + t.id AS thought_id, + COUNT(DISTINCT l.user_id) AS like_count, + COUNT(DISTINCT b.user_id) AS boost_count, + COUNT(DISTINCT r.id) AS reply_count, + COALESCE(BOOL_OR(l.user_id = $2), false) AS liked_by_viewer, + COALESCE(BOOL_OR(b.user_id = $2), false) AS boosted_by_viewer + FROM thoughts t + LEFT JOIN likes l ON l.thought_id = t.id + LEFT JOIN boosts b ON b.thought_id = t.id + LEFT JOIN thoughts r ON r.in_reply_to_id = t.id + WHERE t.id = ANY($1) + GROUP BY t.id", + ) + .bind(&ids[..]) + .bind(viewer_uuid) + .fetch_all(&self.pool) + .await + .into_domain()?; + + let mut result = HashMap::new(); + for row in rows { + let tid = ThoughtId::from_uuid(row.thought_id); + let stats = EngagementStats { + like_count: row.like_count, + boost_count: row.boost_count, + reply_count: row.reply_count, + }; + let viewer = viewer_id.map(|_| ViewerContext { + liked: row.liked_by_viewer, + boosted: row.boosted_by_viewer, + }); + result.insert(tid, (stats, viewer)); + } + Ok(result) + } +} diff --git a/crates/adapters/postgres/src/lib.rs b/crates/adapters/postgres/src/lib.rs index 26c8139..7694f24 100644 --- a/crates/adapters/postgres/src/lib.rs +++ b/crates/adapters/postgres/src/lib.rs @@ -1,4 +1,5 @@ pub mod activitypub; +pub mod engagement; pub mod api_key; pub mod block; pub mod boost;