diff --git a/api/src/auth.rs b/api/src/auth.rs index 33b3d3c..226dd98 100644 --- a/api/src/auth.rs +++ b/api/src/auth.rs @@ -7,7 +7,7 @@ use uuid::Uuid; use crate::AppState; -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct User { pub uuid: Uuid, } @@ -25,7 +25,7 @@ impl FromRequestParts for User { .get("Authorization") .and_then(|h| h.to_str().ok()) .and_then(|h| h.strip_prefix("Bearer ")) - .ok_or_else(|| (StatusCode::UNAUTHORIZED, "Authorization header missing"))?; + .ok_or((StatusCode::UNAUTHORIZED, "Authorization header missing"))?; info!(auth_header); diff --git a/api/src/routes/categories.rs b/api/src/routes/categories.rs index 7c4882b..797a804 100644 --- a/api/src/routes/categories.rs +++ b/api/src/routes/categories.rs @@ -54,7 +54,7 @@ async fn add_category( CategoryModel { uuid: existing_category.id, name: existing_category.name, - color: HexColor::from_str(&existing_category.color).unwrap(), + color: HexColor::from_text(&existing_category.color).unwrap(), icon: existing_category.icon, } } @@ -70,7 +70,7 @@ async fn add_category( CategoryModel { uuid: new_category.id, name: new_category.name, - color: HexColor::from_str(&new_category.color).unwrap(), + color: HexColor::from_text(&new_category.color).unwrap(), icon: new_category.icon, } } @@ -101,7 +101,7 @@ async fn get_all_categories( .map(|category| CategoryModel { uuid: category.id, name: category.name, - color: HexColor::from_str(&category.color).unwrap(), + color: HexColor::from_text(&category.color).unwrap(), icon: category.icon, }) .collect(), @@ -132,7 +132,7 @@ async fn delete_category( let deleted_category = CategoryModel { uuid: category.id, name: category.name.to_string(), - color: HexColor::from_str(&category.color).unwrap(), + color: HexColor::from_text(&category.color).unwrap(), icon: category.icon.to_string(), }; diff --git a/api/src/routes/tags.rs b/api/src/routes/tags.rs index d2dd05d..b87481f 100644 --- a/api/src/routes/tags.rs +++ b/api/src/routes/tags.rs @@ -8,19 +8,16 @@ use axum::routing::{delete, post}; use axum::{Json, Router, extract::State, routing::get}; use futures::Stream; use http::StatusCode; -use sea_orm::ActiveValue::Set; -use sea_orm::{ColumnTrait, EntityTrait, ModelTrait, QueryFilter}; use tokio_stream::StreamExt; use tokio_stream::wrappers::BroadcastStream; use tracing::warn; -use types::{HexColor, Tag as TagModel}; +use types::Tag as TagModel; use uuid::Uuid; use crate::services::events::{self}; +use crate::services::tags::TagService; use crate::{AppState, auth::User}; -use crate::entities::{prelude::*, tag}; - pub fn routes(state: AppState) -> Router { Router::new() .route("/", get(get_all_tags)) @@ -35,44 +32,30 @@ async fn add_tag( user: User, Json(tag): Json, ) -> Result, (StatusCode, &'static str)> { - let existing_tag = Tag::find_by_id(tag.uuid) - .one(&state.db_connection) + let existing_tag = TagService::get_by_id(&user, &state.db_connection, tag.uuid) .await .map_err(|e| { warn!("failed to find tag: {}", e); (StatusCode::INTERNAL_SERVER_ERROR, "failed to find tag") })?; - let tag = tag::ActiveModel { - id: Set(tag.uuid), - owner_id: Set(user.uuid), - name: Set(tag.name), - color: Set(tag.color.as_str().to_string()), - }; - let new_tag = match existing_tag { Some(existing_tag) => { - Tag::update(tag) - .exec(&state.db_connection) - .await - .map_err(|e| { - warn!("failed to add tag: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "failed to add tag") - })?; - - existing_tag.into() - } - None => { - let new_tag = Tag::insert(tag) - .exec_with_returning(&state.db_connection) + TagService::update(&user, &state.db_connection, tag) .await .map_err(|e| { warn!("failed to add tag: {}", e); (StatusCode::INTERNAL_SERVER_ERROR, "failed to add tag") })?; - new_tag.into() + existing_tag } + None => TagService::create(&user, &state.db_connection, tag) + .await + .map_err(|e| { + warn!("failed to add tag: {}", e); + (StatusCode::INTERNAL_SERVER_ERROR, "failed to add tag") + })?, }; state.event_service.broadcast(events::Event::Tag(user.uuid)); @@ -83,9 +66,7 @@ async fn get_all_tags( state: State, user: User, ) -> Result>, (StatusCode, &'static str)> { - let tags = Tag::find() - .filter(tag::Column::OwnerId.eq(user.uuid)) - .all(&state.db_connection) + let tags = TagService::get_all(&user, &state.db_connection) .await .map_err(|e| { warn!("failed to fetch tag: {}", e); @@ -93,7 +74,7 @@ async fn get_all_tags( })?; state.event_service.broadcast(events::Event::Tag(user.uuid)); - Ok(Json(tags.into_iter().map(|tag| tag.into()).collect())) + Ok(Json(tags)) } async fn delete_tag( @@ -101,22 +82,18 @@ async fn delete_tag( Path(tag_uuid): Path, user: User, ) -> Result, (StatusCode, &'static str)> { - let deleted_tag = Tag::find_by_id(tag_uuid) - .filter(tag::Column::OwnerId.eq(user.uuid)) - .one(&state.db_connection) + let to_be_deleted_tag = TagService::get_by_id(&user, &state.db_connection, tag_uuid) .await .map_err(|e| { warn!("failed to fetch tag: {}", e); (StatusCode::INTERNAL_SERVER_ERROR, "failed to fetch tags") })?; - let Some(tag) = deleted_tag else { + let Some(deleted_tag) = to_be_deleted_tag else { return Err((StatusCode::NOT_FOUND, "tag not found")); }; - let deleted_tag = TagModel::from(&tag); - - tag.delete(&state.db_connection) + TagService::delete_by_id(&user, &state.db_connection, tag_uuid) .await .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "failed to delete tag"))?; @@ -140,22 +117,16 @@ async fn sse_handler( }) .then(move |_| { let db = state.db_connection.clone(); + let user = user.clone(); async move { - Tag::find() - .filter(tag::Column::OwnerId.eq(user.uuid)) - .all(&db) + TagService::get_all(&user, &db) .await .map_err(|e| { warn!("failed to fetch tag: {}", e); "failed to fetch tags" }) - .map(|tags| { - tags.into_iter() - .map(|tag| TagModel::from(tag)) - .collect::>() - }) - .map(|tags| Event::default().data(&serde_json::to_string(&tags).unwrap())) + .map(|tags| Event::default().data(serde_json::to_string(&tags).unwrap())) .unwrap() } }) @@ -167,23 +138,3 @@ async fn sse_handler( .text("keep-alive-text"), ) } - -impl From for TagModel { - fn from(value: tag::Model) -> Self { - TagModel { - uuid: value.id, - name: value.name, - color: HexColor::from_str(&value.color).unwrap(), - } - } -} - -impl From<&tag::Model> for TagModel { - fn from(value: &tag::Model) -> Self { - TagModel { - uuid: value.id, - name: value.name.to_string(), - color: HexColor::from_str(&value.color).unwrap(), - } - } -} diff --git a/api/src/services/mod.rs b/api/src/services/mod.rs index a9970c2..00b0443 100644 --- a/api/src/services/mod.rs +++ b/api/src/services/mod.rs @@ -1 +1,2 @@ pub mod events; +pub mod tags; diff --git a/api/src/services/tags.rs b/api/src/services/tags.rs new file mode 100644 index 0000000..9dc4ea6 --- /dev/null +++ b/api/src/services/tags.rs @@ -0,0 +1,101 @@ +use crate::{ + auth::User, + entities::{prelude::*, tag}, +}; + +use sea_orm::{ + ActiveValue::Set, ColumnTrait, DatabaseConnection, DeleteResult, EntityTrait, QueryFilter, +}; +use types::{HexColor, Tag as TagModel}; +use uuid::Uuid; + +pub struct TagService; + +impl TagService { + pub async fn get_all( + user: &User, + db: &DatabaseConnection, + ) -> Result, sea_orm::DbErr> { + Tag::find() + .filter(tag::Column::OwnerId.eq(user.uuid)) + .all(db) + .await + .map(|tags| tags.into_iter().map(|tag| tag.into()).collect()) + } + + pub async fn get_by_id( + user: &User, + db: &DatabaseConnection, + tag_id: Uuid, + ) -> Result, sea_orm::DbErr> { + Tag::find_by_id(tag_id) + .filter(tag::Column::OwnerId.eq(user.uuid)) + .one(db) + .await + .map(|tag| tag.map(|tag| tag.into())) + } + + pub async fn delete_by_id( + user: &User, + db: &DatabaseConnection, + tag_id: Uuid, + ) -> Result { + Tag::delete_by_id(tag_id) + .filter(tag::Column::OwnerId.eq(user.uuid)) + .exec(db) + .await + } + + pub async fn create( + user: &User, + db: &DatabaseConnection, + tag: TagModel, + ) -> Result { + Tag::insert(tag::ActiveModel { + id: Set(tag.uuid), + owner_id: Set(user.uuid), + name: Set(tag.name), + color: Set(tag.color.as_str().to_string()), + }) + .exec_with_returning(db) + .await + .map(|tag| tag.into()) + } + + pub async fn update( + user: &User, + db: &DatabaseConnection, + tag: TagModel, + ) -> Result { + Tag::update(tag::ActiveModel { + id: Set(tag.uuid), + owner_id: Set(user.uuid), + name: Set(tag.name), + color: Set(tag.color.as_str().to_string()), + }) + .filter(tag::Column::OwnerId.eq(user.uuid)) + .exec(db) + .await + .map(|tag| tag.into()) + } +} + +impl From for TagModel { + fn from(value: tag::Model) -> Self { + TagModel { + uuid: value.id, + name: value.name, + color: HexColor::from_text(&value.color).unwrap(), + } + } +} + +impl From<&tag::Model> for TagModel { + fn from(value: &tag::Model) -> Self { + TagModel { + uuid: value.id, + name: value.name.to_string(), + color: HexColor::from_text(&value.color).unwrap(), + } + } +} diff --git a/api/tests/categories/add.rs b/api/tests/categories/add.rs index 52a0960..325e77b 100644 --- a/api/tests/categories/add.rs +++ b/api/tests/categories/add.rs @@ -14,7 +14,7 @@ async fn add_category() { let category = Category { uuid: category_uuid, name: "Test Tag".to_string(), - color: HexColor::from_str("#233212").unwrap(), + color: HexColor::from_text("#233212").unwrap(), icon: "icon".to_string(), }; diff --git a/api/tests/categories/update.rs b/api/tests/categories/update.rs index 55a888b..2305a49 100644 --- a/api/tests/categories/update.rs +++ b/api/tests/categories/update.rs @@ -37,7 +37,7 @@ async fn update_category() { assert_eq!(categories.len(), 1); assert_eq!(categories[0].uuid, category_uuid); assert_eq!(categories[0].name, "Updated Category"); - assert_eq!(categories[0].color, HexColor::from_str("#000000").unwrap()); + assert_eq!(categories[0].color, HexColor::from_text("#000000").unwrap()); assert_eq!(categories[0].icon, "updated icon"); } @@ -74,6 +74,6 @@ async fn update_invalid_category() { assert_eq!(categories.len(), 1); assert_eq!(categories[0].uuid, category_uuid); assert_eq!(categories[0].name, "Test Category"); - assert_eq!(categories[0].color, HexColor::from_str("#233212").unwrap()); + assert_eq!(categories[0].color, HexColor::from_text("#233212").unwrap()); assert_eq!(categories[0].icon, "icon"); } diff --git a/api/tests/tags/add.rs b/api/tests/tags/add.rs index 092e04d..dcd23b4 100644 --- a/api/tests/tags/add.rs +++ b/api/tests/tags/add.rs @@ -14,7 +14,7 @@ async fn add_tag() { let tag = Tag { uuid: tag_uuid, name: "Test Tag".to_string(), - color: HexColor::from_str("#233212").unwrap(), + color: HexColor::from_text("#233212").unwrap(), }; let response = client diff --git a/api/tests/tags/update.rs b/api/tests/tags/update.rs index 7b403ee..520bd0f 100644 --- a/api/tests/tags/update.rs +++ b/api/tests/tags/update.rs @@ -35,7 +35,7 @@ async fn update_tag() { assert_eq!(tags.len(), 1); assert_eq!(tags[0].uuid, tag_uuid); assert_eq!(tags[0].name, "Updated Tag"); - assert_eq!(tags[0].color, HexColor::from_str("#000000").unwrap()); + assert_eq!(tags[0].color, HexColor::from_text("#000000").unwrap()); } #[tokio::test] @@ -69,5 +69,5 @@ async fn update_invalid_tag() { assert_eq!(tags.len(), 1); assert_eq!(tags[0].uuid, tag_uuid); assert_eq!(tags[0].name, "Test Tag"); - assert_eq!(tags[0].color, HexColor::from_str("#233212").unwrap()); + assert_eq!(tags[0].color, HexColor::from_text("#233212").unwrap()); } diff --git a/libs/types/src/lib.rs b/libs/types/src/lib.rs index 4036b5d..ad823df 100644 --- a/libs/types/src/lib.rs +++ b/libs/types/src/lib.rs @@ -132,7 +132,7 @@ impl HexColor { &self.0 } - pub fn from_str(s: &str) -> Option { + pub fn from_text(s: &str) -> Option { if is_valid_hex_color(s) { Some(HexColor(s.to_string())) } else { @@ -147,7 +147,7 @@ impl<'de> Deserialize<'de> for HexColor { D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; - HexColor::from_str(&s).ok_or(serde::de::Error::custom("invalid hex color")) + HexColor::from_text(&s).ok_or(serde::de::Error::custom("invalid hex color")) } }