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::{ use crate::{
deps_struct,
errors::ApiError, errors::ApiError,
extractors::{AuthUser, Deps, FromAppState, OptionalAuthUser}, extractors::{AuthUser, Deps, OptionalAuthUser},
handlers::auth::to_user_response, handlers::feed::to_thought_response,
state::AppState,
}; };
use api_types::{ use api_types::{
requests::{CreateThoughtRequest, EditThoughtRequest}, requests::{CreateThoughtRequest, EditThoughtRequest},
responses::ErrorResponse, responses::ErrorResponse,
}; };
use application::use_cases::thoughts::{ 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::{ use axum::{
extract::Path, extract::Path,
@@ -18,56 +19,20 @@ use axum::{
Json, Json,
}; };
use domain::{ use domain::{
ports::{EventPublisher, OutboxWriter, TagRepository, ThoughtRepository, UserRepository}, models::feed::{EngagementStats, FeedEntry, ViewerContext},
ports::{EngagementRepository, EventPublisher, OutboxWriter, TagRepository, ThoughtRepository, UserRepository},
value_objects::ThoughtId, value_objects::ThoughtId,
}; };
use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
pub struct ThoughtsDeps { deps_struct!(ThoughtsDeps {
pub thoughts: Arc<dyn ThoughtRepository>, thoughts: ThoughtRepository,
pub users: Arc<dyn UserRepository>, users: UserRepository,
pub tags: Arc<dyn TagRepository>, tags: TagRepository,
pub events: Arc<dyn EventPublisher>, events: EventPublisher,
pub outbox: Arc<dyn OutboxWriter>, outbox: OutboxWriter,
} engagement: EngagementRepository,
});
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,
})
}
#[utoipa::path( #[utoipa::path(
post, path = "/thoughts", post, path = "/thoughts",
@@ -106,10 +71,13 @@ pub async fn post_thought(
.find_by_id(&uid) .find_by_id(&uid)
.await? .await?
.ok_or(domain::errors::DomainError::NotFound)?; .ok_or(domain::errors::DomainError::NotFound)?;
Ok(( let entry = FeedEntry {
StatusCode::CREATED, thought: out.thought,
Json(thought_to_json(&out.thought, &author, 0, 0, 0)), 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( #[utoipa::path(
@@ -123,15 +91,17 @@ pub async fn post_thought(
pub async fn get_thought_handler( pub async fn get_thought_handler(
Deps(d): Deps<ThoughtsDeps>, Deps(d): Deps<ThoughtsDeps>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
OptionalAuthUser(_viewer): OptionalAuthUser, OptionalAuthUser(viewer): OptionalAuthUser,
) -> Result<Json<serde_json::Value>, ApiError> { ) -> Result<Json<serde_json::Value>, ApiError> {
let thought = get_thought(&*d.thoughts, &ThoughtId::from_uuid(id)).await?; let entry = get_thought_view(
let author = d &*d.thoughts,
.users &*d.users,
.find_by_id(&thought.user_id) &*d.engagement,
.await? &ThoughtId::from_uuid(id),
.ok_or(domain::errors::DomainError::NotFound)?; viewer.as_ref(),
Ok(Json(thought_to_json(&thought, &author, 0, 0, 0))) )
.await?;
Ok(Json(serde_json::to_value(to_thought_response(&entry)).unwrap()))
} }
#[utoipa::path( #[utoipa::path(
@@ -191,13 +161,19 @@ pub async fn patch_thought(
pub async fn get_thread_handler( pub async fn get_thread_handler(
Deps(d): Deps<ThoughtsDeps>, Deps(d): Deps<ThoughtsDeps>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
OptionalAuthUser(viewer): OptionalAuthUser,
) -> Result<Json<Vec<serde_json::Value>>, ApiError> { ) -> Result<Json<Vec<serde_json::Value>>, ApiError> {
let thoughts = get_thread(&*d.thoughts, &ThoughtId::from_uuid(id)).await?; let entries = get_thread_views(
let mut items = Vec::new(); &*d.thoughts,
for t in &thoughts { &*d.users,
if let Ok(Some(author)) = d.users.find_by_id(&t.user_id).await { &*d.engagement,
items.push(thought_to_json(t, &author, 0, 0, 0)); &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)) Ok(Json(items))
} }