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; } }