Implement compact-recipe/list-by-filter
This commit is contained in:
parent
8ae6548dec
commit
c944b5c6b7
21 changed files with 151 additions and 76 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
meta {
|
meta {
|
||||||
name: AuthPoint
|
name: AuthRestResource
|
||||||
seq: 2
|
seq: 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
meta {
|
meta {
|
||||||
name: CompactRecipePoint
|
name: CompactRecipeRestResource
|
||||||
seq: 4
|
seq: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
meta {
|
meta {
|
||||||
name: RecipePoint
|
name: RecipeRestResource
|
||||||
seq: 3
|
seq: 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -12,7 +12,7 @@ post {
|
||||||
|
|
||||||
body:json {
|
body:json {
|
||||||
{
|
{
|
||||||
"description": "Kuchen"
|
"description": "Salat"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
meta {
|
meta {
|
||||||
name: UserPoint
|
name: UserRestResource
|
||||||
seq: 5
|
seq: 5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10,6 +10,9 @@ script:pre-request {
|
||||||
try{
|
try{
|
||||||
// An dieser Stelle muss überprüft werden, ob diese Funktion gerade aufgerufen wird, ansonsten entsteht eine Endlosschleife.
|
// An dieser Stelle muss überprüft werden, ob diese Funktion gerade aufgerufen wird, ansonsten entsteht eine Endlosschleife.
|
||||||
const blocked = bru.getEnvVar("blocked");
|
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"))){
|
if(blocked === "false" && new Date().valueOf() > Number(bru.getEnvVar("tokenExpireDate"))){
|
||||||
console.log('new Session')
|
console.log('new Session')
|
||||||
|
|
||||||
|
|
|
||||||
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 { CompactRecipeHandler } from "../../handlers/CompactRecipeHandler.js";
|
||||||
import { CompactRecipeDtoEntityMapper } from "../../mappers/CompactRecipeDtoEntityMapper.js";
|
import { CompactRecipeDtoEntityMapper } from "../../mappers/CompactRecipeDtoEntityMapper.js";
|
||||||
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
|
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
|
||||||
|
import {CompactRecipeFilterRequest} from "../dtos/CompactRecipeFilterRequest.js";
|
||||||
|
import {CompactRecipeFilterResponse} from "../dtos/CompactRecipeFilterResponse.js";
|
||||||
|
|
||||||
export const compactRecipeBasicRoute = "/compact-recipe"
|
export const compactRecipeBasicRoute = "/compact-recipe"
|
||||||
/**
|
/**
|
||||||
|
|
@ -16,8 +18,6 @@ const compactRecipeHandler = new CompactRecipeHandler(new RecipeRepository(), ne
|
||||||
/**
|
/**
|
||||||
* Load header data of all recipes
|
* Load header data of all recipes
|
||||||
* Responds with a list of CompactRecipeDtos
|
* Responds with a list of CompactRecipeDtos
|
||||||
* @todo request wrapper DTO
|
|
||||||
* @todo response wrapper DTO
|
|
||||||
*/
|
*/
|
||||||
router.get(
|
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;
|
export default router;
|
||||||
|
|
@ -2,6 +2,8 @@ import { CompactRecipeDto } from "../api/dtos/CompactRecipeDto.js";
|
||||||
import { RecipeEntity } from "../entities/RecipeEntity.js";
|
import { RecipeEntity } from "../entities/RecipeEntity.js";
|
||||||
import { CompactRecipeDtoEntityMapper } from "../mappers/CompactRecipeDtoEntityMapper.js";
|
import { CompactRecipeDtoEntityMapper } from "../mappers/CompactRecipeDtoEntityMapper.js";
|
||||||
import { RecipeRepository } from "../repositories/RecipeRepository.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
|
* Responsible for loading recipe header data
|
||||||
|
|
@ -18,7 +20,6 @@ export class CompactRecipeHandler {
|
||||||
*/
|
*/
|
||||||
async getAllCompactRecipes() {
|
async getAllCompactRecipes() {
|
||||||
const recipeEntities: RecipeEntity[] = await this.repository.findAll();
|
const recipeEntities: RecipeEntity[] = await this.repository.findAll();
|
||||||
// @todo load instruction steps, ingredient groups and ingredients before mapping!
|
|
||||||
let recipeDtos: CompactRecipeDto[] = [];
|
let recipeDtos: CompactRecipeDto[] = [];
|
||||||
recipeEntities.forEach(recipeEntity => {
|
recipeEntities.forEach(recipeEntity => {
|
||||||
recipeDtos.push(this.mapper.toDto(recipeEntity));
|
recipeDtos.push(this.mapper.toDto(recipeEntity));
|
||||||
|
|
@ -38,7 +39,23 @@ export class CompactRecipeHandler {
|
||||||
// get all
|
// get all
|
||||||
return this.getAllCompactRecipes();
|
return this.getAllCompactRecipes();
|
||||||
} else {
|
} 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 { AbstractRepository } from "./AbstractRepository.js";
|
||||||
import { RecipeEntity } from "../entities/RecipeEntity.js";
|
import { RecipeEntity } from "../entities/RecipeEntity.js";
|
||||||
import { AppDataSource } from "../data-source.js";
|
import { AppDataSource } from "../data-source.js";
|
||||||
import { ILike } from "typeorm";
|
import { UUID } from "crypto";
|
||||||
|
|
||||||
export class RecipeRepository extends AbstractRepository<RecipeEntity> {
|
export class RecipeRepository extends AbstractRepository<RecipeEntity> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(RecipeEntity);
|
super(RecipeEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find recipe including all relations by id
|
* Find recipe including all relations by id
|
||||||
* @param id Recipe id
|
* @param id Recipe id
|
||||||
* @returns RecipeEntity including all relations such as ingredients groups, ingredients and instruction steps
|
* @returns RecipeEntity including all relations such as ingredients groups, ingredients and instruction steps
|
||||||
*/
|
*/
|
||||||
async findById(id: string): Promise<RecipeEntity | null> {
|
async findById(id: string): Promise<RecipeEntity | null> {
|
||||||
return this.repo.findOne(
|
return this.repo.findOne({
|
||||||
{ where: { id } as any,
|
where: { id } as any,
|
||||||
relations: [
|
relations: [
|
||||||
'ingredientGroups',
|
"ingredientGroups",
|
||||||
'ingredientGroups.ingredients',
|
"ingredientGroups.ingredients",
|
||||||
'instructionSteps',
|
"instructionSteps",
|
||||||
'tagList',
|
"tagList",
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find all recipes matching the search. Currently it only searches on the title. Fetches only recipe header data but no relations.
|
* 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
|
* @param searchString String to search for. Might be undefined if no search is applied (all recipes or filter for tags only)
|
||||||
* @returns List of recipe entities matching the search criteria
|
* @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...
|
async findCompactRecipeByFilter(
|
||||||
return this.repo.find(
|
searchString: string | undefined,
|
||||||
{ where: {title: ILike(`%${searchString}%`)}}
|
tagIdList: string[] | UUID[] | undefined
|
||||||
);
|
): Promise<RecipeEntity[]> {
|
||||||
}
|
const qb = this.repo.createQueryBuilder("recipe");
|
||||||
|
|
||||||
/**
|
// Filter by title if a search string is provided
|
||||||
* Update recipe and relations
|
if (searchString !== undefined && searchString.length > 0) {
|
||||||
* @param entity Updated entity
|
qb.andWhere("recipe.title ILIKE :title", { title: `%${searchString}%` });
|
||||||
* @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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 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