diff --git a/Cargo.lock b/Cargo.lock index 7d233b9..286c6ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,6 +189,7 @@ dependencies = [ "sea-orm", "serde", "serde_json", + "thiserror 2.0.18", "tokio", "tokio-stream", "tower", diff --git a/api/Cargo.toml b/api/Cargo.toml index 41f56fa..3b57936 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -17,6 +17,7 @@ tower = "0.5.3" tower-http = { version = "0.6.8", features = ["cors", "trace"] } tracing-subscriber = "0.3.23" +thiserror = "2.0.18" color-eyre = "0.6.5" sea-orm = { version = "1.1.20", features = ["debug-print", "runtime-tokio", "sqlx-sqlite"] } diff --git a/api/src/routes/categories.rs b/api/src/routes/categories.rs index dec0ccd..6a24df3 100644 --- a/api/src/routes/categories.rs +++ b/api/src/routes/categories.rs @@ -2,22 +2,44 @@ use std::convert::Infallible; use std::time::Duration; use axum::extract::Path; -use axum::response::Sse; use axum::response::sse::Event; +use axum::response::{IntoResponse, Response, Sse}; use axum::routing::{delete, post}; use axum::{Json, Router, extract::State, routing::get}; use futures::Stream; use http::StatusCode; +use thiserror::Error; use tokio_stream::StreamExt; use tokio_stream::wrappers::BroadcastStream; use tracing::warn; use types::Category as CategoryModel; use uuid::Uuid; -use crate::services::categories::CategoryService; +use crate::services::categories::{CategoryService, CategoryServiceError}; use crate::services::events; use crate::{AppState, auth::User}; +#[derive(Error, Debug, PartialEq)] +enum ApiError { + #[error(transparent)] + ServiceError(#[from] CategoryServiceError), + + #[error("category not found")] + CategoryNotFound, +} + +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + let status = match &self { + ApiError::ServiceError(_) => StatusCode::INTERNAL_SERVER_ERROR, + ApiError::CategoryNotFound => StatusCode::NOT_FOUND, + }; + + warn!("{:?}", self); + status.into_response() + } +} + pub fn routes(state: AppState) -> Router { Router::new() .route("/", get(get_all_categories)) @@ -31,27 +53,13 @@ async fn add_category( state: State, user: User, Json(category): Json, -) -> Result, (StatusCode, &'static str)> { - let existing_category = CategoryService::get_by_id(&user, &state.db_connection, category.uuid) - .await - .map_err(|e| { - warn!("failed to find category: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "failed to find category") - })?; +) -> Result, ApiError> { + let existing_category = + CategoryService::get_by_id(&user, &state.db_connection, category.uuid).await?; let new_category = match existing_category { - Some(_) => CategoryService::update(&user, &state.db_connection, category) - .await - .map_err(|e| { - warn!("failed to add category: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "failed to add category") - })?, - None => CategoryService::create(&user, &state.db_connection, category) - .await - .map_err(|e| { - warn!("failed to add category: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "failed to add category") - })?, + Some(_) => CategoryService::update(&user, &state.db_connection, category).await?, + None => CategoryService::create(&user, &state.db_connection, category).await?, }; state @@ -63,16 +71,8 @@ async fn add_category( async fn get_all_categories( state: State, user: User, -) -> Result>, (StatusCode, &'static str)> { - let categories = CategoryService::get_all(&user, &state.db_connection) - .await - .map_err(|e| { - warn!("failed to fetch category: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - "failed to fetch categories", - ) - })?; +) -> Result>, ApiError> { + let categories = CategoryService::get_all(&user, &state.db_connection).await?; Ok(Json(categories)) } @@ -81,35 +81,18 @@ async fn delete_category( state: State, Path(category_uuid): Path, user: User, -) -> Result, (StatusCode, &'static str)> { +) -> Result, ApiError> { let to_be_deleted_category = CategoryService::get_by_id(&user, &state.db_connection, category_uuid) - .await - .map_err(|e| { - warn!("failed to fetch category: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - "failed to fetch categories", - ) - })?; - - let Some(deleted_category) = to_be_deleted_category else { - return Err((StatusCode::NOT_FOUND, "category not found")); - }; + .await? + .ok_or(ApiError::CategoryNotFound)?; - CategoryService::delete_by_id(&user, &state.db_connection, category_uuid) - .await - .map_err(|_| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - "failed to delete category", - ) - })?; + CategoryService::delete_by_id(&user, &state.db_connection, category_uuid).await?; state .event_service .broadcast(events::Event::Category(user.id)); - Ok(Json(deleted_category)) + Ok(Json(to_be_deleted_category)) } async fn sse_handler( diff --git a/api/src/routes/tags.rs b/api/src/routes/tags.rs index 8476283..b0aff46 100644 --- a/api/src/routes/tags.rs +++ b/api/src/routes/tags.rs @@ -2,12 +2,13 @@ use std::convert::Infallible; use std::time::Duration; use axum::extract::Path; -use axum::response::Sse; use axum::response::sse::Event; +use axum::response::{IntoResponse, Response, Sse}; use axum::routing::{delete, post}; use axum::{Json, Router, extract::State, routing::get}; use futures::Stream; use http::StatusCode; +use thiserror::Error; use tokio_stream::StreamExt; use tokio_stream::wrappers::BroadcastStream; use tracing::warn; @@ -15,9 +16,30 @@ use types::Tag as TagModel; use uuid::Uuid; use crate::services::events::{self}; -use crate::services::tags::TagService; +use crate::services::tags::{TagService, TagServiceError}; use crate::{AppState, auth::User}; +#[derive(Error, Debug, PartialEq)] +enum ApiError { + #[error(transparent)] + ServiceError(#[from] TagServiceError), + + #[error("category not found")] + CategoryNotFound, +} + +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + let status = match &self { + ApiError::ServiceError(_) => StatusCode::INTERNAL_SERVER_ERROR, + ApiError::CategoryNotFound => StatusCode::NOT_FOUND, + }; + + warn!("{:?}", self); + status.into_response() + } +} + pub fn routes(state: AppState) -> Router { Router::new() .route("/", get(get_all_tags)) @@ -31,43 +53,20 @@ async fn add_tag( state: State, user: User, Json(tag): Json, -) -> Result, (StatusCode, &'static str)> { - 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") - })?; +) -> Result, ApiError> { + let existing_tag = TagService::get_by_id(&user, &state.db_connection, tag.uuid).await?; let new_tag = match existing_tag { - Some(_) => 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") - })?, - 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") - })?, + Some(_) => TagService::update(&user, &state.db_connection, tag).await?, + None => TagService::create(&user, &state.db_connection, tag).await?, }; state.event_service.broadcast(events::Event::Tag(user.id)); Ok(Json(new_tag)) } -async fn get_all_tags( - state: State, - user: User, -) -> Result>, (StatusCode, &'static str)> { - let tags = TagService::get_all(&user, &state.db_connection) - .await - .map_err(|e| { - warn!("failed to fetch tag: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "failed to fetch tags") - })?; +async fn get_all_tags(state: State, user: User) -> Result>, ApiError> { + let tags = TagService::get_all(&user, &state.db_connection).await?; Ok(Json(tags)) } @@ -76,24 +75,15 @@ async fn delete_tag( state: State, Path(tag_uuid): Path, user: User, -) -> Result, (StatusCode, &'static str)> { +) -> Result, ApiError> { 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(deleted_tag) = to_be_deleted_tag else { - return Err((StatusCode::NOT_FOUND, "tag not found")); - }; + .await? + .ok_or(ApiError::CategoryNotFound)?; - TagService::delete_by_id(&user, &state.db_connection, tag_uuid) - .await - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "failed to delete tag"))?; + TagService::delete_by_id(&user, &state.db_connection, tag_uuid).await?; state.event_service.broadcast(events::Event::Tag(user.id)); - Ok(Json(deleted_tag)) + Ok(Json(to_be_deleted_tag)) } async fn sse_handler( diff --git a/api/src/routes/todo.rs b/api/src/routes/todo.rs index f3209c0..7bc815b 100644 --- a/api/src/routes/todo.rs +++ b/api/src/routes/todo.rs @@ -3,13 +3,13 @@ use std::{convert::Infallible, time::Duration}; use axum::{ Json, Router, extract::{Path, State}, - response::{Sse, sse::Event}, + response::{IntoResponse, Response, Sse, sse::Event}, routing::{delete, get, post}, }; use futures::Stream; use http::StatusCode; -use sea_orm::DbErr; use serde::Deserialize; +use thiserror::Error; use tokio_stream::{StreamExt, wrappers::BroadcastStream}; use tracing::warn; use types::Todo as TodoModel; @@ -20,10 +20,34 @@ use crate::{ auth::User, services::{ events, - todos::{TodoPosition, TodoService}, + todos::{TodoPosition, TodoService, TodoServiceError}, }, }; +#[derive(Error, Debug, PartialEq)] +enum ApiError { + #[error(transparent)] + ServiceError(#[from] TodoServiceError), + + #[error("todo not found")] + NotFound, +} + +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + let status = match &self { + ApiError::ServiceError(error) => match error { + TodoServiceError::NotFound => StatusCode::NOT_FOUND, + _ => StatusCode::INTERNAL_SERVER_ERROR, + }, + ApiError::NotFound => StatusCode::NOT_FOUND, + }; + + warn!("{:?}", self); + status.into_response() + } +} + pub fn routes(state: AppState) -> Router { Router::new() .route("/", get(get_all_todos)) @@ -39,13 +63,8 @@ pub fn routes(state: AppState) -> Router { async fn get_all_todos( state: State, user: User, -) -> Result>, (StatusCode, &'static str)> { - let tags = TodoService::get_all(&user, &state.db_connection) - .await - .map_err(|e| { - warn!("failed to fetch todo: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "failed to fetch Todos") - })?; +) -> Result>, ApiError> { + let tags = TodoService::get_all(&user, &state.db_connection).await?; Ok(Json(tags)) } @@ -62,33 +81,21 @@ async fn add_todo( state: State, user: User, Json(todo): Json, -) -> Result, (StatusCode, &'static str)> { - let existing_todo = TodoService::get_by_id(&user, &state.db_connection, todo.data.uuid) - .await - .map_err(|e| { - warn!("failed to find todo: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "failed to find todo") - })?; +) -> Result, ApiError> { + let existing_todo = TodoService::get_by_id(&user, &state.db_connection, todo.data.uuid).await?; let new_todo = match existing_todo { - Some(_) => TodoService::update(&user, &state.db_connection, todo.data) - .await - .map_err(|e| { - warn!("failed to add todo: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "failed to add todo") - })?, - None => TodoService::create( - &user, - &state.db_connection, - todo.data, - todo.position.unwrap_or(TodoPosition::Top), - todo.previous_id, - ) - .await - .map_err(|e| { - warn!("failed to add todo: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "failed to add todo") - })?, + Some(_) => TodoService::update(&user, &state.db_connection, todo.data).await?, + None => { + TodoService::create( + &user, + &state.db_connection, + todo.data, + todo.position.unwrap_or(TodoPosition::Top), + todo.previous_id, + ) + .await? + } }; state.event_service.broadcast(events::Event::Todo(user.id)); @@ -99,21 +106,14 @@ async fn delete_todo( state: State, Path(todo_uuid): Path, user: User, -) -> Result, (StatusCode, &'static str)> { - let to_be_deleted_todo = TodoService::get_by_id(&user, &state.db_connection, todo_uuid) - .await - .map_err(|e| { - warn!("failed to fetch tag: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "failed to fetch tags") - })?; +) -> Result, ApiError> { + let to_be_deleted_todo = TodoService::get_by_id(&user, &state.db_connection, todo_uuid).await?; let Some(deleted_todo) = to_be_deleted_todo else { - return Err((StatusCode::NOT_FOUND, "tag not found")); + return Err(ApiError::NotFound); }; - TodoService::delete_by_id(&user, &state.db_connection, todo_uuid) - .await - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "failed to delete tag"))?; + TodoService::delete_by_id(&user, &state.db_connection, todo_uuid).await?; state.event_service.broadcast(events::Event::Todo(user.id)); Ok(Json(deleted_todo)) @@ -167,16 +167,11 @@ async fn add_check( state: State, Path(todo_uuid): Path, user: User, -) -> Result, (StatusCode, &'static str)> { - let checked_todo = TodoService::add_check(&user, &state.db_connection, todo_uuid) - .await - .map_err(|e| { - warn!("failed to fetch todo: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "failed to fetch todo") - })?; +) -> Result, ApiError> { + let checked_todo = TodoService::add_check(&user, &state.db_connection, todo_uuid).await?; let Some(checked_todo) = checked_todo else { - return Err((StatusCode::NOT_FOUND, "todo not found")); + return Err(ApiError::NotFound); }; state.event_service.broadcast(events::Event::Todo(user.id)); @@ -187,16 +182,11 @@ async fn remove_check( state: State, Path(todo_uuid): Path, user: User, -) -> Result, (StatusCode, &'static str)> { - let checked_todo = TodoService::remove_check(&user, &state.db_connection, todo_uuid) - .await - .map_err(|e| { - warn!("failed to fetch todo: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "failed to fetch todo") - })?; +) -> Result, ApiError> { + let checked_todo = TodoService::remove_check(&user, &state.db_connection, todo_uuid).await?; let Some(checked_todo) = checked_todo else { - return Err((StatusCode::NOT_FOUND, "todo not found")); + return Err(ApiError::NotFound); }; state.event_service.broadcast(events::Event::Todo(user.id)); @@ -214,18 +204,8 @@ async fn move_todo( state: State, user: User, Json(move_data): Json, -) -> Result<(), (StatusCode, &'static str)> { - match TodoService::move_todo(&user, &state.db_connection, move_data.to_move, move_data.to).await - { - Err(DbErr::RecordNotFound(_)) => { - return Err((StatusCode::NOT_FOUND, "todo not found")); - } - Err(e) => { - warn!("failed to move todo: {}", e); - return Err((StatusCode::INTERNAL_SERVER_ERROR, "failed to move todo")); - } - _ => {} - } +) -> Result<(), ApiError> { + TodoService::move_todo(&user, &state.db_connection, move_data.to_move, move_data.to).await?; state.event_service.broadcast(events::Event::Todo(user.id)); Ok(()) diff --git a/api/src/routes/webpush.rs b/api/src/routes/webpush.rs index e12c7c1..845d894 100644 --- a/api/src/routes/webpush.rs +++ b/api/src/routes/webpush.rs @@ -1,9 +1,36 @@ -use axum::{Json, Router, extract::State, routing::post}; +use axum::{ + Json, Router, + extract::State, + response::{IntoResponse, Response}, + routing::post, +}; use http::StatusCode; +use thiserror::Error; use tracing::warn; use web_push::SubscriptionInfo; -use crate::{AppState, auth::User, services::webpush::WebPushService}; +use crate::{ + AppState, + auth::User, + services::webpush::{WebPushService, WebPushServiceError}, +}; + +#[derive(Error, Debug, PartialEq)] +enum ApiError { + #[error(transparent)] + ServiceError(#[from] WebPushServiceError), +} + +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + let status = match &self { + ApiError::ServiceError(_) => StatusCode::INTERNAL_SERVER_ERROR, + }; + + warn!("{:?}", self); + status.into_response() + } +} pub fn routes(state: AppState) -> Router { Router::new() @@ -15,17 +42,9 @@ async fn add_subscription( state: State, user: User, Json(subscription): Json, -) -> Result, (StatusCode, &'static str)> { - warn!("subscription: {:?}", subscription); - - let new_subscription = WebPushService::create(&user, &state.db_connection, subscription) - .await - .map_err(|_| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - "failed to create subscription", - ) - })?; +) -> Result, ApiError> { + let new_subscription = + WebPushService::create(&user, &state.db_connection, subscription).await?; Ok(Json(new_subscription)) } diff --git a/api/src/services/categories.rs b/api/src/services/categories.rs index 245d4c2..b1ff96e 100644 --- a/api/src/services/categories.rs +++ b/api/src/services/categories.rs @@ -6,69 +6,69 @@ use crate::{ use sea_orm::{ ActiveValue::Set, ColumnTrait, DatabaseConnection, DeleteResult, EntityTrait, QueryFilter, }; +use thiserror::Error; use types::{Category as CategoryModel, HexColor}; use uuid::Uuid; +#[derive(Error, Debug, PartialEq)] +pub enum CategoryServiceError { + #[error("database error - caused by: {0}")] + DbError(#[from] sea_orm::DbErr), + + #[error("invalid category - caused by: {0}")] + InvalidCategory(String), +} + pub struct CategoryService; impl CategoryService { pub async fn get_all( user: &User, db: &DatabaseConnection, - ) -> Result, sea_orm::DbErr> { - Category::find() + ) -> Result, CategoryServiceError> { + let categories = Category::find() .filter(category::Column::OwnerId.eq(user.id.clone())) .all(db) - .await - .and_then(|categories| { - categories - .into_iter() - .map(|category| { - category.try_into().map_err(|_| { - sea_orm::DbErr::Type("failed to convert category model".to_string()) - }) - }) - .collect() - }) + .await?; + + categories + .into_iter() + .map(|category| category.try_into()) + .collect() } pub async fn get_by_id( user: &User, db: &DatabaseConnection, category_id: Uuid, - ) -> Result, sea_orm::DbErr> { - Category::find_by_id(category_id) + ) -> Result, CategoryServiceError> { + let category = Category::find_by_id(category_id) .filter(category::Column::OwnerId.eq(user.id.clone())) .one(db) - .await - .and_then(|category| { - category - .map(|category| { - category.try_into().map_err(|_| { - sea_orm::DbErr::Type("failed to convert category model".to_string()) - }) - }) - .transpose() - }) + .await?; + + category.map(|category| category.try_into()).transpose() } pub async fn delete_by_id( user: &User, db: &DatabaseConnection, category_id: Uuid, - ) -> Result { - Category::delete_by_id(category_id) + ) -> Result { + let category = Category::delete_by_id(category_id) .filter(category::Column::OwnerId.eq(user.id.clone())) .exec(db) - .await + .await?; + + Ok(category) } pub async fn create( user: &User, db: &DatabaseConnection, category: CategoryModel, - ) -> Result { - Category::insert(category::ActiveModel { + ) -> Result { + let category = Category::insert(category::ActiveModel { id: Set(category.uuid), owner_id: Set(user.id.clone()), name: Set(category.name), @@ -76,20 +76,17 @@ impl CategoryService { icon: Set(category.icon), }) .exec_with_returning(db) - .await - .and_then(|category| { - category - .try_into() - .map_err(|_| sea_orm::DbErr::Type("failed to convert category model".to_string())) - }) + .await?; + + category.try_into() } pub async fn update( user: &User, db: &DatabaseConnection, category: CategoryModel, - ) -> Result { - Category::update(category::ActiveModel { + ) -> Result { + let category = Category::update(category::ActiveModel { id: Set(category.uuid), owner_id: Set(user.id.clone()), name: Set(category.name), @@ -98,23 +95,22 @@ impl CategoryService { }) .filter(category::Column::OwnerId.eq(user.id.clone())) .exec(db) - .await - .and_then(|category| { - category - .try_into() - .map_err(|_| sea_orm::DbErr::Type("failed to convert category model".to_string())) - }) + .await?; + + category.try_into() } } impl TryFrom for CategoryModel { - type Error = String; + type Error = CategoryServiceError; fn try_from(value: category::Model) -> Result { let category = CategoryModel { uuid: value.id, name: value.name.to_string(), - color: HexColor::from_text(&value.color).ok_or("invalid color".to_string())?, + color: HexColor::from_text(&value.color).ok_or( + CategoryServiceError::InvalidCategory("invalid color".to_string()), + )?, icon: value.icon.to_string(), }; @@ -123,13 +119,15 @@ impl TryFrom for CategoryModel { } impl TryFrom<&category::Model> for CategoryModel { - type Error = String; + type Error = CategoryServiceError; fn try_from(value: &category::Model) -> Result { let category = CategoryModel { uuid: value.id, name: value.name.to_string(), - color: HexColor::from_text(&value.color).ok_or("invalid color".to_string())?, + color: HexColor::from_text(&value.color).ok_or( + CategoryServiceError::InvalidCategory("invalid color".to_string()), + )?, icon: value.icon.to_string(), }; diff --git a/api/src/services/tags.rs b/api/src/services/tags.rs index 9866254..dc527fe 100644 --- a/api/src/services/tags.rs +++ b/api/src/services/tags.rs @@ -6,86 +6,83 @@ use crate::{ use sea_orm::{ ActiveValue::Set, ColumnTrait, DatabaseConnection, DeleteResult, EntityTrait, QueryFilter, }; +use thiserror::Error; use types::{HexColor, Tag as TagModel}; use uuid::Uuid; +#[derive(Error, Debug, PartialEq)] +pub enum TagServiceError { + #[error("database error - caused by: {0}")] + DbError(#[from] sea_orm::DbErr), + + #[error("invalid tag - caused by: {0}")] + InvalidTag(String), +} + pub struct TagService; impl TagService { pub async fn get_all( user: &User, db: &DatabaseConnection, - ) -> Result, sea_orm::DbErr> { - Tag::find() + ) -> Result, TagServiceError> { + let tags = Tag::find() .filter(tag::Column::OwnerId.eq(user.id.clone())) .all(db) - .await - .and_then(|tags| { - tags.into_iter() - .map(|tag| { - tag.try_into().map_err(|_| { - sea_orm::DbErr::Type("failed to convert tag model".to_string()) - }) - }) - .collect() - }) + .await?; + + tags.into_iter().map(|tag| tag.try_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) + ) -> Result, TagServiceError> { + let tag = Tag::find_by_id(tag_id) .filter(tag::Column::OwnerId.eq(user.id.clone())) .one(db) - .await - .and_then(|tag| { - tag.map(|tag| { - tag.try_into().map_err(|_| { - sea_orm::DbErr::Type("failed to convert tag model".to_string()) - }) - }) - .transpose() - }) + .await?; + + tag.map(|tag| tag.try_into()).transpose() } pub async fn delete_by_id( user: &User, db: &DatabaseConnection, tag_id: Uuid, - ) -> Result { - Tag::delete_by_id(tag_id) + ) -> Result { + let tag = Tag::delete_by_id(tag_id) .filter(tag::Column::OwnerId.eq(user.id.clone())) .exec(db) - .await + .await?; + + Ok(tag) } pub async fn create( user: &User, db: &DatabaseConnection, tag: TagModel, - ) -> Result { - Tag::insert(tag::ActiveModel { + ) -> Result { + let tag = Tag::insert(tag::ActiveModel { id: Set(tag.uuid), owner_id: Set(user.id.clone()), name: Set(tag.name), color: Set(tag.color.as_str().to_string()), }) .exec_with_returning(db) - .await - .and_then(|tag| { - tag.try_into() - .map_err(|_| sea_orm::DbErr::Type("failed to convert tag model".to_string())) - }) + .await?; + + tag.try_into() } pub async fn update( user: &User, db: &DatabaseConnection, tag: TagModel, - ) -> Result { - Tag::update(tag::ActiveModel { + ) -> Result { + let tag = Tag::update(tag::ActiveModel { id: Set(tag.uuid), owner_id: Set(user.id.clone()), name: Set(tag.name), @@ -93,22 +90,21 @@ impl TagService { }) .filter(tag::Column::OwnerId.eq(user.id.clone())) .exec(db) - .await - .and_then(|tag| { - tag.try_into() - .map_err(|_| sea_orm::DbErr::Type("failed to convert tag model".to_string())) - }) + .await?; + + tag.try_into() } } impl TryFrom for TagModel { - type Error = String; + type Error = TagServiceError; fn try_from(value: tag::Model) -> Result { let tag = TagModel { uuid: value.id, name: value.name, - color: HexColor::from_text(&value.color).ok_or("invalid color")?, + color: HexColor::from_text(&value.color) + .ok_or(TagServiceError::InvalidTag("invalid color".to_string()))?, }; Ok(tag) @@ -116,13 +112,14 @@ impl TryFrom for TagModel { } impl TryFrom<&tag::Model> for TagModel { - type Error = String; + type Error = TagServiceError; fn try_from(value: &tag::Model) -> Result { let tag = TagModel { uuid: value.id, name: value.name.clone(), - color: HexColor::from_text(&value.color).ok_or("invalid color")?, + color: HexColor::from_text(&value.color) + .ok_or(TagServiceError::InvalidTag("invalid color".to_string()))?, }; Ok(tag) diff --git a/api/src/services/todos.rs b/api/src/services/todos.rs index ba3c3e9..ae7d841 100644 --- a/api/src/services/todos.rs +++ b/api/src/services/todos.rs @@ -8,6 +8,7 @@ use sea_orm::{ PaginatorTrait, QueryFilter, QueryOrder, }; use serde::Deserialize; +use thiserror::Error; use tracing::warn; use types::{Time as TimeModel, TimeRange, TimeRecurring, Todo as TodoModel}; use uuid::Uuid; @@ -15,6 +16,18 @@ use uuid::Uuid; use crate::auth::User; use crate::entities::{check, prelude::*, time, todo, todo_tag}; +#[derive(Error, Debug, PartialEq)] +pub enum TodoServiceError { + #[error("database error - caused by: {0}")] + DbError(#[from] sea_orm::DbErr), + + #[error("invalid time - caused by: {0}")] + InvalidTime(String), + + #[error("not found")] + NotFound, +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub enum TodoPosition { @@ -28,7 +41,7 @@ impl TodoService { pub async fn get_all( user: &User, db: &DatabaseConnection, - ) -> Result, sea_orm::DbErr> { + ) -> Result, TodoServiceError> { let todos = Todo::find() .filter(todo::Column::OwnerId.eq(user.id.clone())) .order_by_asc(todo::Column::Order) @@ -105,40 +118,41 @@ impl TodoService { db: &DatabaseConnection, start: NaiveDateTime, end: NaiveDateTime, - ) -> Result, sea_orm::DbErr> { - Todo::find() + ) -> Result, TodoServiceError> { + let todos = Todo::find() .find_also_related(Time) .filter(time::Column::Format.eq("point")) .filter(time::Column::DateTime.gte(start)) .filter(time::Column::DateTime.lt(end)) .all(db) - .await - .map(|todos| { - todos - .into_iter() - .map(|(todo, _)| { - ( - TodoModel { - uuid: todo.id, - title: todo.title, - note: todo.note, - time: None, - tags: vec![], - category: todo.category_id, - checks: vec![], - }, - todo.owner_id, - ) - }) - .collect() + .await?; + + let todos = todos + .into_iter() + .map(|(todo, _)| { + ( + TodoModel { + uuid: todo.id, + title: todo.title, + note: todo.note, + time: None, + tags: vec![], + category: todo.category_id, + checks: vec![], + }, + todo.owner_id, + ) }) + .collect(); + + Ok(todos) } pub async fn get_by_id( user: &User, db: &DatabaseConnection, id: Uuid, - ) -> Result, sea_orm::DbErr> { + ) -> Result, TodoServiceError> { let todo = Todo::find_by_id(id) .filter(todo::Column::OwnerId.eq(user.id.clone())) .one(db) @@ -156,12 +170,7 @@ impl TodoService { uuid: todo.id, title: todo.title.to_string(), note: todo.note.to_string(), - time: time - .map(|time| { - time.try_into() - .map_err(|_| sea_orm::DbErr::Type("failed to convert time".to_string())) - }) - .transpose()?, + time: time.map(|time| time.try_into()).transpose()?, tags: tags.iter().map(|tag| tag.tag_id).collect(), category: todo.category_id, checks: checks.iter().map(|check| check.date).collect(), @@ -174,7 +183,7 @@ impl TodoService { todo: TodoModel, positon: TodoPosition, reference: Option, - ) -> Result { + ) -> Result { let position = match positon { TodoPosition::Top => Todo::find() .order_by_asc(todo::Column::Order) @@ -234,14 +243,14 @@ impl TodoService { Self::get_by_id(user, db, todo.uuid) .await? - .ok_or(sea_orm::DbErr::RecordNotFound(todo.uuid.to_string())) + .ok_or(TodoServiceError::NotFound) } pub async fn update( user: &User, db: &DatabaseConnection, todo: TodoModel, - ) -> Result { + ) -> Result { let updated = Todo::update(todo::ActiveModel { id: Set(todo.uuid), owner_id: Set(user.id.clone()), @@ -301,25 +310,27 @@ impl TodoService { Self::get_by_id(user, db, updated.id) .await? - .ok_or(sea_orm::DbErr::RecordNotFound(todo.uuid.to_string())) + .ok_or(TodoServiceError::NotFound) } pub async fn delete_by_id( user: &User, db: &DatabaseConnection, id: Uuid, - ) -> Result { - Todo::delete_by_id(id) + ) -> Result { + let todo = Todo::delete_by_id(id) .filter(todo::Column::OwnerId.eq(user.id.clone())) .exec(db) - .await + .await?; + + Ok(todo) } pub async fn add_check( user: &User, db: &DatabaseConnection, id: Uuid, - ) -> Result, sea_orm::DbErr> { + ) -> Result, TodoServiceError> { if Self::get_by_id(user, db, id).await?.is_none() { return Ok(None); } @@ -338,7 +349,7 @@ impl TodoService { user: &User, db: &DatabaseConnection, id: Uuid, - ) -> Result, sea_orm::DbErr> { + ) -> Result, TodoServiceError> { if Self::get_by_id(user, db, id).await?.is_none() { return Ok(None); } @@ -365,22 +376,18 @@ impl TodoService { db: &DatabaseConnection, todo: Uuid, target: Uuid, - ) -> Result<(), sea_orm::DbErr> { + ) -> Result<(), TodoServiceError> { let todo = Todo::find_by_id(todo) .filter(todo::Column::OwnerId.eq(user.id.clone())) .one(db) .await? - .ok_or(sea_orm::DbErr::RecordNotFound( - "Failed to find todo".to_string(), - ))?; + .ok_or(TodoServiceError::NotFound)?; let target = Todo::find_by_id(target) .filter(todo::Column::OwnerId.eq(user.id.clone())) .one(db) .await? - .ok_or(sea_orm::DbErr::RecordNotFound( - "Failed to find todo".to_string(), - ))?; + .ok_or(TodoServiceError::NotFound)?; if todo.order < target.order { Self::move_todo_to(user, db, todo, target.order + 1).await?; @@ -446,54 +453,62 @@ impl From for time::ActiveModel { } impl TryFrom for TimeModel { - type Error = String; + type Error = TodoServiceError; fn try_from(value: time::Model) -> Result { let time = match value.format.as_str() { "range" => types::Time::Range { inner: TimeRange { - start: value - .date_start - .ok_or("Required date_start not found".to_string())?, - end: value - .date_end - .ok_or("Required date_end not found".to_string())?, + start: value.date_start.ok_or(TodoServiceError::InvalidTime( + "Required date_start not found".to_string(), + ))?, + end: value.date_end.ok_or(TodoServiceError::InvalidTime( + "Required date_end not found".to_string(), + ))?, }, }, "recurring" => match value .recurrence_mode - .ok_or("Required recurrence_mode not found".to_string())? + .ok_or(TodoServiceError::InvalidTime( + "Required recurrence_mode not found".to_string(), + ))? .as_str() { "weekly" => types::Time::Recurring { inner: TimeRecurring::Weekly { - start: value - .number_start - .ok_or("Required number_start not found".to_string())? - as u64, - end: value - .number_end - .ok_or("Required number_end not found".to_string())? - as u64, + start: value.number_start.ok_or(TodoServiceError::InvalidTime( + "Required number_start not found".to_string(), + ))? as u64, + end: value.number_end.ok_or(TodoServiceError::InvalidTime( + "Required number_end not found".to_string(), + ))? as u64, }, }, "daily" => types::Time::Recurring { inner: TimeRecurring::Daily {}, }, mode => { - panic!("unknown recurrence mode: {}", mode) + return Err(TodoServiceError::InvalidTime(format!( + "unknown recurrence mode: {}", + mode + ))); } }, "point" => types::Time::Point { inner: types::TimePoint { time: value .date_time - .ok_or("Required date_time not found".to_string())? + .ok_or(TodoServiceError::InvalidTime( + "Required date_time not found".to_string(), + ))? .and_utc(), }, }, format => { - panic!("unknown time format: {}", format) + return Err(TodoServiceError::InvalidTime(format!( + "unknown time format: {}", + format + ))); } }; diff --git a/api/src/services/webpush.rs b/api/src/services/webpush.rs index efc5ec2..3353217 100644 --- a/api/src/services/webpush.rs +++ b/api/src/services/webpush.rs @@ -2,6 +2,7 @@ use sea_orm::{ ActiveValue::{NotSet, Set}, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, }; +use thiserror::Error; use web_push::SubscriptionInfo; use crate::{ @@ -9,6 +10,12 @@ use crate::{ entities::{prelude::*, web_push_subscription}, }; +#[derive(Error, Debug, PartialEq)] +pub enum WebPushServiceError { + #[error("database error - caused by: {0}")] + DbError(#[from] sea_orm::DbErr), +} + pub struct WebPushService {} impl WebPushService { @@ -16,8 +23,8 @@ impl WebPushService { user: &User, db: &DatabaseConnection, subscription: SubscriptionInfo, - ) -> Result { - WebPushSubscription::insert(web_push_subscription::ActiveModel { + ) -> Result { + let subscription = WebPushSubscription::insert(web_push_subscription::ActiveModel { id: NotSet, owner_id: Set(user.id.clone()), endpoint: Set(subscription.endpoint), @@ -25,35 +32,35 @@ impl WebPushService { auth: Set(subscription.keys.auth), }) .exec_with_returning(db) - .await - .map(|subscription| { - SubscriptionInfo::new( - subscription.endpoint, - subscription.p256dh, - subscription.auth, - ) - }) + .await?; + + Ok(SubscriptionInfo::new( + subscription.endpoint, + subscription.p256dh, + subscription.auth, + )) } pub async fn get_all_by_owner_id( db: &DatabaseConnection, owner: &str, - ) -> Result, sea_orm::DbErr> { - WebPushSubscription::find() + ) -> Result, WebPushServiceError> { + let subscriptions = WebPushSubscription::find() .filter(web_push_subscription::Column::OwnerId.eq(owner)) .all(db) - .await - .map(|subscriptions| { - subscriptions - .into_iter() - .map(|subscription| { - SubscriptionInfo::new( - subscription.endpoint, - subscription.p256dh, - subscription.auth, - ) - }) - .collect() + .await?; + + let subscriptions = subscriptions + .into_iter() + .map(|subscription| { + SubscriptionInfo::new( + subscription.endpoint, + subscription.p256dh, + subscription.auth, + ) }) + .collect(); + + Ok(subscriptions) } }