Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add postgres car rental read repository #9

Merged
merged 7 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/validate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ jobs:
strategy:
matrix:
node-version: [20.x]
services:
postgres:
image: postgres:13.12
env:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: megahertz
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
Expand Down
12 changes: 12 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: "2.4"
services:
db:
image: postgres:13.12
ports:
- "5432:5432"
environment:
PGUSER: user
PGPASSWORD: password
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: megahertz
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"main": "index.js",
"scripts": {
"test": "jest",
"lint": "eslint src --ext ts,tsx"
"lint": "eslint src --ext ts,tsx",
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js"
},
"author": "Harold Cohen <harold.cohen@thetribe.io>",
"license": "ISC",
Expand All @@ -26,15 +27,18 @@
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.28.1",
"lodash": "^4.17.21",
"pg": "^8.11.3",
"ramda": "^0.29.0",
"reflect-metadata": "^0.1.13",
"ts-node": "^10.9.1",
"tslib": "^2.6.2",
"tsyringe": "^4.8.0",
"typeorm": "^0.3.17",
"typescript": "^5.2.2",
"uuid": "^9.0.1"
},
"devDependencies": {
"@jorgebodega/typeorm-factory": "^1.4.0",
"@thetribe/eslint-config-typescript": "^0.5.1",
"@types/jest": "^29.5.5",
"@typescript-eslint/eslint-plugin": "^6.7.3",
Expand Down
18 changes: 18 additions & 0 deletions src/configuration/database/typeorm/data-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {DataSource} from 'typeorm';

const AppDataSource = new DataSource({
type: "postgres",
host: "localhost",
port: 5432,
username: "user",
password: "password",
database: "megahertz",
synchronize: true,
logging: true,
entities: ['src/driven/repositories/typeorm/entities/*.{ts,js}'],
subscribers: [],
migrations: ['src/driven/repositories/typeorm/migrations/*.{ts,js}'],
});


export default AppDataSource;
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {container} from 'tsyringe';
import InMemoryCarReadRepository from '../../../../../driven/repositories/inMemory/car/read';
import UnitOfWork from '../../../../../driven/repositories/inMemory/common/unitOfWork';
import InMemoryCarRentalReadRepository from '../../../../../driven/repositories/inMemory/carRental/read';
import InMemoryCarRentalWriteRepository from '../../../../../driven/repositories/inMemory/carRental/write';
import InMemoryTransactionManagerProxy from '../../../../../driven/repositories/inMemory/common/transactions/proxy';
import InMemoryCarReadRepository from '../../../../driven/repositories/inMemory/car/read';
import UnitOfWork from '../../../../driven/repositories/inMemory/common/unitOfWork';
import InMemoryCarRentalReadRepository from '../../../../driven/repositories/inMemory/carRental/read';
import InMemoryCarRentalWriteRepository from '../../../../driven/repositories/inMemory/carRental/write';
import InMemoryTransactionManagerProxy from '../../../../driven/repositories/inMemory/common/transactions/proxy';

/**
* Configures tsyringe to use inMemory repositories.
Expand Down
11 changes: 11 additions & 0 deletions src/configuration/injection/containers/repositories/typeorm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {container} from 'tsyringe';
import TypeORMCarRentalReadRepository from '../../../../driven/repositories/typeorm/carRental/read';

/**
* Configures tsyringe to use typeORM repositories.
*/
const useTypeORMRepositories = (): void => {
container.register("CarRentalReadRepositoryInterface", {useClass: TypeORMCarRentalReadRepository});
}

export default useTypeORMRepositories;
4 changes: 3 additions & 1 deletion src/core/domain/car/dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import CarModelDTO from '../carModel/dto';

type CarDTO = {
id: string;
modelId: string;
model: CarModelDTO;
}

export default CarDTO;
2 changes: 1 addition & 1 deletion src/core/domain/car/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default class Car {
toDTO(): CarDTO {
const carDTO = {
id: this.id,
modelId: this.model.toDTO().id,
model: this.model.toDTO(),
};

return Object.freeze(carDTO);
Expand Down
2 changes: 1 addition & 1 deletion src/driven/repositories/inMemory/carRental/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default class InMemoryCarRentalWriteRepository implements CarRentalWriteR
await this.unitOfWork.saveEntity("carRentals", {
id: carRentalDTO.id,
carId: carRentalDTO.car.id,
modelId: carRentalDTO.car.modelId,
modelId: carRentalDTO.car.model.id,
dropOffDateTime: carRentalDTO.dropOffDateTime,
pickupDateTime: carRentalDTO.pickupDateTime,
customerId: carRentalDTO.customerId,
Expand Down
32 changes: 32 additions & 0 deletions src/driven/repositories/typeorm/carRental/read.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import CarRentalReadRepositoryInterface from '../../../../core/domain/carRental/interfaces/repositories/read';
import CarRental from '../../../../core/domain/carRental/model';
import Car from '../../../../core/domain/car/model';
import CarModel from '../../../../core/domain/carModel/model';
import {TypeORMCarRental} from '../entities';
import AppDataSource from '../../../../configuration/database/typeorm/data-source';

export default class TypeORMCarRentalReadRepository implements CarRentalReadRepositoryInterface {

async read(carRentalId: string): Promise<CarRental> {
const repository = AppDataSource.getRepository(TypeORMCarRental);
const retrievedCarRental = await repository.findOne({
where: {id: carRentalId},
relations: ['customer', 'car', 'car.model']
}) as TypeORMCarRental;

return new CarRental({
id: carRentalId,
customerId: retrievedCarRental.customer.id,
totalPrice: retrievedCarRental.totalPrice,
pickupDateTime: retrievedCarRental.pickupDateTime,
dropOffDateTime: retrievedCarRental.dropOffDateTime,
car: new Car({
id: retrievedCarRental.car.id,
model: new CarModel({
id: retrievedCarRental.car.model.id,
dailyRate: retrievedCarRental.car.model.dailyRate,
})
})
})
}
}
12 changes: 12 additions & 0 deletions src/driven/repositories/typeorm/entities/car.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {BaseEntity, Entity, OneToOne, PrimaryColumn, JoinColumn} from 'typeorm';
import {TypeORMCarModel} from './index';

@Entity()
export default class TypeORMCar extends BaseEntity {
@PrimaryColumn()
public id!: string;

@OneToOne(() => TypeORMCarModel)
@JoinColumn()
model!: TypeORMCarModel;
}
10 changes: 10 additions & 0 deletions src/driven/repositories/typeorm/entities/carModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {BaseEntity, Column, Entity, PrimaryColumn} from 'typeorm';

@Entity()
export default class TypeORMCarModel extends BaseEntity {
@PrimaryColumn()
public id!: string;

@Column()
dailyRate!: number;
}
25 changes: 25 additions & 0 deletions src/driven/repositories/typeorm/entities/carRental.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {BaseEntity, Column, Entity, OneToOne, PrimaryColumn, JoinColumn} from 'typeorm';
import {TypeORMCar, TypeORMCustomer} from "./index";

@Entity()
export default class TypeORMCarRental extends BaseEntity {
@PrimaryColumn()
public id!: string;

@Column()
public totalPrice!: number;

@Column()
public pickupDateTime!: Date;

@Column()
public dropOffDateTime!: Date;

@OneToOne(() => TypeORMCustomer)
@JoinColumn()
customer!: TypeORMCustomer;

@OneToOne(() => TypeORMCar)
@JoinColumn()
car!: TypeORMCar;
}
7 changes: 7 additions & 0 deletions src/driven/repositories/typeorm/entities/customer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {Entity, PrimaryColumn} from 'typeorm';

@Entity()
export default class TypeORMCustomer {
@PrimaryColumn()
public id!: string;
}
4 changes: 4 additions & 0 deletions src/driven/repositories/typeorm/entities/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {default as TypeORMCarRental} from './carRental';
export {default as TypeORMCustomer} from './customer';
export {default as TypeORMCar} from './car';
export {default as TypeORMCarModel} from './carModel';
109 changes: 109 additions & 0 deletions tests/integration/typeorm/carRental/query/read.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import 'reflect-metadata';
import {container} from 'tsyringe';
import {v4} from 'uuid';
import {advanceTo} from 'jest-date-mock';
import DateParser from '../../../../utils/dateParser';
import useTestingUtilities from '../../../../configuration/containers/utils';
import TypeORMCarRentalReadRepository from '../../../../../src/driven/repositories/typeorm/carRental/read';
import CarRentalDTO from '../../../../../src/core/domain/carRental/dto';
import useTypeORMRepositories from '../../../../../src/configuration/injection/containers/repositories/typeorm';
import AppDataSource from '../../../../../src/configuration/database/typeorm/data-source';
import TypeORMCarRentalFactory from '../../seeding/factories/carRental';
import TypeORMCustomerFactory from '../../seeding/factories/customer';
import TypeORMCarFactory from '../../seeding/factories/car';
import TypeORMCarModelFactory from '../../seeding/factories/carModel';

describe.each([
{
rental: {
id: v4(),
customerId: v4(),
car: {
id: v4(),
model: {
id: '28837cd2-512c-4212-b934-c10d36ddfd7f',
dailyRate: 100,
}
},
totalPrice: 100,
pickupDateTime: 'today',
dropOffDateTime: 'tomorrow',
}
},
{
rental: {
id: v4(),
customerId: v4(),
car: {
id: v4(),
model: {
id: v4(),
dailyRate: 200,
}
},
totalPrice: 200,
pickupDateTime: 'tomorrow',
dropOffDateTime: 'in 2 days',
}
},
])("Integration tests to read car rentals from a postgres database using typeorm", (testCase) => {
let repository: TypeORMCarRentalReadRepository;
let expectedCarRental: Partial<CarRentalDTO>;
let dateParser: DateParser;

beforeAll(async () => {
advanceTo(Date.now());
useTestingUtilities();
dateParser = container.resolve("DateParser");
useTypeORMRepositories();
repository = container.resolve("CarRentalReadRepositoryInterface");
})

beforeEach(async () => {
await AppDataSource.initialize();
await AppDataSource.synchronize();
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
});
await new TypeORMCarRentalFactory().create({
id: testCase.rental.id,
totalPrice: testCase.rental.totalPrice,
pickupDateTime: dateParser.parse(testCase.rental.pickupDateTime),
dropOffDateTime: dateParser.parse(testCase.rental.dropOffDateTime),
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,
}
})

afterEach(async () => {
await AppDataSource.dropDatabase();
await AppDataSource.destroy();
})

test(`Read a car rental ${testCase.rental.id} should return one car rental`, async () => {
const retrievedCarRental = await repository.read(testCase.rental.id);
expect(retrievedCarRental.toDTO()).toEqual(expectedCarRental);
})
})
15 changes: 15 additions & 0 deletions tests/integration/typeorm/seeding/factories/car.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {v4} from 'uuid';
import {FactorizedAttrs, Factory} from '@jorgebodega/typeorm-factory';
import {TypeORMCar} from '../../../../../src/driven/repositories/typeorm/entities';
import AppDataSource from '../../../../../src/configuration/database/typeorm/data-source';

export default class TypeORMCarFactory extends Factory<TypeORMCar> {
protected entity = TypeORMCar;
protected dataSource = AppDataSource;

protected attrs(): FactorizedAttrs<TypeORMCar> {
return {
id: v4(),
};
}
}
16 changes: 16 additions & 0 deletions tests/integration/typeorm/seeding/factories/carModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {v4} from 'uuid';
import {FactorizedAttrs, Factory} from '@jorgebodega/typeorm-factory';
import {TypeORMCarModel} from '../../../../../src/driven/repositories/typeorm/entities';
import AppDataSource from '../../../../../src/configuration/database/typeorm/data-source';

export default class TypeORMCarModelFactory extends Factory<TypeORMCarModel> {
protected entity = TypeORMCarModel;
protected dataSource = AppDataSource;

protected attrs(): FactorizedAttrs<TypeORMCarModel> {
return {
id: v4(),
dailyRate: 100,
};
}
}
Loading