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