feat: implement tag service with CRUD operations and validation
This commit is contained in:
84
src-tauri/src/services/transaction/tag/dto.rs
Normal file
84
src-tauri/src/services/transaction/tag/dto.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use sea_orm::ActiveValue::Set;
|
||||
|
||||
use crate::db::entities::tag::Model as TagModel;
|
||||
|
||||
pub struct Tag {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub color_code: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<TagModel> for Tag {
|
||||
fn from(model: TagModel) -> Self {
|
||||
Self {
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
color_code: model.color_code,
|
||||
created_at: model.created_at.to_string(),
|
||||
updated_at: model.updated_at.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateTagRequest {
|
||||
pub name: String,
|
||||
pub color_code: String,
|
||||
}
|
||||
|
||||
impl CreateTagRequest {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.name.trim().is_empty() {
|
||||
return Err("Tag name cannot be empty".to_string());
|
||||
}
|
||||
if self.color_code.trim().is_empty() {
|
||||
return Err("Color code cannot be empty".to_string());
|
||||
}
|
||||
// Optionally, add more validation for color code format (e.g., hex code)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CreateTagRequest> for crate::db::entities::tag::ActiveModel {
|
||||
fn from(request: CreateTagRequest) -> Self {
|
||||
Self {
|
||||
name: Set(request.name),
|
||||
color_code: Set(request.color_code),
|
||||
created_at: Set(chrono::Utc::now().to_rfc3339()),
|
||||
updated_at: Set(chrono::Utc::now().to_rfc3339()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UpdateTagRequest {
|
||||
pub name: Option<String>,
|
||||
pub color_code: Option<String>,
|
||||
}
|
||||
|
||||
impl UpdateTagRequest {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if let Some(name) = &self.name {
|
||||
if name.trim().is_empty() {
|
||||
return Err("Tag name cannot be empty".to_string());
|
||||
}
|
||||
}
|
||||
if let Some(color_code) = &self.color_code {
|
||||
if color_code.trim().is_empty() {
|
||||
return Err("Color code cannot be empty".to_string());
|
||||
}
|
||||
// Optionally, add more validation for color code format (e.g., hex code)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_to(&self, tag: &mut crate::db::entities::tag::ActiveModel) {
|
||||
if let Some(name) = &self.name {
|
||||
tag.name = Set(name.clone());
|
||||
}
|
||||
if let Some(color_code) = &self.color_code {
|
||||
tag.color_code = Set(color_code.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
6
src-tauri/src/services/transaction/tag/mod.rs
Normal file
6
src-tauri/src/services/transaction/tag/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
mod dto;
|
||||
pub(super) mod repo;
|
||||
mod service;
|
||||
|
||||
pub use dto::*;
|
||||
pub use service::*;
|
||||
150
src-tauri/src/services/transaction/tag/repo.rs
Normal file
150
src-tauri/src/services/transaction/tag/repo.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
use crate::{
|
||||
db::{
|
||||
entities::{
|
||||
tag::{ActiveModel as TagActiveModel, Entity as TagEntity},
|
||||
transaction_tag::{
|
||||
ActiveModel as TransactionTagActiveModel, Column::TransactionId,
|
||||
Entity as TransactionTagEntity,
|
||||
},
|
||||
},
|
||||
Db,
|
||||
},
|
||||
services::transaction::tag::{
|
||||
CreateTagRequest, Tag, TagServiceError, TagServiceResult, UpdateTagRequest,
|
||||
},
|
||||
};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue::Set, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait,
|
||||
QueryFilter, TransactionTrait,
|
||||
};
|
||||
|
||||
type Result<T> = TagServiceResult<T>;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait TagRepoService: Send + Sync + 'static {
|
||||
async fn get_tags(&self) -> Result<Vec<Tag>>;
|
||||
async fn get_tag(&self, id: i64) -> Result<Option<Tag>>;
|
||||
async fn create_tag(&self, request: CreateTagRequest) -> Result<i64>;
|
||||
async fn create_tag_with_tx(&self, request: CreateTagRequest, tx: &Db) -> Result<i64>;
|
||||
async fn update_tag(&self, id: i64, request: UpdateTagRequest) -> Result<()>;
|
||||
async fn update_tag_with_tx(&self, id: i64, request: UpdateTagRequest, tx: &Db) -> Result<()>;
|
||||
async fn delete_tag(&self, id: i64) -> Result<()>;
|
||||
//
|
||||
async fn update_tags_for_transaction(
|
||||
&self,
|
||||
transaction_id: i64,
|
||||
tag_ids: Vec<i64>,
|
||||
) -> Result<()>;
|
||||
async fn update_tags_for_transaction_with_tx(
|
||||
&self,
|
||||
transaction_id: i64,
|
||||
new_tag_ids: Vec<i64>,
|
||||
tx: &Db,
|
||||
) -> Result<()>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TagRepoServiceImpl {
|
||||
pub db: DatabaseConnection,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl TagRepoService for TagRepoServiceImpl {
|
||||
async fn get_tags(&self) -> Result<Vec<Tag>> {
|
||||
let tags = TagEntity::find().all(&self.db).await?;
|
||||
Ok(tags.into_iter().map(|model| model.into()).collect())
|
||||
}
|
||||
|
||||
async fn get_tag(&self, id: i64) -> Result<Option<Tag>> {
|
||||
let tag = TagEntity::find_by_id(id).one(&self.db).await?;
|
||||
Ok(tag.map(|model| model.into()))
|
||||
}
|
||||
|
||||
async fn create_tag(&self, request: CreateTagRequest) -> Result<i64> {
|
||||
self.create_tag_with_tx(request, &(&self.db).into()).await
|
||||
}
|
||||
|
||||
async fn create_tag_with_tx(&self, request: CreateTagRequest, tx: &Db) -> Result<i64> {
|
||||
request.validate().map_err(TagServiceError::Validation)?;
|
||||
let new_tag: TagActiveModel = request.into();
|
||||
let res = new_tag.insert(tx).await?;
|
||||
Ok(res.id)
|
||||
}
|
||||
|
||||
async fn update_tag(&self, id: i64, request: UpdateTagRequest) -> Result<()> {
|
||||
self.update_tag_with_tx(id, request, &(&self.db).into())
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update_tag_with_tx(&self, id: i64, request: UpdateTagRequest, tx: &Db) -> Result<()> {
|
||||
let tag = TagEntity::find_by_id(id).one(tx).await?.ok_or_else(|| {
|
||||
TagServiceError::TargetNotFound(format!("Tag with id {} not found", id))
|
||||
})?;
|
||||
let mut tag: TagActiveModel = tag.into();
|
||||
|
||||
request.validate().map_err(TagServiceError::Validation)?;
|
||||
request.apply_to(&mut tag);
|
||||
tag.updated_at = Set(chrono::Utc::now().to_rfc3339());
|
||||
tag.update(tx).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_tag(&self, id: i64) -> Result<()> {
|
||||
let tag = TagEntity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
TagServiceError::TargetNotFound(format!("Tag with id {} not found", id))
|
||||
})?;
|
||||
tag.delete(&self.db).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_tags_for_transaction(
|
||||
&self,
|
||||
transaction_id: i64,
|
||||
tag_ids: Vec<i64>,
|
||||
) -> Result<()> {
|
||||
self.update_tags_for_transaction_with_tx(transaction_id, tag_ids, &(&self.db).into())
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update_tags_for_transaction_with_tx(
|
||||
&self,
|
||||
transaction_id: i64,
|
||||
new_tag_ids: Vec<i64>,
|
||||
outer_tx: &Db,
|
||||
) -> Result<()> {
|
||||
let inner_tx = match outer_tx {
|
||||
Db::Connection(conn) => Some(conn.begin().await?),
|
||||
Db::Transaction(_) => None,
|
||||
};
|
||||
let tx = inner_tx.as_ref().map_or_else(
|
||||
|| match outer_tx {
|
||||
Db::Connection(conn) => Db::Connection(conn),
|
||||
Db::Transaction(tx) => Db::Transaction(tx),
|
||||
},
|
||||
Db::Transaction,
|
||||
);
|
||||
// delete existing tags for the transaction
|
||||
TransactionTagEntity::delete_many()
|
||||
.filter(TransactionId.eq(transaction_id))
|
||||
.exec(&tx)
|
||||
.await?;
|
||||
|
||||
// insert new tags for the transaction
|
||||
let models = new_tag_ids
|
||||
.iter()
|
||||
.map(|tag_id| TransactionTagActiveModel {
|
||||
transaction_id: Set(transaction_id),
|
||||
tag_id: Set(*tag_id),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
TransactionTagEntity::insert_many(models).exec(&tx).await?;
|
||||
|
||||
if let Some(inner_tx) = inner_tx {
|
||||
inner_tx.commit().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
128
src-tauri/src/services/transaction/tag/service.rs
Normal file
128
src-tauri/src/services/transaction/tag/service.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use sea_orm::DatabaseConnection;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
db::{Db, DbServiceError},
|
||||
services::transaction::tag::{
|
||||
repo::{TagRepoService, TagRepoServiceImpl},
|
||||
CreateTagRequest, Tag, UpdateTagRequest,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TagServiceError {
|
||||
#[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 TagServiceError {
|
||||
fn from(err: sea_orm::DbErr) -> Self {
|
||||
TagServiceError::DbErr(DbServiceError::from(err))
|
||||
}
|
||||
}
|
||||
|
||||
pub type TagServiceResult<T> = std::result::Result<T, TagServiceError>;
|
||||
type Result<T> = TagServiceResult<T>;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait TagService: Send + Sync + 'static {
|
||||
async fn get_tags(&self) -> Result<Vec<Tag>>;
|
||||
async fn get_tag(&self, id: i64) -> Result<Option<Tag>>;
|
||||
async fn create_tag(&self, request: CreateTagRequest) -> Result<i64>;
|
||||
async fn create_tag_with_tx(&self, request: CreateTagRequest, tx: &Db) -> Result<i64>;
|
||||
async fn update_tag(&self, id: i64, request: UpdateTagRequest) -> Result<()>;
|
||||
async fn update_tag_with_tx(&self, id: i64, request: UpdateTagRequest, tx: &Db) -> Result<()>;
|
||||
async fn delete_tag(&self, id: i64) -> Result<()>;
|
||||
//
|
||||
async fn update_tags_for_transaction(
|
||||
&self,
|
||||
transaction_id: i64,
|
||||
tag_ids: Vec<i64>,
|
||||
) -> Result<()>;
|
||||
async fn update_tags_for_transaction_with_tx(
|
||||
&self,
|
||||
transaction_id: i64,
|
||||
new_tag_ids: Vec<i64>,
|
||||
tx: &Db,
|
||||
) -> Result<()>;
|
||||
}
|
||||
|
||||
pub struct TagServiceImpl<T>
|
||||
where
|
||||
T: TagRepoService + ?Sized,
|
||||
{
|
||||
db: DatabaseConnection,
|
||||
tag_repo: Arc<T>,
|
||||
}
|
||||
|
||||
impl TagServiceImpl<TagRepoServiceImpl> {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self {
|
||||
db: db.clone(),
|
||||
tag_repo: Arc::new(TagRepoServiceImpl { db }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<T> TagService for TagServiceImpl<T>
|
||||
where
|
||||
T: TagRepoService + ?Sized,
|
||||
{
|
||||
async fn get_tags(&self) -> Result<Vec<Tag>> {
|
||||
self.tag_repo.get_tags().await
|
||||
}
|
||||
|
||||
async fn get_tag(&self, id: i64) -> Result<Option<Tag>> {
|
||||
self.tag_repo.get_tag(id).await
|
||||
}
|
||||
|
||||
async fn create_tag(&self, request: CreateTagRequest) -> Result<i64> {
|
||||
self.tag_repo.create_tag(request).await
|
||||
}
|
||||
|
||||
async fn create_tag_with_tx(&self, request: CreateTagRequest, tx: &Db) -> Result<i64> {
|
||||
self.tag_repo.create_tag_with_tx(request, tx).await
|
||||
}
|
||||
|
||||
async fn update_tag(&self, id: i64, request: UpdateTagRequest) -> Result<()> {
|
||||
self.tag_repo.update_tag(id, request).await
|
||||
}
|
||||
|
||||
async fn update_tag_with_tx(&self, id: i64, request: UpdateTagRequest, tx: &Db) -> Result<()> {
|
||||
self.tag_repo.update_tag_with_tx(id, request, tx).await
|
||||
}
|
||||
|
||||
async fn delete_tag(&self, id: i64) -> Result<()> {
|
||||
self.tag_repo.delete_tag(id).await
|
||||
}
|
||||
|
||||
async fn update_tags_for_transaction(
|
||||
&self,
|
||||
transaction_id: i64,
|
||||
tag_ids: Vec<i64>,
|
||||
) -> Result<()> {
|
||||
self.tag_repo
|
||||
.update_tags_for_transaction(transaction_id, tag_ids)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update_tags_for_transaction_with_tx(
|
||||
&self,
|
||||
transaction_id: i64,
|
||||
new_tag_ids: Vec<i64>,
|
||||
tx: &Db,
|
||||
) -> Result<()> {
|
||||
self.tag_repo
|
||||
.update_tags_for_transaction_with_tx(transaction_id, new_tag_ids, tx)
|
||||
.await
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user