diff --git a/crates/adapters/postgres/src/tag.rs b/crates/adapters/postgres/src/tag.rs index 3beca5d..b66e7ea 100644 --- a/crates/adapters/postgres/src/tag.rs +++ b/crates/adapters/postgres/src/tag.rs @@ -51,6 +51,21 @@ impl TagRepository for PgTagRepository { Ok(Paginated { items: rows.into_iter().map(Thought::from).collect(), total, page: page.page, per_page: page.per_page }) } + + async fn popular_tags(&self, limit: usize) -> Result, DomainError> { + sqlx::query_as::<_, (String, i64)>( + "SELECT t.name, COUNT(tt.thought_id) AS thought_count + FROM tags t + JOIN thought_tags tt ON t.id = tt.tag_id + GROUP BY t.id, t.name + ORDER BY thought_count DESC + LIMIT $1" + ) + .bind(limit as i64) + .fetch_all(&self.pool) + .await + .map_err(|e| DomainError::Internal(e.to_string())) + } } #[cfg(test)] diff --git a/crates/presentation/src/handlers/feed.rs b/crates/presentation/src/handlers/feed.rs index a4d4e4e..2248aeb 100644 --- a/crates/presentation/src/handlers/feed.rs +++ b/crates/presentation/src/handlers/feed.rs @@ -133,6 +133,20 @@ pub async fn user_thoughts_handler( }))) } +pub async fn get_popular_tags( + State(s): State, + Query(params): Query>, +) -> Result, ApiError> { + let limit: usize = params.get("limit").and_then(|v| v.parse().ok()).unwrap_or(20); + let tags = s.tags.popular_tags(limit.min(100)).await?; + Ok(Json(serde_json::json!({ + "tags": tags.iter().map(|(name, count)| serde_json::json!({ + "name": name, + "thought_count": count, + })).collect::>() + }))) +} + #[utoipa::path( get, path = "/tags/{name}", params(