fix: move user_feed to FeedRepository — proper counts and viewer flags for user timelines
This commit is contained in:
@@ -196,6 +196,36 @@ impl FeedRepository for PgFeedRepository {
|
||||
per_page: page.per_page,
|
||||
})
|
||||
}
|
||||
|
||||
async fn user_feed(&self, user_id: &UserId, page: &PageParams, viewer_id: Option<&UserId>) -> Result<Paginated<FeedEntry>, DomainError> {
|
||||
let viewer = viewer_id.map(|v| v.as_uuid());
|
||||
let uid = user_id.as_uuid();
|
||||
|
||||
let total: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM thoughts t WHERE t.user_id = $1 AND t.visibility = 'public'"
|
||||
)
|
||||
.bind(uid)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::Internal(e.to_string()))?;
|
||||
|
||||
let sel = feed_select(viewer);
|
||||
let sql = format!("{sel} WHERE t.user_id = $1 AND t.visibility = 'public' ORDER BY t.created_at DESC LIMIT $2 OFFSET $3");
|
||||
let rows = sqlx::query_as::<_, FeedRow>(&sql)
|
||||
.bind(uid)
|
||||
.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)]
|
||||
|
||||
@@ -4,12 +4,11 @@ use sqlx::PgPool;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::{
|
||||
feed::{FeedEntry, PageParams, Paginated},
|
||||
feed::{PageParams, Paginated},
|
||||
thought::{Thought, Visibility},
|
||||
user::User,
|
||||
},
|
||||
ports::ThoughtRepository,
|
||||
value_objects::{Content, Email, PasswordHash, ThoughtId, UserId, Username},
|
||||
value_objects::{Content, ThoughtId, UserId},
|
||||
};
|
||||
|
||||
pub struct PgThoughtRepository { pool: PgPool }
|
||||
@@ -119,9 +118,12 @@ impl ThoughtRepository for PgThoughtRepository {
|
||||
.map(|rows| rows.into_iter().map(Thought::from).collect())
|
||||
}
|
||||
|
||||
async fn list_by_user(&self, user_id: &UserId, page: &PageParams) -> Result<Paginated<FeedEntry>, DomainError> {
|
||||
let total: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM thoughts WHERE user_id=$1")
|
||||
.bind(user_id.as_uuid())
|
||||
async fn list_by_user(&self, user_id: &UserId, page: &PageParams) -> Result<Paginated<Thought>, DomainError> {
|
||||
let uid = user_id.as_uuid();
|
||||
let total: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM thoughts WHERE user_id = $1"
|
||||
)
|
||||
.bind(uid)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::Internal(e.to_string()))?;
|
||||
@@ -129,37 +131,19 @@ impl ThoughtRepository for PgThoughtRepository {
|
||||
let rows = sqlx::query_as::<_, ThoughtRow>(
|
||||
&format!("{THOUGHT_SELECT} WHERE user_id=$1 ORDER BY created_at DESC LIMIT $2 OFFSET $3")
|
||||
)
|
||||
.bind(user_id.as_uuid())
|
||||
.bind(uid)
|
||||
.bind(page.limit())
|
||||
.bind(page.offset())
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::Internal(e.to_string()))?;
|
||||
|
||||
let author = sqlx::query_as::<_, crate::user::UserRow>(
|
||||
"SELECT id,username,email,password_hash,display_name,bio,avatar_url,header_url,custom_css,local,ap_id,inbox_url,public_key,private_key,created_at,updated_at FROM users WHERE id=$1"
|
||||
)
|
||||
.bind(user_id.as_uuid())
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::Internal(e.to_string()))?
|
||||
.ok_or(DomainError::NotFound)?;
|
||||
let author = User::from(author);
|
||||
|
||||
let items = rows.into_iter().map(|r| {
|
||||
let thought = Thought::from(r);
|
||||
FeedEntry {
|
||||
thought,
|
||||
author: author.clone(),
|
||||
like_count: 0,
|
||||
boost_count: 0,
|
||||
reply_count: 0,
|
||||
liked_by_viewer: false,
|
||||
boosted_by_viewer: false,
|
||||
}
|
||||
}).collect();
|
||||
|
||||
Ok(Paginated { items, total, page: page.page, per_page: page.per_page })
|
||||
Ok(Paginated {
|
||||
items: rows.into_iter().map(Thought::from).collect(),
|
||||
total,
|
||||
page: page.page,
|
||||
per_page: page.per_page,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use domain::{
|
||||
feed::{FeedEntry, PageParams, Paginated, UserSummary},
|
||||
user::User,
|
||||
},
|
||||
ports::{FeedRepository, FollowRepository, ThoughtRepository, UserRepository},
|
||||
ports::{FeedRepository, FollowRepository, UserRepository},
|
||||
value_objects::UserId,
|
||||
};
|
||||
|
||||
@@ -17,8 +17,8 @@ pub async fn get_public_feed(feed: &dyn FeedRepository, viewer_id: Option<&UserI
|
||||
feed.public_feed(&page, viewer_id).await
|
||||
}
|
||||
|
||||
pub async fn get_user_feed(thoughts: &dyn ThoughtRepository, user_id: &UserId, page: PageParams) -> Result<Paginated<FeedEntry>, DomainError> {
|
||||
thoughts.list_by_user(user_id, &page).await
|
||||
pub async fn get_user_feed(feed: &dyn FeedRepository, user_id: &UserId, page: PageParams, viewer_id: Option<&UserId>) -> Result<Paginated<FeedEntry>, DomainError> {
|
||||
feed.user_feed(user_id, &page, viewer_id).await
|
||||
}
|
||||
|
||||
pub async fn get_followers(follows: &dyn FollowRepository, user_id: &UserId, page: PageParams) -> Result<Paginated<User>, DomainError> {
|
||||
|
||||
@@ -57,7 +57,7 @@ pub trait ThoughtRepository: Send + Sync {
|
||||
async fn delete(&self, id: &ThoughtId, user_id: &UserId) -> Result<(), DomainError>;
|
||||
async fn update_content(&self, id: &ThoughtId, content: &Content) -> Result<(), DomainError>;
|
||||
async fn get_thread(&self, id: &ThoughtId) -> Result<Vec<Thought>, DomainError>;
|
||||
async fn list_by_user(&self, user_id: &UserId, page: &PageParams) -> Result<Paginated<FeedEntry>, DomainError>;
|
||||
async fn list_by_user(&self, user_id: &UserId, page: &PageParams) -> Result<Paginated<Thought>, DomainError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -139,6 +139,7 @@ pub trait FeedRepository: Send + Sync {
|
||||
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 fn user_feed(&self, user_id: &UserId, page: &PageParams, viewer_id: Option<&UserId>) -> Result<Paginated<FeedEntry>, DomainError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -96,7 +96,7 @@ pub struct TestStore {
|
||||
.filter(|t| t.in_reply_to_id.as_ref() == Some(id) || &t.id == id)
|
||||
.cloned().collect())
|
||||
}
|
||||
async fn list_by_user(&self, _user_id: &UserId, _page: &PageParams) -> Result<Paginated<FeedEntry>, DomainError> {
|
||||
async fn list_by_user(&self, _user_id: &UserId, _page: &PageParams) -> Result<Paginated<Thought>, DomainError> {
|
||||
Ok(Paginated { items: vec![], total: 0, page: 1, per_page: 20 })
|
||||
}
|
||||
}
|
||||
@@ -290,6 +290,9 @@ pub struct TestStore {
|
||||
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 fn user_feed(&self, _user_id: &UserId, _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 {
|
||||
|
||||
@@ -120,11 +120,12 @@ pub async fn get_followers_handler(State(s): State<AppState>, Path(username): Pa
|
||||
pub async fn user_thoughts_handler(
|
||||
State(s): State<AppState>,
|
||||
Path(username): Path<String>,
|
||||
OptionalAuthUser(viewer): OptionalAuthUser,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let user = get_user_by_username(&*s.users, &username).await?;
|
||||
let page = PageParams { page: q.page(), per_page: q.per_page() };
|
||||
let result = get_user_feed(&*s.thoughts, &user.id, page).await?;
|
||||
let result = get_user_feed(&*s.feed, &user.id, page, viewer.as_ref()).await?;
|
||||
Ok(Json(serde_json::json!({
|
||||
"total": result.total,
|
||||
"page": result.page,
|
||||
|
||||
Reference in New Issue
Block a user