Implement compact-recipe/list-by-filter

This commit is contained in:
araemer 2026-02-27 19:52:05 +01:00
parent 8ae6548dec
commit c944b5c6b7
21 changed files with 151 additions and 76 deletions

View file

@ -1,5 +1,5 @@
meta {
name: AuthPoint
name: AuthRestResource
seq: 2
}

View file

@ -1,5 +1,5 @@
meta {
name: CompactRecipePoint
name: CompactRecipeRestResource
seq: 4
}

View file

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

View file

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

View file

@ -1,5 +1,5 @@
meta {
name: RecipePoint
name: RecipeRestResource
seq: 3
}

View file

@ -12,7 +12,7 @@ post {
body:json {
{
"description": "Kuchen"
"description": "Salat"
}
}

View file

@ -1,5 +1,5 @@
meta {
name: UserPoint
name: UserRestResource
seq: 5
}

View file

@ -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')

View 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[];
}

View 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[];
}

View file

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

View file

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

View file

@ -1,7 +1,7 @@
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() {
@ -14,27 +14,51 @@ export class RecipeRepository extends AbstractRepository<RecipeEntity> {
* @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,
return this.repo.findOne({
where: { id } as any,
relations: [
'ingredientGroups',
'ingredientGroups.ingredients',
'instructionSteps',
'tagList',
]
"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
* 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 findCompactRecipeBySearch(searchString : string): Promise<RecipeEntity[] | null>{
// @todo doesn't work like expected...
return this.repo.find(
{ where: {title: ILike(`%${searchString}%`)}}
async findCompactRecipeByFilter(
searchString: string | undefined,
tagIdList: string[] | UUID[] | undefined
): Promise<RecipeEntity[]> {
const qb = this.repo.createQueryBuilder("recipe");
// Filter by title if a search string is provided
if (searchString !== undefined && searchString.length > 0) {
qb.andWhere("recipe.title ILIKE :title", { title: `%${searchString}%` });
}
// 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();
}
/**
@ -54,6 +78,5 @@ export class RecipeRepository extends AbstractRepository<RecipeEntity> {
this.repo.merge(existing, entity);
return this.repo.save(existing);
});
}
}
}