fix: tag feed returns full FeedEntry with author and counts
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 5m2s
test / unit (pull_request) Successful in 16m3s
test / integration (pull_request) Failing after 16m59s

This commit is contained in:
2026-05-14 15:43:02 +02:00
parent 38b4774a63
commit cc9658975f
5 changed files with 44 additions and 15 deletions

View File

@@ -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 }) 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<Paginated<FeedEntry>, 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)] #[cfg(test)]

View File

@@ -2,10 +2,9 @@ use domain::{
errors::DomainError, errors::DomainError,
models::{ models::{
feed::{FeedEntry, PageParams, Paginated, UserSummary}, feed::{FeedEntry, PageParams, Paginated, UserSummary},
thought::Thought,
user::User, user::User,
}, },
ports::{FeedRepository, FollowRepository, TagRepository, ThoughtRepository, UserRepository}, ports::{FeedRepository, FollowRepository, ThoughtRepository, UserRepository},
value_objects::UserId, 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 follows.list_following(user_id, &page).await
} }
pub async fn get_by_tag(tags: &dyn TagRepository, tag_name: &str, page: PageParams) -> Result<Paginated<Thought>, DomainError> { pub async fn get_by_tag(feed: &dyn FeedRepository, tag_name: &str, page: PageParams) -> Result<Paginated<FeedEntry>, DomainError> {
tags.list_thoughts_by_tag(tag_name, &page).await feed.tag_feed(tag_name, &page, None).await
} }
pub async fn search(feed: &dyn FeedRepository, query: &str, page: PageParams, viewer_id: Option<&UserId>) -> Result<Paginated<FeedEntry>, DomainError> { pub async fn search(feed: &dyn FeedRepository, query: &str, page: PageParams, viewer_id: Option<&UserId>) -> Result<Paginated<FeedEntry>, DomainError> {

View File

@@ -138,6 +138,7 @@ pub trait FeedRepository: Send + Sync {
async fn home_feed(&self, following_ids: &[UserId], page: &PageParams, viewer_id: Option<&UserId>) -> Result<Paginated<FeedEntry>, DomainError>; async fn home_feed(&self, following_ids: &[UserId], page: &PageParams, viewer_id: Option<&UserId>) -> Result<Paginated<FeedEntry>, DomainError>;
async fn public_feed(&self, page: &PageParams, viewer_id: Option<&UserId>) -> Result<Paginated<FeedEntry>, DomainError>; async fn public_feed(&self, page: &PageParams, viewer_id: Option<&UserId>) -> Result<Paginated<FeedEntry>, DomainError>;
async fn search(&self, query: &str, page: &PageParams, viewer_id: Option<&UserId>) -> Result<Paginated<FeedEntry>, DomainError>; async fn search(&self, query: &str, page: &PageParams, viewer_id: Option<&UserId>) -> Result<Paginated<FeedEntry>, DomainError>;
async fn tag_feed(&self, tag_name: &str, page: &PageParams, viewer_id: Option<&UserId>) -> Result<Paginated<FeedEntry>, DomainError>;
} }
#[async_trait] #[async_trait]

View File

@@ -287,6 +287,9 @@ pub struct TestStore {
async fn search(&self, _q: &str, _p: &PageParams, _v: Option<&UserId>) -> Result<Paginated<FeedEntry>, DomainError> { async fn search(&self, _q: &str, _p: &PageParams, _v: Option<&UserId>) -> Result<Paginated<FeedEntry>, DomainError> {
Ok(Paginated { items: vec![], total: 0, page: 1, per_page: 20 }) 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<Paginated<FeedEntry>, DomainError> {
Ok(Paginated { items: vec![], total: 0, page: 1, per_page: 20 })
}
} }
#[async_trait] impl SearchPort for TestStore { #[async_trait] impl SearchPort for TestStore {

View File

@@ -161,21 +161,12 @@ pub async fn tag_thoughts_handler(
Query(q): Query<PaginationQuery>, Query(q): Query<PaginationQuery>,
) -> Result<Json<serde_json::Value>, ApiError> { ) -> Result<Json<serde_json::Value>, ApiError> {
let page = PageParams { page: q.page(), per_page: q.per_page() }; 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!({ Ok(Json(serde_json::json!({
"tag": tag_name, "tag": tag_name,
"total": result.total, "total": result.total,
"page": result.page, "page": result.page,
"per_page": result.per_page, "per_page": result.per_page,
"items": result.items.iter().map(|t| serde_json::json!({ "items": result.items.iter().map(to_thought_response).collect::<Vec<_>>(),
"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::<Vec<_>>()
}))) })))
} }