create database structure

This commit is contained in:
Anika Raemer 2025-09-27 17:39:13 +02:00
parent e5b5d7e67d
commit ad6ee64565
15 changed files with 483 additions and 14 deletions

View file

@ -2,7 +2,6 @@ import "reflect-metadata";
import { DataSource } from "typeorm";
import * as dotenv from "dotenv";
import { UserEntity } from "./entities/UserEntity.js";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
@ -26,7 +25,7 @@ export const AppDataSource = new DataSource({
synchronize: NODE_ENV === "dev" ? false : false,
//logging logs sql command on the terminal
logging: NODE_ENV === "dev" ? false : false,
entities: [join(__dirname, "/entities/*.{ts,js}")],
migrations: [join(__dirname, "/migrations/*.{ts,js}")],
entities: [join(__dirname, "/entities/*.{js, ts}")],
migrations: [join(__dirname, "/migrations/*.js")],
subscribers: [],
});

View file

@ -8,9 +8,9 @@ export abstract class AbstractEntity {
@PrimaryGeneratedColumn("uuid")
id?: string;
@CreateDateColumn()
createdAt?: Date;
@CreateDateColumn({name: "create_date"})
createDate?: Date;
@UpdateDateColumn()
updatedAt?: Date;
@UpdateDateColumn({name: "update_date"})
updateDate?: Date;
}

View file

@ -0,0 +1,31 @@
import { Entity, Column, OneToMany, Relation } from "typeorm";
import { AbstractEntity } from "./AbstractEntity.js";
import { RecipeInstructionStepEntity } from "./RecipeInstructionStepEntity.js";
import { RecipeIngredientGroupEntity } from "./RecipeIngredientGroupEntity.js";
/**
* Entity describing a recipe
*/
@Entity({ name: "recipe" })
export class RecipeEntity extends AbstractEntity {
@Column({ nullable: false })
title!: string;
@Column({ nullable: true })
amount?: number;
@Column({ nullable: true, name: "amount_description" })
amountDescription?: string;
// make sure not to induce a circular dependency! user 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!
@OneToMany(() => RecipeIngredientGroupEntity, (ingredientGroup) => ingredientGroup.recipe, {
cascade: true,
})
ingredientGroups!: Relation<RecipeIngredientGroupEntity>[];
}

View file

@ -0,0 +1,20 @@
import { Column, Entity, ManyToOne, Relation} from "typeorm";
import { AbstractEntity } from "./AbstractEntity.js";
import { RecipeIngredientGroupEntity } from "./RecipeIngredientGroupEntity.js";
/**
* Entity describing an ingredient group for a recipe
*/
@Entity({ name: "recipe_ingredient" })
export class RecipeIngredientEntity extends AbstractEntity {
@Column({ nullable: false })
title!: string;
@Column({ nullable: false })
sortOrder!: number;
@ManyToOne(() => RecipeIngredientGroupEntity, (ingredientGroup) => ingredientGroup.ingredients,
{onDelete: "CASCADE"}
)
ingredientGroup!: Relation<RecipeIngredientGroupEntity>;
}

View file

@ -0,0 +1,27 @@
import { Column, Entity, ManyToOne, OneToMany, Relation} from "typeorm";
import { AbstractEntity } from "./AbstractEntity.js";
import { RecipeEntity } from "./RecipeEntity.js";
import { RecipeIngredientEntity } from "./RecipeIngredientEntity.js";
/**
* Entity describing an ingredient group for a recipe
*/
@Entity({ name: "recipe_ingredient_group" })
export class RecipeIngredientGroupEntity extends AbstractEntity {
@Column({ nullable: false })
title!: string;
@Column({ nullable: false })
sortOrder!: number;
@ManyToOne(() => RecipeEntity, (recipe) => recipe.ingredientGroups,
{onDelete: "CASCADE"}
)
recipe!: Relation<RecipeEntity>;
@OneToMany(() => RecipeIngredientEntity, (ingredient) => ingredient.ingredientGroup, {
cascade: true,
})
ingredients!: Relation<RecipeIngredientEntity>[];
}

View file

@ -0,0 +1,21 @@
import { Column, Entity, ManyToOne, Relation} from "typeorm";
import { AbstractEntity } from "./AbstractEntity.js";
import { RecipeEntity } from "./RecipeEntity.js";
/**
* Entity describing an instruction step for a recipe
*/
@Entity({ name: "recipe_instruction_step" })
export class RecipeInstructionStepEntity extends AbstractEntity {
@Column({ nullable: false })
text!: string;
@Column({ nullable: false })
sortOrder!: number;
@ManyToOne(() => RecipeEntity, (recipe) => recipe.instructionSteps,
{onDelete: "CASCADE"}
)
recipe!: Relation<RecipeEntity>;
}

