feat: implement category service with CRUD operations and validation
This commit is contained in:
165
src-tauri/src/services/transaction/category/dto.rs
Normal file
165
src-tauri/src/services/transaction/category/dto.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use sea_orm::ActiveValue::Set;
|
||||
|
||||
use crate::{
|
||||
db::entities::category::{ActiveModel as CategoryActiveModel, Model as CategoryModel},
|
||||
services::transaction::category::{repo::CategoryRepoService, CategoryServiceError},
|
||||
};
|
||||
|
||||
pub struct Category {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub color_code: String,
|
||||
pub parent_id: Option<i64>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<CategoryModel> for Category {
|
||||
fn from(model: CategoryModel) -> Self {
|
||||
Self {
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
color_code: model.color_code,
|
||||
parent_id: model.parent_id,
|
||||
created_at: model.created_at.to_string(),
|
||||
updated_at: model.updated_at.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CategoryWithParent {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub color_code: String,
|
||||
pub parent_id: Option<i64>,
|
||||
pub parent: Option<Box<Category>>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<(CategoryModel, Option<CategoryModel>)> for CategoryWithParent {
|
||||
fn from((model, parent): (CategoryModel, Option<CategoryModel>)) -> Self {
|
||||
Self {
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
color_code: model.color_code,
|
||||
parent_id: model.parent_id,
|
||||
parent: parent.map(|parent_model| Box::new(parent_model.into())),
|
||||
created_at: model.created_at.to_string(),
|
||||
updated_at: model.updated_at.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateCategoryRequest {
|
||||
pub name: String,
|
||||
pub color_code: String,
|
||||
pub parent_id: Option<i64>,
|
||||
}
|
||||
|
||||
impl CreateCategoryRequest {
|
||||
pub async fn validate<T>(
|
||||
&self,
|
||||
category_repo: Arc<T>,
|
||||
) -> std::result::Result<(), CategoryServiceError>
|
||||
where
|
||||
T: CategoryRepoService + ?Sized,
|
||||
{
|
||||
if self.name.trim().is_empty() {
|
||||
return Err(CategoryServiceError::Validation(
|
||||
"Name cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if self.color_code.trim().is_empty() {
|
||||
return Err(CategoryServiceError::Validation(
|
||||
"Color code cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(parent_id) = self.parent_id {
|
||||
let res = category_repo.get_category(parent_id).await?;
|
||||
if res.is_none() {
|
||||
return Err(CategoryServiceError::Validation(format!(
|
||||
"Parent category with id {} does not exist",
|
||||
parent_id
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CreateCategoryRequest> for CategoryActiveModel {
|
||||
fn from(request: CreateCategoryRequest) -> Self {
|
||||
Self {
|
||||
name: Set(request.name),
|
||||
color_code: Set(request.color_code),
|
||||
parent_id: Set(request.parent_id),
|
||||
created_at: Set(chrono::Utc::now().to_rfc3339()),
|
||||
updated_at: Set(chrono::Utc::now().to_rfc3339()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UpdateCategoryRequest {
|
||||
pub name: Option<String>,
|
||||
pub color_code: Option<String>,
|
||||
pub parent_id: Option<Option<i64>>,
|
||||
}
|
||||
|
||||
impl UpdateCategoryRequest {
|
||||
pub async fn validate<T>(
|
||||
&self,
|
||||
category_repo: Arc<T>,
|
||||
) -> std::result::Result<(), CategoryServiceError>
|
||||
where
|
||||
T: CategoryRepoService + ?Sized,
|
||||
{
|
||||
if let Some(name) = &self.name {
|
||||
if name.trim().is_empty() {
|
||||
return Err(CategoryServiceError::Validation(
|
||||
"Name cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(color_code) = &self.color_code {
|
||||
if color_code.trim().is_empty() {
|
||||
return Err(CategoryServiceError::Validation(
|
||||
"Color code cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Some(parent_id)) = self.parent_id {
|
||||
let res = category_repo.get_category(parent_id).await?;
|
||||
if res.is_none() {
|
||||
return Err(CategoryServiceError::Validation(format!(
|
||||
"Parent category with id {} does not exist",
|
||||
parent_id
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_to(self, category: &mut CategoryActiveModel) {
|
||||
if let Some(name) = self.name {
|
||||
category.name = Set(name);
|
||||
}
|
||||
if let Some(color_code) = self.color_code {
|
||||
category.color_code = Set(color_code);
|
||||
}
|
||||
if let Some(Some(parent_id)) = self.parent_id {
|
||||
category.parent_id = Set(Some(parent_id));
|
||||
} else if let Some(None) = self.parent_id {
|
||||
category.parent_id = Set(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
6
src-tauri/src/services/transaction/category/mod.rs
Normal file
6
src-tauri/src/services/transaction/category/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
mod dto;
|
||||
pub(super) mod repo;
|
||||
mod service;
|
||||
|
||||
pub use dto::*;
|
||||
pub use service::*;
|
||||
124
src-tauri/src/services/transaction/category/repo.rs
Normal file
124
src-tauri/src/services/transaction/category/repo.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
db::{
|
||||
entities::category::{ActiveModel as CategoryActiveModel, Entity as CategoryEntity},
|
||||
Db,
|
||||
},
|
||||
services::transaction::category::{
|
||||
Category, CategoryServiceError, CategoryServiceResult, CategoryWithParent,
|
||||
CreateCategoryRequest, UpdateCategoryRequest,
|
||||
},
|
||||
};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, DatabaseConnection, EntityTrait, Linked, ModelTrait, RelationTrait,
|
||||
};
|
||||
|
||||
pub type Result<T> = CategoryServiceResult<T>;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait CategoryRepoService: Send + Sync + 'static {
|
||||
async fn get_categories(&self) -> Result<Vec<Category>>;
|
||||
async fn get_category(&self, id: i64) -> Result<Option<CategoryWithParent>>;
|
||||
async fn create_category(&self, request: CreateCategoryRequest) -> Result<i64>;
|
||||
async fn create_category_with_tx(&self, request: CreateCategoryRequest, tx: &Db)
|
||||
-> Result<i64>;
|
||||
async fn update_category(&self, id: i64, request: UpdateCategoryRequest) -> Result<()>;
|
||||
async fn update_category_with_tx(
|
||||
&self,
|
||||
id: i64,
|
||||
request: UpdateCategoryRequest,
|
||||
tx: &Db,
|
||||
) -> Result<()>;
|
||||
async fn delete_category(&self, id: i64) -> Result<()>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CategoryRepoServiceImpl {
|
||||
pub db: DatabaseConnection,
|
||||
}
|
||||
|
||||
struct CategorySelfRefLink;
|
||||
|
||||
impl Linked for CategorySelfRefLink {
|
||||
type FromEntity = CategoryEntity;
|
||||
type ToEntity = CategoryEntity;
|
||||
|
||||
fn link(&self) -> Vec<sea_orm::RelationDef> {
|
||||
vec![
|
||||
// Use the BelongsTo or HasMany relation you defined in your enum
|
||||
crate::db::entities::category::Relation::SelfRef.def(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl CategoryRepoService for CategoryRepoServiceImpl {
|
||||
async fn get_categories(&self) -> Result<Vec<Category>> {
|
||||
let categories = CategoryEntity::find().all(&self.db).await?;
|
||||
Ok(categories.into_iter().map(|model| model.into()).collect())
|
||||
}
|
||||
|
||||
async fn get_category(&self, id: i64) -> Result<Option<CategoryWithParent>> {
|
||||
let category = CategoryEntity::find_by_id(id)
|
||||
.find_also_linked(CategorySelfRefLink)
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
|
||||
Ok(category.map(|res| res.into()))
|
||||
}
|
||||
|
||||
async fn create_category(&self, request: CreateCategoryRequest) -> Result<i64> {
|
||||
self.create_category_with_tx(request, &(&self.db).into())
|
||||
.await
|
||||
}
|
||||
|
||||
async fn create_category_with_tx(
|
||||
&self,
|
||||
request: CreateCategoryRequest,
|
||||
tx: &Db,
|
||||
) -> Result<i64> {
|
||||
request.validate(Arc::new(self.clone())).await?;
|
||||
|
||||
let new_category: CategoryActiveModel = request.into();
|
||||
let res = new_category.insert(tx).await?;
|
||||
Ok(res.id)
|
||||
}
|
||||
|
||||
async fn update_category(&self, id: i64, request: UpdateCategoryRequest) -> Result<()> {
|
||||
self.update_category_with_tx(id, request, &(&self.db).into())
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update_category_with_tx(
|
||||
&self,
|
||||
id: i64,
|
||||
request: UpdateCategoryRequest,
|
||||
tx: &Db,
|
||||
) -> Result<()> {
|
||||
let category = CategoryEntity::find_by_id(id)
|
||||
.one(tx)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CategoryServiceError::TargetNotFound(format!("Category with id {} not found", id))
|
||||
})?;
|
||||
let mut category: CategoryActiveModel = category.into();
|
||||
request.validate(Arc::new(self.clone())).await?;
|
||||
|
||||
request.apply_to(&mut category);
|
||||
|
||||
category.update(tx).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_category(&self, id: i64) -> Result<()> {
|
||||
let category = CategoryEntity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CategoryServiceError::TargetNotFound(format!("Category with id {} not found", id))
|
||||
})?;
|
||||
category.delete(&self.db).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
115
src-tauri/src/services/transaction/category/service.rs
Normal file
115
src-tauri/src/services/transaction/category/service.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use sea_orm::DatabaseConnection;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
db::{Db, DbServiceError},
|
||||
services::transaction::category::{
|
||||
repo::{CategoryRepoService, CategoryRepoServiceImpl},
|
||||
Category, CategoryWithParent, CreateCategoryRequest, UpdateCategoryRequest,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CategoryServiceError {
|
||||
#[error("database error: {0}")]
|
||||
DbErr(#[from] DbServiceError),
|
||||
#[error("Target not found: {0}")]
|
||||
TargetNotFound(String),
|
||||
#[error("Validation error: {0}")]
|
||||
Validation(String),
|
||||
#[error("unknown error: {0}")]
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<sea_orm::DbErr> for CategoryServiceError {
|
||||
fn from(err: sea_orm::DbErr) -> Self {
|
||||
CategoryServiceError::DbErr(DbServiceError::from(err))
|
||||
}
|
||||
}
|
||||
|
||||
pub type CategoryServiceResult<T> = std::result::Result<T, CategoryServiceError>;
|
||||
|
||||
type Result<T> = CategoryServiceResult<T>;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait CategoryService: Send + Sync + 'static {
|
||||
async fn get_categories(&self) -> Result<Vec<Category>>;
|
||||
async fn get_category(&self, id: i64) -> Result<Option<CategoryWithParent>>;
|
||||
async fn create_category(&self, request: CreateCategoryRequest) -> Result<i64>;
|
||||
async fn create_category_with_tx(&self, request: CreateCategoryRequest, tx: &Db)
|
||||
-> Result<i64>;
|
||||
async fn update_category(&self, id: i64, request: UpdateCategoryRequest) -> Result<()>;
|
||||
async fn update_category_with_tx(
|
||||
&self,
|
||||
id: i64,
|
||||
request: UpdateCategoryRequest,
|
||||
tx: &Db,
|
||||
) -> Result<()>;
|
||||
async fn delete_category(&self, id: i64) -> Result<()>;
|
||||
}
|
||||
|
||||
pub struct CategoryServiceImpl<T>
|
||||
where
|
||||
T: CategoryRepoService + ?Sized,
|
||||
{
|
||||
db: DatabaseConnection,
|
||||
category_repo: Arc<T>,
|
||||
}
|
||||
|
||||
impl CategoryServiceImpl<CategoryRepoServiceImpl> {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self {
|
||||
db: db.clone(),
|
||||
category_repo: Arc::new(CategoryRepoServiceImpl { db }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<T> CategoryService for CategoryServiceImpl<T>
|
||||
where
|
||||
T: CategoryRepoService + ?Sized,
|
||||
{
|
||||
async fn get_categories(&self) -> Result<Vec<Category>> {
|
||||
self.category_repo.get_categories().await
|
||||
}
|
||||
|
||||
async fn get_category(&self, id: i64) -> Result<Option<CategoryWithParent>> {
|
||||
self.category_repo.get_category(id).await
|
||||
}
|
||||
|
||||
async fn create_category(&self, request: CreateCategoryRequest) -> Result<i64> {
|
||||
self.category_repo.create_category(request).await
|
||||
}
|
||||
|
||||
async fn create_category_with_tx(
|
||||
&self,
|
||||
request: CreateCategoryRequest,
|
||||
tx: &Db,
|
||||
) -> Result<i64> {
|
||||
self.category_repo
|
||||
.create_category_with_tx(request, tx)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update_category(&self, id: i64, request: UpdateCategoryRequest) -> Result<()> {
|
||||
self.category_repo.update_category(id, request).await
|
||||
}
|
||||
|
||||
async fn update_category_with_tx(
|
||||
&self,
|
||||
id: i64,
|
||||
request: UpdateCategoryRequest,
|
||||
tx: &Db,
|
||||
) -> Result<()> {
|
||||
self.category_repo
|
||||
.update_category_with_tx(id, request, tx)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn delete_category(&self, id: i64) -> Result<()> {
|
||||
self.category_repo.delete_category(id).await
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user