feat: implement user follow/unfollow functionality and thought retrieval by user
- Added follow and unfollow endpoints for users. - Implemented logic to retrieve thoughts by a specific user. - Updated user error handling to include cases for already following and not following. - Created persistence layer for follow relationships. - Enhanced user and thought schemas to support new features. - Added tests for follow/unfollow endpoints and thought retrieval. - Updated frontend to display thoughts and allow posting new thoughts.
This commit is contained in:
@@ -1,12 +1,22 @@
|
||||
#[derive(Debug)]
|
||||
pub enum UserError {
|
||||
NotFound,
|
||||
NotFollowing,
|
||||
Forbidden,
|
||||
UsernameTaken,
|
||||
AlreadyFollowing,
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for UserError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
UserError::NotFound => write!(f, "User not found"),
|
||||
UserError::NotFollowing => write!(f, "You are not following this user"),
|
||||
UserError::Forbidden => write!(f, "You do not have permission to perform this action"),
|
||||
UserError::UsernameTaken => write!(f, "Username is already taken"),
|
||||
UserError::AlreadyFollowing => write!(f, "You are already following this user"),
|
||||
UserError::Internal(msg) => write!(f, "Internal server error: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
46
thoughts-backend/app/src/persistence/follow.rs
Normal file
46
thoughts-backend/app/src/persistence/follow.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, DbConn, DbErr, EntityTrait, QueryFilter, Set};
|
||||
|
||||
use crate::error::UserError;
|
||||
use models::domains::follow;
|
||||
|
||||
pub async fn follow_user(db: &DbConn, follower_id: i32, followee_id: i32) -> Result<(), DbErr> {
|
||||
if follower_id == followee_id {
|
||||
return Err(DbErr::Custom("Users cannot follow themselves".to_string()));
|
||||
}
|
||||
|
||||
let follow = follow::ActiveModel {
|
||||
follower_id: Set(follower_id),
|
||||
followed_id: Set(followee_id),
|
||||
};
|
||||
|
||||
follow.save(db).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn unfollow_user(
|
||||
db: &DbConn,
|
||||
follower_id: i32,
|
||||
followee_id: i32,
|
||||
) -> Result<(), UserError> {
|
||||
let deleted_result = follow::Entity::delete_many()
|
||||
.filter(follow::Column::FollowerId.eq(follower_id))
|
||||
.filter(follow::Column::FollowedId.eq(followee_id))
|
||||
.exec(db)
|
||||
.await
|
||||
.map_err(|e| UserError::Internal(e.to_string()))?;
|
||||
|
||||
if deleted_result.rows_affected == 0 {
|
||||
return Err(UserError::NotFollowing);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_followed_ids(db: &DbConn, user_id: i32) -> Result<Vec<i32>, 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())
|
||||
}
|
@@ -1 +1,3 @@
|
||||
pub mod follow;
|
||||
pub mod thought;
|
||||
pub mod user;
|
||||
|
67
thoughts-backend/app/src/persistence/thought.rs
Normal file
67
thoughts-backend/app/src/persistence/thought.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DbConn, DbErr, EntityTrait, JoinType, QueryFilter, QueryOrder,
|
||||
QuerySelect, RelationTrait, Set,
|
||||
};
|
||||
|
||||
use models::{
|
||||
domains::{thought, user},
|
||||
params::thought::CreateThoughtParams,
|
||||
schemas::thought::ThoughtWithAuthor,
|
||||
};
|
||||
|
||||
use crate::error::UserError;
|
||||
|
||||
pub async fn create_thought(
|
||||
db: &DbConn,
|
||||
author_id: i32,
|
||||
params: CreateThoughtParams,
|
||||
) -> Result<thought::Model, DbErr> {
|
||||
thought::ActiveModel {
|
||||
author_id: Set(author_id),
|
||||
content: Set(params.content),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_thought(db: &DbConn, thought_id: i32) -> Result<Option<thought::Model>, DbErr> {
|
||||
thought::Entity::find_by_id(thought_id).one(db).await
|
||||
}
|
||||
|
||||
pub async fn delete_thought(db: &DbConn, thought_id: i32) -> Result<(), DbErr> {
|
||||
thought::Entity::delete_by_id(thought_id).exec(db).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_thoughts_by_user(
|
||||
db: &DbConn,
|
||||
user_id: i32,
|
||||
) -> Result<Vec<ThoughtWithAuthor>, DbErr> {
|
||||
thought::Entity::find()
|
||||
.column_as(user::Column::Username, "author_username")
|
||||
.join(JoinType::InnerJoin, thought::Relation::User.def().rev())
|
||||
.filter(thought::Column::AuthorId.eq(user_id))
|
||||
.order_by_desc(thought::Column::CreatedAt)
|
||||
.into_model::<ThoughtWithAuthor>()
|
||||
.all(db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_feed_for_user(
|
||||
db: &DbConn,
|
||||
followed_ids: Vec<i32>,
|
||||
) -> Result<Vec<ThoughtWithAuthor>, UserError> {
|
||||
if followed_ids.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
thought::Entity::find()
|
||||
.column_as(user::Column::Username, "author_username")
|
||||
.join(JoinType::InnerJoin, thought::Relation::User.def().rev())
|
||||
.filter(thought::Column::AuthorId.is_in(followed_ids))
|
||||
.order_by_desc(thought::Column::CreatedAt)
|
||||
.into_model::<ThoughtWithAuthor>()
|
||||
.all(db)
|
||||
.await
|
||||
.map_err(|e| UserError::Internal(e.to_string()))
|
||||
}
|
@@ -26,3 +26,13 @@ pub async fn search_users(db: &DbConn, query: UserQuery) -> Result<Vec<user::Mod
|
||||
pub async fn get_user(db: &DbConn, id: i32) -> Result<Option<user::Model>, DbErr> {
|
||||
user::Entity::find_by_id(id).one(db).await
|
||||
}
|
||||
|
||||
pub async fn get_user_by_username(
|
||||
db: &DbConn,
|
||||
username: &str,
|
||||
) -> Result<Option<user::Model>, DbErr> {
|
||||
user::Entity::find()
|
||||
.filter(user::Column::Username.eq(username))
|
||||
.one(db)
|
||||
.await
|
||||
}
|
||||
|
Reference in New Issue
Block a user