add list mappers

This commit is contained in:
araemer 2026-02-27 20:00:35 +01:00
parent c944b5c6b7
commit 2fbb1523fd
5 changed files with 142 additions and 152 deletions

View file

@ -14,22 +14,13 @@ export const compactRecipeBasicRoute = "/compact-recipe"
const router = Router(); const router = Router();
// Inject repo + mapper here // Inject repo + mapper here
const compactRecipeHandler = new CompactRecipeHandler(new RecipeRepository(), new CompactRecipeDtoEntityMapper()); const recipeRepository = new RecipeRepository();
/** const compactRecipeDtoEntityMapper = new CompactRecipeDtoEntityMapper();
* Load header data of all recipes const compactRecipeHandler = new CompactRecipeHandler(recipeRepository, compactRecipeDtoEntityMapper);
* 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);
})
);
/**
* Load recipe header data of all recipes matching the filter. If no filter is given, all recipes are returned.
*/
router.get( router.get(
"/list-by-filter", "/list-by-filter",
asyncHandler(async (req , res) => { asyncHandler(async (req , res) => {

View file

@ -16,46 +16,25 @@ export class CompactRecipeHandler {
/** /**
* Load list of all recipes * Load list of all recipes
* @returns List of all recipes * @returns List of all recipes as CompactRecipeDtos
*/ */
async getAllCompactRecipes() { async getAllCompactRecipes(): Promise<CompactRecipeDto[]> {
const recipeEntities: RecipeEntity[] = await this.repository.findAll(); const recipeEntities: RecipeEntity[] = await this.repository.findAll();
let recipeDtos: CompactRecipeDto[] = []; return this.mapper.mapEntityListToDtoList(recipeEntities);
recipeEntities.forEach(recipeEntity => {
recipeDtos.push(this.mapper.toDto(recipeEntity));
});
return recipeDtos;
} }
/** /**
* Get all recipes matching search * Get all recipes matching the given filter criteria.
* * @param request Filter request containing optional search string and tag ID list
* Recipe title must contain type string * @returns CompactRecipeFilterResponse containing the matching recipes
* @todo Full text search??
*/ */
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<CompactRecipeFilterResponse> { async getRecipesByFilter(request: CompactRecipeFilterRequest): Promise<CompactRecipeFilterResponse> {
const searchString = request.searchString; const recipeEntities: RecipeEntity[] = await this.repository.findCompactRecipeByFilter(
const tagIdList = request.tagIdList; request.searchString,
var recipeEntities : RecipeEntity[] = await this.repository.findCompactRecipeByFilter(searchString, tagIdList); request.tagIdList
);
const response = new CompactRecipeFilterResponse(); const response = new CompactRecipeFilterResponse();
// @todo move list mapping to mapper response.compactRecipeList = this.mapper.mapEntityListToDtoList(recipeEntities);
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; return response;
} }
} }

View file

@ -26,14 +26,32 @@ export abstract class AbstractDtoEntityMapper<
} }
/** /**
* Merge entity list with changes contained in DTO list * 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 dtos List of dtos
* @param entities List of entities * @param entities List of entities
* @returns Merged list * @returns Merged list
* *
* elements no longer contained in the dto list will be removed from the entity 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 * New elements will be mapped to entity and added to the entity list.
* existing elements will be updated * Existing elements will be updated.
*/ */
mergeDtoListIntoEntityList(dtos: D[], entities: E[]): E[] { mergeDtoListIntoEntityList(dtos: D[], entities: E[]): E[] {
const updatedEntities: E[] = []; const updatedEntities: E[] = [];
@ -55,27 +73,31 @@ export abstract class AbstractDtoEntityMapper<
} }
// Abstract methods to be implemented by subclasses // Abstract methods to be implemented by subclasses
/** /**
* Maps an entity to DTO * Maps an entity to DTO.
* @param entity Entity that is mapped to DTO * @param entity Entity that is mapped to DTO
*/ */
abstract toDto(entity: E): D; abstract toDto(entity: E): D;
/** /**
* Maps a DTO to entity * Maps a DTO to entity.
* @param dto DTO to map to entity * @param dto DTO to map to entity
*/ */
abstract toEntity(dto: D): E; abstract toEntity(dto: D): E;
/** /**
* Merge changes in DTO into entity * Merge changes in DTO into entity.
* @param dto Dto containing changes * @param dto Dto containing changes
* @param entity existing entity * @param entity existing entity
* *
* Used for merging user changes (DTO) into the existing entity (database). * Used for merging user changes (DTO) into the existing entity (database).
*/ */
abstract mergeDtoIntoEntity(dto: D, entity: E): E; abstract mergeDtoIntoEntity(dto: D, entity: E): E;
/** /**
* Defines how to create a new entity. Required by mergeDtoListIntoEntityList * Defines how to create a new entity. Required by mergeDtoListIntoEntityList
* to add new elements to the list * to add new elements to the list.
*/ */
abstract createNewEntity(): E; abstract createNewEntity(): E;
} }

View file

@ -3,6 +3,9 @@ import { RecipeEntity } from "../entities/RecipeEntity.js";
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js"; import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";
export class CompactRecipeDtoEntityMapper extends AbstractDtoEntityMapper<RecipeEntity, CompactRecipeDto> { export class CompactRecipeDtoEntityMapper extends AbstractDtoEntityMapper<RecipeEntity, CompactRecipeDto> {
constructor() {
super();
}
toDto(entity: RecipeEntity): CompactRecipeDto { toDto(entity: RecipeEntity): CompactRecipeDto {
const dto = new CompactRecipeDto(); const dto = new CompactRecipeDto();
@ -29,4 +32,7 @@ export class CompactRecipeDtoEntityMapper extends AbstractDtoEntityMapper<Recipe
throw new Error("Mapping CompactRecipeDto to RecipeEntity is not allowed!"); throw new Error("Mapping CompactRecipeDto to RecipeEntity is not allowed!");
} }
mapDtoListToEntityList(dtos: CompactRecipeDto[]): RecipeEntity[] {
throw new Error("Mapping CompactRecipeDto to RecipeEntity is not allowed!");
}
} }

View file

@ -6,7 +6,6 @@ import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";
import { RecipeIngredientGroupDtoEntityMapper } from "./RecipeIngredientGroupDtoEntityMapper.js"; import { RecipeIngredientGroupDtoEntityMapper } from "./RecipeIngredientGroupDtoEntityMapper.js";
import { RecipeInstructionStepDtoEntityMapper } from "./RecipeInstructionStepDtoEntityMapper.js"; import { RecipeInstructionStepDtoEntityMapper } from "./RecipeInstructionStepDtoEntityMapper.js";
import { TagDtoEntityMapper } from "./TagDtoEntityMapper.js"; import { TagDtoEntityMapper } from "./TagDtoEntityMapper.js";
import {TagEntity} from "../entities/TagEntity.js";
export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper<RecipeEntity, RecipeDto> { export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper<RecipeEntity, RecipeDto> {
constructor( constructor(
@ -40,9 +39,7 @@ export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper<RecipeEntity,
}); });
// map tags // map tags
dto.tagList = (entity.tagList ?? []).map((tagEntity) => dto.tagList = this.tagMapper.mapEntityListToDtoList(entity.tagList ?? []);
this.tagMapper.toDto(tagEntity)
);
return dto; return dto;
} }
@ -90,9 +87,7 @@ export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper<RecipeEntity,
}); });
// map tags // map tags
entity.tagList = (dto.tagList ?? []).map((tagDto) => entity.tagList = this.tagMapper.mapDtoListToEntityList(dto.tagList ?? []);
this.tagMapper.toEntity(tagDto)
);
return entity; return entity;
} }
@ -115,15 +110,12 @@ export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper<RecipeEntity,
); );
// --- Tags --- // --- Tags ---
/* Only build a stub here instead of using the mapper to avoid causing a cascade on the tag table while // Tags are looked up by ID and replaced wholesale: the recipe holds
* updating the join table correctly. For this purpose, the ORM only needs to know the tag ID in order to // references to existing tag entities, so we map each DTO to its entity
* signal that we are dealing with an existing tag. // form and let TypeORM sync the join table on save.
*/ entity.tagList = (dto.tagList ?? []).map((tagDto) =>
entity.tagList = (dto.tagList ?? []).map((tagDto) => { this.tagMapper.toEntity(tagDto)
const stub = new TagEntity(); );
stub.id = tagDto.id;
return stub;
});
return entity; return entity;
} }