diff --git a/bruno/recipe-backend/getCompactRecipes.bru b/bruno/recipe-backend/getCompactRecipes.bru index d62a677..b96cdfc 100644 --- a/bruno/recipe-backend/getCompactRecipes.bru +++ b/bruno/recipe-backend/getCompactRecipes.bru @@ -11,7 +11,7 @@ get { } auth:bearer { - token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc1OTA3NjA2NCwiZXhwIjoxNzU5MTYyNDY0fQ.exf_3fCrarW0LhUqPuadvp89BOUazEXtdSTkGDIAU_Q + token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc1ODk4Njk4MSwiZXhwIjoxNzU5MDczMzgxfQ.rYvECzhI3Tptse3yVjZvR9RXgs1gkwAt2_5-hpAXvB0 } settings { diff --git a/bruno/recipe-backend/updateRecipe.bru b/bruno/recipe-backend/updateRecipe.bru index a4b695f..85a4191 100644 --- a/bruno/recipe-backend/updateRecipe.bru +++ b/bruno/recipe-backend/updateRecipe.bru @@ -11,7 +11,7 @@ put { } auth:bearer { - token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc1OTE3MjI3MywiZXhwIjoxNzU5MjU4NjczfQ._X_ZtBGtx0_14Nx90ctSQL-ieVPptaPc7WjG3FnyOOA + token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc1OTA3NjA2NCwiZXhwIjoxNzU5MTYyNDY0fQ.exf_3fCrarW0LhUqPuadvp89BOUazEXtdSTkGDIAU_Q } body:json { @@ -27,7 +27,7 @@ body:json { "id": "9042d658-0102-4e63-8637-a82c5653aa9d", "createdAt": "2025-09-28T10:24:05.429Z", "updatedAt": "2025-09-28T10:24:05.429Z", - "text": "Mürbteig von 400 g Mehl herstellen.", + "text": "Mürbteig von 400 g Mehl herstellen", "sortOrder": 1 }, { @@ -57,9 +57,6 @@ body:json { "updatedAt": "2025-09-28T10:24:05.429Z", "text": "Backen", "sortOrder": 5 - }, { - "text": "Essen", - "sortOrder": 6 } ], "ingredientGroups": [ diff --git a/src/handlers/AuthHandler.ts b/src/controllers/AuthController.ts similarity index 98% rename from src/handlers/AuthHandler.ts rename to src/controllers/AuthController.ts index 32065fb..15664a6 100644 --- a/src/handlers/AuthHandler.ts +++ b/src/controllers/AuthController.ts @@ -9,7 +9,7 @@ import { LoginRequestDto } from "../dtos/LoginRequestDto.js"; * Controller responsible for authentication, e.g., login or issueing a token with extended * lifetime */ -export class AuthHandler { +export class AuthController { constructor( private userRepository: UserRepository, private mapper: UserDtoEntityMapper diff --git a/src/handlers/CompactRecipeHandler.ts b/src/controllers/CompactRecipeController.ts similarity index 67% rename from src/handlers/CompactRecipeHandler.ts rename to src/controllers/CompactRecipeController.ts index 6e332e2..395f3d9 100644 --- a/src/handlers/CompactRecipeHandler.ts +++ b/src/controllers/CompactRecipeController.ts @@ -6,7 +6,7 @@ import { RecipeRepository } from "../repositories/RecipeRepository.js"; /** * Responsible for loading recipe header data */ -export class CompactRecipeHandler { +export class CompactRecipeController { constructor( private repository: RecipeRepository, private mapper: CompactRecipeDtoEntityMapper @@ -25,19 +25,4 @@ export class CompactRecipeHandler { }); return recipeDtos; } - - /** - * Get all recipes matching search - * - * Recipe title must contain type string - * @todo Full text search?? - */ - async getMatchingRecipes(searchString : string){ - if(!searchString || searchString.length===0){ - // get all - return this.getAllCompactRecipes(); - } else { - return this.repository.findCompactRecipeBySearch(searchString); - } - } } \ No newline at end of file diff --git a/src/handlers/RecipeHandler.ts b/src/controllers/RecipeController.ts similarity index 89% rename from src/handlers/RecipeHandler.ts rename to src/controllers/RecipeController.ts index eee5eda..44b3b37 100644 --- a/src/handlers/RecipeHandler.ts +++ b/src/controllers/RecipeController.ts @@ -5,11 +5,12 @@ import { NotFoundError, ValidationError } from "../errors/httpErrors.js"; import { RecipeInstructionStepDto } from "../dtos/RecipeInstructionStepDto.js"; import { RecipeIngredientGroupDto } from "../dtos/RecipeIngredientGroupDto.js"; import { RecipeIngredientDto } from "../dtos/RecipeIngredientDto.js"; +import { Entity } from "typeorm"; /** * Controls all recipe specific actions */ -export class RecipeHandler { +export class RecipeController { constructor( private recipeRepository: RecipeRepository, private mapper: RecipeDtoEntityMapper @@ -37,19 +38,9 @@ export class RecipeHandler { if (!this.isRecipeDtoValid(dto)) { throw new ValidationError("recipe data is not valid!") } - const recipeId = dto.id - if(recipeId === undefined){ - throw new ValidationError("Trying to update recipe without ID!") - } - // Load current version of recipe from database - const recipeEntity = await this.recipeRepository.findById(recipeId); - if(!recipeEntity){ - throw new ValidationError("No recipe with ID " + recipeId + " found in database!") - } - // merge changes into entity - this.mapper.mergeDtoIntoEntity(dto, recipeEntity); - // persist changes - const savedEntity = await this.recipeRepository.update(recipeEntity); + const recipeEntity = this.mapper.toEntity(dto); + // @todo doesn't create new ingredient groups, ingredients or instruction steps yet + const savedEntity = await this.recipeRepository.save(recipeEntity); return this.mapper.toDto(savedEntity); } /** diff --git a/src/handlers/UserHandler.ts b/src/controllers/UserController.ts similarity index 98% rename from src/handlers/UserHandler.ts rename to src/controllers/UserController.ts index bccb8e3..68ad0ef 100644 --- a/src/handlers/UserHandler.ts +++ b/src/controllers/UserController.ts @@ -9,7 +9,7 @@ import { UUID } from "crypto"; /** * Controls all user specific actions */ -export class UserHandler { +export class UserController { constructor( private userRepository: UserRepository, private mapper: UserDtoEntityMapper diff --git a/src/data-source.ts b/src/data-source.ts index d0b32e5..b05c91b 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -11,16 +11,9 @@ const __dirname = dirname(__filename); dotenv.config(); -/** - * Load config - */ const { DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD, DB_DATABASE, NODE_ENV } = process.env; - /** - * Configures data source - */ - export const AppDataSource = new DataSource({ type: "postgres", host: DB_HOST, @@ -31,7 +24,7 @@ export const AppDataSource = new DataSource({ synchronize: NODE_ENV === "dev" ? false : false, //logging logs sql command on the terminal - logging: NODE_ENV === "dev" ? ["query", "error"] : false, + logging: NODE_ENV === "dev" ? false : false, entities: [join(__dirname, "/entities/*.{js, ts}")], migrations: [join(__dirname, "/migrations/*.js")], subscribers: [], diff --git a/src/dtos/RecipeIngredientDto.ts b/src/dtos/RecipeIngredientDto.ts index 3d7c91f..20cfa9f 100644 --- a/src/dtos/RecipeIngredientDto.ts +++ b/src/dtos/RecipeIngredientDto.ts @@ -1,3 +1,4 @@ +import { UUID } from "crypto"; import { AbstractDto } from "./AbstractDto.js"; export class RecipeIngredientDto extends AbstractDto{ @@ -6,5 +7,5 @@ export class RecipeIngredientDto extends AbstractDto{ amount?: number; unit?: string; sortOrder!: number; - ingredientGroupId?: string; + ingredientGroupId?: UUID; } \ No newline at end of file diff --git a/src/dtos/RecipeIngredientGroupDto.ts b/src/dtos/RecipeIngredientGroupDto.ts index 324d3c5..32d6cc0 100644 --- a/src/dtos/RecipeIngredientGroupDto.ts +++ b/src/dtos/RecipeIngredientGroupDto.ts @@ -1,9 +1,10 @@ +import { UUID } from "crypto"; import { AbstractDto } from "./AbstractDto.js"; import { RecipeIngredientDto } from "./RecipeIngredientDto.js"; export class RecipeIngredientGroupDto extends AbstractDto{ title?: string; sortOrder!: number; - recipeId?: string; + recipeId?: UUID; ingredients!: RecipeIngredientDto[]; } \ No newline at end of file diff --git a/src/endpoints/AuthPoint.ts b/src/endpoints/AuthPoint.ts index f2b91fb..7745240 100644 --- a/src/endpoints/AuthPoint.ts +++ b/src/endpoints/AuthPoint.ts @@ -1,5 +1,5 @@ import { Router } from "express"; -import { AuthHandler } from "../handlers/AuthHandler.js"; +import { AuthController } from "../controllers/AuthController.js"; import { UserRepository } from "../repositories/UserRepository.js"; import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js"; import { @@ -14,13 +14,8 @@ export const authBasicRoute = "/auth" const router = Router(); const userRepository = new UserRepository(); const mapper = new UserDtoEntityMapper(); -const authController = new AuthHandler(userRepository, mapper); +const authController = new AuthController(userRepository, mapper); -/** - * Login using username and password - * Consumes LoginRequestDto - * Responds with LoginResponseDto - */ router.post("/login", async (req, res) => { console.log("login point called") try { diff --git a/src/endpoints/CompactRecipePoint.ts b/src/endpoints/CompactRecipePoint.ts index e7c6956..ea1db47 100644 --- a/src/endpoints/CompactRecipePoint.ts +++ b/src/endpoints/CompactRecipePoint.ts @@ -1,7 +1,7 @@ import { Router } from "express"; import { asyncHandler } from "../utils/asyncHandler.js"; import { RecipeRepository } from "../repositories/RecipeRepository.js"; -import { CompactRecipeHandler } from "../handlers/CompactRecipeHandler.js"; +import { CompactRecipeController } from "../controllers/CompactRecipeController.js"; import { CompactRecipeDtoEntityMapper } from "../mappers/CompactRecipeDtoEntityMapper.js"; /** @@ -12,18 +12,14 @@ const router = Router(); // Inject repo + mapper here const recipeRepository = new RecipeRepository(); const compactRecipeMapper = new CompactRecipeDtoEntityMapper(); -const compactRecipeHandler = new CompactRecipeHandler(recipeRepository, compactRecipeMapper); +const compactRecipeController = new CompactRecipeController(recipeRepository, compactRecipeMapper); /** * 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); + const response = await compactRecipeController.getAllCompactRecipes(); res.status(201).json(response); }) ); diff --git a/src/endpoints/RecipePoint.ts b/src/endpoints/RecipePoint.ts index 3949add..dde7a17 100644 --- a/src/endpoints/RecipePoint.ts +++ b/src/endpoints/RecipePoint.ts @@ -1,7 +1,7 @@ import { Router } from "express"; import { RecipeRepository } from "../repositories/RecipeRepository.js"; import { RecipeDtoEntityMapper } from "../mappers/RecipeDtoEntityMapper.js"; -import { RecipeHandler } from "../handlers/RecipeHandler.js"; +import { RecipeController } from "../controllers/RecipeController.js"; import { asyncHandler } from "../utils/asyncHandler.js"; import { RecipeDto } from "../dtos/RecipeDto.js"; import { RecipeIngredientDtoEntityMapper } from "../mappers/RecipeIngredientDtoEntityMapper.js"; @@ -20,12 +20,10 @@ const recipeIngredientMapper = new RecipeIngredientDtoEntityMapper(); const recipeIngredientGroupMapper = new RecipeIngredientGroupDtoEntityMapper(recipeIngredientMapper); const recipeInstructionStepMapper = new RecipeInstructionStepDtoEntityMapper(); const recipeMapper = new RecipeDtoEntityMapper(recipeInstructionStepMapper, recipeIngredientGroupMapper); -const recipeController = new RecipeHandler(recipeRepository, recipeMapper); +const recipeController = new RecipeController(recipeRepository, recipeMapper); /** * Create new recipe - * Consumes: RecipeDto - * Responds with RecipeDto */ router.post( "/", @@ -36,10 +34,6 @@ router.post( }) ); -/** - * Load recipe by id - * Responds with RecipeDto - */ router.get( "/:id", asyncHandler(async(req, res) => { @@ -49,12 +43,6 @@ router.get( }) ); -/** - * Saves existing recipe - * Also handles changes to instructions steps and ingredient (groups) - * Consumes: RecipeDto - * Responds with RecipeDto - */ router.put( "/:id", asyncHandler(async(req, res) =>{ diff --git a/src/endpoints/UserPoint.ts b/src/endpoints/UserPoint.ts index 58d8266..edf9167 100644 --- a/src/endpoints/UserPoint.ts +++ b/src/endpoints/UserPoint.ts @@ -1,5 +1,5 @@ import { Router } from "express"; -import { UserHandler } from "../handlers/UserHandler.js"; +import { UserController } from "../controllers/UserController.js"; import { CreateUserRequestDto } from "../dtos/CreateUserRequestDto.js"; import { UserRepository } from "../repositories/UserRepository.js"; import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js"; @@ -13,12 +13,10 @@ const router = Router(); // Inject repo + mapper here const userRepository = new UserRepository(); const userMapper = new UserDtoEntityMapper(); -const userController = new UserHandler(userRepository, userMapper); +const userController = new UserController(userRepository, userMapper); /** * Create a new user - * Consumes CreateUserRequestDto - * Responds with UserDto */ router.post( "/", @@ -31,7 +29,6 @@ router.post( /** * Get user data for current user - * Responds with UserDto */ router.get("/me", asyncHandler(async (req, res) => { diff --git a/src/entities/AbstractEntity.ts b/src/entities/AbstractEntity.ts index 07fb2c2..86401fd 100644 --- a/src/entities/AbstractEntity.ts +++ b/src/entities/AbstractEntity.ts @@ -4,9 +4,6 @@ import { UpdateDateColumn, } from "typeorm"; -/** - * Abstract entity containing basic fields that all entities have in common - */ export abstract class AbstractEntity { @PrimaryGeneratedColumn("uuid") id?: string; diff --git a/src/entities/RecipeEntity.ts b/src/entities/RecipeEntity.ts index 2f3b8fe..726a505 100644 --- a/src/entities/RecipeEntity.ts +++ b/src/entities/RecipeEntity.ts @@ -19,13 +19,13 @@ export class RecipeEntity extends AbstractEntity { // make sure not to induce a circular dependency! user arrow function without brackets! @OneToMany(() => RecipeInstructionStepEntity, (instructionStep) => instructionStep.recipe, { - cascade: true + cascade: true, }) instructionSteps!: RecipeInstructionStepEntity[]; // make sure not to induce a circular dependency! user arrow function without brackets! @OneToMany(() => RecipeIngredientGroupEntity, (ingredientGroup) => ingredientGroup.recipe, { - cascade: true + cascade: true, }) ingredientGroups!: Relation[]; } \ No newline at end of file diff --git a/src/entities/RecipeIngredientEntity.ts b/src/entities/RecipeIngredientEntity.ts index 6b5a466..eb90523 100644 --- a/src/entities/RecipeIngredientEntity.ts +++ b/src/entities/RecipeIngredientEntity.ts @@ -24,11 +24,7 @@ export class RecipeIngredientEntity extends AbstractEntity { @JoinColumn({name: "recipe_ingredient_group_id"}) @ManyToOne(() => RecipeIngredientGroupEntity, (ingredientGroup) => ingredientGroup.ingredients, - { - onDelete: "CASCADE", - nullable: false, - orphanedRowAction: "delete" - } + {onDelete: "CASCADE", nullable: false} ) ingredientGroup!: Relation; } \ No newline at end of file diff --git a/src/entities/RecipeIngredientGroupEntity.ts b/src/entities/RecipeIngredientGroupEntity.ts index 6cd763e..64c64c3 100644 --- a/src/entities/RecipeIngredientGroupEntity.ts +++ b/src/entities/RecipeIngredientGroupEntity.ts @@ -16,16 +16,12 @@ export class RecipeIngredientGroupEntity extends AbstractEntity { @JoinColumn({name: "recipe_id"}) @ManyToOne(() => RecipeEntity, (recipe) => recipe.ingredientGroups, - { - onDelete: "CASCADE", - nullable: false, - orphanedRowAction: "delete" // delete removed groups - } + {onDelete: "CASCADE", nullable: false} ) recipe!: Relation; @OneToMany(() => RecipeIngredientEntity, (ingredient) => ingredient.ingredientGroup, { - cascade: true + cascade: true, }) ingredients!: Relation[]; diff --git a/src/entities/RecipeInstructionStepEntity.ts b/src/entities/RecipeInstructionStepEntity.ts index 1a1417f..ed238d1 100644 --- a/src/entities/RecipeInstructionStepEntity.ts +++ b/src/entities/RecipeInstructionStepEntity.ts @@ -15,11 +15,7 @@ export class RecipeInstructionStepEntity extends AbstractEntity { @JoinColumn({name: "recipe_id"}) @ManyToOne(() => RecipeEntity, (recipe) => recipe.instructionSteps, - { - onDelete: "CASCADE", - nullable: false, - orphanedRowAction: "delete" // delete removed groups - } + {onDelete: "CASCADE", nullable: false} ) recipe!: Relation; diff --git a/src/entities/UserEntity.ts b/src/entities/UserEntity.ts index f53b82c..c306eac 100644 --- a/src/entities/UserEntity.ts +++ b/src/entities/UserEntity.ts @@ -1,9 +1,7 @@ import { Entity, Column } from "typeorm"; import { AbstractEntity } from "./AbstractEntity.js"; -/** - * Entity describing a user - */ +// @todo Add migration to update table @Entity({ name: "user" }) export class UserEntity extends AbstractEntity { @Column({ nullable: false, name: "user_name" }) diff --git a/src/index.ts b/src/index.ts index d0d06fc..73cf6fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,24 +27,9 @@ async function startServer() { await AppDataSource.runMigrations(); console.log("Migrations executed"); - // Enable CORS before anything else - // @todo move to middleware util - app.use((req: Request, res: Response, next: NextFunction) => { - res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS'); - res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - - // Handle preflight requests quickly - if (req.method === 'OPTIONS') { - return res.sendStatus(200); - } - - next(); - }); - // Activate Authentication app.use(authentication); - + // Setup routes app.use(authBasicRoute, authRoutes); app.use("/user", userRoutes); diff --git a/src/mappers/AbstractDtoEntityMapper.ts b/src/mappers/AbstractDtoEntityMapper.ts index dd878bc..51a5f27 100644 --- a/src/mappers/AbstractDtoEntityMapper.ts +++ b/src/mappers/AbstractDtoEntityMapper.ts @@ -25,57 +25,7 @@ export abstract class AbstractDtoEntityMapper< 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; - } - // 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; } diff --git a/src/mappers/CompactRecipeDtoEntityMapper.ts b/src/mappers/CompactRecipeDtoEntityMapper.ts index b8040a3..77f2268 100644 --- a/src/mappers/CompactRecipeDtoEntityMapper.ts +++ b/src/mappers/CompactRecipeDtoEntityMapper.ts @@ -17,16 +17,4 @@ export class CompactRecipeDtoEntityMapper extends AbstractDtoEntityMapper this.instructionStepMapper.toDto(stepEntity)); - // @todo map ids dto.instructions.forEach(step => step.recipeId = entity.id); // set recipe relation explicitly! - + const instructionStepEntities = entity.instructionSteps; + const instructionStepDtos = instructionStepEntities.map((stepEntity) => this.instructionStepMapper.toDto(stepEntity)); + dto.instructions = instructionStepDtos; // map ingredient groups - dto.ingredientGroups = entity.ingredientGroups.map((groupEntity) => this.ingredientGroupMapper.toDto(groupEntity)); + const ingredientGroupEntities = entity.ingredientGroups; + const ingredientGroupDtos = ingredientGroupEntities.map((groupEntity) => this.ingredientGroupMapper.toDto(groupEntity)); + dto.ingredientGroups = ingredientGroupDtos; return dto; } @@ -40,47 +42,16 @@ export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper { - const stepEntity = this.instructionStepMapper.toEntity(stepDto); - - // Always set the relation - stepEntity.recipe = entity; - - // If it's a new step (no id from client), let DB generate a new UUID - if (!stepDto.id) { - delete (stepEntity as any).id; - } - - return stepEntity; - }); + const instructionStepDtos = dto.instructions; + const instructionStepEntities = instructionStepDtos.map((stepDto) => this.instructionStepMapper.toEntity(stepDto)); + entity.instructionSteps = instructionStepEntities; // map ingredient groups - entity.ingredientGroups = dto.ingredientGroups.map((groupDto) => { - const groupEntity = this.ingredientGroupMapper.toEntity(groupDto); - groupEntity.recipe = entity; - if (!groupDto.id) { - delete (groupEntity as any).id; - } - return groupEntity; - }); + const ingredientGroupDtos = dto.ingredientGroups; + const ingredientGroupEntities = ingredientGroupDtos.map((ingredientGroupDto) => this.ingredientGroupMapper.toEntity(ingredientGroupDto)); + entity.ingredientGroups = ingredientGroupEntities; - return entity; + return entity; } - mergeDtoIntoEntity(dto: RecipeDto, entity: RecipeEntity): RecipeEntity { - entity.title = dto.title; - entity.amount = dto.amount; - entity.amountDescription = dto.amountDescription; - - // --- Instruction Steps --- - entity.instructionSteps = this.instructionStepMapper.mergeDtoListIntoEntityList(dto.instructions, entity.instructionSteps); - - // --- Ingredient Groups --- - entity.ingredientGroups = this.ingredientGroupMapper.mergeDtoListIntoEntityList(dto.ingredientGroups, entity.ingredientGroups); - return entity - } - - createNewEntity(): RecipeEntity { - return new RecipeEntity(); - } } \ No newline at end of file diff --git a/src/mappers/RecipeIngredientDtoEntityMapper.ts b/src/mappers/RecipeIngredientDtoEntityMapper.ts index fb1f6cd..0a8baab 100644 --- a/src/mappers/RecipeIngredientDtoEntityMapper.ts +++ b/src/mappers/RecipeIngredientDtoEntityMapper.ts @@ -29,17 +29,4 @@ export class RecipeIngredientDtoEntityMapper extends AbstractDtoEntityMapper { - constructor( - private ingredientMapper: RecipeIngredientDtoEntityMapper - ) { - super(); - } +export class RecipeIngredientGroupDtoEntityMapper extends AbstractDtoEntityMapper{ + constructor( + private ingredientMapper : RecipeIngredientDtoEntityMapper + ){ + super(); + } - toDto(entity: RecipeIngredientGroupEntity): RecipeIngredientGroupDto { - const dto = new RecipeIngredientGroupDto(); - this.mapBaseEntityToDto(entity, dto); + toDto(entity: RecipeIngredientGroupEntity): RecipeIngredientGroupDto { + const dto = new RecipeIngredientGroupDto(); + this.mapBaseEntityToDto(entity, dto); - dto.title = entity.title; - dto.sortOrder = entity.sortOrder + dto.title = entity.title; + dto.sortOrder = entity.sortOrder - // map ingredients - dto.ingredients = entity.ingredients?.map((ingredientEntity) => this.ingredientMapper.toDto(ingredientEntity)); + // map ingredients + const ingredientEntities = entity.ingredients; + const ingredientDtos = ingredientEntities?.map((ingredientEntity) => this.ingredientMapper.toDto(ingredientEntity)); + dto.ingredients = ingredientDtos; - return dto; - } + return dto; + } toEntity(dto: RecipeIngredientGroupDto): RecipeIngredientGroupEntity { const entity = new RecipeIngredientGroupEntity(); @@ -31,34 +33,11 @@ export class RecipeIngredientGroupDtoEntityMapper extends AbstractDtoEntityMappe entity.sortOrder = dto.sortOrder // map ingredients - entity.ingredients = dto.ingredients.map((ingredientDto) => { - const ingredientEntity = this.ingredientMapper.toEntity(ingredientDto); - ingredientEntity.ingredientGroup = entity; - // remove id from new entity completely and allow ORM to generate a new one - if (!ingredientDto.id) { - delete (ingredientEntity as any).id; - } - return ingredientEntity; - }); + const ingredientDtos = dto.ingredients; + const ingredientEntities = ingredientDtos?.map((ingredientDto) => this.ingredientMapper.toEntity(ingredientDto)); + entity.ingredients = ingredientEntities; return entity; } - createNewEntity(): RecipeIngredientGroupEntity { - return new RecipeIngredientGroupEntity(); - } - - mergeDtoIntoEntity(dto: RecipeIngredientGroupDto, entity: RecipeIngredientGroupEntity): RecipeIngredientGroupEntity { - entity.title = dto.title; - entity.sortOrder = dto.sortOrder; - - // sync ingredients inside each group - entity.ingredients = this.ingredientMapper.mergeDtoListIntoEntityList( - dto.ingredients, - entity.ingredients, - ); - - return entity; - } - } \ No newline at end of file diff --git a/src/mappers/RecipeInstructionStepDtoEntityMapper.ts b/src/mappers/RecipeInstructionStepDtoEntityMapper.ts index 14f02ff..be7ca3a 100644 --- a/src/mappers/RecipeInstructionStepDtoEntityMapper.ts +++ b/src/mappers/RecipeInstructionStepDtoEntityMapper.ts @@ -24,14 +24,4 @@ export class RecipeInstructionStepDtoEntityMapper extends AbstractDtoEntityMappe return entity; } - createNewEntity(): RecipeInstructionStepEntity { - return new RecipeInstructionStepEntity(); - } - - mergeDtoIntoEntity(dto: RecipeInstructionStepDto, entity: RecipeInstructionStepEntity): RecipeInstructionStepEntity { - entity.text = dto.text; - entity.sortOrder = dto.sortOrder; - return entity; - } - } \ No newline at end of file diff --git a/src/mappers/UserDtoEntityMapper.ts b/src/mappers/UserDtoEntityMapper.ts index 675f2fa..927e4f6 100644 --- a/src/mappers/UserDtoEntityMapper.ts +++ b/src/mappers/UserDtoEntityMapper.ts @@ -20,16 +20,6 @@ export class UserDtoEntityMapper extends AbstractDtoEntityMapper { protected repo: Repository; @@ -25,11 +22,20 @@ export abstract class AbstractRepository { return this.repo.save(entity); } + /* async update(id: string, partialData: DeepPartial): Promise { + await this.repo.update(id as any, partialData); + const updated = await this.findById(id); + if (!updated) { + throw new Error("Entity not found after update"); + } + return updated; + } */ + async delete(id: string): Promise { await this.repo.delete(id as any); } - async update(entity: T): Promise { + async save(entity: T): Promise { return this.repo.save(entity); } diff --git a/src/repositories/RecipeRepository.ts b/src/repositories/RecipeRepository.ts index 8c62cb2..ef48156 100644 --- a/src/repositories/RecipeRepository.ts +++ b/src/repositories/RecipeRepository.ts @@ -1,7 +1,5 @@ import { AbstractRepository } from "./AbstractRepository.js"; import { RecipeEntity } from "../entities/RecipeEntity.js"; -import { AppDataSource } from "../data-source.js"; -import { ILike, Like } from "typeorm"; export class RecipeRepository extends AbstractRepository { constructor() { @@ -22,37 +20,6 @@ export class RecipeRepository extends AbstractRepository { 'instructionSteps' ] }); + } - - /** - * 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 - * @returns List of recipe entities matching the search criteria - */ - async findCompactRecipeBySearch(searchString : string): Promise{ - // @todo doesn't work like expected... - return this.repo.find( - { where: {title: ILike(`%${searchString}%`)}} - ); - } - - /** - * Update recipe and relations - * @param entity Updated entity - * @returns Updated Entity - */ - async updateRecipe(entity: RecipeEntity): Promise { - return AppDataSource.transaction(async (em) => { - // load existing data - const existing = await this.repo.findOneOrFail({ - where: { id: entity.id }, - relations: ["instructionSteps", "ingredientGroups", "ingredientGroups.ingredients"], - }); - - // merge new entity and existing entity - this.repo.merge(existing, entity); - return this.repo.save(existing); - }); -} - } \ No newline at end of file