diff --git a/src/api/endpoints/CompactRecipeRestResource.ts b/src/api/endpoints/CompactRecipeRestResource.ts index d28b02d..ade518f 100644 --- a/src/api/endpoints/CompactRecipeRestResource.ts +++ b/src/api/endpoints/CompactRecipeRestResource.ts @@ -14,22 +14,13 @@ export const compactRecipeBasicRoute = "/compact-recipe" const router = Router(); // Inject repo + mapper here -const compactRecipeHandler = new CompactRecipeHandler(new RecipeRepository(), new CompactRecipeDtoEntityMapper()); -/** - * Load header data of all recipes - * Responds with a list of CompactRecipeDtos - */ -router.get( - "/", - asyncHandler(async (req, res) => { - // extract search string from query parameters, convert to lower case for case insensitive search - const searchString : string = req.query.search ? req.query.search.toString().toLowerCase() : ""; - console.log("Searching for recipes with title containing", searchString) - const response = await compactRecipeHandler.getMatchingRecipes(searchString); - res.status(HttpStatusCode.OK).json(response); - }) -); +const recipeRepository = new RecipeRepository(); +const compactRecipeDtoEntityMapper = new CompactRecipeDtoEntityMapper(); +const compactRecipeHandler = new CompactRecipeHandler(recipeRepository, compactRecipeDtoEntityMapper); +/** + * Load recipe header data of all recipes matching the filter. If no filter is given, all recipes are returned. + */ router.get( "/list-by-filter", asyncHandler(async (req , res) => { diff --git a/src/handlers/CompactRecipeHandler.ts b/src/handlers/CompactRecipeHandler.ts index a31d950..5d659ee 100644 --- a/src/handlers/CompactRecipeHandler.ts +++ b/src/handlers/CompactRecipeHandler.ts @@ -2,8 +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"; +import { CompactRecipeFilterRequest } from "../api/dtos/CompactRecipeFilterRequest.js"; +import { CompactRecipeFilterResponse } from "../api/dtos/CompactRecipeFilterResponse.js"; /** * Responsible for loading recipe header data @@ -16,46 +16,25 @@ export class CompactRecipeHandler { /** * Load list of all recipes - * @returns List of all recipes + * @returns List of all recipes as CompactRecipeDtos */ - async getAllCompactRecipes() { + async getAllCompactRecipes(): Promise { const recipeEntities: RecipeEntity[] = await this.repository.findAll(); - let recipeDtos: CompactRecipeDto[] = []; - recipeEntities.forEach(recipeEntity => { - recipeDtos.push(this.mapper.toDto(recipeEntity)); - }); - return recipeDtos; + return this.mapper.mapEntityListToDtoList(recipeEntities); } /** - * Get all recipes matching search - * - * Recipe title must contain type string - * @todo Full text search?? - + * Get all recipes matching the given filter criteria. + * @param request Filter request containing optional search string and tag ID list + * @returns CompactRecipeFilterResponse containing the matching recipes */ - async getMatchingRecipes(searchString : string){ - if(!searchString || searchString.length===0){ - // get all - return this.getAllCompactRecipes(); - } else { - 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); + async getRecipesByFilter(request: CompactRecipeFilterRequest): Promise { + const recipeEntities: RecipeEntity[] = await this.repository.findCompactRecipeByFilter( + request.searchString, + request.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; + response.compactRecipeList = this.mapper.mapEntityListToDtoList(recipeEntities); return response; } } \ No newline at end of file diff --git a/src/mappers/AbstractDtoEntityMapper.ts b/src/mappers/AbstractDtoEntityMapper.ts index 935c341..99c85b8 100644 --- a/src/mappers/AbstractDtoEntityMapper.ts +++ b/src/mappers/AbstractDtoEntityMapper.ts @@ -2,80 +2,102 @@ import { AbstractDto } from "../api/dtos/AbstractDto.js"; import { AbstractEntity } from "../entities/AbstractEntity.js"; export abstract class AbstractDtoEntityMapper< - E extends AbstractEntity, - D extends AbstractDto + E extends AbstractEntity, + D extends AbstractDto > { - /** - * Map base entity fields (id, createdAt, updatedAt) to DTO. - */ - protected mapBaseEntityToDto(entity: E, dto: D): D { - dto.id = entity.id; - dto.createdAt = entity.createDate; - dto.updatedAt = entity.updateDate; - return dto; - } - - /** - * Map base DTO fields to entity. - */ - protected mapBaseDtoToEntity(dto: D, entity: E): E { - entity.id = dto.id; - entity.createDate = dto.createdAt; - entity.updateDate = dto.updatedAt; - return entity; - } - - /** - * Merge entity list with changes contained in DTO list - * @param dtos List of dtos - * @param entities List of entities - * @returns Merged list - * - * elements no longer contained in the dto list will be removed from the entity list - * new elements will be mapped to entity and added to the entity list - * existing elements will be updated - */ - mergeDtoListIntoEntityList(dtos: D[], entities: E[]) : E[]{ - const updatedEntities: E[] = []; - const existingMap = new Map(entities?.map(e => [e.id, e]) ?? []); - - for (const dto of dtos) { - if (dto.id && existingMap.has(dto.id)) { - // update existing - const entity = existingMap.get(dto.id)!; - updatedEntities.push(this.mergeDtoIntoEntity(dto, entity)); - } else { - // create new - const newEntity = this.createNewEntity(); - updatedEntities.push(this.mergeDtoIntoEntity(dto, newEntity)); - } + /** + * Map base entity fields (id, createdAt, updatedAt) to DTO. + */ + protected mapBaseEntityToDto(entity: E, dto: D): D { + dto.id = entity.id; + dto.createdAt = entity.createDate; + dto.updatedAt = entity.updateDate; + return dto; } - return updatedEntities; - } + /** + * Map base DTO fields to entity. + */ + protected mapBaseDtoToEntity(dto: D, entity: E): E { + entity.id = dto.id; + entity.createDate = dto.createdAt; + entity.updateDate = dto.updatedAt; + return entity; + } - // Abstract methods to be implemented by subclasses - /** - * Maps an entity to DTO - * @param entity Entity that is mapped to DTO - */ - abstract toDto(entity: E): D; - /** - * Maps a DTO to entity - * @param dto DTO to map to entity - */ - abstract toEntity(dto: D): E; - /** - * Merge changes in DTO into entity - * @param dto Dto containing changes - * @param entity existing entity - * - * Used for merging user changes (DTO) into the existing entity (database). - */ - abstract mergeDtoIntoEntity(dto: D, entity: E): E; - /** - * Defines how to create a new entity. Required by mergeDtoListIntoEntityList - * to add new elements to the list - */ - abstract createNewEntity() : E; -} + /** + * Maps a list of entities to a list of DTOs. + * @param entities List of entities to map + * @returns List of DTOs + */ + mapEntityListToDtoList(entities: E[]): D[] { + return entities.map(entity => this.toDto(entity)); + } + + /** + * Maps a list of DTOs to a list of entities. + * @param dtos List of DTOs to map + * @returns List of entities + */ + mapDtoListToEntityList(dtos: D[]): E[] { + return dtos.map(dto => this.toEntity(dto)); + } + + /** + * Merge entity list with changes contained in DTO list. + * @param dtos List of dtos + * @param entities List of entities + * @returns Merged list + * + * Elements no longer contained in the dto list will be removed from the entity list. + * New elements will be mapped to entity and added to the entity list. + * Existing elements will be updated. + */ + mergeDtoListIntoEntityList(dtos: D[], entities: E[]): E[] { + const updatedEntities: E[] = []; + const existingMap = new Map(entities?.map(e => [e.id, e]) ?? []); + + for (const dto of dtos) { + if (dto.id && existingMap.has(dto.id)) { + // update existing + const entity = existingMap.get(dto.id)!; + updatedEntities.push(this.mergeDtoIntoEntity(dto, entity)); + } else { + // create new + const newEntity = this.createNewEntity(); + updatedEntities.push(this.mergeDtoIntoEntity(dto, newEntity)); + } + } + + return updatedEntities; + } + + // Abstract methods to be implemented by subclasses + + /** + * Maps an entity to DTO. + * @param entity Entity that is mapped to DTO + */ + abstract toDto(entity: E): D; + + /** + * Maps a DTO to entity. + * @param dto DTO to map to entity + */ + abstract toEntity(dto: D): E; + + /** + * Merge changes in DTO into entity. + * @param dto Dto containing changes + * @param entity existing entity + * + * Used for merging user changes (DTO) into the existing entity (database). + */ + abstract mergeDtoIntoEntity(dto: D, entity: E): E; + + /** + * Defines how to create a new entity. Required by mergeDtoListIntoEntityList + * to add new elements to the list. + */ + abstract createNewEntity(): E; +} \ No newline at end of file diff --git a/src/mappers/CompactRecipeDtoEntityMapper.ts b/src/mappers/CompactRecipeDtoEntityMapper.ts index 3578df8..890423c 100644 --- a/src/mappers/CompactRecipeDtoEntityMapper.ts +++ b/src/mappers/CompactRecipeDtoEntityMapper.ts @@ -2,7 +2,10 @@ import { CompactRecipeDto } from "../api/dtos/CompactRecipeDto.js"; import { RecipeEntity } from "../entities/RecipeEntity.js"; import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js"; -export class CompactRecipeDtoEntityMapper extends AbstractDtoEntityMapper{ +export class CompactRecipeDtoEntityMapper extends AbstractDtoEntityMapper { + constructor() { + super(); + } toDto(entity: RecipeEntity): CompactRecipeDto { const dto = new CompactRecipeDto(); @@ -13,20 +16,23 @@ export class CompactRecipeDtoEntityMapper extends AbstractDtoEntityMapper { constructor( @@ -40,9 +39,7 @@ export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper - this.tagMapper.toDto(tagEntity) - ); + dto.tagList = this.tagMapper.mapEntityListToDtoList(entity.tagList ?? []); return dto; } @@ -90,9 +87,7 @@ export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper - this.tagMapper.toEntity(tagDto) - ); + entity.tagList = this.tagMapper.mapDtoListToEntityList(dto.tagList ?? []); return entity; } @@ -115,15 +110,12 @@ export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper { - const stub = new TagEntity(); - stub.id = tagDto.id; - return stub; - }); + // Tags are looked up by ID and replaced wholesale: the recipe holds + // references to existing tag entities, so we map each DTO to its entity + // form and let TypeORM sync the join table on save. + entity.tagList = (dto.tagList ?? []).map((tagDto) => + this.tagMapper.toEntity(tagDto) + ); return entity; }