From 0523650096d6c54b0285af2f495ed7763f692065 Mon Sep 17 00:00:00 2001 From: Harold Cohen Date: Wed, 15 Nov 2023 18:31:43 +0100 Subject: [PATCH] feat: add typeorm car rental write repo, refactor db master/slave config --- .../injection/containers/database.ts | 41 ++++---- .../containers/repositories/typeorm.ts | 4 + .../repositories/typeorm/carRental/read.ts | 2 +- .../repositories/typeorm/carRental/write.ts | 29 ++++++ .../typeorm/carRental/command/create.test.ts | 97 +++++++++++++++++++ .../typeorm/carRental/query/read.test.ts | 40 +++----- .../carRental/utils/populateFromTestCase.ts | 27 ++++++ .../typeorm/carRental/utils/testCase.types.ts | 18 ++++ .../typeorm/seeding/factories/car.ts | 3 +- .../typeorm/seeding/factories/carModel.ts | 2 +- .../typeorm/seeding/factories/carRental.ts | 2 +- .../typeorm/seeding/factories/customer.ts | 2 +- tests/integration/typeorm/utils/misc.ts | 23 +++++ tests/integration/typeorm/utils/populate.ts | 30 ++++++ tests/integration/typeorm/utils/setup.ts | 9 +- tests/integration/typeorm/utils/tearDown.ts | 8 +- 16 files changed, 272 insertions(+), 65 deletions(-) create mode 100644 src/driven/repositories/typeorm/carRental/write.ts create mode 100644 tests/integration/typeorm/carRental/command/create.test.ts create mode 100644 tests/integration/typeorm/carRental/utils/populateFromTestCase.ts create mode 100644 tests/integration/typeorm/carRental/utils/testCase.types.ts create mode 100644 tests/integration/typeorm/utils/misc.ts create mode 100644 tests/integration/typeorm/utils/populate.ts diff --git a/src/configuration/injection/containers/database.ts b/src/configuration/injection/containers/database.ts index e35bece..6584fc6 100644 --- a/src/configuration/injection/containers/database.ts +++ b/src/configuration/injection/containers/database.ts @@ -8,34 +8,31 @@ import 'dotenv/config'; const useAppDataSources = (): void => { // Most of the variables are currently not typed or parsed correctly // See ticket https://github.com/thetribeio/megahertz/issues/15 - const commandAppDataSource = new DataSource({ + const AppDataSource = new DataSource({ type: "postgres", - host: process.env.TYPEORM_MASTER_DATABASE_HOST as string, - port: Number(process.env.TYPEORM_MASTER_DATABASE_PORT), - username: process.env.TYPEORM_MASTER_DATABASE_USER as string, - password: process.env.TYPEORM_MASTER_DATABASE_PASSWORD as string, - database: process.env.TYPEORM_MASTER_DATABASE_NAME as string, synchronize: true, - logging: false, + logging: true, entities: ['src/driven/repositories/typeorm/entities/*.{ts,js}'], subscribers: [], migrations: ['src/driven/repositories/typeorm/migrations/*.{ts,js}'], + replication: { + master: { + host: process.env.TYPEORM_MASTER_DATABASE_HOST as string, + port: Number(process.env.TYPEORM_MASTER_DATABASE_PORT), + username: process.env.TYPEORM_MASTER_DATABASE_USER as string, + password: process.env.TYPEORM_MASTER_DATABASE_PASSWORD as string, + database: process.env.TYPEORM_MASTER_DATABASE_NAME as string, + }, + slaves: [{ + host: process.env.TYPEORM_DATABASE_HOST as string, + port: Number(process.env.TYPEORM_DATABASE_PORT), + username: process.env.TYPEORM_DATABASE_USER as string, + password: process.env.TYPEORM_DATABASE_PASSWORD as string, + database: process.env.TYPEORM_DATABASE_NAME as string, + }] + }, }); - const queryAppDataSource = new DataSource({ - type: "postgres", - host: process.env.TYPEORM_DATABASE_HOST as string, - port: Number(process.env.TYPEORM_DATABASE_PORT), - username: process.env.TYPEORM_DATABASE_USER as string, - password: process.env.TYPEORM_DATABASE_PASSWORD as string, - database: process.env.TYPEORM_DATABASE_NAME as string, - synchronize: true, - logging: false, - entities: ['src/driven/repositories/typeorm/entities/*.{ts,js}'], - subscribers: [], - migrations: ['src/driven/repositories/typeorm/migrations/*.{ts,js}'], - }); - container.register("QueryDataSource", {useValue: queryAppDataSource}); - container.register("CommandDataSource", {useValue: commandAppDataSource}); + container.register("DataSource", {useValue: AppDataSource}); } export default useAppDataSources; \ No newline at end of file diff --git a/src/configuration/injection/containers/repositories/typeorm.ts b/src/configuration/injection/containers/repositories/typeorm.ts index 55a921b..dced977 100644 --- a/src/configuration/injection/containers/repositories/typeorm.ts +++ b/src/configuration/injection/containers/repositories/typeorm.ts @@ -1,5 +1,6 @@ import {container} from 'tsyringe'; import TypeORMCarRentalReadRepository from 'src/driven/repositories/typeorm/carRental/read'; +import TypeORMCarRentalWriteRepository from "src/driven/repositories/typeorm/carRental/write"; /** * Configures tsyringe to use typeORM repositories. @@ -8,6 +9,9 @@ const useTypeORMRepositories = (): void => { container.register("CarRentalReadRepositoryInterface", {useClass: TypeORMCarRentalReadRepository}); const carRentalReadRepository = container.resolve("CarRentalReadRepositoryInterface"); container.registerInstance("CarRentalReadRepositoryInterface", carRentalReadRepository); + container.register("CarRentalWriteRepositoryInterface", {useClass: TypeORMCarRentalWriteRepository}); + const carRentalWriteRepository = container.resolve("CarRentalWriteRepositoryInterface"); + container.registerInstance("CarRentalWriteRepositoryInterface", carRentalWriteRepository); } export default useTypeORMRepositories; \ No newline at end of file diff --git a/src/driven/repositories/typeorm/carRental/read.ts b/src/driven/repositories/typeorm/carRental/read.ts index a8a7b84..b244e39 100644 --- a/src/driven/repositories/typeorm/carRental/read.ts +++ b/src/driven/repositories/typeorm/carRental/read.ts @@ -11,7 +11,7 @@ export default class TypeORMCarRentalReadRepository implements CarRentalReadRepo private readonly dataSource: DataSource; - constructor(@inject("QueryDataSource") dataSource: DataSource) { + constructor(@inject("DataSource") dataSource: DataSource) { this.dataSource = dataSource; } diff --git a/src/driven/repositories/typeorm/carRental/write.ts b/src/driven/repositories/typeorm/carRental/write.ts new file mode 100644 index 0000000..15beec8 --- /dev/null +++ b/src/driven/repositories/typeorm/carRental/write.ts @@ -0,0 +1,29 @@ +import CarRentalWriteRepositoryInterface from "src/core/domain/carRental/interfaces/repositories/write"; +import CarRentalDTO from "src/core/domain/carRental/dto"; +import {TypeORMCarRental} from "src/driven/repositories/typeorm/entities"; +import {DataSource, Repository} from "typeorm"; +import {inject, singleton} from "tsyringe"; + +@singleton() +export default class TypeORMCarRentalWriteRepository implements CarRentalWriteRepositoryInterface { + + private readonly dataSource: DataSource; + + constructor(@inject("DataSource") dataSource: DataSource) { + this.dataSource = dataSource; + } + + async create(carRentalDTO: CarRentalDTO): Promise { + const repository = this.dataSource.getRepository(TypeORMCarRental); + const typeORMCarRental = new TypeORMCarRental(); + typeORMCarRental.id = carRentalDTO.id; + typeORMCarRental.totalPrice = carRentalDTO.totalPrice; + typeORMCarRental.pickupDateTime = carRentalDTO.pickupDateTime; + typeORMCarRental.dropOffDateTime = carRentalDTO.dropOffDateTime; + typeORMCarRental.customer = {id: carRentalDTO.customerId}; + typeORMCarRental.car = {id: carRentalDTO.car.id}; + + await repository.save(typeORMCarRental); + } + +} \ No newline at end of file diff --git a/tests/integration/typeorm/carRental/command/create.test.ts b/tests/integration/typeorm/carRental/command/create.test.ts new file mode 100644 index 0000000..1e552c4 --- /dev/null +++ b/tests/integration/typeorm/carRental/command/create.test.ts @@ -0,0 +1,97 @@ +import 'reflect-metadata'; +import {container} from "tsyringe"; +import TypeORMCarRentalWriteRepository from "src/driven/repositories/typeorm/carRental/write"; +import useAppDataSources from "src/configuration/injection/containers/database"; +import useTypeORMRepositories from "src/configuration/injection/containers/repositories/typeorm"; +import CarRentalDTO from "src/core/domain/carRental/dto"; +import TypeORMCarRentalReadRepository from "src/driven/repositories/typeorm/carRental/read"; +import {runDataSourceBeforeEachOps} from "tests/integration/typeorm/utils/setup"; +import DateParser from "tests/utils/dateParser"; +import {advanceTo} from "jest-date-mock"; +import useTestingUtilities from "tests/configuration/containers/utils"; +import {DataSource} from "typeorm"; +import {v4} from "uuid"; +import {runDataSourceAfterEachOps} from "tests/integration/typeorm/utils/tearDown"; +import { + populateCarAndCarModelFromCarRentalTestCase, + populateCustomerFromCarRentalTestCase +} from "tests/integration/typeorm/carRental/utils/populateFromTestCase"; +import {expectedCarRentalFromTestCase} from "tests/integration/typeorm/utils/misc"; + +describe.each([ + { + rental: { + id: v4(), + customerId: v4(), + car: { + id: v4(), + model: { + id: v4(), + dailyRate: 100, + } + }, + totalPrice: 100, + pickupDateTime: 'today', + dropOffDateTime: 'tomorrow', + } + }, + { + rental: { + id: v4(), + customerId: v4(), + car: { + id: v4(), + model: { + id: v4(), + dailyRate: 100, + } + }, + totalPrice: 200, + pickupDateTime: 'tomorrow', + dropOffDateTime: 'in 2 days', + } + }, +])("Integration tests to create car rental in a postgres database using typeorm", (testCase) => { + let repository: TypeORMCarRentalWriteRepository; + let readRepository: TypeORMCarRentalReadRepository; + let carRentalToCreate: CarRentalDTO; + let expectedCarRental: CarRentalDTO; + let dateParser: DateParser; + + beforeAll(() => { + advanceTo(Date.now()); + useTestingUtilities(); + useAppDataSources(); + useTypeORMRepositories(); + dateParser = container.resolve("DateParser"); + }) + + beforeEach(async () => { + repository = container.resolve("CarRentalWriteRepositoryInterface"); + readRepository = container.resolve("CarRentalReadRepositoryInterface"); + await runDataSourceBeforeEachOps(); + await populateCustomerFromCarRentalTestCase(testCase.rental); + await populateCarAndCarModelFromCarRentalTestCase(testCase.rental); + expectedCarRental = expectedCarRentalFromTestCase( + testCase.rental, + dateParser, + ); + carRentalToCreate = expectedCarRental; + }) + + afterEach(async () => { + await runDataSourceAfterEachOps(); + }) + + test(`Create a car rental ${testCase.rental.id} should create one car rental in the database`, async () => { + const dataSource: DataSource = container.resolve("DataSource"); + const queryRunner = dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + await repository.create(carRentalToCreate); + await queryRunner.commitTransaction(); + await queryRunner.release(); + const retrievedCarRental = await readRepository.read(testCase.rental.id); + expect(retrievedCarRental.toDTO()).toEqual(expectedCarRental); + }) +}) \ No newline at end of file diff --git a/tests/integration/typeorm/carRental/query/read.test.ts b/tests/integration/typeorm/carRental/query/read.test.ts index bb4c9cc..e558440 100644 --- a/tests/integration/typeorm/carRental/query/read.test.ts +++ b/tests/integration/typeorm/carRental/query/read.test.ts @@ -14,6 +14,11 @@ import TypeORMCarFactory from 'tests/integration/typeorm/seeding/factories/car'; import TypeORMCarModelFactory from 'tests/integration/typeorm/seeding/factories/carModel'; import {runDataSourceBeforeEachOps} from 'tests/integration/typeorm/utils/setup'; import {runDataSourceAfterEachOps} from 'tests/integration/typeorm/utils/tearDown'; +import { + populateCarAndCarModelFromCarRentalTestCase, + populateCustomerFromCarRentalTestCase +} from "tests/integration/typeorm/carRental/utils/populateFromTestCase"; +import {expectedCarRentalFromTestCase} from "tests/integration/typeorm/utils/misc"; describe.each([ { @@ -48,7 +53,7 @@ describe.each([ dropOffDateTime: 'in 2 days', } }, -])("Integration tests to read car rentals from a postgres database using typeorm", (testCase) => { +])("Integration tests to read car rental from a postgres database using typeorm", (testCase) => { let repository: TypeORMCarRentalReadRepository; let expectedCarRental: Partial; let dateParser: DateParser; @@ -56,25 +61,16 @@ describe.each([ beforeAll(async () => { advanceTo(Date.now()); useTestingUtilities(); - dateParser = container.resolve("DateParser"); useAppDataSources(); useTypeORMRepositories(); + dateParser = container.resolve("DateParser"); repository = container.resolve("CarRentalReadRepositoryInterface"); }) beforeEach(async () => { await runDataSourceBeforeEachOps(); - const customer = await new TypeORMCustomerFactory().create({ - id: testCase.rental.customerId, - }); - const model = await new TypeORMCarModelFactory().create({ - id: testCase.rental.car.model.id, - dailyRate: testCase.rental.car.model.dailyRate, - }); - const car = await new TypeORMCarFactory().create({ - id: testCase.rental.car.id, - model - }); + const customer = await populateCustomerFromCarRentalTestCase(testCase.rental); + const car = await populateCarAndCarModelFromCarRentalTestCase(testCase.rental); await new TypeORMCarRentalFactory().create({ id: testCase.rental.id, totalPrice: testCase.rental.totalPrice, @@ -83,20 +79,10 @@ describe.each([ customer, car, }); - expectedCarRental = { - id: testCase.rental.id, - customerId: testCase.rental.customerId, - car: { - id: testCase.rental.car.id, - model: { - id: testCase.rental.car.model.id, - dailyRate: testCase.rental.car.model.dailyRate, - }, - }, - pickupDateTime: dateParser.parse(testCase.rental.pickupDateTime), - dropOffDateTime: dateParser.parse(testCase.rental.dropOffDateTime), - totalPrice: testCase.rental.totalPrice, - } + expectedCarRental = expectedCarRentalFromTestCase( + testCase.rental, + dateParser, + ); }) afterEach(async () => { diff --git a/tests/integration/typeorm/carRental/utils/populateFromTestCase.ts b/tests/integration/typeorm/carRental/utils/populateFromTestCase.ts new file mode 100644 index 0000000..b55cc92 --- /dev/null +++ b/tests/integration/typeorm/carRental/utils/populateFromTestCase.ts @@ -0,0 +1,27 @@ +import {populateCar, populateCarModel, populateCustomer} from "tests/integration/typeorm/utils/populate"; +import {TypeORMCar, TypeORMCarModel, TypeORMCustomer} from "src/driven/repositories/typeorm/entities"; +import {CarRentalTestCaseEntry} from "tests/integration/typeorm/carRental/utils/testCase.types"; + +export const populateCustomerFromCarRentalTestCase = async ( + testCase: CarRentalTestCaseEntry, +): Promise => { + return await populateCustomer({id: testCase.customerId}); +} + +export const populateCarModelFromCarRentalTestCase = async ( + testCase: CarRentalTestCaseEntry, +): Promise => { + return await populateCarModel({ + id: testCase.car.model.id, + dailyRate: testCase.car.model.dailyRate, + }); +} + +export const populateCarAndCarModelFromCarRentalTestCase = async ( + testCase: CarRentalTestCaseEntry, +): Promise => { + return await populateCar({ + id: testCase.car.id, + model: await populateCarModelFromCarRentalTestCase(testCase), + }) +} \ No newline at end of file diff --git a/tests/integration/typeorm/carRental/utils/testCase.types.ts b/tests/integration/typeorm/carRental/utils/testCase.types.ts new file mode 100644 index 0000000..51dd571 --- /dev/null +++ b/tests/integration/typeorm/carRental/utils/testCase.types.ts @@ -0,0 +1,18 @@ +export type CarRentalTestCaseEntry = { + id: string; + pickupDateTime: string; + dropOffDateTime: string; + customerId: string; + car: CarTestCaseEntry; + totalPrice: number; +}; + +export type CarModelTestCaseEntry = { + id: string; + dailyRate: number; +} + +export type CarTestCaseEntry = { + id: string; + model: CarModelTestCaseEntry; +}; diff --git a/tests/integration/typeorm/seeding/factories/car.ts b/tests/integration/typeorm/seeding/factories/car.ts index e272a7d..a6e970e 100644 --- a/tests/integration/typeorm/seeding/factories/car.ts +++ b/tests/integration/typeorm/seeding/factories/car.ts @@ -4,9 +4,10 @@ import {v4} from 'uuid'; import {FactorizedAttrs, Factory} from '@jorgebodega/typeorm-factory'; import {TypeORMCar} from 'src/driven/repositories/typeorm/entities'; + export default class TypeORMCarFactory extends Factory { protected entity = TypeORMCar; - protected dataSource = container.resolve("CommandDataSource") as DataSource; + protected dataSource = container.resolve("DataSource") as DataSource; protected attrs(): FactorizedAttrs { return { diff --git a/tests/integration/typeorm/seeding/factories/carModel.ts b/tests/integration/typeorm/seeding/factories/carModel.ts index 9db1c85..00bf92b 100644 --- a/tests/integration/typeorm/seeding/factories/carModel.ts +++ b/tests/integration/typeorm/seeding/factories/carModel.ts @@ -6,7 +6,7 @@ import {TypeORMCarModel} from 'src/driven/repositories/typeorm/entities'; export default class TypeORMCarModelFactory extends Factory { protected entity = TypeORMCarModel; - protected dataSource = container.resolve("CommandDataSource") as DataSource; + protected dataSource = container.resolve("DataSource") as DataSource; protected attrs(): FactorizedAttrs { return { diff --git a/tests/integration/typeorm/seeding/factories/carRental.ts b/tests/integration/typeorm/seeding/factories/carRental.ts index 14bc66e..b0d74da 100644 --- a/tests/integration/typeorm/seeding/factories/carRental.ts +++ b/tests/integration/typeorm/seeding/factories/carRental.ts @@ -6,7 +6,7 @@ import {TypeORMCar, TypeORMCarRental, TypeORMCustomer} from 'src/driven/reposito export default class TypeORMCarRentalFactory extends Factory { protected entity = TypeORMCarRental; - protected dataSource = container.resolve("CommandDataSource") as DataSource; + protected dataSource = container.resolve("DataSource") as DataSource; protected attrs(): FactorizedAttrs { return { diff --git a/tests/integration/typeorm/seeding/factories/customer.ts b/tests/integration/typeorm/seeding/factories/customer.ts index 196ff0d..83178af 100644 --- a/tests/integration/typeorm/seeding/factories/customer.ts +++ b/tests/integration/typeorm/seeding/factories/customer.ts @@ -7,7 +7,7 @@ import {TypeORMCustomer} from 'src/driven/repositories/typeorm/entities'; export default class TypeORMCustomerFactory extends Factory { protected entity = TypeORMCustomer; - protected dataSource = container.resolve("CommandDataSource") as DataSource; + protected dataSource = container.resolve("DataSource") as DataSource; protected attrs(): FactorizedAttrs { return { diff --git a/tests/integration/typeorm/utils/misc.ts b/tests/integration/typeorm/utils/misc.ts new file mode 100644 index 0000000..07d97f3 --- /dev/null +++ b/tests/integration/typeorm/utils/misc.ts @@ -0,0 +1,23 @@ +import CarRentalDTO from "src/core/domain/carRental/dto"; +import {CarRentalTestCaseEntry} from "tests/integration/typeorm/carRental/utils/testCase.types"; +import DateParser from "tests/utils/dateParser"; + +export const expectedCarRentalFromTestCase = ( + testCase: CarRentalTestCaseEntry, + dateParser: DateParser, +): CarRentalDTO => { + return { + id: testCase.id, + customerId: testCase.customerId, + car: { + id: testCase.car.id, + model: { + id: testCase.car.model.id, + dailyRate: testCase.car.model.dailyRate, + }, + }, + pickupDateTime: dateParser.parse(testCase.pickupDateTime), + dropOffDateTime: dateParser.parse(testCase.dropOffDateTime), + totalPrice: testCase.totalPrice, + } as CarRentalDTO; +} \ No newline at end of file diff --git a/tests/integration/typeorm/utils/populate.ts b/tests/integration/typeorm/utils/populate.ts new file mode 100644 index 0000000..c0aa7f7 --- /dev/null +++ b/tests/integration/typeorm/utils/populate.ts @@ -0,0 +1,30 @@ +import TypeORMCustomerFactory from "tests/integration/typeorm/seeding/factories/customer"; +import TypeORMCarModelFactory from "tests/integration/typeorm/seeding/factories/carModel"; +import {TypeORMCar, TypeORMCarModel, TypeORMCustomer} from "src/driven/repositories/typeorm/entities"; +import TypeORMCarFactory from "tests/integration/typeorm/seeding/factories/car"; + +export const populateCustomer = async ( + {id}: { id: string } +): Promise => { + return await new TypeORMCustomerFactory().create({ + id + }); +} + +export const populateCarModel = async ( + {id, dailyRate}: { id: string, dailyRate: number } +): Promise => { + return await new TypeORMCarModelFactory().create({ + id, + dailyRate, + }); +} + +export const populateCar = async ( + {id, model}: { id: string, model: TypeORMCarModel } +): Promise => { + return await new TypeORMCarFactory().create({ + id, + model, + }); +} \ No newline at end of file diff --git a/tests/integration/typeorm/utils/setup.ts b/tests/integration/typeorm/utils/setup.ts index 83298c2..2b13638 100644 --- a/tests/integration/typeorm/utils/setup.ts +++ b/tests/integration/typeorm/utils/setup.ts @@ -2,10 +2,7 @@ import {container} from 'tsyringe'; import {DataSource} from 'typeorm'; export const runDataSourceBeforeEachOps = async () => { - const queryDataSource: DataSource = container.resolve("QueryDataSource"); - await queryDataSource.initialize(); - await queryDataSource.synchronize(); - const commandDataSource: DataSource = container.resolve("CommandDataSource"); - await commandDataSource.initialize(); - await commandDataSource.synchronize(); + const dataSource: DataSource = container.resolve("DataSource"); + await dataSource.initialize(); + await dataSource.synchronize(); } \ No newline at end of file diff --git a/tests/integration/typeorm/utils/tearDown.ts b/tests/integration/typeorm/utils/tearDown.ts index 2d4032c..6730723 100644 --- a/tests/integration/typeorm/utils/tearDown.ts +++ b/tests/integration/typeorm/utils/tearDown.ts @@ -2,9 +2,7 @@ import {container} from 'tsyringe'; import {DataSource} from 'typeorm'; export const runDataSourceAfterEachOps = async () => { - const queryDataSource: DataSource = container.resolve("QueryDataSource"); - const commandDataSource: DataSource = container.resolve("CommandDataSource"); - await commandDataSource.dropDatabase(); - await queryDataSource.destroy(); - await commandDataSource.destroy(); + const dataSource: DataSource = container.resolve("DataSource"); + await dataSource.dropDatabase(); + await dataSource.destroy(); } \ No newline at end of file