feat(presentation/thoughts): use enrichment use cases — real engagement stats, no hardcoded zeros

This commit is contained in:
2026-05-16 11:16:54 +02:00
parent eea1d3fe24
commit d701a40e61

View File

@@ -1,15 +1,16 @@
use crate::{
deps_struct,
errors::ApiError,
extractors::{AuthUser, Deps, FromAppState, OptionalAuthUser},
handlers::auth::to_user_response,
state::AppState,
extractors::{AuthUser, Deps, OptionalAuthUser},
handlers::feed::to_thought_response,
};
use api_types::{
requests::{CreateThoughtRequest, EditThoughtRequest},
responses::ErrorResponse,
};
use application::use_cases::thoughts::{
create_thought, delete_thought, edit_thought, get_thought, get_thread, CreateThoughtInput,
create_thought, delete_thought, edit_thought, get_thread_views, get_thought_view,
CreateThoughtInput,
};
use axum::{
extract::Path,
@@ -18,56 +19,20 @@ use axum::{
Json,
};
use domain::{
ports::{EventPublisher, OutboxWriter, TagRepository, ThoughtRepository, UserRepository},
models::feed::{EngagementStats, FeedEntry, ViewerContext},
ports::{EngagementRepository, EventPublisher, OutboxWriter, TagRepository, ThoughtRepository, UserRepository},
value_objects::ThoughtId,
};
use std::sync::Arc;
use uuid::Uuid;
pub struct ThoughtsDeps {
pub thoughts: Arc<dyn ThoughtRepository>,
pub users: Arc<dyn UserRepository>,
pub tags: Arc<dyn TagRepository>,
pub events: Arc<dyn EventPublisher>,
pub outbox: Arc<dyn OutboxWriter>,
}
impl FromAppState for ThoughtsDeps {
fn from_state(s: &AppState) -> Self {
Self {
thoughts: s.thoughts.clone(),
users: s.users.clone(),
tags: s.tags.clone(),
events: s.events.clone(),
outbox: s.outbox.clone(),
}
}
}
fn thought_to_json(
t: &domain::models::thought::Thought,
author: &domain::models::user::User,
like_count: i64,
boost_count: i64,
reply_count: i64,
) -> serde_json::Value {
serde_json::json!({
"id": t.id.as_uuid(),
"content": t.content.as_str(),
"author": to_user_response(author),
"replyToId": t.in_reply_to_id.as_ref().map(|x| x.as_uuid()),
"visibility": t.visibility.as_str(),
"contentWarning": t.content_warning,
"sensitive": t.sensitive,
"likeCount": like_count,
"boostCount": boost_count,
"replyCount": reply_count,
"likedByViewer": false,
"boostedByViewer": false,
"createdAt": t.created_at,
"updatedAt": t.updated_at,
})
}
deps_struct!(ThoughtsDeps {
thoughts: ThoughtRepository,
users: UserRepository,
tags: TagRepository,
events: EventPublisher,
outbox: OutboxWriter,
engagement: EngagementRepository,
});
#[utoipa::path(
post, path = "/thoughts",
@@ -106,10 +71,13 @@ pub async fn post_thought(
.find_by_id(&uid)
.await?
.ok_or(domain::errors::DomainError::NotFound)?;
Ok((
StatusCode::CREATED,
Json(thought_to_json(&out.thought, &author, 0, 0, 0)),
))
let entry = FeedEntry {
thought: out.thought,
author,
stats: EngagementStats { like_count: 0, boost_count: 0, reply_count: 0 },
viewer: Some(ViewerContext { liked: false, boosted: false }),
};
Ok((StatusCode::CREATED, Json(to_thought_response(&entry))))
}
#[utoipa::path(
@@ -123,15 +91,17 @@ pub async fn post_thought(
pub async fn get_thought_handler(
Deps(d): Deps<ThoughtsDeps>,
Path(id): Path<Uuid>,
OptionalAuthUser(_viewer): OptionalAuthUser,
OptionalAuthUser(viewer): OptionalAuthUser,
) -> Result<Json<serde_json::Value>, ApiError> {
let thought = get_thought(&*d.thoughts, &ThoughtId::from_uuid(id)).await?;
let author = d
.users
.find_by_id(&thought.user_id)
.await?
.ok_or(domain::errors::DomainError::NotFound)?;
Ok(Json(thought_to_json(&thought, &author, 0, 0, 0)))
let entry = get_thought_view(
&*d.thoughts,
&*d.users,
&*d.engagement,
&ThoughtId::from_uuid(id),
viewer.as_ref(),
)
.await?;
Ok(Json(serde_json::to_value(to_thought_response(&entry)).unwrap()))
}
#[utoipa::path(
@@ -191,13 +161,19 @@ pub async fn patch_thought(
pub async fn get_thread_handler(
Deps(d): Deps<ThoughtsDeps>,
Path(id): Path<Uuid>,
OptionalAuthUser(viewer): OptionalAuthUser,
) -> Result<Json<Vec<serde_json::Value>>, ApiError> {
let thoughts = get_thread(&*d.thoughts, &ThoughtId::from_uuid(id)).await?;
let mut items = Vec::new();
for t in &thoughts {
if let Ok(Some(author)) = d.users.find_by_id(&t.user_id).await {
items.push(thought_to_json(t, &author, 0, 0, 0));
}
}
let entries = get_thread_views(
&*d.thoughts,
&*d.users,
&*d.engagement,
&ThoughtId::from_uuid(id),
viewer.as_ref(),
)
.await?;
let items: Vec<_> = entries
.iter()
.map(|e| serde_json::to_value(to_thought_response(e)).unwrap())
.collect();
Ok(Json(items))
}