fix relations

This commit is contained in:
Anika Raemer 2025-10-10 19:41:34 +02:00
parent 58ef7fbc00
commit 0587153829
8 changed files with 64 additions and 79 deletions

View file

@ -5,51 +5,63 @@ meta {
} }
post { post {
url: http://localhost:4000/recipe url: http://localhost:4000/recipe/create-or-update
body: json body: json
auth: bearer auth: bearer
} }
auth:bearer { auth:bearer {
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc1ODk4Njk4MSwiZXhwIjoxNzU5MDczMzgxfQ.rYvECzhI3Tptse3yVjZvR9RXgs1gkwAt2_5-hpAXvB0 token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc2MDExNzA3MywiZXhwIjoxNzYwMjAzNDczfQ.NEfrUuzFcxocgN52uhptku5QVUbg03nmrN1E6A6XycA
} }
body:json { body:json {
{ {
"title": "Spaghetti mit Tomatensosse", "title": "Apfelkuchen Edeltrud 2",
"amount": "4", "amount": "1",
"amountDescription": "Personen", "amountDescription": "Kuchen",
"instructions": [{ "instructions": [{
"text": "Spaghetti nach Packungsanleitung zubereiten", "text": "Mürbteig von 400 g Mehl herstellen",
"sortOrder": 1 "sortOrder": 1
}, },
{ {
"text": "Tomatensosse erhitzen", "text": "Äpfel schälen und kleinschneiden.",
"sortOrder": 2 "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 "sortOrder": 3
},
{
"text": "Backen",
"sortOrder": 4
}], }],
"ingredientGroups": [ "ingredientGroups": [
{ {
"title": "Für den Teig",
"sortOrder": 1, "sortOrder": 1,
"ingredients":[ "ingredients":[
{ {
"name": "Spaghetti", "name": "Mehl",
"amount": 500, "amount": 400,
"unit": "g", "unit": "g",
"sortOrder": 1 "sortOrder": 1
}
]
},
{
"title": "Für die Füllung",
"sortOrder": 2,
"ingredients":[
{
"name": "große Äpfel",
"amount": 5,
"sortOrder": 1
}, },
{ {
"name": "Tomatensosse", "name": "Rosinen",
"amount": 1, "amount": 100,
"unit": "Glas", "unit": "g",
"sortOrder": 2 "sortOrder": 2
},
{
"name": "Parmesan",
"sortOrder": 3
} }
] ]
} }

View file

@ -4,14 +4,14 @@ meta {
seq: 7 seq: 7
} }
put { post {
url: http://localhost:4000/recipe/44a8f38c-9387-439e-aed6-c3369b776b1c url: http://localhost:4000/recipe/create-or-update
body: json body: json
auth: bearer auth: bearer
} }
auth:bearer { auth:bearer {
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc1OTE3MjI3MywiZXhwIjoxNzU5MjU4NjczfQ._X_ZtBGtx0_14Nx90ctSQL-ieVPptaPc7WjG3FnyOOA token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc1OTU5MTk1MiwiZXhwIjoxNzU5Njc4MzUyfQ.gkvuBtq8OaC7OqnArPcrV7jd34Ll7jHYXRbvz847aiw
} }
body:json { body:json {
@ -23,13 +23,6 @@ body:json {
"amount": 1, "amount": 1,
"amountDescription": "Kuchen (26cm Durchmesser)", "amountDescription": "Kuchen (26cm Durchmesser)",
"instructions": [ "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", "id": "42f834f1-54d1-4149-ad2e-e6565add719b",
"createdAt": "2025-09-28T10:24:05.429Z", "createdAt": "2025-09-28T10:24:05.429Z",
@ -43,13 +36,6 @@ body:json {
"updatedAt": "2025-09-28T10:24:05.429Z", "updatedAt": "2025-09-28T10:24:05.429Z",
"text": "Restlichen Teig ausrollen, Kuchen damit abdecken und mit Milch bepinseln.", "text": "Restlichen Teig ausrollen, Kuchen damit abdecken und mit Milch bepinseln.",
"sortOrder": 4 "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", "id": "e0435853-b1b9-46cb-b53f-f5345ffca729",
@ -57,9 +43,13 @@ body:json {
"updatedAt": "2025-09-28T10:24:05.429Z", "updatedAt": "2025-09-28T10:24:05.429Z",
"text": "Backen", "text": "Backen",
"sortOrder": 5 "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": [ "ingredientGroups": [

View file

@ -7,7 +7,6 @@ import { RecipeDto } from "../dtos/RecipeDto.js";
import { RecipeIngredientDtoEntityMapper } from "../mappers/RecipeIngredientDtoEntityMapper.js"; import { RecipeIngredientDtoEntityMapper } from "../mappers/RecipeIngredientDtoEntityMapper.js";
import { RecipeIngredientGroupDtoEntityMapper } from "../mappers/RecipeIngredientGroupDtoEntityMapper.js"; import { RecipeIngredientGroupDtoEntityMapper } from "../mappers/RecipeIngredientGroupDtoEntityMapper.js";
import { RecipeInstructionStepDtoEntityMapper } from "../mappers/RecipeInstructionStepDtoEntityMapper.js"; import { RecipeInstructionStepDtoEntityMapper } from "../mappers/RecipeInstructionStepDtoEntityMapper.js";
import { ValidationError } from "../errors/httpErrors.js";
/** /**
* Handles all recipe related routes * Handles all recipe related routes
@ -22,20 +21,6 @@ const recipeInstructionStepMapper = new RecipeInstructionStepDtoEntityMapper();
const recipeMapper = new RecipeDtoEntityMapper(recipeInstructionStepMapper, recipeIngredientGroupMapper); const recipeMapper = new RecipeDtoEntityMapper(recipeInstructionStepMapper, recipeIngredientGroupMapper);
const recipeController = new RecipeHandler(recipeRepository, recipeMapper); 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. * 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; export default router;

View file

@ -16,4 +16,12 @@ export abstract class AbstractEntity {
@UpdateDateColumn({name: "update_date"}) @UpdateDateColumn({name: "update_date"})
updateDate?: 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;
}
} }

View file

@ -41,11 +41,11 @@ export class RecipeHandler {
} }
var savedEntity: RecipeEntity; var savedEntity: RecipeEntity;
const recipeId = dto.id const recipeId = dto.id
if(recipeId === undefined){ if(recipeId === undefined || recipeId.length === 0){
// create new recipe // create new recipe
const recipeEntity = this.mapper.toEntity(dto) const recipeEntity = this.mapper.toEntity(dto)
delete (recipeEntity as any).id;
savedEntity = await this.recipeRepository.create(recipeEntity); savedEntity = await this.recipeRepository.create(recipeEntity);
throw new ValidationError("Trying to update recipe without ID!")
} else { } else {
// save existing Recipe // save existing Recipe
// First: Load current version of recipe from database // First: Load current version of recipe from database

View file

@ -43,8 +43,10 @@ export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper<RecipeEntity,
entity.instructionSteps = dto.instructions.map((stepDto) => { entity.instructionSteps = dto.instructions.map((stepDto) => {
const stepEntity = this.instructionStepMapper.toEntity(stepDto); const stepEntity = this.instructionStepMapper.toEntity(stepDto);
// Always set the relation // Set the relation if the entity already exists in DB
if(entity.hasValidId()){
stepEntity.recipe = entity; stepEntity.recipe = entity;
}
// If it's a new step (no id from client), let DB generate a new UUID // If it's a new step (no id from client), let DB generate a new UUID
if (!stepDto.id) { if (!stepDto.id) {
@ -57,7 +59,11 @@ export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper<RecipeEntity,
// map ingredient groups // map ingredient groups
entity.ingredientGroups = dto.ingredientGroups.map((groupDto) => { entity.ingredientGroups = dto.ingredientGroups.map((groupDto) => {
const groupEntity = this.ingredientGroupMapper.toEntity(groupDto); const groupEntity = this.ingredientGroupMapper.toEntity(groupDto);
// Set the relation if the entity already exists in DB
if(entity.hasValidId()){
groupEntity.recipe = entity; groupEntity.recipe = entity;
}
// If it's a new group (no id from client), let DB generate a new UUID
if (!groupDto.id) { if (!groupDto.id) {
delete (groupEntity as any).id; delete (groupEntity as any).id;
} }

View file

@ -33,7 +33,10 @@ export class RecipeIngredientGroupDtoEntityMapper extends AbstractDtoEntityMappe
// map ingredients // map ingredients
entity.ingredients = dto.ingredients.map((ingredientDto) => { entity.ingredients = dto.ingredients.map((ingredientDto) => {
const ingredientEntity = this.ingredientMapper.toEntity(ingredientDto); const ingredientEntity = this.ingredientMapper.toEntity(ingredientDto);
// set relation if entity already exists in DB
if(entity.hasValidId()){
ingredientEntity.ingredientGroup = entity; ingredientEntity.ingredientGroup = entity;
}
// remove id from new entity completely and allow ORM to generate a new one // remove id from new entity completely and allow ORM to generate a new one
if (!ingredientDto.id) { if (!ingredientDto.id) {
delete (ingredientEntity as any).id; delete (ingredientEntity as any).id;

View file

@ -8,8 +8,9 @@ import { Request, Response, NextFunction } from "express";
*/ */
export function corsHeaders (req: Request, res: Response, next: NextFunction) { export function corsHeaders (req: Request, res: Response, next: NextFunction) {
// allow requests from all sources (*) for now // allow requests from all sources (*) for now
// @todo restrict access!
res.header('Access-Control-Allow-Origin', '*'); 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'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Handle preflight requests quickly // Handle preflight requests quickly