diff --git a/src/data-source.ts b/src/data-source.ts index bcbae59..b05c91b 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -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: [], }); \ No newline at end of file diff --git a/src/entities/AbstractEntity.ts b/src/entities/AbstractEntity.ts index be561b9..86401fd 100644 --- a/src/entities/AbstractEntity.ts +++ b/src/entities/AbstractEntity.ts @@ -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; } diff --git a/src/entities/RecipeEntity.ts b/src/entities/RecipeEntity.ts new file mode 100644 index 0000000..726a505 --- /dev/null +++ b/src/entities/RecipeEntity.ts @@ -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[]; +} \ No newline at end of file diff --git a/src/entities/RecipeIngredientEntity.ts b/src/entities/RecipeIngredientEntity.ts new file mode 100644 index 0000000..1a7197d --- /dev/null +++ b/src/entities/RecipeIngredientEntity.ts @@ -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; +} \ No newline at end of file diff --git a/src/entities/RecipeIngredientGroupEntity.ts b/src/entities/RecipeIngredientGroupEntity.ts new file mode 100644 index 0000000..746ba8d --- /dev/null +++ b/src/entities/RecipeIngredientGroupEntity.ts @@ -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; + + @OneToMany(() => RecipeIngredientEntity, (ingredient) => ingredient.ingredientGroup, { + cascade: true, + }) + ingredients!: Relation[]; + +} \ No newline at end of file diff --git a/src/entities/RecipeInstructionStepEntity.ts b/src/entities/RecipeInstructionStepEntity.ts new file mode 100644 index 0000000..d4cb539 --- /dev/null +++ b/src/entities/RecipeInstructionStepEntity.ts @@ -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; + +} \ No newline at end of file diff --git a/src/entities/UserEntity.ts b/src/entities/UserEntity.ts index 0587bca..c306eac 100644 --- a/src/entities/UserEntity.ts +++ b/src/entities/UserEntity.ts @@ -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" }) diff --git a/src/index.ts b/src/index.ts index 2838a7d..3bd3c18 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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"); diff --git a/src/mappers/AbstractDtoEntityMapper.ts b/src/mappers/AbstractDtoEntityMapper.ts index 2a9f8a5..51a5f27 100644 --- a/src/mappers/AbstractDtoEntityMapper.ts +++ b/src/mappers/AbstractDtoEntityMapper.ts @@ -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; } diff --git a/src/migrations/1758477574859-CreateUserTable.ts b/src/migrations/1661234567890-CreateUserTable.ts similarity index 100% rename from src/migrations/1758477574859-CreateUserTable.ts rename to src/migrations/1661234567890-CreateUserTable.ts diff --git a/src/migrations/1758958856261-RenameUserColumns.ts b/src/migrations/1758958856261-RenameUserColumns.ts new file mode 100644 index 0000000..5dfff1c --- /dev/null +++ b/src/migrations/1758958856261-RenameUserColumns.ts @@ -0,0 +1,83 @@ +import { MigrationInterface, QueryRunner, TableUnique } from "typeorm"; + +export class RenameUserColumns1758958856261 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + // 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 { + // 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", + }) + ); + } + +} diff --git a/src/migrations/1758959405239-CreateRecipeTable.ts b/src/migrations/1758959405239-CreateRecipeTable.ts new file mode 100644 index 0000000..a483856 --- /dev/null +++ b/src/migrations/1758959405239-CreateRecipeTable.ts @@ -0,0 +1,48 @@ +import { MigrationInterface, QueryRunner, Table } from "typeorm"; + +export class CreateRecipeTable1758959405239 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + // 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 { + // Drop the table + await queryRunner.dropTable("recipe"); + } +} diff --git a/src/migrations/1758959437946-CreateRecipeIngredientGroupTable.ts b/src/migrations/1758959437946-CreateRecipeIngredientGroupTable.ts new file mode 100644 index 0000000..3edb069 --- /dev/null +++ b/src/migrations/1758959437946-CreateRecipeIngredientGroupTable.ts @@ -0,0 +1,79 @@ +import { + MigrationInterface, + QueryRunner, + Table, + TableForeignKey, +} from "typeorm"; + +export class CreateRecipeIngredientGroupTable1758959437946 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + // 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 { + // 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"); + } +} diff --git a/src/migrations/1758959442589-CreateRecipeIngredientTable.ts b/src/migrations/1758959442589-CreateRecipeIngredientTable.ts new file mode 100644 index 0000000..8aecda9 --- /dev/null +++ b/src/migrations/1758959442589-CreateRecipeIngredientTable.ts @@ -0,0 +1,79 @@ +import { + MigrationInterface, + QueryRunner, + Table, + TableForeignKey, +} from "typeorm"; + +export class CreateRecipeIngredientTable1758959442589 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + // 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 { + // 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"); + } +} diff --git a/src/migrations/1758959460127-CreateRecipeInstructionStepTable.ts b/src/migrations/1758959460127-CreateRecipeInstructionStepTable.ts new file mode 100644 index 0000000..a604b6f --- /dev/null +++ b/src/migrations/1758959460127-CreateRecipeInstructionStepTable.ts @@ -0,0 +1,79 @@ +import { + MigrationInterface, + QueryRunner, + Table, + TableForeignKey, +} from "typeorm"; + +export class CreateRecipeInstructionStepTable1758959460127 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + // 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 { + // 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"); + } +}