feat: enhance user registration and follow functionality, add popular tags endpoint, and update tests

This commit is contained in:
2025-09-06 16:49:38 +02:00
parent 508f218fc0
commit 728bf0e231
23 changed files with 216 additions and 64 deletions

View File

@@ -14,3 +14,4 @@ models = { path = "../models" }
validator = "0.20"
rand = "0.8.5"
sea-orm = { version = "1.1.12" }
chrono = { workspace = true }

View File

@@ -13,7 +13,6 @@ fn hash_password(password: &str) -> Result<String, BcryptError> {
}
pub async fn register_user(db: &DbConn, params: RegisterParams) -> Result<user::Model, UserError> {
// Validate the parameters
params
.validate()
.map_err(|e| UserError::Validation(e.to_string()))?;
@@ -22,8 +21,10 @@ pub async fn register_user(db: &DbConn, params: RegisterParams) -> Result<user::
hash_password(&params.password).map_err(|e| UserError::Internal(e.to_string()))?;
let new_user = user::ActiveModel {
username: Set(params.username),
username: Set(params.username.clone()),
password_hash: Set(Some(hashed_password)),
email: Set(Some(params.email)),
display_name: Set(Some(params.username)),
..Default::default()
};

View File

@@ -7,7 +7,7 @@ use models::domains::follow;
pub async fn add_follower(
db: &DbConn,
followed_id: Uuid,
following_id: Uuid,
follower_actor_id: &str,
) -> Result<(), UserError> {
let follower_username = follower_actor_id
@@ -20,21 +20,21 @@ pub async fn add_follower(
.map_err(|e| UserError::Internal(e.to_string()))?
.ok_or(UserError::NotFound)?;
follow_user(db, follower.id, followed_id)
follow_user(db, follower.id, following_id)
.await
.map_err(|e| UserError::Internal(e.to_string()))?;
Ok(())
}
pub async fn follow_user(db: &DbConn, follower_id: Uuid, followed_id: Uuid) -> Result<(), DbErr> {
if follower_id == followed_id {
pub async fn follow_user(db: &DbConn, follower_id: Uuid, following_id: Uuid) -> Result<(), DbErr> {
if follower_id == following_id {
return Err(DbErr::Custom("Users cannot follow themselves".to_string()));
}
let follow = follow::ActiveModel {
follower_id: Set(follower_id),
followed_id: Set(followed_id),
following_id: Set(following_id),
};
follow.insert(db).await?;
@@ -44,11 +44,11 @@ pub async fn follow_user(db: &DbConn, follower_id: Uuid, followed_id: Uuid) -> R
pub async fn unfollow_user(
db: &DbConn,
follower_id: Uuid,
followed_id: Uuid,
following_id: Uuid,
) -> Result<(), UserError> {
let deleted_result = follow::Entity::delete_many()
.filter(follow::Column::FollowerId.eq(follower_id))
.filter(follow::Column::FollowedId.eq(followed_id))
.filter(follow::Column::FollowingId.eq(following_id))
.exec(db)
.await
.map_err(|e| UserError::Internal(e.to_string()))?;
@@ -60,18 +60,18 @@ pub async fn unfollow_user(
Ok(())
}
pub async fn get_followed_ids(db: &DbConn, user_id: Uuid) -> Result<Vec<Uuid>, DbErr> {
pub async fn get_following_ids(db: &DbConn, user_id: Uuid) -> Result<Vec<Uuid>, DbErr> {
let followed_users = follow::Entity::find()
.filter(follow::Column::FollowerId.eq(user_id))
.all(db)
.await?;
Ok(followed_users.into_iter().map(|f| f.followed_id).collect())
Ok(followed_users.into_iter().map(|f| f.following_id).collect())
}
pub async fn get_follower_ids(db: &DbConn, user_id: Uuid) -> Result<Vec<Uuid>, DbErr> {
let followers = follow::Entity::find()
.filter(follow::Column::FollowedId.eq(user_id))
.filter(follow::Column::FollowingId.eq(user_id))
.all(db)
.await?;
Ok(followers.into_iter().map(|f| f.follower_id).collect())

View File

@@ -1,6 +1,8 @@
use models::domains::{tag, thought_tag};
use chrono::{Duration, Utc};
use models::domains::{tag, thought, thought_tag};
use sea_orm::{
sqlx::types::uuid, ColumnTrait, ConnectionTrait, DbErr, EntityTrait, QueryFilter, Set,
prelude::Expr, sea_query::Alias, sqlx::types::uuid, ColumnTrait, ConnectionTrait, DbErr,
EntityTrait, QueryFilter, QueryOrder, QuerySelect, RelationTrait, Set,
};
use std::collections::HashSet;
@@ -84,3 +86,34 @@ where
thought_tag::Entity::insert_many(links).exec(db).await?;
Ok(())
}
pub async fn get_popular_tags<C>(db: &C) -> Result<Vec<String>, DbErr>
where
C: ConnectionTrait,
{
let seven_days_ago = Utc::now() - Duration::days(7);
let popular_tags = tag::Entity::find()
.select_only()
.column(tag::Column::Name)
.column_as(Expr::col((tag::Entity, tag::Column::Id)).count(), "count")
.join(
sea_orm::JoinType::InnerJoin,
tag::Relation::ThoughtTag.def(),
)
.join(
sea_orm::JoinType::InnerJoin,
thought_tag::Relation::Thought.def(),
)
.filter(thought::Column::CreatedAt.gte(seven_days_ago))
.group_by(tag::Column::Name)
.group_by(tag::Column::Id)
.order_by_desc(Expr::col(Alias::new("count")))
.order_by_asc(tag::Column::Name)
.limit(10)
.into_tuple::<(String, i64)>()
.all(db)
.await?;
Ok(popular_tags.into_iter().map(|(name, _)| name).collect())
}

View File

@@ -69,9 +69,9 @@ pub async fn get_thoughts_by_user(
pub async fn get_feed_for_user(
db: &DbConn,
followed_ids: Vec<Uuid>,
following_ids: Vec<Uuid>,
) -> Result<Vec<ThoughtWithAuthor>, UserError> {
if followed_ids.is_empty() {
if following_ids.is_empty() {
return Ok(vec![]);
}
@@ -83,7 +83,7 @@ pub async fn get_feed_for_user(
.column(thought::Column::AuthorId)
.column_as(user::Column::Username, "author_username")
.join(JoinType::InnerJoin, thought::Relation::User.def())
.filter(thought::Column::AuthorId.is_in(followed_ids))
.filter(thought::Column::AuthorId.is_in(following_ids))
.order_by_desc(thought::Column::CreatedAt)
.into_model::<ThoughtWithAuthor>()
.all(db)

View File

@@ -1,6 +1,7 @@
use sea_orm::prelude::Uuid;
use sea_orm::{
ActiveModelTrait, ColumnTrait, DbConn, DbErr, EntityTrait, QueryFilter, Set, TransactionTrait,
ActiveModelTrait, ColumnTrait, DbConn, DbErr, EntityTrait, JoinType, QueryFilter, QueryOrder,
QuerySelect, RelationTrait, Set, TransactionTrait,
};
use models::domains::{top_friends, user};
@@ -127,3 +128,12 @@ pub async fn update_user_profile(
.await
.map_err(|e| UserError::Internal(e.to_string()))
}
pub async fn get_top_friends(db: &DbConn, user_id: Uuid) -> Result<Vec<user::Model>, DbErr> {
user::Entity::find()
.join(JoinType::InnerJoin, top_friends::Relation::User.def().rev())
.filter(top_friends::Column::UserId.eq(user_id))
.order_by_asc(top_friends::Column::Position)
.all(db)
.await
}