263 lines
7.8 KiB
Rust
263 lines
7.8 KiB
Rust
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<AppState>,
|
|
Json(request): Json<CreateRecipeRequest>,
|
|
) -> JsonResult<CreateRecipeResponse> {
|
|
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<AppState>,
|
|
) -> JsonResult<ListRecipesResponse> {
|
|
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<S> FromRequestParts<S> for RecipeExtractor
|
|
where
|
|
S: Send + Sync,
|
|
AppState: FromRef<S>,
|
|
{
|
|
type Rejection = RouteError;
|
|
|
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
|
let State(app_state): State<AppState> = 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<RecipeId> = 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<AppState>,
|
|
RecipeExtractor(recipe): RecipeExtractor,
|
|
) -> JsonResult<RecipeInfo> {
|
|
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<AppState>,
|
|
RecipeExtractor(recipe): RecipeExtractor,
|
|
Json(req): Json<RecipeRenameRequest>,
|
|
) -> JsonResult<EmptyResponse> {
|
|
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<AppState>,
|
|
RecipeExtractor(recipe): RecipeExtractor,
|
|
IngredientExtractor(ingredient): IngredientExtractor,
|
|
Json(req): Json<RecipeIngredientEditRequest>,
|
|
) -> JsonResult<EmptyResponse> {
|
|
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<AppState>,
|
|
RecipeExtractor(recipe): RecipeExtractor,
|
|
IngredientExtractor(ingredient): IngredientExtractor,
|
|
) -> JsonResult<EmptyResponse> {
|
|
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<AppState>,
|
|
RecipeExtractor(recipe): RecipeExtractor,
|
|
IngredientExtractor(ingredient): IngredientExtractor,
|
|
Json(req): Json<AddRecipeIngredientRequest>,
|
|
) -> JsonResult<EmptyResponse> {
|
|
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<AppState>,
|
|
RecipeExtractor(recipe): RecipeExtractor,
|
|
Json(req): Json<RecipeEditStepsRequest>,
|
|
) -> JsonResult<EmptyResponse> {
|
|
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<AppState>,
|
|
RecipeExtractor(recipe): RecipeExtractor,
|
|
Json(req): Json<RecipeEditRating>,
|
|
) -> JsonResult<EmptyResponse> {
|
|
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<AppState>,
|
|
RecipeExtractor(recipe): RecipeExtractor,
|
|
Json(req): Json<RecipeEditPersonCount>,
|
|
) -> JsonResult<EmptyResponse> {
|
|
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())
|
|
}
|