184 lines
No EOL
7.2 KiB
TypeScript
184 lines
No EOL
7.2 KiB
TypeScript
import { RecipeDto } from "../api/dtos/RecipeDto.js";
|
|
import { RecipeDtoEntityMapper } from "../mappers/RecipeDtoEntityMapper.js";
|
|
import { RecipeRepository } from "../repositories/RecipeRepository.js";
|
|
import { NotFoundError, ValidationError } from "../api/errors/httpErrors.js";
|
|
import { RecipeInstructionStepDto } from "../api/dtos/RecipeInstructionStepDto.js";
|
|
import { RecipeIngredientGroupDto } from "../api/dtos/RecipeIngredientGroupDto.js";
|
|
import { RecipeIngredientDto } from "../api/dtos/RecipeIngredientDto.js";
|
|
import { RecipeEntity } from "../entities/RecipeEntity.js";
|
|
|
|
/**
|
|
* Controls all recipe specific actions
|
|
*/
|
|
export class RecipeHandler {
|
|
constructor(
|
|
private recipeRepository: RecipeRepository,
|
|
private mapper: RecipeDtoEntityMapper
|
|
) { }
|
|
|
|
/**
|
|
* Load a specific recipe
|
|
* @param id recipe id
|
|
* @returns RecipeDto for requested recipe
|
|
*/
|
|
async getRecipeById(id: string){
|
|
const recipeEntity = await this.recipeRepository.findById(id);
|
|
if(recipeEntity === null){
|
|
throw new NotFoundError("recipe with id " + id + " not found!")
|
|
}
|
|
const recipeDto = this.mapper.toDto(recipeEntity);
|
|
return recipeDto;
|
|
}
|
|
|
|
/**
|
|
* Save or update recipe depending on whether the DTO contains an ID
|
|
* @param dto recipe data
|
|
* @returns Recipe as saved in the database
|
|
*/
|
|
async createOrUpdateRecipe(dto: RecipeDto){
|
|
if (!this.isRecipeDtoValid(dto)) {
|
|
throw new ValidationError("recipe data is not valid!")
|
|
}
|
|
var savedEntity: RecipeEntity;
|
|
const recipeId = dto.id
|
|
if(recipeId === undefined || recipeId.length === 0){
|
|
// create new recipe
|
|
const recipeEntity = this.mapper.toEntity(dto)
|
|
delete (recipeEntity as any).id;
|
|
savedEntity = await this.recipeRepository.create(recipeEntity);
|
|
} else {
|
|
// save existing Recipe
|
|
// First: Load current version of recipe from database
|
|
const recipeEntity = await this.recipeRepository.findById(recipeId);
|
|
if(!recipeEntity){
|
|
throw new ValidationError("No recipe with ID " + recipeId + " found in database!")
|
|
}
|
|
// merge changes into entity
|
|
this.mapper.mergeDtoIntoEntity(dto, recipeEntity);
|
|
// persist changes
|
|
savedEntity = await this.recipeRepository.update(recipeEntity);
|
|
}
|
|
return this.mapper.toDto(savedEntity);
|
|
}
|
|
/**
|
|
* Update recipe data
|
|
* @param RecipeDto containing the entire updated recipe
|
|
* @returns Up-to-date RecipeDto as saved in the database
|
|
*/
|
|
async updateRecipe(dto: RecipeDto){
|
|
if (!this.isRecipeDtoValid(dto)) {
|
|
throw new ValidationError("recipe data is not valid!")
|
|
}
|
|
const recipeId = dto.id
|
|
if(recipeId === undefined){
|
|
throw new ValidationError("Trying to update recipe without ID!")
|
|
}
|
|
// Load current version of recipe from database
|
|
const recipeEntity = await this.recipeRepository.findById(recipeId);
|
|
if(!recipeEntity){
|
|
throw new ValidationError("No recipe with ID " + recipeId + " found in database!")
|
|
}
|
|
// merge changes into entity
|
|
this.mapper.mergeDtoIntoEntity(dto, recipeEntity);
|
|
// persist changes
|
|
const savedEntity = await this.recipeRepository.update(recipeEntity);
|
|
return this.mapper.toDto(savedEntity);
|
|
}
|
|
/**
|
|
* Create a new recipe
|
|
* @param dto RecipeDto containing the new recipe
|
|
*/
|
|
async createRecipe(dto: RecipeDto) {
|
|
if (!this.isRecipeDtoValid(dto)) {
|
|
throw new ValidationError("recipe data is not valid!")
|
|
}
|
|
const recipeEntity = this.mapper.toEntity(dto)
|
|
const savedEntity = await this.recipeRepository.create(recipeEntity);
|
|
return this.mapper.toDto(savedEntity);
|
|
}
|
|
|
|
/**
|
|
* Validates a recipe.
|
|
* A recipe must have a non-empty title, servings info and valid instructions and ingredients
|
|
* @param dto RecipeDTO
|
|
* @returns true if the recipe is valid
|
|
*/
|
|
private isRecipeDtoValid(dto: RecipeDto): boolean {
|
|
|
|
return dto.title !== undefined
|
|
&& dto.title.length !== 0
|
|
&& dto.amount !== undefined
|
|
&& dto.amountDescription !== undefined
|
|
&& dto.amountDescription.length !== 0
|
|
&& this.isInstructionsValid(dto.instructions)
|
|
&& this.isIngredientGroupsValid(dto.ingredientGroups);
|
|
}
|
|
|
|
/**
|
|
* Validates the ingredient groups if a recipe - each group must contain a valid ingredient list
|
|
* @param ingredientGroups Array of ingredient groups
|
|
* @returns true if all ingredient groups are valid
|
|
*/
|
|
private isIngredientGroupsValid(ingredientGroups: RecipeIngredientGroupDto[] | undefined): boolean {
|
|
return ingredientGroups !== undefined
|
|
&& ingredientGroups.length !== 0
|
|
&& ingredientGroups.every(group => {
|
|
return (
|
|
group.sortOrder !== undefined
|
|
&& this.isIngredientsValid(group.ingredients)
|
|
);
|
|
});
|
|
}
|
|
/**
|
|
* Validates an ingredient list ensuring that it is present, contains at least one ingredient and
|
|
* that all ingredients in the list are valid
|
|
* @param ingredients Array if ingredients
|
|
* @returns true if the ingredient list is valid
|
|
*/
|
|
private isIngredientsValid(ingredients: RecipeIngredientDto[] | undefined): boolean {
|
|
return ingredients !== undefined
|
|
&& ingredients.length !== 0
|
|
&& ingredients.every(ingredient => {
|
|
return this.isIngredientValid(ingredient);
|
|
});
|
|
}
|
|
/**
|
|
* Validates an ingredient - An ingredient must have a name and a sortOrder
|
|
* @param ingredient RecipeIngredientDto
|
|
* @returns true if the ingredient is valid
|
|
*/
|
|
private isIngredientValid(ingredient: RecipeIngredientDto): boolean {
|
|
return ingredient !== null
|
|
&& ingredient.sortOrder !== undefined
|
|
&& ingredient.name !== undefined
|
|
&& ingredient.name.length !== 0
|
|
}
|
|
// @todo create instruction handler/controller for validation?
|
|
/**
|
|
* Validates instructions
|
|
* The list must be present and must contain at least one value with a non-empty
|
|
* text field and sort order
|
|
* @param instructions Array if instruction step DTOs
|
|
* @returns Boolean indicating whether the instruction steps are valid
|
|
*/
|
|
private isInstructionsValid(instructions: RecipeInstructionStepDto[] | undefined): boolean {
|
|
return instructions !== undefined
|
|
&& instructions.length !== 0
|
|
&& instructions.every(step => {
|
|
return this.isInstructionStepValid(step);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Validates a single instruction step. Each step must have a well-defined non-empty text
|
|
* and a sort order
|
|
* @param step InstructionStepDto describing a single step of the recipe
|
|
* @returns true if the step is valid
|
|
*/
|
|
private isInstructionStepValid(step: RecipeInstructionStepDto): boolean {
|
|
return step !== null
|
|
&& step.text !== undefined
|
|
&& step.text.length !== 0
|
|
&& step.sortOrder !== undefined;
|
|
}
|
|
} |