First unchecked draft for tags

This commit is contained in:
araemer 2026-02-21 08:28:15 +01:00
parent f936e84168
commit 70b132dc6f
51 changed files with 494 additions and 69 deletions

View 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
}

View 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
View file

@ -0,0 +1,8 @@
import {AbstractDto} from "./AbstractDto.js";
/**
* DTO describing a tag
*/
export class TagDto extends AbstractDto {
description!: string;
}

View file

@ -1,7 +1,7 @@
import { Router } from "express";
import { AuthHandler } from "../handlers/AuthHandler.js";
import { UserRepository } from "../repositories/UserRepository.js";
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
import { AuthHandler } from "../../handlers/AuthHandler.js";
import { UserRepository } from "../../repositories/UserRepository.js";
import { UserDtoEntityMapper } from "../../mappers/UserDtoEntityMapper.js";
import {
ValidationError,
UnauthorizedError,

View file

@ -1,8 +1,8 @@
import { Router } from "express";
import { asyncHandler } from "../utils/asyncHandler.js";
import { RecipeRepository } from "../repositories/RecipeRepository.js";
import { CompactRecipeHandler } from "../handlers/CompactRecipeHandler.js";
import { CompactRecipeDtoEntityMapper } from "../mappers/CompactRecipeDtoEntityMapper.js";
import { asyncHandler } from "../../utils/asyncHandler.js";
import { RecipeRepository } from "../../repositories/RecipeRepository.js";
import { CompactRecipeHandler } from "../../handlers/CompactRecipeHandler.js";
import { CompactRecipeDtoEntityMapper } from "../../mappers/CompactRecipeDtoEntityMapper.js";
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
/**

View file

@ -1,12 +1,12 @@
import { Router } from "express";
import { RecipeRepository } from "../repositories/RecipeRepository.js";
import { RecipeDtoEntityMapper } from "../mappers/RecipeDtoEntityMapper.js";
import { RecipeHandler } from "../handlers/RecipeHandler.js";
import { asyncHandler } from "../utils/asyncHandler.js";
import { RecipeRepository } from "../../repositories/RecipeRepository.js";
import { RecipeDtoEntityMapper } from "../../mappers/RecipeDtoEntityMapper.js";
import { RecipeHandler } from "../../handlers/RecipeHandler.js";
import { asyncHandler } from "../../utils/asyncHandler.js";
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 { RecipeIngredientDtoEntityMapper } from "../../mappers/RecipeIngredientDtoEntityMapper.js";
import { RecipeIngredientGroupDtoEntityMapper } from "../../mappers/RecipeIngredientGroupDtoEntityMapper.js";
import { RecipeInstructionStepDtoEntityMapper } from "../../mappers/RecipeInstructionStepDtoEntityMapper.js";
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
/**

View 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;

View file

@ -1,16 +1,16 @@
import { Router } from "express";
import { UserHandler } from "../handlers/UserHandler.js";
import { UserHandler } from "../../handlers/UserHandler.js";
import { CreateUserRequest } from "../dtos/CreateUserRequest.js";
import { UserRepository } from "../repositories/UserRepository.js";
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
import { asyncHandler } from "../utils/asyncHandler.js";
import { UserRepository } from "../../repositories/UserRepository.js";
import { UserDtoEntityMapper } from "../../mappers/UserDtoEntityMapper.js";
import { asyncHandler } from "../../utils/asyncHandler.js";
import { CreateUserResponse } from "../dtos/CreateUserResponse.js";
import { UserDto } from "../dtos/UserDto.js";
import {
requireAdmin,
requireAdminOrOwner,
requireAdminOrSelf
} from "../middleware/authorizationMiddleware.js";
} from "../../middleware/authorizationMiddleware.js";
import { UserListResponse } from "../dtos/UserListResponse.js";
import { HttpStatusCode } from "../apiHelpers/HttpStatusCodes.js";
import { InternalServerError, NotFoundError } from "../errors/httpErrors.js";

View file

@ -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 { RecipeInstructionStepEntity } from "./RecipeInstructionStepEntity.js";
import { RecipeIngredientGroupEntity } from "./RecipeIngredientGroupEntity.js";
import { TagEntity } from "./TagEntity.js";
/**
* Entity describing a recipe
@ -17,15 +18,39 @@ export class RecipeEntity extends AbstractEntity {
@Column({ nullable: true, name: "amount_description" })
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, {
cascade: true
})
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, {
cascade: true
})
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
View 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[];
}

View file

@ -1,6 +1,6 @@
import { Entity, Column } from "typeorm";
import { AbstractEntity } from "./AbstractEntity.js";
import { UserRole } from "../enums/UserRole.js";
import { UserRole } from "../api/enums/UserRole.js";
/**
* Entity describing a user

View file

@ -1,9 +1,9 @@
import { UserRepository } from "../repositories/UserRepository.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 { LoginResponse } from "../dtos/LoginResponse.js";
import { LoginRequest } from "../dtos/LoginRequest.js";
import { LoginResponse } from "../api/dtos/LoginResponse.js";
import { LoginRequest } from "../api/dtos/LoginRequest.js";
/**
* Controller responsible for authentication, e.g., login or issueing a token with extended

View file

@ -1,4 +1,4 @@
import { CompactRecipeDto } from "../dtos/CompactRecipeDto.js";
import { CompactRecipeDto } from "../api/dtos/CompactRecipeDto.js";
import { RecipeEntity } from "../entities/RecipeEntity.js";
import { CompactRecipeDtoEntityMapper } from "../mappers/CompactRecipeDtoEntityMapper.js";
import { RecipeRepository } from "../repositories/RecipeRepository.js";

View file

@ -1,10 +1,10 @@
import { RecipeDto } from "../dtos/RecipeDto.js";
import { RecipeDto } from "../api/dtos/RecipeDto.js";
import { RecipeDtoEntityMapper } from "../mappers/RecipeDtoEntityMapper.js";
import { RecipeRepository } from "../repositories/RecipeRepository.js";
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 { NotFoundError, ValidationError } from "../api/errors/httpErrors.js";
import { RecipeInstructionStepDto } from "../api/dtos/RecipeInstructionStepDto.js";
import { RecipeIngredientGroupDto } from "../api/dtos/RecipeIngredientGroupDto.js";
import { RecipeIngredientDto } from "../api/dtos/RecipeIngredientDto.js";
import { RecipeEntity } from "../entities/RecipeEntity.js";
/**
@ -62,7 +62,7 @@ export class RecipeHandler {
}
/**
* 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
*/
async updateRecipe(dto: RecipeDto){

View 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);
}
}

