diff --git a/bruno/recipe-backend/AuthRestResource/folder.bru b/bruno/recipe-backend/AuthRestResource/folder.bru index 1f467e3..caa99bd 100644 --- a/bruno/recipe-backend/AuthRestResource/folder.bru +++ b/bruno/recipe-backend/AuthRestResource/folder.bru @@ -1,5 +1,5 @@ meta { - name: AuthPoint + name: AuthRestResource seq: 2 } diff --git a/bruno/recipe-backend/CompactRecipeRestResource/folder.bru b/bruno/recipe-backend/CompactRecipeRestResource/folder.bru index 91c7c34..529703f 100644 --- a/bruno/recipe-backend/CompactRecipeRestResource/folder.bru +++ b/bruno/recipe-backend/CompactRecipeRestResource/folder.bru @@ -1,5 +1,5 @@ meta { - name: CompactRecipePoint + name: CompactRecipeRestResource seq: 4 } diff --git a/bruno/recipe-backend/CompactRecipeRestResource/getCompactRecipes.bru b/bruno/recipe-backend/CompactRecipeRestResource/getCompactRecipes.bru deleted file mode 100644 index b896439..0000000 --- a/bruno/recipe-backend/CompactRecipeRestResource/getCompactRecipes.bru +++ /dev/null @@ -1,19 +0,0 @@ -meta { - name: getCompactRecipes - type: http - seq: 2 -} - -get { - url: {{url}}/compact-recipe?search=kuchen - body: none - auth: inherit -} - -params:query { - search: kuchen -} - -settings { - encodeUrl: true -} diff --git a/bruno/recipe-backend/CompactRecipeRestResource/getListByFilter.bru b/bruno/recipe-backend/CompactRecipeRestResource/getListByFilter.bru new file mode 100644 index 0000000..6261f0c --- /dev/null +++ b/bruno/recipe-backend/CompactRecipeRestResource/getListByFilter.bru @@ -0,0 +1,22 @@ +meta { + name: getListByFilter + type: http + seq: 2 +} + +get { + url: {{url}}/compact-recipe/list-by-filter + body: json + auth: inherit +} + +body:json { + { + "searchString": "kuchen", + "tagIdList": ["7200f6e8-3cd0-439e-a395-d7ed43e29a3e"] + } +} + +settings { + encodeUrl: true +} diff --git a/bruno/recipe-backend/RecipeRestResourcePoint/createRecipe.bru b/bruno/recipe-backend/RecipeRestResource/createRecipe.bru similarity index 100% rename from bruno/recipe-backend/RecipeRestResourcePoint/createRecipe.bru rename to bruno/recipe-backend/RecipeRestResource/createRecipe.bru diff --git a/bruno/recipe-backend/RecipeRestResourcePoint/folder.bru b/bruno/recipe-backend/RecipeRestResource/folder.bru similarity index 61% rename from bruno/recipe-backend/RecipeRestResourcePoint/folder.bru rename to bruno/recipe-backend/RecipeRestResource/folder.bru index 88336f3..8c3ff12 100644 --- a/bruno/recipe-backend/RecipeRestResourcePoint/folder.bru +++ b/bruno/recipe-backend/RecipeRestResource/folder.bru @@ -1,5 +1,5 @@ meta { - name: RecipePoint + name: RecipeRestResource seq: 3 } diff --git a/bruno/recipe-backend/RecipeRestResourcePoint/getRecipeById.bru b/bruno/recipe-backend/RecipeRestResource/getRecipeById.bru similarity index 100% rename from bruno/recipe-backend/RecipeRestResourcePoint/getRecipeById.bru rename to bruno/recipe-backend/RecipeRestResource/getRecipeById.bru diff --git a/bruno/recipe-backend/RecipeRestResourcePoint/updateRecipe.bru b/bruno/recipe-backend/RecipeRestResource/updateRecipe.bru similarity index 100% rename from bruno/recipe-backend/RecipeRestResourcePoint/updateRecipe.bru rename to bruno/recipe-backend/RecipeRestResource/updateRecipe.bru diff --git a/bruno/recipe-backend/TagRestResource/createOrUpdate.bru b/bruno/recipe-backend/TagRestResource/createOrUpdate.bru index d04999f..3db47de 100644 --- a/bruno/recipe-backend/TagRestResource/createOrUpdate.bru +++ b/bruno/recipe-backend/TagRestResource/createOrUpdate.bru @@ -12,7 +12,7 @@ post { body:json { { - "description": "Kuchen" + "description": "Salat" } } diff --git a/bruno/recipe-backend/UserRestPoint/changePassword.bru b/bruno/recipe-backend/UserRestResource/changePassword.bru similarity index 100% rename from bruno/recipe-backend/UserRestPoint/changePassword.bru rename to bruno/recipe-backend/UserRestResource/changePassword.bru diff --git a/bruno/recipe-backend/UserRestPoint/createUser.bru b/bruno/recipe-backend/UserRestResource/createUser.bru similarity index 100% rename from bruno/recipe-backend/UserRestPoint/createUser.bru rename to bruno/recipe-backend/UserRestResource/createUser.bru diff --git a/bruno/recipe-backend/UserRestPoint/folder.bru b/bruno/recipe-backend/UserRestResource/folder.bru similarity index 63% rename from bruno/recipe-backend/UserRestPoint/folder.bru rename to bruno/recipe-backend/UserRestResource/folder.bru index 3a22d7c..01eb5e4 100644 --- a/bruno/recipe-backend/UserRestPoint/folder.bru +++ b/bruno/recipe-backend/UserRestResource/folder.bru @@ -1,5 +1,5 @@ meta { - name: UserPoint + name: UserRestResource seq: 5 } diff --git a/bruno/recipe-backend/UserRestPoint/getAllUsers.bru b/bruno/recipe-backend/UserRestResource/getAllUsers.bru similarity index 100% rename from bruno/recipe-backend/UserRestPoint/getAllUsers.bru rename to bruno/recipe-backend/UserRestResource/getAllUsers.bru diff --git a/bruno/recipe-backend/UserRestPoint/me.bru b/bruno/recipe-backend/UserRestResource/me.bru similarity index 100% rename from bruno/recipe-backend/UserRestPoint/me.bru rename to bruno/recipe-backend/UserRestResource/me.bru diff --git a/bruno/recipe-backend/UserRestPoint/updateUser.bru b/bruno/recipe-backend/UserRestResource/updateUser.bru similarity index 100% rename from bruno/recipe-backend/UserRestPoint/updateUser.bru rename to bruno/recipe-backend/UserRestResource/updateUser.bru diff --git a/bruno/recipe-backend/collection.bru b/bruno/recipe-backend/collection.bru index 04196f0..8d3c6b9 100644 --- a/bruno/recipe-backend/collection.bru +++ b/bruno/recipe-backend/collection.bru @@ -10,6 +10,9 @@ script:pre-request { try{ // An dieser Stelle muss überprüft werden, ob diese Funktion gerade aufgerufen wird, ansonsten entsteht eine Endlosschleife. const blocked = bru.getEnvVar("blocked"); + console.log("blocked", blocked) + console.log("current date", new Date()) + console.log("expire date", bru.getEnvVar("tokenExpireDate")) if(blocked === "false" && new Date().valueOf() > Number(bru.getEnvVar("tokenExpireDate"))){ console.log('new Session') diff --git a/src/api/dtos/CompactRecipeFilterRequest.ts b/src/api/dtos/CompactRecipeFilterRequest.ts new file mode 100644 index 0000000..0d162c7 --- /dev/null +++ b/src/api/dtos/CompactRecipeFilterRequest.ts @@ -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[]; +} \ No newline at end of file diff --git a/src/api/dtos/CompactRecipeFilterResponse.ts b/src/api/dtos/CompactRecipeFilterResponse.ts new file mode 100644 index 0000000..408ba93 --- /dev/null +++ b/src/api/dtos/CompactRecipeFilterResponse.ts @@ -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[]; +} \ No newline at end of file diff --git a/src/api/endpoints/CompactRecipeRestResource.ts b/src/api/endpoints/CompactRecipeRestResource.ts index 1f282e9..d28b02d 100644 --- a/src/api/endpoints/CompactRecipeRestResource.ts +++ b/src/api/endpoints/CompactRecipeRestResource.ts @@ -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; \ No newline at end of file diff --git a/src/handlers/CompactRecipeHandler.ts b/src/handlers/CompactRecipeHandler.ts index f83a490..a31d950 100644 --- a/src/handlers/CompactRecipeHandler.ts +++ b/src/handlers/CompactRecipeHandler.ts @@ -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 { + 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; + } } \ No newline at end of file diff --git a/src/repositories/RecipeRepository.ts b/src/repositories/RecipeRepository.ts index f4226c1..fc13a3e 100644 --- a/src/repositories/RecipeRepository.ts +++ b/src/repositories/RecipeRepository.ts @@ -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 { - 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 { - 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 { + 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{ - // @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 { + const qb = this.repo.createQueryBuilder("recipe"); - /** - * Update recipe and relations - * @param entity Updated entity - * @returns Updated Entity - */ - async updateRecipe(entity: RecipeEntity): Promise { - 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 { + 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); + }); + } } \ No newline at end of file