diff --git a/api/src/lib.rs b/api/src/lib.rs index f8595d7..4220fb3 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -83,3 +83,16 @@ pub struct EditIngredientRequest { #[serde(default)] pub has_unit: bool, } + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct CreateRecipeRequest { + pub name: String, + pub rating: u8, + pub ingredients: Vec<(i64, f64)>, + pub steps: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct CreateRecipeResponse { + pub id: i64, +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index a56620f..2870971 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -23,6 +23,7 @@ use crate::entity::{prelude::*, user}; mod household; mod ingredients; +mod recipe; #[derive(thiserror::Error, Debug)] enum RouteError { @@ -42,6 +43,8 @@ enum RouteError { PathRejection(#[from] axum::extract::rejection::PathRejection), #[error("The supplied ressource does not exist")] RessourceNotFound, + #[error("The request was malformed")] + InvalidRequest(String), #[error("Error in DB transaction")] TxnError(#[from] TransactionError>), } @@ -73,6 +76,7 @@ impl IntoResponse for RouteError { ) .into_response(), RouteError::RessourceNotFound => StatusCode::NOT_FOUND.into_response(), + RouteError::InvalidRequest(reason) => (StatusCode::BAD_REQUEST, reason).into_response(), e => { tracing::error!("Internal error: {e:?}"); StatusCode::INTERNAL_SERVER_ERROR.into_response() @@ -212,4 +216,8 @@ pub(crate) fn router(api_allowed: Option) -> Router { .get(ingredients::list_ingredients) .layer(mk_service(vec![Method::GET, Method::POST])), ) + .route( + "/household/:house_id/recipe", + post(recipe::create_recipe).layer(mk_service(vec![Method::POST])), + ) } diff --git a/src/routes/recipe.rs b/src/routes/recipe.rs new file mode 100644 index 0000000..9fd7a32 --- /dev/null +++ b/src/routes/recipe.rs @@ -0,0 +1,62 @@ +use api::{CreateRecipeRequest, CreateRecipeResponse}; +use axum::{extract::State, Json}; +use sea_orm::{prelude::*, ActiveValue, TransactionTrait}; + +use crate::entity::{ingredient, prelude::*, recipe, recipe_ingerdients, recipe_steps}; + +use super::{household::AuthorizedHousehold, AppState, JsonResult, RouteError}; + +pub(super) async fn create_recipe( + AuthorizedHousehold(household): AuthorizedHousehold, + State(state): State, + Json(request): Json, +) -> JsonResult { + let id = state + .db + .transaction(|txn| { + Box::pin(async move { + let model = recipe::ActiveModel { + name: ActiveValue::Set(request.name), + ranking: ActiveValue::Set(request.rating as i32), + ..Default::default() + }; + + let recipe = model.insert(txn).await?; + for (num, text) in request.steps.into_iter().enumerate() { + let model = recipe_steps::ActiveModel { + num: ActiveValue::Set(num as _), + recipe_id: ActiveValue::Set(recipe.id), + text: ActiveValue::Set(text), + }; + + model.insert(txn).await?; + } + + for (ig, amount) in request.ingredients { + if 0 == household + .find_related(Ingredient) + .filter(ingredient::Column::Id.eq(ig)) + .count(txn) + .await? + { + Err(RouteError::InvalidRequest(format!( + "No such ingredient {ig}" + )))?; + } + + let model = recipe_ingerdients::ActiveModel { + recipe_id: ActiveValue::Set(recipe.id), + ingredient_id: ActiveValue::Set(ig), + amount: ActiveValue::Set(amount), + }; + + model.insert(txn).await?; + } + + Ok(recipe.id) + }) + }) + .await?; + + Ok(CreateRecipeResponse { id }.into()) +}