View file

@ -1,13 +1,13 @@
import {ConflictError, NotFoundError, ValidationError} from "../errors/httpErrors.js";
import {CreateUserRequest} from "../dtos/CreateUserRequest.js";
import {UserDto} from "../dtos/UserDto.js";
import {ConflictError, NotFoundError, ValidationError} from "../api/errors/httpErrors.js";
import {CreateUserRequest} from "../api/dtos/CreateUserRequest.js";
import {UserDto} from "../api/dtos/UserDto.js";
import {encrypt} from "../utils/encryptionUtils.js";
import {UserRepository} from "../repositories/UserRepository.js";
import {UserDtoEntityMapper} from "../mappers/UserDtoEntityMapper.js";
import {UUID} from "crypto";
import {ChangeUserPasswordRequest} from "../dtos/ChangeUserPasswordRequest.js";
import {ChangeUserPasswordRequest} from "../api/dtos/ChangeUserPasswordRequest.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

View file

@ -6,11 +6,11 @@ import { requestLoggerMiddleware } from "./middleware/requestLoggerMiddleware.js
import { errorLoggerMiddleware } from "./middleware/errorLoggerMiddleware.js";
import { corsHeaders } from "./middleware/corsMiddleware.js";
import { logger } from "./utils/logger.js";
import { HttpStatusCode } from "./apiHelpers/HttpStatusCodes.js";
import authRoutes, { authBasicRoute } from "./endpoints/AuthRestResource.js";
import userRoutes, { userBasicRoute } from "./endpoints/UserRestResource.js";
import compactRecipeRoutes from "./endpoints/CompactRecipeRestResource.js";
import recipeRoutes from "./endpoints/RecipeRestResource.js";
import { HttpStatusCode } from "./api/apiHelpers/HttpStatusCodes.js";
import authRoutes, { authBasicRoute } from "./api/endpoints/AuthRestResource.js";
import userRoutes, { userBasicRoute } from "./api/endpoints/UserRestResource.js";
import compactRecipeRoutes from "./api/endpoints/CompactRecipeRestResource.js";
import recipeRoutes from "./api/endpoints/RecipeRestResource.js";
dotenv.config();

View file

@ -1,4 +1,4 @@
import { AbstractDto } from "../dtos/AbstractDto.js";
import { AbstractDto } from "../api/dtos/AbstractDto.js";
import { AbstractEntity } from "../entities/AbstractEntity.js";
export abstract class AbstractDtoEntityMapper<

View file

@ -1,4 +1,4 @@
import { CompactRecipeDto } from "../dtos/CompactRecipeDto.js";
import { CompactRecipeDto } from "../api/dtos/CompactRecipeDto.js";
import { RecipeEntity } from "../entities/RecipeEntity.js";
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";

