Fix bug when saving tags for recipes

This commit is contained in:
araemer 2026-02-22 13:18:31 +01:00
parent 66da81baf8
commit 96d87fed13
8 changed files with 81 additions and 17 deletions

View file

@ -5,7 +5,7 @@ meta {
} }
get { get {
url: {{url}}/recipe/fa608340-d679-4267-8b89-c743bd7fc234 url: {{url}}/recipe/44a8f38c-9387-439e-aed6-c3369b776b1c
body: none body: none
auth: inherit auth: inherit
} }

View file

@ -0,0 +1,21 @@
meta {
name: createOrUpdate
type: http
seq: 2
}
post {
url: {{url}}/tag/create-or-update
body: json
auth: inherit
}
body:json {
{
"description": "Kuchen"
}
}
settings {
encodeUrl: true
}

View file

@ -0,0 +1,15 @@
meta {
name: delete
type: http
seq: 3
}
delete {
url: {{url}}/tag/374d5df2-f7ff-45cd-ac67-c213233bf83f
body: none
auth: inherit
}
settings {
encodeUrl: true
}

View file

@ -0,0 +1,8 @@
meta {
name: TagRestResource
seq: 5
}
auth {
mode: inherit
}

View file

@ -0,0 +1,15 @@
meta {
name: getAll
type: http
seq: 1
}
get {
url: {{url}}/tag/all
body: none
auth: inherit
}
settings {
encodeUrl: true
}

View file

@ -8,9 +8,9 @@ import {requireAdmin} from "../../middleware/authorizationMiddleware.js";
* REST resource for tags. * REST resource for tags.
* *
* Routes: * Routes:
* GET /tags list all tags (authenticated users) * GET /tag list all tags (authenticated users)
* POST /tags/create-or-update create or update a tag (authenticated users) * POST /tag/create-or-update create or update a tag (authenticated users)
* DELETE /tags/:id delete a tag (administrators only) * DELETE /tag/:id delete a tag (administrators only)
* *
* Authentication / authorisation is assumed to be enforced by middleware * Authentication / authorisation is assumed to be enforced by middleware
* applied upstream (e.g. a global JWT guard). The adminOnly middleware below * applied upstream (e.g. a global JWT guard). The adminOnly middleware below
@ -22,7 +22,7 @@ const tagHandler = new TagHandler();
/** /**
* GET /tags * GET /tag/all
* Returns all available tags. * Returns all available tags.
*/ */
router.get("/all", async (_req: Request, res: Response, next: NextFunction) => { router.get("/all", async (_req: Request, res: Response, next: NextFunction) => {
@ -35,9 +35,9 @@ router.get("/all", async (_req: Request, res: Response, next: NextFunction) => {
}); });
/** /**
* POST /tags/create-or-update * POST /tag/create-or-update
* Creates a new tag or updates an existing one. * Creates a new tag or updates an existing one.
* If a tag with the given descriprion already exists in the database, that tag is returned instead of creating * If a tag with the given description already exists in the database, that tag is returned instead of creating
* a duplicate. * a duplicate.
* Body: TagDto * Body: TagDto
*/ */
@ -55,7 +55,7 @@ router.post(
); );
/** /**
* DELETE /tags/:id * DELETE /tag/:id
* Deletes a tag by ID. Restricted to administrators. * Deletes a tag by ID. Restricted to administrators.
* The database cascade removes all entries in the recipe_tag mapping table. * The database cascade removes all entries in the recipe_tag mapping table.
*/ */

View file

@ -6,6 +6,7 @@ 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(
@ -114,12 +115,15 @@ export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper<RecipeEntity,
); );
// --- Tags --- // --- Tags ---
// Tags are looked up by ID and replaced wholesale: the recipe holds /* Only build a stub here instead of using the mapper to avoid causing a cascade on the tag table while
// references to existing tag entities, so we map each DTO to its entity * updating the join table correctly. For this purpose, the ORM only needs to know the tag ID in order to
// form and let TypeORM sync the join table on save. * signal that we are dealing with an existing tag.
entity.tagList = (dto.tagList ?? []).map((tagDto) => */
this.tagMapper.toEntity(tagDto) entity.tagList = (dto.tagList ?? []).map((tagDto) => {
); const stub = new TagEntity();
stub.id = tagDto.id;
return stub;
});
return entity; return entity;
} }

View file

@ -1,7 +1,7 @@
import { AbstractRepository } from "./AbstractRepository.js"; import { AbstractRepository } from "./AbstractRepository.js";
import { RecipeEntity } from "../entities/RecipeEntity.js"; import { RecipeEntity } from "../entities/RecipeEntity.js";
import { AppDataSource } from "../data-source.js"; import { AppDataSource } from "../data-source.js";
import { ILike, Like } from "typeorm"; import { ILike } from "typeorm";
export class RecipeRepository extends AbstractRepository<RecipeEntity> { export class RecipeRepository extends AbstractRepository<RecipeEntity> {
constructor() { constructor() {
@ -19,7 +19,8 @@ export class RecipeRepository extends AbstractRepository<RecipeEntity> {
relations: [ relations: [
'ingredientGroups', 'ingredientGroups',
'ingredientGroups.ingredients', 'ingredientGroups.ingredients',
'instructionSteps' 'instructionSteps',
'tagList',
] ]
}); });
} }
@ -46,7 +47,7 @@ export class RecipeRepository extends AbstractRepository<RecipeEntity> {
// load existing data // load existing data
const existing = await this.repo.findOneOrFail({ const existing = await this.repo.findOneOrFail({
where: { id: entity.id }, where: { id: entity.id },
relations: ["instructionSteps", "ingredientGroups", "ingredientGroups.ingredients"], relations: ["instructionSteps", "ingredientGroups", "ingredientGroups.ingredients", "tagList"],
}); });
// merge new entity and existing entity // merge new entity and existing entity