Implement compact-recipe/list-by-filter
This commit is contained in:
parent
8ae6548dec
commit
c944b5c6b7
21 changed files with 151 additions and 76 deletions
13
src/api/dtos/CompactRecipeFilterRequest.ts
Normal file
13
src/api/dtos/CompactRecipeFilterRequest.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Request wrapper for searching recipes based on a filter
|
||||
*/
|
||||
export class CompactRecipeFilterRequest {
|
||||
/**
|
||||
* Search string applied to the recipe title
|
||||
*/
|
||||
searchString?: string;
|
||||
/**
|
||||
* List of tags that must be applied to the recipe
|
||||
*/
|
||||
tagIdList?: string[];
|
||||
}
|
||||
8
src/api/dtos/CompactRecipeFilterResponse.ts
Normal file
8
src/api/dtos/CompactRecipeFilterResponse.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import {CompactRecipeDto} from "./CompactRecipeDto.js";
|
||||
|
||||
/**
|
||||
* Filter response containing a list of all recipes matching the search
|
||||
*/
|
||||
export class CompactRecipeFilterResponse {
|
||||
compactRecipeList! : CompactRecipeDto[];
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ import { RecipeRepository } from "../../repositories/RecipeRepository.js";
|
|||
import { CompactRecipeHandler } from "../../handlers/CompactRecipeHandler.js";
|
||||
import { CompactRecipeDtoEntityMapper } from "../../mappers/CompactRecipeDtoEntityMapper.js";
|
||||
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
|
||||
import {CompactRecipeFilterRequest} from "../dtos/CompactRecipeFilterRequest.js";
|
||||
import {CompactRecipeFilterResponse} from "../dtos/CompactRecipeFilterResponse.js";
|
||||
|
||||
export const compactRecipeBasicRoute = "/compact-recipe"
|
||||
/**
|
||||
|
|
@ -16,8 +18,6 @@ const compactRecipeHandler = new CompactRecipeHandler(new RecipeRepository(), ne
|
|||
/**
|
||||
* Load header data of all recipes
|
||||
* Responds with a list of CompactRecipeDtos
|
||||
* @todo request wrapper DTO
|
||||
* @todo response wrapper DTO
|
||||
*/
|
||||
router.get(
|
||||
"/",
|
||||
|
|
@ -30,4 +30,12 @@ router.get(
|
|||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/list-by-filter",
|
||||
asyncHandler(async (req , res) => {
|
||||
const request : CompactRecipeFilterRequest = req.body;
|
||||
const response : CompactRecipeFilterResponse = await compactRecipeHandler.getRecipesByFilter(request);
|
||||
res.status(HttpStatusCode.OK).json(response);
|
||||
})
|
||||
)
|
||||
export default router;
|
||||
|
|
@ -2,6 +2,8 @@ import { CompactRecipeDto } from "../api/dtos/CompactRecipeDto.js";
|
|||
import { RecipeEntity } from "../entities/RecipeEntity.js";
|
||||
import { CompactRecipeDtoEntityMapper } from "../mappers/CompactRecipeDtoEntityMapper.js";
|
||||
import { RecipeRepository } from "../repositories/RecipeRepository.js";
|
||||
import {CompactRecipeFilterRequest} from "../api/dtos/CompactRecipeFilterRequest.js";
|
||||
import {CompactRecipeFilterResponse} from "../api/dtos/CompactRecipeFilterResponse.js";
|
||||
|
||||
/**
|
||||
* Responsible for loading recipe header data
|
||||
|
|
@ -18,7 +20,6 @@ export class CompactRecipeHandler {
|
|||
*/
|
||||
async getAllCompactRecipes() {
|
||||
const recipeEntities: RecipeEntity[] = await this.repository.findAll();
|
||||
// @todo load instruction steps, ingredient groups and ingredients before mapping!
|
||||
let recipeDtos: CompactRecipeDto[] = [];
|
||||
recipeEntities.forEach(recipeEntity => {
|
||||
recipeDtos.push(this.mapper.toDto(recipeEntity));
|
||||
|
|
@ -38,7 +39,23 @@ export class CompactRecipeHandler {
|
|||
// get all
|
||||
return this.getAllCompactRecipes();
|
||||
} else {
|
||||
return this.repository.findCompactRecipeBySearch(searchString);
|
||||
const tagIdList : string[] = [];
|
||||
return this.repository.findCompactRecipeByFilter(searchString, tagIdList);
|
||||
}
|
||||
}
|
||||
|
||||
async getRecipesByFilter(request: CompactRecipeFilterRequest) : Promise<CompactRecipeFilterResponse> {
|
||||
const searchString = request.searchString;
|
||||
const tagIdList = request.tagIdList;
|
||||
var recipeEntities : RecipeEntity[] = await this.repository.findCompactRecipeByFilter(searchString, tagIdList);
|
||||
const response = new CompactRecipeFilterResponse();
|
||||
// @todo move list mapping to mapper
|
||||
let recipeDtos: CompactRecipeDto[] = [];
|
||||
// Add mapper function to map the result list.
|
||||
recipeEntities.forEach(recipeEntity => {
|
||||
recipeDtos.push(this.mapper.toDto(recipeEntity));
|
||||
});
|
||||
response.compactRecipeList = recipeDtos;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +1,82 @@
|
|||
import { AbstractRepository } from "./AbstractRepository.js";
|
||||
import { RecipeEntity } from "../entities/RecipeEntity.js";
|
||||
import { AppDataSource } from "../data-source.js";
|
||||
import { ILike } from "typeorm";
|
||||
import { UUID } from "crypto";
|
||||
|
||||
export class RecipeRepository extends AbstractRepository<RecipeEntity> {
|
||||
constructor() {
|
||||
super(RecipeEntity);
|
||||
}
|
||||
constructor() {
|
||||
super(RecipeEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find recipe including all relations by id
|
||||
* @param id Recipe id
|
||||
* @returns RecipeEntity including all relations such as ingredients groups, ingredients and instruction steps
|
||||
*/
|
||||
async findById(id: string): Promise<RecipeEntity | null> {
|
||||
return this.repo.findOne(
|
||||
{ where: { id } as any,
|
||||
relations: [
|
||||
'ingredientGroups',
|
||||
'ingredientGroups.ingredients',
|
||||
'instructionSteps',
|
||||
'tagList',
|
||||
]
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Find recipe including all relations by id
|
||||
* @param id Recipe id
|
||||
* @returns RecipeEntity including all relations such as ingredients groups, ingredients and instruction steps
|
||||
*/
|
||||
async findById(id: string): Promise<RecipeEntity | null> {
|
||||
return this.repo.findOne({
|
||||
where: { id } as any,
|
||||
relations: [
|
||||
"ingredientGroups",
|
||||
"ingredientGroups.ingredients",
|
||||
"instructionSteps",
|
||||
"tagList",
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all recipes matching the search. Currently it only searches on the title. Fetches only recipe header data but no relations.
|
||||
* @param searchString String to search for
|
||||
* @returns List of recipe entities matching the search criteria
|
||||
*/
|
||||
async findCompactRecipeBySearch(searchString : string): Promise<RecipeEntity[] | null>{
|
||||
// @todo doesn't work like expected...
|
||||
return this.repo.find(
|
||||
{ where: {title: ILike(`%${searchString}%`)}}
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Find all recipes matching the search. Currently, it only searches on the title. Fetches only recipe header data but no relations.
|
||||
* @param searchString String to search for. Might be undefined if no search is applied (all recipes or filter for tags only)
|
||||
* @param tagIdList List of tags. The recipe must have all the tags to be taken into account. May be undefined if not filtering for tags
|
||||
* @returns List of recipe entities matching the filter criteria
|
||||
*/
|
||||
async findCompactRecipeByFilter(
|
||||
searchString: string | undefined,
|
||||
tagIdList: string[] | UUID[] | undefined
|
||||
): Promise<RecipeEntity[]> {
|
||||
const qb = this.repo.createQueryBuilder("recipe");
|
||||
|
||||
/**
|
||||
* Update recipe and relations
|
||||
* @param entity Updated entity
|
||||
* @returns Updated Entity
|
||||
*/
|
||||
async updateRecipe(entity: RecipeEntity): Promise<RecipeEntity> {
|
||||
return AppDataSource.transaction(async (em) => {
|
||||
// load existing data
|
||||
const existing = await this.repo.findOneOrFail({
|
||||
where: { id: entity.id },
|
||||
relations: ["instructionSteps", "ingredientGroups", "ingredientGroups.ingredients", "tagList"],
|
||||
});
|
||||
// Filter by title if a search string is provided
|
||||
if (searchString !== undefined && searchString.length > 0) {
|
||||
qb.andWhere("recipe.title ILIKE :title", { title: `%${searchString}%` });
|
||||
}
|
||||
|
||||
// merge new entity and existing entity
|
||||
this.repo.merge(existing, entity);
|
||||
return this.repo.save(existing);
|
||||
});
|
||||
}
|
||||
// Filter by tags: the recipe must have ALL tags in the list
|
||||
if (tagIdList !== undefined && tagIdList.length > 0) {
|
||||
// Join recipe_tag for each required tag individually so that only
|
||||
// recipes possessing every tag are returned (AND semantics, not OR)
|
||||
tagIdList.forEach((tagId, index) => {
|
||||
const alias = `tag_${index}`;
|
||||
const param = `tagId_${index}`;
|
||||
qb.innerJoin(
|
||||
"recipe.tagList",
|
||||
alias,
|
||||
`${alias}.id = :${param}`,
|
||||
{ [param]: tagId }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return qb.getMany();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update recipe and relations
|
||||
* @param entity Updated entity
|
||||
* @returns Updated Entity
|
||||
*/
|
||||
async updateRecipe(entity: RecipeEntity): Promise<RecipeEntity> {
|
||||
return AppDataSource.transaction(async (em) => {
|
||||
// load existing data
|
||||
const existing = await this.repo.findOneOrFail({
|
||||
where: { id: entity.id },
|
||||
relations: ["instructionSteps", "ingredientGroups", "ingredientGroups.ingredients", "tagList"],
|
||||
});
|
||||
|
||||
// merge new entity and existing entity
|
||||
this.repo.merge(existing, entity);
|
||||
return this.repo.save(existing);
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue