feat(presentation): wire SearchPort, /search returns thoughts + users

This commit is contained in:
2026-05-14 09:38:02 +02:00
parent 4eeaea2a14
commit b599047d98
4 changed files with 34 additions and 4 deletions

View File

@@ -12,6 +12,7 @@ domain = { workspace = true }
application = { workspace = true }
api-types = { workspace = true }
postgres = { workspace = true }
postgres-search = { workspace = true }
auth = { workspace = true }
axum = { workspace = true }
sqlx = { workspace = true }

View File

@@ -1,6 +1,6 @@
use axum::{extract::{Path, Query, State}, Json};
use api_types::requests::{PaginationQuery, SearchQuery};
use application::use_cases::feed::{get_home_feed, get_public_feed, get_followers, get_following, search};
use application::use_cases::feed::{get_home_feed, get_public_feed, get_followers, get_following};
use domain::models::feed::PageParams;
use crate::{errors::ApiError, extractors::{AuthUser, OptionalAuthUser}, handlers::auth::to_user_response, state::AppState};
use application::use_cases::profile::get_user_by_username;
@@ -17,10 +17,36 @@ pub async fn public_feed(State(s): State<AppState>, OptionalAuthUser(viewer): Op
Ok(Json(serde_json::json!({ "items": result.items.iter().map(|e| e.thought.id.as_uuid()).collect::<Vec<_>>(), "total": result.total, "page": result.page })))
}
pub async fn search_handler(State(s): State<AppState>, OptionalAuthUser(viewer): OptionalAuthUser, Query(q): Query<SearchQuery>) -> Result<Json<serde_json::Value>, ApiError> {
pub async fn search_handler(
State(s): State<AppState>,
OptionalAuthUser(viewer): OptionalAuthUser,
Query(q): Query<SearchQuery>,
) -> Result<Json<serde_json::Value>, ApiError> {
let page = PageParams { page: q.page.unwrap_or(1), per_page: q.per_page.unwrap_or(20) };
let result = search(&*s.feed, &q.q, page, viewer.as_ref()).await?;
Ok(Json(serde_json::json!({ "items": result.items.iter().map(|e| e.thought.id.as_uuid()).collect::<Vec<_>>(), "total": result.total })))
let query = q.q.trim().to_string();
let (thoughts_result, users_result) = tokio::join!(
s.search.search_thoughts(&query, &page, viewer.as_ref()),
s.search.search_users(&query, &page),
);
let thoughts = thoughts_result?.items.into_iter().map(|e| serde_json::json!({
"id": e.thought.id.as_uuid(),
"content": e.thought.content.as_str(),
"author": to_user_response(&e.author),
"like_count": e.like_count,
"boost_count": e.boost_count,
"reply_count": e.reply_count,
"created_at": e.thought.created_at,
})).collect::<Vec<_>>();
let users = users_result?.items.into_iter().map(|u| to_user_response(&u)).collect::<Vec<_>>();
Ok(Json(serde_json::json!({
"query": query,
"thoughts": thoughts,
"users": users,
})))
}
pub async fn get_following_handler(State(s): State<AppState>, Path(username): Path<String>, Query(q): Query<PaginationQuery>) -> Result<Json<serde_json::Value>, ApiError> {

View File

@@ -7,6 +7,7 @@ pub mod state;
use std::sync::Arc;
use sqlx::PgPool;
use state::AppState;
use postgres_search::PgSearchRepository;
use async_trait::async_trait;
use domain::{errors::DomainError, events::DomainEvent, ports::EventPublisher};
@@ -34,6 +35,7 @@ pub fn build_state(pool: PgPool, jwt_secret: String) -> AppState {
notifications: Arc::new(postgres::notification::PgNotificationRepository::new(pool.clone())),
remote_actors: Arc::new(postgres::remote_actor::PgRemoteActorRepository::new(pool.clone())),
feed: Arc::new(postgres::feed::PgFeedRepository::new(pool.clone())),
search: Arc::new(PgSearchRepository::new(pool.clone())),
auth: Arc::new(auth::JwtAuthService::new(jwt_secret, 86400 * 30)),
hasher: Arc::new(auth::Argon2PasswordHasher),
events: Arc::new(NoOpEventPublisher),

View File

@@ -15,6 +15,7 @@ pub struct AppState {
pub notifications: Arc<dyn NotificationRepository>,
pub remote_actors: Arc<dyn RemoteActorRepository>,
pub feed: Arc<dyn FeedRepository>,
pub search: Arc<dyn SearchPort>,
pub auth: Arc<dyn AuthService>,
pub hasher: Arc<dyn PasswordHasher>,
pub events: Arc<dyn EventPublisher>,