View file

@ -1,6 +1,6 @@
import { RecipeDto } from "../dtos/RecipeDto.js";
import { RecipeIngredientGroupDto } from "../dtos/RecipeIngredientGroupDto.js";
import { RecipeInstructionStepDto } from "../dtos/RecipeInstructionStepDto.js";
import { RecipeDto } from "../api/dtos/RecipeDto.js";
import { RecipeIngredientGroupDto } from "../api/dtos/RecipeIngredientGroupDto.js";
import { RecipeInstructionStepDto } from "../api/dtos/RecipeInstructionStepDto.js";
import { RecipeEntity } from "../entities/RecipeEntity.js";
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";
import { RecipeIngredientGroupDtoEntityMapper } from "./RecipeIngredientGroupDtoEntityMapper.js";

View file

@ -1,4 +1,4 @@
import { RecipeIngredientDto } from "../dtos/RecipeIngredientDto.js";
import { RecipeIngredientDto } from "../api/dtos/RecipeIngredientDto.js";
import { RecipeIngredientEntity } from "../entities/RecipeIngredientEntity.js";
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";

View file

@ -1,5 +1,5 @@
import { RecipeIngredientDto } from "../dtos/RecipeIngredientDto.js";
import { RecipeIngredientGroupDto } from "../dtos/RecipeIngredientGroupDto.js";
import { RecipeIngredientDto } from "../api/dtos/RecipeIngredientDto.js";
import { RecipeIngredientGroupDto } from "../api/dtos/RecipeIngredientGroupDto.js";
import { RecipeIngredientGroupEntity } from "../entities/RecipeIngredientGroupEntity.js";
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";
import { RecipeIngredientDtoEntityMapper } from "./RecipeIngredientDtoEntityMapper.js";

View file

@ -1,4 +1,4 @@
import { RecipeInstructionStepDto } from "../dtos/RecipeInstructionStepDto.js";
import { RecipeInstructionStepDto } from "../api/dtos/RecipeInstructionStepDto.js";
import { RecipeInstructionStepEntity } from "../entities/RecipeInstructionStepEntity.js";
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";

View 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();
}
}

View file

@ -1,6 +1,6 @@
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.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> {
toDto(entity: UserEntity): UserDto {

View file

@ -1,9 +1,9 @@
import { NextFunction, Request, Response } from "express";
import jwt from "jsonwebtoken";
import dotenv from "dotenv";
import { authBasicRoute } from "../endpoints/AuthRestResource.js";
import { AuthPayload } from "../dtos/AuthPayload.js";
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
import { authBasicRoute } from "../api/endpoints/AuthRestResource.js";
import { AuthPayload } from "../api/dtos/AuthPayload.js";
import {HttpStatusCode} from "../api/apiHelpers/HttpStatusCodes.js";
dotenv.config();

View file

@ -1,7 +1,7 @@
import { NextFunction, Request, Response } from "express";
import { ForbiddenError } from "../errors/httpErrors.js";
import { HttpStatusCode } from "../apiHelpers/HttpStatusCodes.js";
import { UserRole } from "../enums/UserRole.js";
import { ForbiddenError } from "../api/errors/httpErrors.js";
import { HttpStatusCode } from "../api/apiHelpers/HttpStatusCodes.js";
import { UserRole } from "../api/enums/UserRole.js";
/**
* Middleware to check if the current user has one of the required roles

View file

@ -1,5 +1,5 @@
import { Request, Response, NextFunction } from "express";
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
import {HttpStatusCode} from "../api/apiHelpers/HttpStatusCodes.js";
/**
* Add CORS header

View file

@ -1,6 +1,6 @@
// middleware/errorHandler.ts
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.

View file

@ -1,7 +1,7 @@
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 { HttpStatusCode } from "../apiHelpers/HttpStatusCodes.js";
import { HttpStatusCode } from "../api/apiHelpers/HttpStatusCodes.js";
/**
* Error logging and handling middleware

View 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);
}
}

View 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);
}
}

View 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();
}
}

View file

@ -1,7 +1,7 @@
import jwt from "jsonwebtoken";
import bcrypt from "bcrypt";
import dotenv from "dotenv";
import { AuthPayload } from "../dtos/AuthPayload.js";
import { AuthPayload } from "../api/dtos/AuthPayload.js";
dotenv.config();
const { JWT_SECRET = "" } = process.env;

View file

@ -1,6 +1,6 @@
import winston from "winston";
import path from "path";
import { HttpStatusCode } from "../apiHelpers/HttpStatusCodes.js";
import { HttpStatusCode } from "../api/apiHelpers/HttpStatusCodes.js";
// Define log format
const logFormat = winston.format.combine(