feat: implement tag service with CRUD operations and validation

This commit is contained in:
GW_MC
2026-05-28 04:01:37 +00:00
parent ea16cc2d55
commit 26c308fcb6
4 changed files with 368 additions and 0 deletions

View 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());
}
}
}

View File

@@ -0,0 +1,6 @@
mod dto;
pub(super) mod repo;
mod service;
pub use dto::*;
pub use service::*;

View 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(())
}
}

View 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
}
}