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();
|
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) => {
|
||||||
|
|
|
||||||
|
|
@ -2,8 +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 { CompactRecipeFilterRequest } from "../api/dtos/CompactRecipeFilterRequest.js";
|
||||||
import {CompactRecipeFilterResponse} from "../api/dtos/CompactRecipeFilterResponse.js";
|
import { CompactRecipeFilterResponse } from "../api/dtos/CompactRecipeFilterResponse.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Responsible for loading recipe header data
|
* Responsible for loading recipe header data
|
||||||
|
|
@ -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){
|
async getRecipesByFilter(request: CompactRecipeFilterRequest): Promise<CompactRecipeFilterResponse> {
|
||||||
if(!searchString || searchString.length===0){
|
const recipeEntities: RecipeEntity[] = await this.repository.findCompactRecipeByFilter(
|
||||||
// get all
|
request.searchString,
|
||||||
return this.getAllCompactRecipes();
|
request.tagIdList
|
||||||
} 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);
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,80 +2,102 @@ import { AbstractDto } from "../api/dtos/AbstractDto.js";
|
||||||
import { AbstractEntity } from "../entities/AbstractEntity.js";
|
import { AbstractEntity } from "../entities/AbstractEntity.js";
|
||||||
|
|
||||||
export abstract class AbstractDtoEntityMapper<
|
export abstract class AbstractDtoEntityMapper<
|
||||||
E extends AbstractEntity,
|
E extends AbstractEntity,
|
||||||
D extends AbstractDto
|
D extends AbstractDto
|
||||||
> {
|
> {
|
||||||
/**
|
/**
|
||||||
* Map base entity fields (id, createdAt, updatedAt) to DTO.
|
* Map base entity fields (id, createdAt, updatedAt) to DTO.
|
||||||
*/
|
*/
|
||||||
protected mapBaseEntityToDto(entity: E, dto: D): D {
|
protected mapBaseEntityToDto(entity: E, dto: D): D {
|
||||||
dto.id = entity.id;
|
dto.id = entity.id;
|
||||||
dto.createdAt = entity.createDate;
|
dto.createdAt = entity.createDate;
|
||||||
dto.updatedAt = entity.updateDate;
|
dto.updatedAt = entity.updateDate;
|
||||||
return dto;
|
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 a list of entities to a list of DTOs.
|
||||||
* Maps an entity to DTO
|
* @param entities List of entities to map
|
||||||
* @param entity Entity that is mapped to DTO
|
* @returns List of DTOs
|
||||||
*/
|
*/
|
||||||
abstract toDto(entity: E): D;
|
mapEntityListToDtoList(entities: E[]): D[] {
|
||||||
/**
|
return entities.map(entity => this.toDto(entity));
|
||||||
* Maps a DTO to entity
|
}
|
||||||
* @param dto DTO to map to entity
|
|
||||||
*/
|
/**
|
||||||
abstract toEntity(dto: D): E;
|
* Maps a list of DTOs to a list of entities.
|
||||||
/**
|
* @param dtos List of DTOs to map
|
||||||
* Merge changes in DTO into entity
|
* @returns List of entities
|
||||||
* @param dto Dto containing changes
|
*/
|
||||||
* @param entity existing entity
|
mapDtoListToEntityList(dtos: D[]): E[] {
|
||||||
*
|
return dtos.map(dto => this.toEntity(dto));
|
||||||
* Used for merging user changes (DTO) into the existing entity (database).
|
}
|
||||||
*/
|
|
||||||
abstract mergeDtoIntoEntity(dto: D, entity: E): E;
|
/**
|
||||||
/**
|
* Merge entity list with changes contained in DTO list.
|
||||||
* Defines how to create a new entity. Required by mergeDtoListIntoEntityList
|
* @param dtos List of dtos
|
||||||
* to add new elements to the list
|
* @param entities List of entities
|
||||||
*/
|
* @returns Merged list
|
||||||
abstract createNewEntity() : E;
|
*
|
||||||
}
|
* 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 { 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();
|
||||||
|
|
@ -13,20 +16,23 @@ export class CompactRecipeDtoEntityMapper extends AbstractDtoEntityMapper<Recipe
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
toEntity(dto: CompactRecipeDto): RecipeEntity {
|
toEntity(dto: CompactRecipeDto): RecipeEntity {
|
||||||
throw new Error("Mapping CompactRecipeDto to RecipeEntity is not allowed!");
|
throw new Error("Mapping CompactRecipeDto to RecipeEntity is not allowed!");
|
||||||
}
|
}
|
||||||
|
|
||||||
createNewEntity() : RecipeEntity {
|
createNewEntity(): RecipeEntity {
|
||||||
throw new Error("Mapping CompactRecipeDto to RecipeEntity is not allowed!");
|
throw new Error("Mapping CompactRecipeDto to RecipeEntity is not allowed!");
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeDtoIntoEntity(dto: CompactRecipeDto, entity: RecipeEntity): RecipeEntity {
|
mergeDtoIntoEntity(dto: CompactRecipeDto, entity: RecipeEntity): RecipeEntity {
|
||||||
throw new Error("Mapping CompactRecipeDto to RecipeEntity is not allowed!");
|
throw new Error("Mapping CompactRecipeDto to RecipeEntity is not allowed!");
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeDtoListIntoEntityList(dtos: CompactRecipeDto[], entities: RecipeEntity[]) : RecipeEntity[]{
|
mergeDtoListIntoEntityList(dtos: CompactRecipeDto[], entities: RecipeEntity[]): RecipeEntity[] {
|
||||||
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!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue