add list mappers
This commit is contained in:
parent
c944b5c6b7
commit
2fbb1523fd
5 changed files with 142 additions and 152 deletions
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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<CompactRecipeDto[]> {
|
||||
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<CompactRecipeFilterResponse> {
|
||||
const searchString = request.searchString;
|
||||
const tagIdList = request.tagIdList;
|
||||
var recipeEntities : RecipeEntity[] = await this.repository.findCompactRecipeByFilter(searchString, tagIdList);
|
||||
async getRecipesByFilter(request: CompactRecipeFilterRequest): Promise<CompactRecipeFilterResponse> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<RecipeEntity,CompactRecipeDto>{
|
||||
export class CompactRecipeDtoEntityMapper extends AbstractDtoEntityMapper<RecipeEntity, CompactRecipeDto> {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
toDto(entity: RecipeEntity): CompactRecipeDto {
|
||||
const dto = new CompactRecipeDto();
|
||||
|
|
@ -13,20 +16,23 @@ export class CompactRecipeDtoEntityMapper extends AbstractDtoEntityMapper<Recipe
|
|||
return dto;
|
||||
}
|
||||
|
||||
toEntity(dto: CompactRecipeDto): RecipeEntity {
|
||||
throw new Error("Mapping CompactRecipeDto to RecipeEntity is not allowed!");
|
||||
}
|
||||
toEntity(dto: CompactRecipeDto): RecipeEntity {
|
||||
throw new Error("Mapping CompactRecipeDto to RecipeEntity is not allowed!");
|
||||
}
|
||||
|
||||
createNewEntity() : RecipeEntity {
|
||||
throw new Error("Mapping CompactRecipeDto to RecipeEntity is not allowed!");
|
||||
}
|
||||
createNewEntity(): RecipeEntity {
|
||||
throw new Error("Mapping CompactRecipeDto to RecipeEntity is not allowed!");
|
||||
}
|
||||
|
||||
mergeDtoIntoEntity(dto: CompactRecipeDto, entity: RecipeEntity): RecipeEntity {
|
||||
throw new Error("Mapping CompactRecipeDto to RecipeEntity is not allowed!");
|
||||
}
|
||||
mergeDtoIntoEntity(dto: CompactRecipeDto, entity: RecipeEntity): RecipeEntity {
|
||||
throw new Error("Mapping CompactRecipeDto to RecipeEntity is not allowed!");
|
||||
}
|
||||
|
||||
mergeDtoListIntoEntityList(dtos: CompactRecipeDto[], entities: RecipeEntity[]) : RecipeEntity[]{
|
||||
throw new Error("Mapping CompactRecipeDto to RecipeEntity is not allowed!");
|
||||
}
|
||||
mergeDtoListIntoEntityList(dtos: CompactRecipeDto[], entities: RecipeEntity[]): RecipeEntity[] {
|
||||
throw new Error("Mapping CompactRecipeDto to RecipeEntity is not allowed!");
|
||||
}
|
||||
|
||||
mapDtoListToEntityList(dtos: CompactRecipeDto[]): RecipeEntity[] {
|
||||
throw new Error("Mapping CompactRecipeDto to RecipeEntity is not allowed!");
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@ import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";
|
|||
import { RecipeIngredientGroupDtoEntityMapper } from "./RecipeIngredientGroupDtoEntityMapper.js";
|
||||
import { RecipeInstructionStepDtoEntityMapper } from "./RecipeInstructionStepDtoEntityMapper.js";
|
||||
import { TagDtoEntityMapper } from "./TagDtoEntityMapper.js";
|
||||
import {TagEntity} from "../entities/TagEntity.js";
|
||||
|
||||
export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper<RecipeEntity, RecipeDto> {
|
||||
constructor(
|
||||
|
|
@ -40,9 +39,7 @@ export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper<RecipeEntity,
|
|||
});
|
||||
|
||||
// map tags
|
||||
dto.tagList = (entity.tagList ?? []).map((tagEntity) =>
|
||||
this.tagMapper.toDto(tagEntity)
|
||||
);
|
||||
dto.tagList = this.tagMapper.mapEntityListToDtoList(entity.tagList ?? []);
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
|
@ -90,9 +87,7 @@ export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper<RecipeEntity,
|
|||
});
|
||||
|
||||
// map tags
|
||||
entity.tagList = (dto.tagList ?? []).map((tagDto) =>
|
||||
this.tagMapper.toEntity(tagDto)
|
||||
);
|
||||
entity.tagList = this.tagMapper.mapDtoListToEntityList(dto.tagList ?? []);
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
|
@ -115,15 +110,12 @@ export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper<RecipeEntity,
|
|||
);
|
||||
|
||||
// --- Tags ---
|
||||
/* Only build a stub here instead of using the mapper to avoid causing a cascade on the tag table while
|
||||
* updating the join table correctly. For this purpose, the ORM only needs to know the tag ID in order to
|
||||
* signal that we are dealing with an existing tag.
|
||||
*/
|
||||
entity.tagList = (dto.tagList ?? []).map((tagDto) => {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue