diff --git a/bruno/recipe-backend/createRecipe.bru b/bruno/recipe-backend/createRecipe.bru index d44d720..5a24bab 100644 --- a/bruno/recipe-backend/createRecipe.bru +++ b/bruno/recipe-backend/createRecipe.bru @@ -5,51 +5,63 @@ meta { } post { - url: http://localhost:4000/recipe + url: http://localhost:4000/recipe/create-or-update body: json auth: bearer } auth:bearer { - token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc1ODk4Njk4MSwiZXhwIjoxNzU5MDczMzgxfQ.rYvECzhI3Tptse3yVjZvR9RXgs1gkwAt2_5-hpAXvB0 + token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc2MDExNzA3MywiZXhwIjoxNzYwMjAzNDczfQ.NEfrUuzFcxocgN52uhptku5QVUbg03nmrN1E6A6XycA } body:json { { - "title": "Spaghetti mit Tomatensosse", - "amount": "4", - "amountDescription": "Personen", + "title": "Apfelkuchen Edeltrud 2", + "amount": "1", + "amountDescription": "Kuchen", "instructions": [{ - "text": "Spaghetti nach Packungsanleitung zubereiten", + "text": "Mürbteig von 400 g Mehl herstellen", "sortOrder": 1 }, { - "text": "Tomatensosse erhitzen", + "text": "Äpfel schälen und kleinschneiden.", "sortOrder": 2 }, { - "text": "Vermischen, mit geriebenem Parmesan bestreuen und servieren", + "text": "Springform fetten, zwei Drittel des Teigs hineindrücken, Äpfel mit Rosinen vermischen und einfüllen, restlichen Teig ausrollen, damit abdecken und mit Milch bepinseln", "sortOrder": 3 + }, + { + "text": "Backen", + "sortOrder": 4 }], "ingredientGroups": [ { + "title": "Für den Teig", "sortOrder": 1, "ingredients":[ { - "name": "Spaghetti", - "amount": 500, + "name": "Mehl", + "amount": 400, "unit": "g", "sortOrder": 1 + } + ] + }, + { + "title": "Für die Füllung", + "sortOrder": 2, + "ingredients":[ + { + "name": "große Äpfel", + "amount": 5, + "sortOrder": 1 }, { - "name": "Tomatensosse", - "amount": 1, - "unit": "Glas", + "name": "Rosinen", + "amount": 100, + "unit": "g", "sortOrder": 2 - }, - { - "name": "Parmesan", - "sortOrder": 3 } ] } diff --git a/bruno/recipe-backend/updateRecipe.bru b/bruno/recipe-backend/updateRecipe.bru index a4b695f..a138e2a 100644 --- a/bruno/recipe-backend/updateRecipe.bru +++ b/bruno/recipe-backend/updateRecipe.bru @@ -4,14 +4,14 @@ meta { seq: 7 } -put { - url: http://localhost:4000/recipe/44a8f38c-9387-439e-aed6-c3369b776b1c +post { + url: http://localhost:4000/recipe/create-or-update body: json auth: bearer } auth:bearer { - token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc1OTE3MjI3MywiZXhwIjoxNzU5MjU4NjczfQ._X_ZtBGtx0_14Nx90ctSQL-ieVPptaPc7WjG3FnyOOA + token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc1OTU5MTk1MiwiZXhwIjoxNzU5Njc4MzUyfQ.gkvuBtq8OaC7OqnArPcrV7jd34Ll7jHYXRbvz847aiw } body:json { @@ -23,13 +23,6 @@ body:json { "amount": 1, "amountDescription": "Kuchen (26cm Durchmesser)", "instructions": [ - { - "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.", - "sortOrder": 1 - }, { "id": "42f834f1-54d1-4149-ad2e-e6565add719b", "createdAt": "2025-09-28T10:24:05.429Z", @@ -43,13 +36,6 @@ body:json { "updatedAt": "2025-09-28T10:24:05.429Z", "text": "Restlichen Teig ausrollen, Kuchen damit abdecken und mit Milch bepinseln.", "sortOrder": 4 - }, - { - "id": "a45ad765-f775-4969-ad36-ca2d5645a2fc", - "createdAt": "2025-09-28T10:24:05.429Z", - "updatedAt": "2025-09-28T10:24:05.429Z", - "text": "Springform fetten, zwei Drittel des Teigs hineindrücken, Äpfel mit Rosinen vermischen und einfüllen.", - "sortOrder": 3 }, { "id": "e0435853-b1b9-46cb-b53f-f5345ffca729", @@ -57,9 +43,13 @@ body:json { "updatedAt": "2025-09-28T10:24:05.429Z", "text": "Backen", "sortOrder": 5 - }, { - "text": "Essen", - "sortOrder": 6 + }, + { + "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.", + "sortOrder": 1 } ], "ingredientGroups": [ diff --git a/src/endpoints/RecipePoint.ts b/src/endpoints/RecipePoint.ts index f09bb3d..ebaa8fb 100644 --- a/src/endpoints/RecipePoint.ts +++ b/src/endpoints/RecipePoint.ts @@ -7,7 +7,6 @@ import { RecipeDto } from "../dtos/RecipeDto.js"; import { RecipeIngredientDtoEntityMapper } from "../mappers/RecipeIngredientDtoEntityMapper.js"; import { RecipeIngredientGroupDtoEntityMapper } from "../mappers/RecipeIngredientGroupDtoEntityMapper.js"; import { RecipeInstructionStepDtoEntityMapper } from "../mappers/RecipeInstructionStepDtoEntityMapper.js"; -import { ValidationError } from "../errors/httpErrors.js"; /** * Handles all recipe related routes @@ -22,20 +21,6 @@ const recipeInstructionStepMapper = new RecipeInstructionStepDtoEntityMapper(); const recipeMapper = new RecipeDtoEntityMapper(recipeInstructionStepMapper, recipeIngredientGroupMapper); const recipeController = new RecipeHandler(recipeRepository, recipeMapper); -/** - * Create new recipe - * Consumes: RecipeDto - * Responds with RecipeDto - * DEPRECATED! - */ -router.post( - "/", - asyncHandler(async (req, res) => { - const requestDto : RecipeDto = req.body; - const responseDto = await recipeController.createRecipe(requestDto); - res.status(201).json(responseDto); - }) -); /** * Save or update recipe. @@ -67,24 +52,4 @@ router.get( }) ); -/** - * Saves existing recipe - * Also handles changes to instructions steps and ingredient (groups) - * Consumes: RecipeDto - * Responds with RecipeDto - * DEPRECATED - */ -router.put( - "/:id", - asyncHandler(async(req, res) =>{ - const id = req.params.id; - const recipeDto : RecipeDto = req.body; - if(id != recipeDto.id){ - throw new ValidationError("Cannot save recipe! ID in request body " + recipeDto.id + " doesn't match ID in path " + id +"!") - } - const responseDto = await recipeController.updateRecipe(recipeDto); - res.status(201).json(responseDto); - }) -); - export default router; \ No newline at end of file diff --git a/src/entities/AbstractEntity.ts b/src/entities/AbstractEntity.ts index 07fb2c2..6a660c6 100644 --- a/src/entities/AbstractEntity.ts +++ b/src/entities/AbstractEntity.ts @@ -16,4 +16,12 @@ export abstract class AbstractEntity { @UpdateDateColumn({name: "update_date"}) updateDate?: Date; + + /** + * Checks whether entity has a valid ID + * @todo check for valid UUID... + */ + hasValidId() : boolean { + return this.id !== undefined && this.id.length > 0; + } } diff --git a/src/handlers/RecipeHandler.ts b/src/handlers/RecipeHandler.ts index fd4df75..4aef98e 100644 --- a/src/handlers/RecipeHandler.ts +++ b/src/handlers/RecipeHandler.ts @@ -41,11 +41,11 @@ export class RecipeHandler { } var savedEntity: RecipeEntity; const recipeId = dto.id - if(recipeId === undefined){ + if(recipeId === undefined || recipeId.length === 0){ // create new recipe const recipeEntity = this.mapper.toEntity(dto) + delete (recipeEntity as any).id; savedEntity = await this.recipeRepository.create(recipeEntity); - throw new ValidationError("Trying to update recipe without ID!") } else { // save existing Recipe // First: Load current version of recipe from database diff --git a/src/mappers/RecipeDtoEntityMapper.ts b/src/mappers/RecipeDtoEntityMapper.ts index 8a56aae..eb8bdbc 100644 --- a/src/mappers/RecipeDtoEntityMapper.ts +++ b/src/mappers/RecipeDtoEntityMapper.ts @@ -43,8 +43,10 @@ export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper { const stepEntity = this.instructionStepMapper.toEntity(stepDto); - // Always set the relation - stepEntity.recipe = entity; + // Set the relation if the entity already exists in DB + if(entity.hasValidId()){ + stepEntity.recipe = entity; + } // If it's a new step (no id from client), let DB generate a new UUID if (!stepDto.id) { @@ -57,7 +59,11 @@ export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper { const groupEntity = this.ingredientGroupMapper.toEntity(groupDto); - groupEntity.recipe = entity; + // Set the relation if the entity already exists in DB + if(entity.hasValidId()){ + groupEntity.recipe = entity; + } + // If it's a new group (no id from client), let DB generate a new UUID if (!groupDto.id) { delete (groupEntity as any).id; } diff --git a/src/mappers/RecipeIngredientGroupDtoEntityMapper.ts b/src/mappers/RecipeIngredientGroupDtoEntityMapper.ts index bfdee0d..9520138 100644 --- a/src/mappers/RecipeIngredientGroupDtoEntityMapper.ts +++ b/src/mappers/RecipeIngredientGroupDtoEntityMapper.ts @@ -33,7 +33,10 @@ export class RecipeIngredientGroupDtoEntityMapper extends AbstractDtoEntityMappe // map ingredients entity.ingredients = dto.ingredients.map((ingredientDto) => { const ingredientEntity = this.ingredientMapper.toEntity(ingredientDto); - ingredientEntity.ingredientGroup = entity; + // set relation if entity already exists in DB + if(entity.hasValidId()){ + 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; diff --git a/src/middleware/corsMiddleware.ts b/src/middleware/corsMiddleware.ts index 9d3f6d7..9d05381 100644 --- a/src/middleware/corsMiddleware.ts +++ b/src/middleware/corsMiddleware.ts @@ -8,8 +8,9 @@ import { Request, Response, NextFunction } from "express"; */ export function corsHeaders (req: Request, res: Response, next: NextFunction) { // allow requests from all sources (*) for now + // @todo restrict access! res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS'); + res.header('Access-Control-Allow-Methods', 'GET,POST,DELETE,OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); // Handle preflight requests quickly