First unchecked draft for tags
This commit is contained in:
parent
f936e84168
commit
70b132dc6f
51 changed files with 494 additions and 69 deletions
22
bruno/recipe-backend/UserPoint/changePassword.bru
Normal file
22
bruno/recipe-backend/UserPoint/changePassword.bru
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
meta {
|
||||||
|
name: changePassword
|
||||||
|
type: http
|
||||||
|
seq: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{url}}/user/change-password
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"userId": "9c913747-ba57-4b12-87d0-3339f4a8117c",
|
||||||
|
"password": "test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
}
|
||||||
25
bruno/recipe-backend/collection.bru
Normal file
25
bruno/recipe-backend/collection.bru
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
auth {
|
||||||
|
mode: bearer
|
||||||
|
}
|
||||||
|
|
||||||
|
auth:bearer {
|
||||||
|
token: {{token}}
|
||||||
|
}
|
||||||
|
|
||||||
|
script:pre-request {
|
||||||
|
try{
|
||||||
|
// An dieser Stelle muss überprüft werden, ob diese Funktion gerade aufgerufen wird, ansonsten entsteht eine Endlosschleife.
|
||||||
|
const blocked = bru.getEnvVar("blocked");
|
||||||
|
if(blocked === "false" && new Date().valueOf() > Number(bru.getEnvVar("tokenExpireDate"))){
|
||||||
|
console.log('new Session')
|
||||||
|
|
||||||
|
bru.setEnvVar("blocked",true)
|
||||||
|
// Absoluter Pfad von der Collection-Root
|
||||||
|
await bru.runRequest('AuthPoint/login.bru')
|
||||||
|
|
||||||
|
bru.setEnvVar("blocked",false)
|
||||||
|
}
|
||||||
|
} catch (e){
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/api/dtos/TagDto.ts
Normal file
8
src/api/dtos/TagDto.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import {AbstractDto} from "./AbstractDto.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO describing a tag
|
||||||
|
*/
|
||||||
|
export class TagDto extends AbstractDto {
|
||||||
|
description!: string;
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { AuthHandler } from "../handlers/AuthHandler.js";
|
import { AuthHandler } from "../../handlers/AuthHandler.js";
|
||||||
import { UserRepository } from "../repositories/UserRepository.js";
|
import { UserRepository } from "../../repositories/UserRepository.js";
|
||||||
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
|
import { UserDtoEntityMapper } from "../../mappers/UserDtoEntityMapper.js";
|
||||||
import {
|
import {
|
||||||
ValidationError,
|
ValidationError,
|
||||||
UnauthorizedError,
|
UnauthorizedError,
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { asyncHandler } from "../utils/asyncHandler.js";
|
import { asyncHandler } from "../../utils/asyncHandler.js";
|
||||||
import { RecipeRepository } from "../repositories/RecipeRepository.js";
|
import { RecipeRepository } from "../../repositories/RecipeRepository.js";
|
||||||
import { CompactRecipeHandler } from "../handlers/CompactRecipeHandler.js";
|
import { CompactRecipeHandler } from "../../handlers/CompactRecipeHandler.js";
|
||||||
import { CompactRecipeDtoEntityMapper } from "../mappers/CompactRecipeDtoEntityMapper.js";
|
import { CompactRecipeDtoEntityMapper } from "../../mappers/CompactRecipeDtoEntityMapper.js";
|
||||||
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
|
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { RecipeRepository } from "../repositories/RecipeRepository.js";
|
import { RecipeRepository } from "../../repositories/RecipeRepository.js";
|
||||||
import { RecipeDtoEntityMapper } from "../mappers/RecipeDtoEntityMapper.js";
|
import { RecipeDtoEntityMapper } from "../../mappers/RecipeDtoEntityMapper.js";
|
||||||
import { RecipeHandler } from "../handlers/RecipeHandler.js";
|
import { RecipeHandler } from "../../handlers/RecipeHandler.js";
|
||||||
import { asyncHandler } from "../utils/asyncHandler.js";
|
import { asyncHandler } from "../../utils/asyncHandler.js";
|
||||||
import { RecipeDto } from "../dtos/RecipeDto.js";
|
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 {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
|
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
72
src/api/endpoints/TagRestResource.ts
Normal file
72
src/api/endpoints/TagRestResource.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { Router, Request, Response, NextFunction } from "express";
|
||||||
|
import { TagHandler } from "../../handlers/TagHandler.js";
|
||||||
|
import { TagDto } from "../dtos/TagDto.js";
|
||||||
|
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
|
||||||
|
import {requireAdmin} from "../../middleware/authorizationMiddleware.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST resource for tags.
|
||||||
|
*
|
||||||
|
* Routes:
|
||||||
|
* GET /tags — list all tags (authenticated users)
|
||||||
|
* POST /tags/create-or-update — create or update a tag (authenticated users)
|
||||||
|
* DELETE /tags/:id — delete a tag (administrators only)
|
||||||
|
*
|
||||||
|
* Authentication / authorisation is assumed to be enforced by middleware
|
||||||
|
* applied upstream (e.g. a global JWT guard). The adminOnly middleware below
|
||||||
|
* adds the role check that restricts DELETE to administrators.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
const tagHandler = new TagHandler();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /tags
|
||||||
|
* Returns all available tags.
|
||||||
|
*/
|
||||||
|
router.get("/", async (_req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const tags = await tagHandler.getAll();
|
||||||
|
res.json(tags);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /tags/create-or-update
|
||||||
|
* Creates a new tag or updates an existing one.
|
||||||
|
* Body: TagDto
|
||||||
|
*/
|
||||||
|
router.post(
|
||||||
|
"/create-or-update",
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const dto: TagDto = req.body;
|
||||||
|
const result = await tagHandler.createOrUpdate(dto);
|
||||||
|
res.status(HttpStatusCode.CREATED).json(result);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /tags/:id
|
||||||
|
* Deletes a tag by ID. Restricted to administrators.
|
||||||
|
* The database cascade removes all entries in the recipe_tag mapping table.
|
||||||
|
*/
|
||||||
|
router.delete(
|
||||||
|
"/:id",
|
||||||
|
requireAdmin,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
await tagHandler.delete(req.params.id);
|
||||||
|
res.status(HttpStatusCode.NO_CONTENT).send();
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { UserHandler } from "../handlers/UserHandler.js";
|
import { UserHandler } from "../../handlers/UserHandler.js";
|
||||||
import { CreateUserRequest } from "../dtos/CreateUserRequest.js";
|
import { CreateUserRequest } from "../dtos/CreateUserRequest.js";
|
||||||
import { UserRepository } from "../repositories/UserRepository.js";
|
import { UserRepository } from "../../repositories/UserRepository.js";
|
||||||
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
|
import { UserDtoEntityMapper } from "../../mappers/UserDtoEntityMapper.js";
|
||||||
import { asyncHandler } from "../utils/asyncHandler.js";
|
import { asyncHandler } from "../../utils/asyncHandler.js";
|
||||||
import { CreateUserResponse } from "../dtos/CreateUserResponse.js";
|
import { CreateUserResponse } from "../dtos/CreateUserResponse.js";
|
||||||
import { UserDto } from "../dtos/UserDto.js";
|
import { UserDto } from "../dtos/UserDto.js";
|
||||||
import {
|
import {
|
||||||
requireAdmin,
|
requireAdmin,
|
||||||
requireAdminOrOwner,
|
requireAdminOrOwner,
|
||||||
requireAdminOrSelf
|
requireAdminOrSelf
|
||||||
} from "../middleware/authorizationMiddleware.js";
|
} from "../../middleware/authorizationMiddleware.js";
|
||||||
import { UserListResponse } from "../dtos/UserListResponse.js";
|
import { UserListResponse } from "../dtos/UserListResponse.js";
|
||||||
import { HttpStatusCode } from "../apiHelpers/HttpStatusCodes.js";
|
import { HttpStatusCode } from "../apiHelpers/HttpStatusCodes.js";
|
||||||
import { InternalServerError, NotFoundError } from "../errors/httpErrors.js";
|
import { InternalServerError, NotFoundError } from "../errors/httpErrors.js";
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { Entity, Column, OneToMany, Relation } from "typeorm";
|
import { Entity, Column, OneToMany, ManyToMany, JoinTable, Relation } from "typeorm";
|
||||||
import { AbstractEntity } from "./AbstractEntity.js";
|
import { AbstractEntity } from "./AbstractEntity.js";
|
||||||
import { RecipeInstructionStepEntity } from "./RecipeInstructionStepEntity.js";
|
import { RecipeInstructionStepEntity } from "./RecipeInstructionStepEntity.js";
|
||||||
import { RecipeIngredientGroupEntity } from "./RecipeIngredientGroupEntity.js";
|
import { RecipeIngredientGroupEntity } from "./RecipeIngredientGroupEntity.js";
|
||||||
|
import { TagEntity } from "./TagEntity.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity describing a recipe
|
* Entity describing a recipe
|
||||||
|
|
@ -17,15 +18,39 @@ export class RecipeEntity extends AbstractEntity {
|
||||||
@Column({ nullable: true, name: "amount_description" })
|
@Column({ nullable: true, name: "amount_description" })
|
||||||
amountDescription?: string;
|
amountDescription?: string;
|
||||||
|
|
||||||
// make sure not to induce a circular dependency! user arrow function without brackets!
|
// make sure not to induce a circular dependency! use arrow function without brackets!
|
||||||
@OneToMany(() => RecipeInstructionStepEntity, (instructionStep) => instructionStep.recipe, {
|
@OneToMany(() => RecipeInstructionStepEntity, (instructionStep) => instructionStep.recipe, {
|
||||||
cascade: true
|
cascade: true
|
||||||
})
|
})
|
||||||
instructionSteps!: RecipeInstructionStepEntity[];
|
instructionSteps!: RecipeInstructionStepEntity[];
|
||||||
|
|
||||||
// make sure not to induce a circular dependency! user arrow function without brackets!
|
// make sure not to induce a circular dependency! use arrow function without brackets!
|
||||||
@OneToMany(() => RecipeIngredientGroupEntity, (ingredientGroup) => ingredientGroup.recipe, {
|
@OneToMany(() => RecipeIngredientGroupEntity, (ingredientGroup) => ingredientGroup.recipe, {
|
||||||
cascade: true
|
cascade: true
|
||||||
})
|
})
|
||||||
ingredientGroups!: Relation<RecipeIngredientGroupEntity>[];
|
ingredientGroups!: Relation<RecipeIngredientGroupEntity>[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tags associated with this recipe.
|
||||||
|
*
|
||||||
|
* RecipeEntity is the *owning* side of the relation:
|
||||||
|
* - @JoinTable creates and owns the `recipe_tag` mapping table.
|
||||||
|
* - cascade insert/update allows tags to be persisted via recipe save.
|
||||||
|
*
|
||||||
|
* Deleting a Recipe removes its rows from recipe_tag automatically via the
|
||||||
|
* ON DELETE CASCADE FK defined in the migration. Tags themselves are unaffected.
|
||||||
|
* Deleting a Tag similarly removes its rows from recipe_tag via the other FK.
|
||||||
|
*
|
||||||
|
* make sure not to induce a circular dependency! use arrow function without brackets!
|
||||||
|
*/
|
||||||
|
@ManyToMany(() => TagEntity, (tag) => tag.recipeList, {
|
||||||
|
cascade: ["insert", "update"],
|
||||||
|
eager: false,
|
||||||
|
})
|
||||||
|
@JoinTable({
|
||||||
|
name: "recipe_tag",
|
||||||
|
joinColumn: { name: "recipe_id", referencedColumnName: "id" },
|
||||||
|
inverseJoinColumn: { name: "tag_id", referencedColumnName: "id" },
|
||||||
|
})
|
||||||
|
tagList!: TagEntity[];
|
||||||
}
|
}
|
||||||
20
src/entities/TagEntity.ts
Normal file
20
src/entities/TagEntity.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Column, Entity, ManyToMany } from "typeorm";
|
||||||
|
import { AbstractEntity } from "./AbstractEntity.js";
|
||||||
|
import { RecipeEntity } from "./RecipeEntity.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persisted tag that can be attached to any number of recipes.
|
||||||
|
* The join table (recipe_tag) is owned by RecipeEntity.
|
||||||
|
*/
|
||||||
|
@Entity({ name: "tag" })
|
||||||
|
export class TagEntity extends AbstractEntity {
|
||||||
|
@Column({ type: "varchar", length: 100, unique: true })
|
||||||
|
description!: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inverse side of the ManyToMany relation.
|
||||||
|
* The join table is declared on RecipeEntity to keep ownership there.
|
||||||
|
*/
|
||||||
|
@ManyToMany(() => RecipeEntity, recipe => recipe.tagList)
|
||||||
|
recipeList!: RecipeEntity[];
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Entity, Column } from "typeorm";
|
import { Entity, Column } from "typeorm";
|
||||||
import { AbstractEntity } from "./AbstractEntity.js";
|
import { AbstractEntity } from "./AbstractEntity.js";
|
||||||
import { UserRole } from "../enums/UserRole.js";
|
import { UserRole } from "../api/enums/UserRole.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity describing a user
|
* Entity describing a user
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { UserRepository } from "../repositories/UserRepository.js";
|
import { UserRepository } from "../repositories/UserRepository.js";
|
||||||
import { encrypt } from "../utils/encryptionUtils.js";
|
import { encrypt } from "../utils/encryptionUtils.js";
|
||||||
import { ValidationError, UnauthorizedError } from "../errors/httpErrors.js";
|
import { ValidationError, UnauthorizedError } from "../api/errors/httpErrors.js";
|
||||||
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
|
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
|
||||||
import { LoginResponse } from "../dtos/LoginResponse.js";
|
import { LoginResponse } from "../api/dtos/LoginResponse.js";
|
||||||
import { LoginRequest } from "../dtos/LoginRequest.js";
|
import { LoginRequest } from "../api/dtos/LoginRequest.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller responsible for authentication, e.g., login or issueing a token with extended
|
* Controller responsible for authentication, e.g., login or issueing a token with extended
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { CompactRecipeDto } from "../dtos/CompactRecipeDto.js";
|
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";
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { RecipeDto } from "../dtos/RecipeDto.js";
|
import { RecipeDto } from "../api/dtos/RecipeDto.js";
|
||||||
import { RecipeDtoEntityMapper } from "../mappers/RecipeDtoEntityMapper.js";
|
import { RecipeDtoEntityMapper } from "../mappers/RecipeDtoEntityMapper.js";
|
||||||
import { RecipeRepository } from "../repositories/RecipeRepository.js";
|
import { RecipeRepository } from "../repositories/RecipeRepository.js";
|
||||||
import { NotFoundError, ValidationError } from "../errors/httpErrors.js";
|
import { NotFoundError, ValidationError } from "../api/errors/httpErrors.js";
|
||||||
import { RecipeInstructionStepDto } from "../dtos/RecipeInstructionStepDto.js";
|
import { RecipeInstructionStepDto } from "../api/dtos/RecipeInstructionStepDto.js";
|
||||||
import { RecipeIngredientGroupDto } from "../dtos/RecipeIngredientGroupDto.js";
|
import { RecipeIngredientGroupDto } from "../api/dtos/RecipeIngredientGroupDto.js";
|
||||||
import { RecipeIngredientDto } from "../dtos/RecipeIngredientDto.js";
|
import { RecipeIngredientDto } from "../api/dtos/RecipeIngredientDto.js";
|
||||||
import { RecipeEntity } from "../entities/RecipeEntity.js";
|
import { RecipeEntity } from "../entities/RecipeEntity.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -62,7 +62,7 @@ export class RecipeHandler {
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Update recipe data
|
* Update recipe data
|
||||||
* @param RecipeDto containing the entire updated recipe
|
* @param dto containing the entire updated recipe
|
||||||
* @returns Up-to-date RecipeDto as saved in the database
|
* @returns Up-to-date RecipeDto as saved in the database
|
||||||
*/
|
*/
|
||||||
async updateRecipe(dto: RecipeDto){
|
async updateRecipe(dto: RecipeDto){
|
||||||
|
|
|
||||||
73
src/handlers/TagHandler.ts
Normal file
73
src/handlers/TagHandler.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { TagRepository } from "../repositories/TagRepository.js";
|
||||||
|
import { TagDtoEntityMapper } from "../mappers/TagDtoEntityMapper.js";
|
||||||
|
import { TagDto } from "../api/dtos/TagDto.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles business logic for tag operations.
|
||||||
|
* Called by TagRestResource; keeps routing and business logic separated.
|
||||||
|
*/
|
||||||
|
export class TagHandler {
|
||||||
|
private readonly tagRepository: TagRepository;
|
||||||
|
private readonly tagMapper: TagDtoEntityMapper;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.tagRepository = new TagRepository();
|
||||||
|
this.tagMapper = new TagDtoEntityMapper();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all tags.
|
||||||
|
*/
|
||||||
|
async getAll(): Promise<TagDto[]> {
|
||||||
|
const entities = await this.tagRepository.findAll();
|
||||||
|
return entities.map(e => this.tagMapper.toDto(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new tag or updates an existing one.
|
||||||
|
*
|
||||||
|
* - If a tag with the same description already exists (case-insensitive)
|
||||||
|
* the existing tag is returned unchanged (idempotent behaviour matching
|
||||||
|
* the "create-or-update" contract).
|
||||||
|
* - If a DTO with an explicit ID is supplied, the description of that tag
|
||||||
|
* is updated.
|
||||||
|
* - Otherwise a brand-new tag is created.
|
||||||
|
*/
|
||||||
|
async createOrUpdate(dto: TagDto): Promise<TagDto> {
|
||||||
|
// Update path: ID supplied — find and merge changes into existing entity
|
||||||
|
if (dto.id) {
|
||||||
|
const existing = await this.tagRepository.findById(dto.id);
|
||||||
|
if (!existing) {
|
||||||
|
throw new Error(`Tag with id ${dto.id} not found`);
|
||||||
|
}
|
||||||
|
const updated = await this.tagRepository.update(
|
||||||
|
this.tagMapper.mergeDtoIntoEntity(dto, existing)
|
||||||
|
);
|
||||||
|
return this.tagMapper.toDto(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create path: check for duplicate description first
|
||||||
|
const duplicate = await this.tagRepository.findByDescription(dto.description);
|
||||||
|
if (duplicate) {
|
||||||
|
return this.tagMapper.toDto(duplicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await this.tagRepository.create(this.tagMapper.toEntity(dto));
|
||||||
|
return this.tagMapper.toDto(created);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a tag by ID.
|
||||||
|
* The ON DELETE CASCADE on the recipe_tag join table ensures all
|
||||||
|
* references are cleaned up automatically by the database.
|
||||||
|
*
|
||||||
|
* @throws Error when the tag does not exist.
|
||||||
|
*/
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
const existing = await this.tagRepository.findById(id);
|
||||||
|
if (!existing) {
|
||||||
|
throw new Error(`Tag with id ${id} not found`);
|
||||||
|
}
|
||||||
|
await this.tagRepository.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import {ConflictError, NotFoundError, ValidationError} from "../errors/httpErrors.js";
|
import {ConflictError, NotFoundError, ValidationError} from "../api/errors/httpErrors.js";
|
||||||
import {CreateUserRequest} from "../dtos/CreateUserRequest.js";
|
import {CreateUserRequest} from "../api/dtos/CreateUserRequest.js";
|
||||||
import {UserDto} from "../dtos/UserDto.js";
|
import {UserDto} from "../api/dtos/UserDto.js";
|
||||||
import {encrypt} from "../utils/encryptionUtils.js";
|
import {encrypt} from "../utils/encryptionUtils.js";
|
||||||
import {UserRepository} from "../repositories/UserRepository.js";
|
import {UserRepository} from "../repositories/UserRepository.js";
|
||||||
import {UserDtoEntityMapper} from "../mappers/UserDtoEntityMapper.js";
|
import {UserDtoEntityMapper} from "../mappers/UserDtoEntityMapper.js";
|
||||||
import {UUID} from "crypto";
|
import {UUID} from "crypto";
|
||||||
import {ChangeUserPasswordRequest} from "../dtos/ChangeUserPasswordRequest.js";
|
import {ChangeUserPasswordRequest} from "../api/dtos/ChangeUserPasswordRequest.js";
|
||||||
import {UserEntity} from "../entities/UserEntity.js";
|
import {UserEntity} from "../entities/UserEntity.js";
|
||||||
import {UserRole, UserRoleHelper} from "../enums/UserRole.js";
|
import {UserRole, UserRoleHelper} from "../api/enums/UserRole.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controls all user specific actions
|
* Controls all user specific actions
|
||||||
|
|
|
||||||
10
src/index.ts
10
src/index.ts
|
|
@ -6,11 +6,11 @@ import { requestLoggerMiddleware } from "./middleware/requestLoggerMiddleware.js
|
||||||
import { errorLoggerMiddleware } from "./middleware/errorLoggerMiddleware.js";
|
import { errorLoggerMiddleware } from "./middleware/errorLoggerMiddleware.js";
|
||||||
import { corsHeaders } from "./middleware/corsMiddleware.js";
|
import { corsHeaders } from "./middleware/corsMiddleware.js";
|
||||||
import { logger } from "./utils/logger.js";
|
import { logger } from "./utils/logger.js";
|
||||||
import { HttpStatusCode } from "./apiHelpers/HttpStatusCodes.js";
|
import { HttpStatusCode } from "./api/apiHelpers/HttpStatusCodes.js";
|
||||||
import authRoutes, { authBasicRoute } from "./endpoints/AuthRestResource.js";
|
import authRoutes, { authBasicRoute } from "./api/endpoints/AuthRestResource.js";
|
||||||
import userRoutes, { userBasicRoute } from "./endpoints/UserRestResource.js";
|
import userRoutes, { userBasicRoute } from "./api/endpoints/UserRestResource.js";
|
||||||
import compactRecipeRoutes from "./endpoints/CompactRecipeRestResource.js";
|
import compactRecipeRoutes from "./api/endpoints/CompactRecipeRestResource.js";
|
||||||
import recipeRoutes from "./endpoints/RecipeRestResource.js";
|
import recipeRoutes from "./api/endpoints/RecipeRestResource.js";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { AbstractDto } from "../dtos/AbstractDto.js";
|
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<
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { CompactRecipeDto } from "../dtos/CompactRecipeDto.js";
|
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";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { RecipeDto } from "../dtos/RecipeDto.js";
|
import { RecipeDto } from "../api/dtos/RecipeDto.js";
|
||||||
import { RecipeIngredientGroupDto } from "../dtos/RecipeIngredientGroupDto.js";
|
import { RecipeIngredientGroupDto } from "../api/dtos/RecipeIngredientGroupDto.js";
|
||||||
import { RecipeInstructionStepDto } from "../dtos/RecipeInstructionStepDto.js";
|
import { RecipeInstructionStepDto } from "../api/dtos/RecipeInstructionStepDto.js";
|
||||||
import { RecipeEntity } from "../entities/RecipeEntity.js";
|
import { RecipeEntity } from "../entities/RecipeEntity.js";
|
||||||
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";
|
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";
|
||||||
import { RecipeIngredientGroupDtoEntityMapper } from "./RecipeIngredientGroupDtoEntityMapper.js";
|
import { RecipeIngredientGroupDtoEntityMapper } from "./RecipeIngredientGroupDtoEntityMapper.js";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { RecipeIngredientDto } from "../dtos/RecipeIngredientDto.js";
|
import { RecipeIngredientDto } from "../api/dtos/RecipeIngredientDto.js";
|
||||||
import { RecipeIngredientEntity } from "../entities/RecipeIngredientEntity.js";
|
import { RecipeIngredientEntity } from "../entities/RecipeIngredientEntity.js";
|
||||||
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";
|
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { RecipeIngredientDto } from "../dtos/RecipeIngredientDto.js";
|
import { RecipeIngredientDto } from "../api/dtos/RecipeIngredientDto.js";
|
||||||
import { RecipeIngredientGroupDto } from "../dtos/RecipeIngredientGroupDto.js";
|
import { RecipeIngredientGroupDto } from "../api/dtos/RecipeIngredientGroupDto.js";
|
||||||
import { RecipeIngredientGroupEntity } from "../entities/RecipeIngredientGroupEntity.js";
|
import { RecipeIngredientGroupEntity } from "../entities/RecipeIngredientGroupEntity.js";
|
||||||
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";
|
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";
|
||||||
import { RecipeIngredientDtoEntityMapper } from "./RecipeIngredientDtoEntityMapper.js";
|
import { RecipeIngredientDtoEntityMapper } from "./RecipeIngredientDtoEntityMapper.js";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { RecipeInstructionStepDto } from "../dtos/RecipeInstructionStepDto.js";
|
import { RecipeInstructionStepDto } from "../api/dtos/RecipeInstructionStepDto.js";
|
||||||
import { RecipeInstructionStepEntity } from "../entities/RecipeInstructionStepEntity.js";
|
import { RecipeInstructionStepEntity } from "../entities/RecipeInstructionStepEntity.js";
|
||||||
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";
|
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";
|
||||||
|
|
||||||
|
|
|
||||||
44
src/mappers/TagDtoEntityMapper.ts
Normal file
44
src/mappers/TagDtoEntityMapper.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";
|
||||||
|
import { TagEntity } from "../entities/TagEntity.js";
|
||||||
|
import { TagDto } from "../api/dtos/TagDto.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps between TagDto (API layer) and TagEntity (persistence layer).
|
||||||
|
*/
|
||||||
|
export class TagDtoEntityMapper extends AbstractDtoEntityMapper<TagEntity, TagDto> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a TagEntity to a TagDto for outbound API responses.
|
||||||
|
*/
|
||||||
|
toDto(entity: TagEntity): TagDto {
|
||||||
|
const dto = new TagDto();
|
||||||
|
this.mapBaseEntityToDto(entity, dto);
|
||||||
|
dto.description = entity.description;
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a TagDto to a new TagEntity.
|
||||||
|
*/
|
||||||
|
toEntity(dto: TagDto): TagEntity {
|
||||||
|
const entity = this.createNewEntity();
|
||||||
|
return this.mergeDtoIntoEntity(dto, entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges changes from a TagDto into an existing TagEntity.
|
||||||
|
* Used when updating a tag.
|
||||||
|
*/
|
||||||
|
mergeDtoIntoEntity(dto: TagDto, entity: TagEntity): TagEntity {
|
||||||
|
this.mapBaseDtoToEntity(dto, entity);
|
||||||
|
entity.description = dto.description;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a blank TagEntity. Required by mergeDtoListIntoEntityList.
|
||||||
|
*/
|
||||||
|
createNewEntity(): TagEntity {
|
||||||
|
return new TagEntity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";
|
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";
|
||||||
import { UserEntity } from "../entities/UserEntity.js";
|
import { UserEntity } from "../entities/UserEntity.js";
|
||||||
import { UserDto } from "../dtos/UserDto.js";
|
import { UserDto } from "../api/dtos/UserDto.js";
|
||||||
|
|
||||||
export class UserDtoEntityMapper extends AbstractDtoEntityMapper<UserEntity, UserDto> {
|
export class UserDtoEntityMapper extends AbstractDtoEntityMapper<UserEntity, UserDto> {
|
||||||
toDto(entity: UserEntity): UserDto {
|
toDto(entity: UserEntity): UserDto {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import { authBasicRoute } from "../endpoints/AuthRestResource.js";
|
import { authBasicRoute } from "../api/endpoints/AuthRestResource.js";
|
||||||
import { AuthPayload } from "../dtos/AuthPayload.js";
|
import { AuthPayload } from "../api/dtos/AuthPayload.js";
|
||||||
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
|
import {HttpStatusCode} from "../api/apiHelpers/HttpStatusCodes.js";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { ForbiddenError } from "../errors/httpErrors.js";
|
import { ForbiddenError } from "../api/errors/httpErrors.js";
|
||||||
import { HttpStatusCode } from "../apiHelpers/HttpStatusCodes.js";
|
import { HttpStatusCode } from "../api/apiHelpers/HttpStatusCodes.js";
|
||||||
import { UserRole } from "../enums/UserRole.js";
|
import { UserRole } from "../api/enums/UserRole.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Middleware to check if the current user has one of the required roles
|
* Middleware to check if the current user has one of the required roles
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
|
import {HttpStatusCode} from "../api/apiHelpers/HttpStatusCodes.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add CORS header
|
* Add CORS header
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// middleware/errorHandler.ts
|
// middleware/errorHandler.ts
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { HttpError, InternalServerError } from "../errors/httpErrors.js";
|
import { HttpError, InternalServerError } from "../api/errors/httpErrors.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Express global error-handling middleware.
|
* Express global error-handling middleware.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { HttpError } from "../errors/httpErrors.js";
|
import { HttpError } from "../api/errors/httpErrors.js";
|
||||||
import { logger } from "../utils/logger.js";
|
import { logger } from "../utils/logger.js";
|
||||||
import { HttpStatusCode } from "../apiHelpers/HttpStatusCodes.js";
|
import { HttpStatusCode } from "../api/apiHelpers/HttpStatusCodes.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error logging and handling middleware
|
* Error logging and handling middleware
|
||||||
|
|
|
||||||
46
src/migrations/1771658108802-CreateTagTable.ts
Normal file
46
src/migrations/1771658108802-CreateTagTable.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import {MigrationInterface, QueryRunner, Table} from "typeorm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the `tag` table.
|
||||||
|
*/
|
||||||
|
export class CreateTagTable1771658108802 implements MigrationInterface {
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: "tag",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
type: "uuid",
|
||||||
|
isPrimary: true,
|
||||||
|
generationStrategy: "uuid",
|
||||||
|
default: "uuid_generate_v4()",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
type: "varchar",
|
||||||
|
length: "100",
|
||||||
|
isUnique: true,
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create_date",
|
||||||
|
type: "timestamp",
|
||||||
|
default: "now()",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update_date",
|
||||||
|
type: "timestamp",
|
||||||
|
default: "now()",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
true // ifNotExists
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropTable("tag", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/migrations/1771658131258-CreateRecipeTagTable.ts
Normal file
66
src/migrations/1771658131258-CreateRecipeTagTable.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import {MigrationInterface, QueryRunner, Table, TableForeignKey} from "typeorm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the `recipe_tag` many-to-many join table.
|
||||||
|
*
|
||||||
|
* Both foreign keys are defined with ON DELETE CASCADE so that:
|
||||||
|
* - deleting a Recipe automatically removes its rows from this table, and
|
||||||
|
* - deleting a Tag automatically removes its rows from this table.
|
||||||
|
* In both cases the other side of the relation is left untouched.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class CreateRecipeTagTable1771658131258 implements MigrationInterface {
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: "recipe_tag",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: "recipe_id",
|
||||||
|
type: "uuid",
|
||||||
|
isPrimary: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tag_id",
|
||||||
|
type: "uuid",
|
||||||
|
isPrimary: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
true // ifNotExists
|
||||||
|
);
|
||||||
|
|
||||||
|
// FK: recipe_tag.recipe_id → recipe.id (cascade on recipe delete)
|
||||||
|
await queryRunner.createForeignKey(
|
||||||
|
"recipe_tag",
|
||||||
|
new TableForeignKey({
|
||||||
|
name: "FK_recipe_tag_recipe",
|
||||||
|
columnNames: ["recipe_id"],
|
||||||
|
referencedTableName: "recipe",
|
||||||
|
referencedColumnNames: ["id"],
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
onUpdate: "CASCADE",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// FK: recipe_tag.tag_id → tag.id (cascade on tag delete)
|
||||||
|
await queryRunner.createForeignKey(
|
||||||
|
"recipe_tag",
|
||||||
|
new TableForeignKey({
|
||||||
|
name: "FK_recipe_tag_tag",
|
||||||
|
columnNames: ["tag_id"],
|
||||||
|
referencedTableName: "tag",
|
||||||
|
referencedColumnNames: ["id"],
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
onUpdate: "CASCADE",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropForeignKey("recipe_tag", "FK_recipe_tag_tag");
|
||||||
|
await queryRunner.dropForeignKey("recipe_tag", "FK_recipe_tag_recipe");
|
||||||
|
await queryRunner.dropTable("recipe_tag", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/repositories/TagRepository.ts
Normal file
24
src/repositories/TagRepository.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { AbstractRepository } from "./AbstractRepository.js";
|
||||||
|
import { TagEntity } from "../entities/TagEntity.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository for TagEntity.
|
||||||
|
* The AbstractRepository already provides findById, findAll, create, update,
|
||||||
|
* and delete. Tag-specific queries can be added here as needed.
|
||||||
|
*/
|
||||||
|
export class TagRepository extends AbstractRepository<TagEntity> {
|
||||||
|
constructor() {
|
||||||
|
super(TagEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up a tag by its description (case-insensitive).
|
||||||
|
* Useful for detecting duplicates before creating a new tag.
|
||||||
|
*/
|
||||||
|
async findByDescription(description: string): Promise<TagEntity | null> {
|
||||||
|
return this.repo
|
||||||
|
.createQueryBuilder("tag")
|
||||||
|
.where("LOWER(tag.description) = LOWER(:description)", { description })
|
||||||
|
.getOne();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import { AuthPayload } from "../dtos/AuthPayload.js";
|
import { AuthPayload } from "../api/dtos/AuthPayload.js";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
const { JWT_SECRET = "" } = process.env;
|
const { JWT_SECRET = "" } = process.env;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import winston from "winston";
|
import winston from "winston";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { HttpStatusCode } from "../apiHelpers/HttpStatusCodes.js";
|
import { HttpStatusCode } from "../api/apiHelpers/HttpStatusCodes.js";
|
||||||
|
|
||||||
// Define log format
|
// Define log format
|
||||||
const logFormat = winston.format.combine(
|
const logFormat = winston.format.combine(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue