feat: v2 rewrite — hexagonal arch, ActivityPub federation, NATS, deployment-ready #1
@@ -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<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)]
|
||||
|
||||
@@ -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<Paginated<Thought>, 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<Paginated<FeedEntry>, 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<Paginated<FeedEntry>, DomainError> {
|
||||
|
||||
@@ -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 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 tag_feed(&self, tag_name: &str, page: &PageParams, viewer_id: Option<&UserId>) -> Result<Paginated<FeedEntry>, DomainError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -287,6 +287,9 @@ pub struct TestStore {
|
||||
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 })
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -161,21 +161,12 @@ pub async fn tag_thoughts_handler(
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> Result<Json<serde_json::Value>, 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::<Vec<_>>()
|
||||
"items": result.items.iter().map(to_thought_response).collect::<Vec<_>>(),
|
||||
})))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user