use api::{ AddRecipeIngredientRequest, CreateRecipeRequest, CreateRecipeResponse, EmptyResponse, IngredientInfo, ListRecipesResponse, RecipeEditRating, RecipeEditStepsRequest, RecipeInfo, RecipeIngredientEditRequest, RecipeRenameRequest, RecipeEditPersonCount, }; use axum::{ async_trait, extract::{FromRef, FromRequestParts, Path, State}, http::request::Parts, Json, }; use sea_orm::{prelude::*, ActiveValue, TransactionTrait}; use crate::entity::{ingredient, prelude::*, recipe, recipe_ingredients}; use super::{ household::AuthorizedHousehold, ingredients::IngredientExtractor, 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), household: ActiveValue::Set(household.id), steps: ActiveValue::Set(request.steps), person_count: ActiveValue::Set(request.person_count.max(1) as i32), ..Default::default() }; let recipe = 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_ingredients::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()) } pub(super) async fn list_recipes( AuthorizedHousehold(household): AuthorizedHousehold, State(state): State, ) -> JsonResult { Ok(ListRecipesResponse { recipes: household .find_related(Recipe) .all(&state.db) .await? .into_iter() .map(|r| (r.id, r.name, r.ranking as _)) .collect(), } .into()) } pub(super) struct RecipeExtractor(recipe::Model); #[async_trait] impl FromRequestParts for RecipeExtractor where S: Send + Sync, AppState: FromRef, { type Rejection = RouteError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let State(app_state): State = State::from_request_parts(parts, state) .await .expect("No state"); #[derive(serde::Deserialize)] pub(super) struct RecipeId { recipe_id: i64, } let household = AuthorizedHousehold::from_request_parts(parts, state).await?; let Path(recipe): Path = Path::from_request_parts(parts, state).await?; match household .0 .find_related(Recipe) .filter(recipe::Column::Id.eq(recipe.recipe_id)) .one(&app_state.db) .await? { None => Err(RouteError::RessourceNotFound), Some(r) => Ok(Self(r)), } } } pub(super) async fn fetch_recipe( State(state): State, RecipeExtractor(recipe): RecipeExtractor, ) -> JsonResult { let recipe_ingredients = recipe.find_related(Ingredient).all(&state.db).await?; let mut ingredients = Vec::new(); for ingredient in recipe_ingredients { ingredients.push(( ingredient.id, IngredientInfo { name: ingredient.name, unit: ingredient.unit, }, RecipeIngredients::find_by_id((recipe.id, ingredient.id)) .one(&state.db) .await? .expect("Ingredient should exist as it was fetched") .amount, )); } Ok(RecipeInfo { person_count: recipe.person_count as _, name: recipe.name, steps: recipe.steps, rating: recipe.ranking as _, ingredients, } .into()) } pub(super) async fn edit_name( State(state): State, RecipeExtractor(recipe): RecipeExtractor, Json(req): Json, ) -> JsonResult { let active_model = recipe::ActiveModel { name: ActiveValue::Set(req.name), id: ActiveValue::Set(recipe.id), ..Default::default() }; active_model.update(&state.db).await?; Ok(EmptyResponse {}.into()) } pub(super) async fn edit_ig_amount( State(state): State, RecipeExtractor(recipe): RecipeExtractor, IngredientExtractor(ingredient): IngredientExtractor, Json(req): Json, ) -> JsonResult { let active_model = recipe_ingredients::ActiveModel { recipe_id: ActiveValue::Set(recipe.id), ingredient_id: ActiveValue::Set(ingredient.id), amount: ActiveValue::Set(req.amount), }; active_model.update(&state.db).await?; Ok(EmptyResponse {}.into()) } pub(super) async fn delete_ig( State(state): State, RecipeExtractor(recipe): RecipeExtractor, IngredientExtractor(ingredient): IngredientExtractor, ) -> JsonResult { RecipeIngredients::delete_by_id((recipe.id, ingredient.id)) .exec(&state.db) .await?; Ok(EmptyResponse {}.into()) } pub(super) async fn add_ig_request( State(state): State, RecipeExtractor(recipe): RecipeExtractor, IngredientExtractor(ingredient): IngredientExtractor, Json(req): Json, ) -> JsonResult { let model = recipe_ingredients::ActiveModel { recipe_id: ActiveValue::Set(recipe.id), ingredient_id: ActiveValue::Set(ingredient.id), amount: ActiveValue::Set(req.amount), }; model.insert(&state.db).await?; Ok(EmptyResponse {}.into()) } pub(super) async fn edit_step( State(state): State, RecipeExtractor(recipe): RecipeExtractor, Json(req): Json, ) -> JsonResult { let model = recipe::ActiveModel { id: ActiveValue::Set(recipe.id), steps: ActiveValue::Set(req.steps), ..Default::default() }; model.update(&state.db).await?; Ok(EmptyResponse {}.into()) } pub(super) async fn edit_rating( State(state): State, RecipeExtractor(recipe): RecipeExtractor, Json(req): Json, ) -> JsonResult { let model = recipe::ActiveModel { id: ActiveValue::Set(recipe.id), ranking: ActiveValue::Set(req.rating as _), ..Default::default() }; model.update(&state.db).await?; Ok(EmptyResponse {}.into()) } pub(super) async fn edit_person_count( State(state): State, RecipeExtractor(recipe): RecipeExtractor, Json(req): Json, ) -> JsonResult { let model = recipe::ActiveModel { id: ActiveValue::Set(recipe.id), person_count: ActiveValue::Set(req.person_count.max(1) as _), ..Default::default() }; model.update(&state.db).await?; Ok(EmptyResponse {}.into()) }