use chrono::Local; use orm::{OrmConnection, entities::category}; use sea_orm::{ ActiveValue::{NotSet, Set}, ConnectionTrait, EntityTrait, ExprTrait, IntoActiveModel, Order, QueryFilter, QueryOrder, TransactionSession, TransactionTrait, sea_query::Expr, }; use thiserror::Error; use types::tag::{Category, HexColor}; use uuid::Uuid; use crate::User; #[derive(Error, Debug, PartialEq)] pub enum CategoryServiceError { #[error("database error - caused by: {0}")] DbError(#[from] sea_orm::DbErr), #[error("invalid category - caused by: {0}")] InvalidCategory(String), #[error("category not found")] NotFound, } pub struct CategoryService; impl CategoryService { pub async fn get_all( user: &User, db: &OrmConnection, ) -> Result, CategoryServiceError> { let categories = category::Entity::find() .filter(category::COLUMN.owner_id.eq(user.id().to_string())) .filter(category::COLUMN.deleted.is_null()) .order_by(category::Column::Order, Order::Asc) .all(db) .await?; categories .into_iter() .map(|c| to_category_dto(&c.into_ex())) .collect() } pub async fn get_by_id( user: &User, db: &OrmConnection, category_id: Uuid, ) -> Result, CategoryServiceError> { let category = category::Entity::find_by_id(category_id) .filter(category::COLUMN.owner_id.eq(user.id().to_string())) .one(db) .await?; category.map(|c| to_category_dto(&c.into_ex())).transpose() } pub async fn delete_by_id( user: &User, db: &OrmConnection, category_id: Uuid, ) -> Result<(), CategoryServiceError> { category::Entity::update(category::ActiveModel { id: Set(category_id), deleted: Set(Some(Local::now().into())), ..Default::default() }) .validate()? .filter(category::COLUMN.owner_id.eq(user.id().to_string())) .exec(db) .await?; Ok(()) } pub async fn create( user: &User, db: &OrmConnection, category: Category, ) -> Result { let tx = db.begin().await?; let position_index = category::Entity::find() .order_by_asc(category::COLUMN.order) .one(&tx) .await? .map_or(0, |todo| todo.order - 1); let category = category::Entity::insert(category::ActiveModel { id: Set(category.uuid), owner_id: Set(user.id().to_string()), name: Set(category.name), color: Set(category.color.as_str().to_string()), icon: Set(category.icon), deleted: NotSet, order: Set(position_index), }) .exec_with_returning(&tx) .await?; tx.commit().await?; to_category_dto(&category.into_ex()) } pub async fn update( user: &User, db: &OrmConnection, category: Category, ) -> Result { let category = category::Entity::update(category::ActiveModel { id: Set(category.uuid), owner_id: Set(user.id().to_string()), name: Set(category.name), color: Set(category.color.as_str().to_string()), icon: Set(category.icon), deleted: NotSet, order: NotSet, }) .validate()? .filter(category::COLUMN.owner_id.eq(user.id().to_string())) .exec(db) .await?; to_category_dto(&category.into_ex()) } pub async fn move_category( user: &User, db: &OrmConnection, category: Uuid, target: Uuid, ) -> Result<(), CategoryServiceError> { let tx = db.begin().await?; let category = category::Entity::find_by_id(category) .filter(category::COLUMN.owner_id.eq(user.id().to_string())) .one(&tx) .await? .ok_or(CategoryServiceError::NotFound)?; let target = category::Entity::find_by_id(target) .filter(category::COLUMN.owner_id.eq(user.id().to_string())) .one(&tx) .await? .ok_or(CategoryServiceError::NotFound)?; if category.order < target.order { Self::move_to(user, &tx, category, target.order + 1).await?; } else { Self::move_to(user, &tx, category, target.order).await?; } tx.commit().await?; Ok(()) } async fn move_to( user: &User, db: &T, category: category::Model, target: i32, ) -> Result<(), sea_orm::DbErr> { let tx = db.begin().await?; let mut model = category.into_active_model(); model.order = Set(target); category::Entity::update_many() .col_expr( category::COLUMN.order, Expr::col(category::COLUMN.order).add(1), ) .filter(category::COLUMN.order.gte(target)) .filter(category::COLUMN.owner_id.eq(user.id().to_string())) .exec(db) .await?; category::Entity::update(model).exec(db).await?; tx.commit().await?; Ok(()) } } pub fn to_category_dto(value: &category::ModelEx) -> Result { let category = Category { uuid: value.id, name: value.name.clone(), color: HexColor::from_text(&value.color).ok_or(CategoryServiceError::InvalidCategory( "invalid color".to_string(), ))?, icon: value.icon.clone(), }; Ok(category) }