feat: Refactor user and thought models to use UUIDs instead of integers
- Updated user and thought models to utilize UUIDs for primary keys. - Modified persistence functions to accommodate UUIDs for user and thought IDs. - Implemented tag functionality with new Tag and ThoughtTag models. - Added migration scripts to create new tables for tags and thought-tag relationships. - Enhanced thought creation to parse hashtags and link them to thoughts. - Updated tests to reflect changes in user and thought ID types.
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, DbConn, DbErr, EntityTrait, QueryFilter, Set};
|
||||
use sea_orm::{
|
||||
prelude::Uuid, ActiveModelTrait, ColumnTrait, DbConn, DbErr, EntityTrait, QueryFilter, Set,
|
||||
};
|
||||
|
||||
use crate::{error::UserError, persistence::user::get_user_by_username};
|
||||
use models::domains::follow;
|
||||
|
||||
pub async fn add_follower(
|
||||
db: &DbConn,
|
||||
followed_id: i32,
|
||||
followed_id: Uuid,
|
||||
follower_actor_id: &str,
|
||||
) -> Result<(), UserError> {
|
||||
let follower_username = follower_actor_id
|
||||
@@ -25,7 +27,7 @@ pub async fn add_follower(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn follow_user(db: &DbConn, follower_id: i32, followed_id: i32) -> Result<(), DbErr> {
|
||||
pub async fn follow_user(db: &DbConn, follower_id: Uuid, followed_id: Uuid) -> Result<(), DbErr> {
|
||||
if follower_id == followed_id {
|
||||
return Err(DbErr::Custom("Users cannot follow themselves".to_string()));
|
||||
}
|
||||
@@ -41,8 +43,8 @@ pub async fn follow_user(db: &DbConn, follower_id: i32, followed_id: i32) -> Res
|
||||
|
||||
pub async fn unfollow_user(
|
||||
db: &DbConn,
|
||||
follower_id: i32,
|
||||
followed_id: i32,
|
||||
follower_id: Uuid,
|
||||
followed_id: Uuid,
|
||||
) -> Result<(), UserError> {
|
||||
let deleted_result = follow::Entity::delete_many()
|
||||
.filter(follow::Column::FollowerId.eq(follower_id))
|
||||
@@ -58,7 +60,7 @@ pub async fn unfollow_user(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_followed_ids(db: &DbConn, user_id: i32) -> Result<Vec<i32>, DbErr> {
|
||||
pub async fn get_followed_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)
|
||||
@@ -67,7 +69,7 @@ pub async fn get_followed_ids(db: &DbConn, user_id: i32) -> Result<Vec<i32>, DbE
|
||||
Ok(followed_users.into_iter().map(|f| f.followed_id).collect())
|
||||
}
|
||||
|
||||
pub async fn get_follower_ids(db: &DbConn, user_id: i32) -> Result<Vec<i32>, DbErr> {
|
||||
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))
|
||||
.all(db)
|
||||
|
@@ -1,4 +1,5 @@
|
||||
pub mod auth;
|
||||
pub mod follow;
|
||||
pub mod tag;
|
||||
pub mod thought;
|
||||
pub mod user;
|
||||
|
86
thoughts-backend/app/src/persistence/tag.rs
Normal file
86
thoughts-backend/app/src/persistence/tag.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use models::domains::{tag, thought_tag};
|
||||
use sea_orm::{
|
||||
sqlx::types::uuid, ColumnTrait, ConnectionTrait, DbErr, EntityTrait, QueryFilter, Set,
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub fn parse_hashtags(content: &str) -> Vec<String> {
|
||||
content
|
||||
.split_whitespace()
|
||||
.filter_map(|word| {
|
||||
if word.starts_with('#') && word.len() > 1 {
|
||||
Some(word[1..].to_lowercase().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn find_or_create_tags<C>(db: &C, names: Vec<String>) -> Result<Vec<tag::Model>, DbErr>
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
if names.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let existing_tags = tag::Entity::find()
|
||||
.filter(tag::Column::Name.is_in(names.clone()))
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
let existing_names: HashSet<String> = existing_tags.iter().map(|t| t.name.clone()).collect();
|
||||
let new_names: Vec<String> = names
|
||||
.into_iter()
|
||||
.filter(|n| !existing_names.contains(n))
|
||||
.collect();
|
||||
|
||||
if !new_names.is_empty() {
|
||||
let new_tags: Vec<tag::ActiveModel> = new_names
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|name| tag::ActiveModel {
|
||||
name: Set(name),
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
tag::Entity::insert_many(new_tags).exec(db).await?;
|
||||
}
|
||||
|
||||
tag::Entity::find()
|
||||
.filter(
|
||||
tag::Column::Name.is_in(
|
||||
existing_names
|
||||
.union(&new_names.into_iter().collect())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
)
|
||||
.all(db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn link_tags_to_thought<C>(
|
||||
db: &C,
|
||||
thought_id: uuid::Uuid,
|
||||
tags: Vec<tag::Model>,
|
||||
) -> Result<(), DbErr>
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
if tags.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let links: Vec<thought_tag::ActiveModel> = tags
|
||||
.into_iter()
|
||||
.map(|tag| thought_tag::ActiveModel {
|
||||
thought_id: Set(thought_id),
|
||||
tag_id: Set(tag.id),
|
||||
})
|
||||
.collect();
|
||||
|
||||
thought_tag::Entity::insert_many(links).exec(db).await?;
|
||||
Ok(())
|
||||
}
|
@@ -1,42 +1,56 @@
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DbConn, DbErr, EntityTrait, JoinType, QueryFilter, QueryOrder,
|
||||
QuerySelect, RelationTrait, Set,
|
||||
prelude::Uuid, ActiveModelTrait, ColumnTrait, DbConn, DbErr, EntityTrait, JoinType,
|
||||
QueryFilter, QueryOrder, QuerySelect, RelationTrait, Set, TransactionTrait,
|
||||
};
|
||||
|
||||
use models::{
|
||||
domains::{thought, user},
|
||||
domains::{tag, thought, thought_tag, user},
|
||||
params::thought::CreateThoughtParams,
|
||||
schemas::thought::ThoughtWithAuthor,
|
||||
};
|
||||
|
||||
use crate::error::UserError;
|
||||
use crate::{
|
||||
error::UserError,
|
||||
persistence::tag::{find_or_create_tags, link_tags_to_thought, parse_hashtags},
|
||||
};
|
||||
|
||||
pub async fn create_thought(
|
||||
db: &DbConn,
|
||||
author_id: i32,
|
||||
author_id: Uuid,
|
||||
params: CreateThoughtParams,
|
||||
) -> Result<thought::Model, DbErr> {
|
||||
thought::ActiveModel {
|
||||
let txn = db.begin().await?;
|
||||
|
||||
let new_thought = thought::ActiveModel {
|
||||
author_id: Set(author_id),
|
||||
content: Set(params.content),
|
||||
content: Set(params.content.clone()),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(db)
|
||||
.await
|
||||
.insert(&txn)
|
||||
.await?;
|
||||
|
||||
let tag_names = parse_hashtags(¶ms.content);
|
||||
if !tag_names.is_empty() {
|
||||
let tags = find_or_create_tags(&txn, tag_names).await?;
|
||||
link_tags_to_thought(&txn, new_thought.id, tags).await?;
|
||||
}
|
||||
|
||||
txn.commit().await?;
|
||||
Ok(new_thought)
|
||||
}
|
||||
|
||||
pub async fn get_thought(db: &DbConn, thought_id: i32) -> Result<Option<thought::Model>, DbErr> {
|
||||
pub async fn get_thought(db: &DbConn, thought_id: Uuid) -> 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> {
|
||||
pub async fn delete_thought(db: &DbConn, thought_id: Uuid) -> 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,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<ThoughtWithAuthor>, DbErr> {
|
||||
thought::Entity::find()
|
||||
.select_only()
|
||||
@@ -55,7 +69,7 @@ pub async fn get_thoughts_by_user(
|
||||
|
||||
pub async fn get_feed_for_user(
|
||||
db: &DbConn,
|
||||
followed_ids: Vec<i32>,
|
||||
followed_ids: Vec<Uuid>,
|
||||
) -> Result<Vec<ThoughtWithAuthor>, UserError> {
|
||||
if followed_ids.is_empty() {
|
||||
return Ok(vec![]);
|
||||
@@ -76,3 +90,24 @@ pub async fn get_feed_for_user(
|
||||
.await
|
||||
.map_err(|e| UserError::Internal(e.to_string()))
|
||||
}
|
||||
|
||||
pub async fn get_thoughts_by_tag_name(
|
||||
db: &DbConn,
|
||||
tag_name: &str,
|
||||
) -> Result<Vec<ThoughtWithAuthor>, DbErr> {
|
||||
thought::Entity::find()
|
||||
.select_only()
|
||||
.column(thought::Column::Id)
|
||||
.column(thought::Column::Content)
|
||||
.column(thought::Column::CreatedAt)
|
||||
.column(thought::Column::AuthorId)
|
||||
.column_as(user::Column::Username, "author_username")
|
||||
.join(JoinType::InnerJoin, thought::Relation::User.def())
|
||||
.join(JoinType::InnerJoin, thought::Relation::ThoughtTag.def())
|
||||
.join(JoinType::InnerJoin, thought_tag::Relation::Tag.def())
|
||||
.filter(tag::Column::Name.eq(tag_name.to_lowercase()))
|
||||
.order_by_desc(thought::Column::CreatedAt)
|
||||
.into_model::<ThoughtWithAuthor>()
|
||||
.all(db)
|
||||
.await
|
||||
}
|
||||
|
@@ -1,8 +1,9 @@
|
||||
use sea_orm::prelude::Uuid;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DbConn, DbErr, EntityTrait, QueryFilter, Set, TransactionTrait,
|
||||
};
|
||||
|
||||
use models::domains::user;
|
||||
use models::domains::{top_friends, user};
|
||||
use models::params::user::{CreateUserParams, UpdateUserParams};
|
||||
use models::queries::user::UserQuery;
|
||||
|
||||
@@ -27,7 +28,7 @@ pub async fn search_users(db: &DbConn, query: UserQuery) -> Result<Vec<user::Mod
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_user(db: &DbConn, id: i32) -> Result<Option<user::Model>, DbErr> {
|
||||
pub async fn get_user(db: &DbConn, id: Uuid) -> Result<Option<user::Model>, DbErr> {
|
||||
user::Entity::find_by_id(id).one(db).await
|
||||
}
|
||||
|
||||
@@ -41,7 +42,7 @@ pub async fn get_user_by_username(
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_users_by_ids(db: &DbConn, ids: Vec<i32>) -> Result<Vec<user::Model>, DbErr> {
|
||||
pub async fn get_users_by_ids(db: &DbConn, ids: Vec<Uuid>) -> Result<Vec<user::Model>, DbErr> {
|
||||
user::Entity::find()
|
||||
.filter(user::Column::Id.is_in(ids))
|
||||
.all(db)
|
||||
@@ -50,7 +51,7 @@ pub async fn get_users_by_ids(db: &DbConn, ids: Vec<i32>) -> Result<Vec<user::Mo
|
||||
|
||||
pub async fn update_user_profile(
|
||||
db: &DbConn,
|
||||
user_id: i32,
|
||||
user_id: Uuid,
|
||||
params: UpdateUserParams,
|
||||
) -> Result<user::Model, UserError> {
|
||||
let mut user: user::ActiveModel = get_user(db, user_id)
|
||||
@@ -75,26 +76,47 @@ pub async fn update_user_profile(
|
||||
user.custom_css = Set(Some(custom_css));
|
||||
}
|
||||
|
||||
// This is a complex operation, so we use a transaction
|
||||
if let Some(friend_usernames) = params.top_friends {
|
||||
let txn = db
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| UserError::Internal(e.to_string()))?;
|
||||
|
||||
// 1. Delete old top friends
|
||||
// In a real app, you would create a `top_friends` entity and use it here.
|
||||
// For now, we'll skip this to avoid creating the model.
|
||||
top_friends::Entity::delete_many()
|
||||
.filter(top_friends::Column::UserId.eq(user_id))
|
||||
.exec(&txn)
|
||||
.await
|
||||
.map_err(|e| UserError::Internal(e.to_string()))?;
|
||||
|
||||
// 2. Find new friends by username
|
||||
let _friends = user::Entity::find()
|
||||
.filter(user::Column::Username.is_in(friend_usernames))
|
||||
let friends = user::Entity::find()
|
||||
.filter(user::Column::Username.is_in(friend_usernames.clone()))
|
||||
.all(&txn)
|
||||
.await
|
||||
.map_err(|e| UserError::Internal(e.to_string()))?;
|
||||
|
||||
// 3. Insert new friends
|
||||
// This part would involve inserting into the `top_friends` table.
|
||||
if friends.len() != friend_usernames.len() {
|
||||
return Err(UserError::Validation(
|
||||
"One or more usernames in top_friends do not exist".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let new_top_friends: Vec<top_friends::ActiveModel> = friends
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, friend)| top_friends::ActiveModel {
|
||||
user_id: Set(user_id),
|
||||
friend_id: Set(friend.id),
|
||||
position: Set((index + 1) as i16),
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !new_top_friends.is_empty() {
|
||||
top_friends::Entity::insert_many(new_top_friends)
|
||||
.exec(&txn)
|
||||
.await
|
||||
.map_err(|e| UserError::Internal(e.to_string()))?;
|
||||
}
|
||||
|
||||
txn.commit()
|
||||
.await
|
||||
|
Reference in New Issue
Block a user