From cc9658975ff1d677b642075e7b4a07aba524a496 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 14 May 2026 15:43:02 +0200 Subject: [PATCH] fix: tag feed returns full FeedEntry with author and counts --- crates/adapters/postgres/src/feed.rs | 35 ++++++++++++++++++++++++ crates/application/src/use_cases/feed.rs | 7 ++--- crates/domain/src/ports.rs | 1 + crates/domain/src/testing.rs | 3 ++ crates/presentation/src/handlers/feed.rs | 13 ++------- 5 files changed, 44 insertions(+), 15 deletions(-) diff --git a/crates/adapters/postgres/src/feed.rs b/crates/adapters/postgres/src/feed.rs index 85a4bac..bd9b553 100644 --- a/crates/adapters/postgres/src/feed.rs +++ b/crates/adapters/postgres/src/feed.rs @@ -141,6 +141,41 @@ impl FeedRepository for PgFeedRepository { Ok(Paginated { items: rows.into_iter().map(row_to_entry).collect(), total, page: page.page, per_page: page.per_page }) } + + async fn tag_feed(&self, tag_name: &str, page: &PageParams, _viewer_id: Option<&UserId>) -> Result, DomainError> { + let total: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM thoughts t + JOIN thought_tags tt ON tt.thought_id = t.id + JOIN tags tg ON tg.id = tt.tag_id + WHERE tg.name = $1 AND t.visibility = 'public'" + ) + .bind(tag_name) + .fetch_one(&self.pool) + .await + .map_err(|e| DomainError::Internal(e.to_string()))?; + + let sql = format!( + "{FEED_SELECT} + JOIN thought_tags tt ON tt.thought_id = t.id + JOIN tags tg ON tg.id = tt.tag_id + WHERE tg.name = $1 AND t.visibility = 'public' + ORDER BY t.created_at DESC LIMIT $2 OFFSET $3" + ); + let rows = sqlx::query_as::<_, FeedRow>(&sql) + .bind(tag_name) + .bind(page.limit()) + .bind(page.offset()) + .fetch_all(&self.pool) + .await + .map_err(|e| DomainError::Internal(e.to_string()))?; + + Ok(Paginated { + items: rows.into_iter().map(row_to_entry).collect(), + total, + page: page.page, + per_page: page.per_page, + }) + } } #[cfg(test)] diff --git a/crates/application/src/use_cases/feed.rs b/crates/application/src/use_cases/feed.rs index fc63eae..ef5d38a 100644 --- a/crates/application/src/use_cases/feed.rs +++ b/crates/application/src/use_cases/feed.rs @@ -2,10 +2,9 @@ use domain::{ errors::DomainError, models::{ feed::{FeedEntry, PageParams, Paginated, UserSummary}, - thought::Thought, user::User, }, - ports::{FeedRepository, FollowRepository, TagRepository, ThoughtRepository, UserRepository}, + ports::{FeedRepository, FollowRepository, ThoughtRepository, UserRepository}, value_objects::UserId, }; @@ -30,8 +29,8 @@ pub async fn get_following(follows: &dyn FollowRepository, user_id: &UserId, pag follows.list_following(user_id, &page).await } -pub async fn get_by_tag(tags: &dyn TagRepository, tag_name: &str, page: PageParams) -> Result, DomainError> { - tags.list_thoughts_by_tag(tag_name, &page).await +pub async fn get_by_tag(feed: &dyn FeedRepository, tag_name: &str, page: PageParams) -> Result, DomainError> { + feed.tag_feed(tag_name, &page, None).await } pub async fn search(feed: &dyn FeedRepository, query: &str, page: PageParams, viewer_id: Option<&UserId>) -> Result, DomainError> { diff --git a/crates/domain/src/ports.rs b/crates/domain/src/ports.rs index 604b84d..63b8c31 100644 --- a/crates/domain/src/ports.rs +++ b/crates/domain/src/ports.rs @@ -138,6 +138,7 @@ pub trait FeedRepository: Send + Sync { async fn home_feed(&self, following_ids: &[UserId], page: &PageParams, viewer_id: Option<&UserId>) -> Result, DomainError>; async fn public_feed(&self, page: &PageParams, viewer_id: Option<&UserId>) -> Result, DomainError>; async fn search(&self, query: &str, page: &PageParams, viewer_id: Option<&UserId>) -> Result, DomainError>; + async fn tag_feed(&self, tag_name: &str, page: &PageParams, viewer_id: Option<&UserId>) -> Result, DomainError>; } #[async_trait] diff --git a/crates/domain/src/testing.rs b/crates/domain/src/testing.rs index 91819df..b4e5e42 100644 --- a/crates/domain/src/testing.rs +++ b/crates/domain/src/testing.rs @@ -287,6 +287,9 @@ pub struct TestStore { async fn search(&self, _q: &str, _p: &PageParams, _v: Option<&UserId>) -> Result, DomainError> { Ok(Paginated { items: vec![], total: 0, page: 1, per_page: 20 }) } + async fn tag_feed(&self, _tag_name: &str, _page: &PageParams, _viewer_id: Option<&UserId>) -> Result, DomainError> { + Ok(Paginated { items: vec![], total: 0, page: 1, per_page: 20 }) + } } #[async_trait] impl SearchPort for TestStore { diff --git a/crates/presentation/src/handlers/feed.rs b/crates/presentation/src/handlers/feed.rs index 2248aeb..117141a 100644 --- a/crates/presentation/src/handlers/feed.rs +++ b/crates/presentation/src/handlers/feed.rs @@ -161,21 +161,12 @@ pub async fn tag_thoughts_handler( Query(q): Query, ) -> Result, ApiError> { let page = PageParams { page: q.page(), per_page: q.per_page() }; - let result = get_by_tag(&*s.tags, &tag_name, page).await?; + let result = get_by_tag(&*s.feed, &tag_name, page).await?; Ok(Json(serde_json::json!({ "tag": tag_name, "total": result.total, "page": result.page, "per_page": result.per_page, - "items": result.items.iter().map(|t| serde_json::json!({ - "id": t.id.as_uuid(), - "content": t.content.as_str(), - "in_reply_to_id": t.in_reply_to_id.as_ref().map(|id| id.as_uuid()), - "visibility": t.visibility.as_str(), - "content_warning": t.content_warning, - "sensitive": t.sensitive, - "created_at": t.created_at, - "updated_at": t.updated_at, - })).collect::>() + "items": result.items.iter().map(to_thought_response).collect::>(), }))) }