View file

@ -1,9 +1,10 @@
import { Entity, Column } from "typeorm";
import { AbstractEntity } from "./AbstractEntity.js";
// @todo Add migration to update table
@Entity({ name: "user" })
export class UserEntity extends AbstractEntity {
@Column({ nullable: false })
@Column({ nullable: false, name: "user_name" })
userName!: string;
@Column({ nullable: false })
@ -12,10 +13,10 @@ export class UserEntity extends AbstractEntity {
@Column({ nullable: false })
password!: string;
@Column({ nullable: true })
@Column({ nullable: true, name: "first_name"})
firstName?: string;
@Column({ nullable: true })
@Column({ nullable: true, name: "last_name"})
lastName?: string;
@Column({ default: "user" })

View file

@ -16,11 +16,13 @@ app.use(errorHandler);
async function startServer() {
try {
console.log("starting server")
// Initialize database
await AppDataSource.initialize();
console.log("Data Source initialized");
// Run pending migrations
console.log(AppDataSource.migrations);
await AppDataSource.runMigrations();
console.log("Migrations executed");

View file

@ -10,8 +10,8 @@ export abstract class AbstractDtoEntityMapper<
*/
protected mapBaseEntityToDto(entity: E, dto: D): D {
dto.id = entity.id;
dto.createdAt = entity.createdAt;
dto.updatedAt = entity.updatedAt;
dto.createdAt = entity.createDate;
dto.updatedAt = entity.updateDate;
return dto;
}
@ -20,8 +20,8 @@ export abstract class AbstractDtoEntityMapper<
*/
protected mapBaseDtoToEntity(dto: D, entity: E): E {
entity.id = dto.id;
entity.createdAt = dto.createdAt;
entity.updatedAt = dto.updatedAt;
entity.createDate = dto.createdAt;
entity.updateDate = dto.updatedAt;
return entity;
}

View file

@ -0,0 +1,83 @@
import { MigrationInterface, QueryRunner, TableUnique } from "typeorm";
export class RenameUserColumns1758958856261 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// drop constraint
await queryRunner.dropUniqueConstraint("user", "UQ_user_userName");
// rename columns
await queryRunner.renameColumn(
"user",
"userName",
"user_name"
)
await queryRunner.renameColumn(
"user",
"firstName",
"first_name"
)
await queryRunner.renameColumn(
"user",
"lastName",
"last_name"
)
await queryRunner.renameColumn(
"user",
"createdAt",
"create_date"
)
await queryRunner.renameColumn(
"user",
"updatedAt",
"update_date"
)
// Add a unique constraint on user_name
await queryRunner.createUniqueConstraint(
"user",
new TableUnique({
columnNames: ["user_name"],
name: "UQ_user_user_name",
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// drop constraint
await queryRunner.dropUniqueConstraint("user", "UQ_user_user_name");
// rename columns
await queryRunner.renameColumn(
"user",
"user_name",
"userName",
)
await queryRunner.renameColumn(
"user",
"first_name",
"firstName"
)
await queryRunner.renameColumn(
"user",
"last_name",
"lastName"
)
await queryRunner.renameColumn(
"user",
"create_date",
"createdAt"
)
await queryRunner.renameColumn(
"user",
"update_date",
"updatedAt"
)
// Add a unique constraint on userName
await queryRunner.createUniqueConstraint(
"user",
new TableUnique({
columnNames: ["userName"],
name: "UQ_user_userName",
})
);
}
}

View file

@ -0,0 +1,48 @@
import { MigrationInterface, QueryRunner, Table } from "typeorm";
export class CreateRecipeTable1758959405239 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Create table
await queryRunner.createTable(
new Table({
name: "recipe",
columns: [
{
name: "id",
type: "uuid",
isPrimary: true,
isGenerated: true,
generationStrategy: "uuid",
},
{
name: "amount",
type: "varchar",
isNullable: true,
},
{
name: "amount_description",
type: "varchar",
isNullable: true,
},
{
name: "create_date",
type: "timestamp",
default: "now()",
},
{
name: "update_date",
type: "timestamp",
default: "now()",
},
],
}),
true
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Drop the table
await queryRunner.dropTable("recipe");
}
}

View file

@ -0,0 +1,79 @@
import {
MigrationInterface,
QueryRunner,
Table,
TableForeignKey,
} from "typeorm";
export class CreateRecipeIngredientGroupTable1758959437946
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
// Create table
await queryRunner.createTable(
new Table({
name: "recipe_ingredient_group",
columns: [
{
name: "id",
type: "uuid",
isPrimary: true,
isGenerated: true,
generationStrategy: "uuid",
},
{
name: "create_date",
type: "timestamp",
default: "now()",
},
{
name: "update_date",
type: "timestamp",
default: "now()",
},
{
name: "title",
type: "varchar",
isNullable: false,
},
{
name: "sortOrder",
type: "int",
isNullable: false,
},
{
name: "recipe_id", // foreign key column
type: "uuid",
isNullable: false,
},
],
}),
true
);
// Add foreign key to recipe table
await queryRunner.createForeignKey(
"recipe_ingredient_group",
new TableForeignKey({
columnNames: ["recipe_id"],
referencedTableName: "recipe",
referencedColumnNames: ["id"],
onDelete: "CASCADE", // delete ingredient groups if recipe is deleted
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Drop foreign key first
const table = await queryRunner.getTable("recipe_ingredient_group");
const foreignKey = table?.foreignKeys.find(
(fk) => fk.columnNames.indexOf("recipe_id") !== -1
);
if (foreignKey) {
await queryRunner.dropForeignKey("recipe_ingredient_group", foreignKey);
}
// Drop table
await queryRunner.dropTable("recipe_ingredient_group");
}
}

View file

@ -0,0 +1,79 @@
import {
MigrationInterface,
QueryRunner,
Table,
TableForeignKey,
} from "typeorm";
export class CreateRecipeIngredientTable1758959442589
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
// Create table
await queryRunner.createTable(
new Table({
name: "recipe_ingredient",
columns: [
{
name: "id",
type: "uuid",
isPrimary: true,
isGenerated: true,
generationStrategy: "uuid",
},
{
name: "create_date",
type: "timestamp",
default: "now()",
},
{
name: "update_date",
type: "timestamp",
default: "now()",
},
{
name: "title",
type: "varchar",
isNullable: false,
},
{
name: "sortOrder",
type: "int",
isNullable: false,
},
{
name: "recipe_ingredient_group_id", // foreign key column
type: "uuid",
isNullable: false,
},
],
}),
true
);
// Add foreign key to recipe table
await queryRunner.createForeignKey(
"recipe_ingredient",
new TableForeignKey({
columnNames: ["recipe_ingredient_group_id"],
referencedTableName: "recipe_ingredient_group",
referencedColumnNames: ["id"],
onDelete: "CASCADE", // delete ingredient if ingredient_group is deleted
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Drop foreign key first
const table = await queryRunner.getTable("recipe_ingredient");
const foreignKey = table?.foreignKeys.find(
(fk) => fk.columnNames.indexOf("recipe_ingredient_group_id") !== -1
);
if (foreignKey) {
await queryRunner.dropForeignKey("recipe_ingredient", foreignKey);
}
// Drop table
await queryRunner.dropTable("recipe_ingredient");
}
}

View file

@ -0,0 +1,79 @@
import {
MigrationInterface,
QueryRunner,
Table,
TableForeignKey,
} from "typeorm";
export class CreateRecipeInstructionStepTable1758959460127
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
// Create table
await queryRunner.createTable(
new Table({
name: "recipe_instruction_step",
columns: [
{
name: "id",
type: "uuid",
isPrimary: true,
isGenerated: true,
generationStrategy: "uuid",
},
{
name: "create_date",
type: "timestamp",
default: "now()",
},
{
name: "update_date",
type: "timestamp",
default: "now()",
},
{
name: "text",
type: "varchar",
isNullable: false,
},
{
name: "sortOrder",
type: "int",
isNullable: false,
},
{
name: "recipe_id", // foreign key column
type: "uuid",
isNullable: false,
},
],
}),
true
);
// Add foreign key to recipe table
await queryRunner.createForeignKey(
"recipe_instruction_step",
new TableForeignKey({
columnNames: ["recipe_id"],
referencedTableName: "recipe",
referencedColumnNames: ["id"],
onDelete: "CASCADE", // delete instruction steps if recipe is deleted
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Drop foreign key first
const table = await queryRunner.getTable("recipe_instruction_step");
const foreignKey = table?.foreignKeys.find(
(fk) => fk.columnNames.indexOf("recipe_id") !== -1
);
if (foreignKey) {
await queryRunner.dropForeignKey("recipe_instruction_step", foreignKey);
}
// Drop table
await queryRunner.dropTable("recipe_instruction_step");
}
}