diff --git a/api/src/lib.rs b/api/src/lib.rs index 1b233e4..ed3f793 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -48,3 +48,33 @@ pub struct UserInfo { pub name: String, pub id: Uuid, } + +#[derive(Serialize, Deserialize)] +pub struct CreateIngredientRequest { + pub name: String, + pub unit: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct CreateIngredientResponse { + pub id: i64, +} + +#[derive(Serialize, Deserialize)] +pub struct IngredientInfo { + pub name: String, + pub unit: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct IngredientList { + pub ingredients: HashMap, +} + +#[derive(Serialize, Deserialize)] +pub struct EditIngredientRequest { + pub name: Option, + pub unit: Option, + #[serde(default)] + pub has_unit: bool, +} diff --git a/src/routes/ingredients.rs b/src/routes/ingredients.rs new file mode 100644 index 0000000..3b7c2de --- /dev/null +++ b/src/routes/ingredients.rs @@ -0,0 +1,99 @@ +use axum::{ + extract::{Path, State}, + Json, +}; + +use api::{ + CreateIngredientRequest, CreateIngredientResponse, EditIngredientRequest, EmptyResponse, + IngredientInfo, IngredientList, +}; +use sea_orm::{prelude::*, ActiveValue}; +use serde::Deserialize; + +use super::{household::AuthorizedHousehold, AppState, JsonResult}; +use crate::entity::{ingredient, prelude::*}; + +pub(super) async fn create_ingredient( + AuthorizedHousehold(household): AuthorizedHousehold, + State(state): State, + Json(request): Json, +) -> JsonResult { + let model = ingredient::ActiveModel { + household: ActiveValue::Set(household.id), + name: ActiveValue::Set(request.name), + unit: ActiveValue::Set(request.unit), + ..Default::default() + }; + + let ingredient = model.insert(&state.db).await?; + + Ok(Json(CreateIngredientResponse { id: ingredient.id })) +} + +pub(super) async fn list_ingredients( + AuthorizedHousehold(household): AuthorizedHousehold, + State(state): State, +) -> JsonResult { + let ingredients = household.find_related(Ingredient).all(&state.db).await?; + + Ok(Json(IngredientList { + ingredients: ingredients + .into_iter() + .map(|m| { + ( + m.id, + IngredientInfo { + name: m.name, + unit: m.unit, + }, + ) + }) + .collect(), + })) +} + +#[derive(Deserialize)] +pub struct IngredientId { + iid: i64, +} + +pub(super) async fn remove_ingredient( + AuthorizedHousehold(household): AuthorizedHousehold, + State(state): State, + Path(IngredientId { iid }): Path, +) -> JsonResult { + Ingredient::delete_by_id(iid) + .filter(ingredient::Column::Household.eq(household.id)) + .exec(&state.db) + .await?; + + Ok(Json(EmptyResponse {})) +} + +pub(super) async fn edit_ingredient( + AuthorizedHousehold(household): AuthorizedHousehold, + State(state): State, + Path(IngredientId { iid }): Path, + Json(request): Json, +) -> JsonResult { + let Some(ingredient) = Ingredient::find_by_id(iid) + .filter(ingredient::Column::Household.eq(household.id)) + .one(&state.db) + .await? else { + return Err(super::RouteError::RessourceNotFound); + }; + + let mut ingredient: ingredient::ActiveModel = ingredient.into(); + + if let Some(name) = request.name { + ingredient.name = ActiveValue::Set(name); + } + + if request.has_unit || request.unit.is_some() { + ingredient.unit = ActiveValue::Set(request.unit); + } + + ingredient.update(&state.db).await?; + + Ok(Json(EmptyResponse {})) +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 2a41fdf..d4c6aaf 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -11,7 +11,7 @@ use axum::{ HeaderValue, Method, StatusCode, }, response::IntoResponse, - routing::{get, post, put}, + routing::{delete, get, post, put}, Json, Router, TypedHeader, }; use jwt_simple::prelude::*; @@ -22,6 +22,7 @@ use tower_http::cors::{self, AllowOrigin, CorsLayer}; use crate::entity::{prelude::*, user}; mod household; +mod ingredients; #[derive(thiserror::Error, Debug)] enum RouteError { @@ -39,6 +40,8 @@ enum RouteError { Unauthorized, #[error("Could not fetch required value from path")] PathRejection(#[from] axum::extract::rejection::PathRejection), + #[error("The supplied ressource does not exist")] + RessourceNotFound, } impl IntoResponse for RouteError { @@ -60,6 +63,7 @@ impl IntoResponse for RouteError { "Unauthorized to access this ressource", ) .into_response(), + RouteError::RessourceNotFound => StatusCode::NOT_FOUND.into_response(), e => { tracing::error!("Internal error: {e:?}"); StatusCode::INTERNAL_SERVER_ERROR.into_response() @@ -186,4 +190,16 @@ pub(crate) fn router(api_allowed: Option) -> Router { .delete(household::leave) .layer(mk_service(vec![Method::PUT, Method::DELETE])), ) + .route( + "/household/:house_id/ingredients/:iid", + delete(ingredients::remove_ingredient) + .patch(ingredients::edit_ingredient) + .layer(mk_service(vec![Method::DELETE, Method::PATCH])), + ) + .route( + "/household/:house_id/ingredients", + post(ingredients::create_ingredient) + .get(ingredients::list_ingredients) + .layer(mk_service(vec![Method::GET, Method::POST])), + ) }