Skip to content
Open
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
5 changes: 5 additions & 0 deletions src/AppProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum AppProvider {
HEALTH_INDICATOR = 'HEALTH_INDICATOR',
PHONE_VALIDATOR = 'PHONE_VALIDATOR',
USER_REPOSITORY = 'USER_REPOSITORY',
}
12 changes: 7 additions & 5 deletions src/api/health/HealthController.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { Controller, Get } from '@nestjs/common'
import { Controller, Get, Inject } from '@nestjs/common'
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'
import { HealthCheckResult, HealthCheckService } from '@nestjs/terminus'
import { DatabaseHealthIndicatorTypeOrm } from '../../shared/infrastructure/database/DatabaseHealthIndicatorTypeOrm'
import { AppProvider } from '../../AppProvider'
import { CustomHealthIndicator } from '../../shared/infrastructure/services/CustomHealthIndicator'

@ApiTags('Health')
@Controller('health')
export class HealthController {
constructor(
private readonly health: HealthCheckService,
private readonly db: DatabaseHealthIndicatorTypeOrm
@Inject(AppProvider.HEALTH_INDICATOR)
private readonly db: CustomHealthIndicator,
private readonly health: HealthCheckService
) {}

@ApiOperation({ summary: 'Check if the API is working fine' })
@ApiOkResponse({ description: 'The API health is good' })
@Get()
async checkHealth(): Promise<HealthCheckResult> {
return this.health.check([async () => this.db.checkHealth('db')])
return this.health.check([() => this.db.checkHealth('db')])
}
}
8 changes: 7 additions & 1 deletion src/api/health/HealthModule.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { Module } from '@nestjs/common'
import { TerminusModule } from '@nestjs/terminus'
import { AppProvider } from '../../AppProvider'
import { DatabaseHealthIndicatorTypeOrm } from '../../shared/infrastructure/database/DatabaseHealthIndicatorTypeOrm'
import { HealthController } from './HealthController'

@Module({
imports: [TerminusModule],
controllers: [HealthController],
providers: [DatabaseHealthIndicatorTypeOrm],
providers: [
{
provide: AppProvider.HEALTH_INDICATOR,
useClass: DatabaseHealthIndicatorTypeOrm,
},
],
})
export class HealthModule {}
2 changes: 0 additions & 2 deletions src/application/users/domain/UserRepository.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { UserId } from '../../../shared/domain/ids/UserId'
import { User } from './User'

export const USER_REPOSITORY_TOKEN = 'UserRepository'

export interface UserRepository {
create(user: User): Promise<void>
update(user: User): Promise<void>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Global, Module } from '@nestjs/common'
import { USER_REPOSITORY_TOKEN } from '../../domain/UserRepository'
import { AppProvider } from '../../../../AppProvider'
import { UserRepositoryTypeORM } from './UserRepositoryTypeORM'

@Global()
@Module({
exports: [USER_REPOSITORY_TOKEN],
exports: [AppProvider.USER_REPOSITORY],
providers: [
{
provide: USER_REPOSITORY_TOKEN,
provide: AppProvider.USER_REPOSITORY,
useClass: UserRepositoryTypeORM,
},
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { DataSource } from 'typeorm'
import { v4 as generateUuidV4 } from 'uuid'
import { dropTables } from '../../../../../test/utils/DropTables'
import { typeOrm } from '../../../../database/orm.config'
import { UserId } from '../../../../shared/domain/ids/UserId'
import { describeDatabase } from '../../../../../test/utils/describeDatabase'
import { EntityAlreadyCreatedError } from '../../../../shared/domain/errors/EntityAlreadyCreatedError'
import { UserId } from '../../../../shared/domain/ids/UserId'
import {
JANE_CONTACT,
MICHAEL,
Expand All @@ -13,21 +11,13 @@ import {
import { UserBuilder } from '../../../../utils/UserBuilder'
import { UserRepositoryTypeORM } from './UserRepositoryTypeORM'

describe('UserRepositoryTypeORM', () => {
let dataSource: DataSource
describeDatabase('UserRepositoryTypeORM', ({ entityManager }) => {
let userRepository: UserRepositoryTypeORM

beforeAll(async () => {
dataSource = await typeOrm.initialize()
}, 15000)

beforeEach(async () => {
await dropTables(dataSource)
userRepository = new UserRepositoryTypeORM(dataSource)
userRepository = new UserRepositoryTypeORM(await entityManager)
})

afterAll(() => dataSource.destroy())

it('saves the user correctly', async () => {
const userId = generateUuidV4()
const user = UserBuilder.withUserId(userId).buildDomainObject()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common'
import { DataSource, In, Repository } from 'typeorm'
import { EntityManager, In, Repository } from 'typeorm'
import { EntityAlreadyCreatedError } from '../../../../shared/domain/errors/EntityAlreadyCreatedError'
import { DomainId } from '../../../../shared/domain/hex/DomainId'
import { UserId } from '../../../../shared/domain/ids/UserId'
Expand All @@ -11,8 +11,8 @@ import { UserEntity } from '../entities/UserEntity'
export class UserRepositoryTypeORM implements UserRepository {
private userRepository: Repository<UserEntity>

constructor(private readonly dataSource: DataSource) {
this.userRepository = this.dataSource.getRepository(UserEntity)
constructor(private readonly entityManager: EntityManager) {
this.userRepository = this.entityManager.getRepository(UserEntity)
}

async create(user: User): Promise<void> {
Expand Down
5 changes: 3 additions & 2 deletions src/application/users/use-cases/ContactsInCommonFetcher.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Inject, Injectable } from '@nestjs/common'
import { AppProvider } from '../../../AppProvider'
import { UseCase } from '../../../shared/domain/hex/UseCase'
import { UserId } from '../../../shared/domain/ids/UserId'
import { UserFinder } from '../domain/UserFinder'
import { UserRepository, USER_REPOSITORY_TOKEN } from '../domain/UserRepository'
import { UserRepository } from '../domain/UserRepository'

@Injectable()
export class ContactsInCommonFetcher extends UseCase {
private userFinder: UserFinder

constructor(@Inject(USER_REPOSITORY_TOKEN) private userRepository: UserRepository) {
constructor(@Inject(AppProvider.USER_REPOSITORY) private userRepository: UserRepository) {
super()

this.userFinder = new UserFinder(userRepository)
Expand Down
5 changes: 3 additions & 2 deletions src/application/users/use-cases/UserContactsUpdater.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Inject, Injectable } from '@nestjs/common'
import { AppProvider } from '../../../AppProvider'
import { UseCase } from '../../../shared/domain/hex/UseCase'
import { UserId } from '../../../shared/domain/ids/UserId'
import { Contact } from '../domain/Contact'
import { UserFinder } from '../domain/UserFinder'
import { UserRepository, USER_REPOSITORY_TOKEN } from '../domain/UserRepository'
import { UserRepository } from '../domain/UserRepository'

@Injectable()
export class UserContactsUpdater extends UseCase {
private userFinder: UserFinder

constructor(@Inject(USER_REPOSITORY_TOKEN) private userRepository: UserRepository) {
constructor(@Inject(AppProvider.USER_REPOSITORY) private userRepository: UserRepository) {
super()

this.userFinder = new UserFinder(userRepository)
Expand Down
12 changes: 5 additions & 7 deletions src/application/users/use-cases/UserCreator.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import { Inject, Injectable } from '@nestjs/common'
import { AppProvider } from '../../../AppProvider'
import { UseCase } from '../../../shared/domain/hex/UseCase'
import { UserId } from '../../../shared/domain/ids/UserId'
import {
PhoneValidator,
PHONE_VALIDATOR_TOKEN,
} from '../../../shared/domain/services/PhoneValidator'
import { PhoneValidator } from '../../../shared/domain/services/PhoneValidator'
import { InvalidPhoneError } from '../domain/errors/InvalidPhoneError'
import { PhoneInUseError } from '../domain/errors/PhoneInUseError'
import { User } from '../domain/User'
import { UserRepository, USER_REPOSITORY_TOKEN } from '../domain/UserRepository'
import { UserRepository } from '../domain/UserRepository'

@Injectable()
export class UserCreator extends UseCase {
constructor(
@Inject(PHONE_VALIDATOR_TOKEN) private phoneValidator: PhoneValidator,
@Inject(USER_REPOSITORY_TOKEN) private userRepository: UserRepository
@Inject(AppProvider.PHONE_VALIDATOR) private phoneValidator: PhoneValidator,
@Inject(AppProvider.USER_REPOSITORY) private userRepository: UserRepository
) {
super()
}
Expand Down
5 changes: 3 additions & 2 deletions src/application/users/use-cases/UserFinder.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Inject, Injectable } from '@nestjs/common'
import { AppProvider } from '../../../AppProvider'
import { UseCase } from '../../../shared/domain/hex/UseCase'
import { UserId } from '../../../shared/domain/ids/UserId'
import { User } from '../domain/User'
import { UserFinder as DomainUserFinder } from '../domain/UserFinder'
import { UserRepository, USER_REPOSITORY_TOKEN } from '../domain/UserRepository'
import { UserRepository } from '../domain/UserRepository'

@Injectable()
export class UserFinder extends UseCase {
private userFinder: DomainUserFinder

constructor(@Inject(USER_REPOSITORY_TOKEN) private userRepository: UserRepository) {
constructor(@Inject(AppProvider.USER_REPOSITORY) private userRepository: UserRepository) {
super()

this.userFinder = new DomainUserFinder(userRepository)
Expand Down
2 changes: 0 additions & 2 deletions src/shared/domain/services/PhoneValidator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
export const PHONE_VALIDATOR_TOKEN = 'PhoneValidator'

export interface PhoneValidator {
validate(phone: string): Promise<boolean>
}
24 changes: 12 additions & 12 deletions src/shared/infrastructure/database/DatabaseHealthIndicatorFake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@ import { CustomHealthIndicator } from '../services/CustomHealthIndicator'

@Injectable()
export class DatabaseHealthIndicatorFake extends HealthIndicator implements CustomHealthIndicator {
constructor(private throwError: boolean = false) {
super()
}

static withError(): DatabaseHealthIndicatorFake {
return new DatabaseHealthIndicatorFake(true)
}
private isConnected = true

checkHealth(key: string): HealthIndicatorResult {
async checkHealth(key: string): Promise<HealthIndicatorResult> {
const result = this.getStatus(
key,
!this.throwError,
this.throwError ? { message: 'DatabaseHealthIndicatorFake mocked health error' } : undefined
this.isConnected,
!this.isConnected ? { message: 'Database connection is down' } : undefined
)
if (this.throwError) {
throw new HealthCheckError('DatabaseHealthIndicatorFake mocked health error', result)

if (!this.isConnected) {
throw new HealthCheckError('Database connection is down', result)
}

return result
}

markAsDown() {
this.isConnected = false
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
import { Injectable } from '@nestjs/common'
import { HealthCheckError, HealthIndicator, HealthIndicatorResult } from '@nestjs/terminus'
import { DataSource } from 'typeorm'
import { HealthCheckError, HealthIndicatorResult, TypeOrmHealthIndicator } from '@nestjs/terminus'
import { CustomHealthIndicator } from '../services/CustomHealthIndicator'

@Injectable()
export class DatabaseHealthIndicatorTypeOrm
extends HealthIndicator
implements CustomHealthIndicator
{
constructor(private readonly dataSource: DataSource) {
super()
}
export class DatabaseHealthIndicatorTypeOrm implements CustomHealthIndicator {
constructor(private readonly database: TypeOrmHealthIndicator) {}

checkHealth(key: string): HealthIndicatorResult {
const isConnected = this.dataSource.isInitialized
const result = this.getStatus(
key,
isConnected,
!isConnected ? { message: 'Database connection is down' } : undefined
)
async checkHealth(key: string): Promise<HealthIndicatorResult> {
const isConnected = await this.database.pingCheck(key)

if (!isConnected) {
throw new HealthCheckError('Database connection is down', result)
throw new HealthCheckError('Database connection is down', isConnected)
}

return result
return isConnected
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { HealthIndicatorResult } from '@nestjs/terminus'

export interface CustomHealthIndicator {
checkHealth(key: string): HealthIndicatorResult
checkHealth(key: string): Promise<HealthIndicatorResult>
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { HttpModule } from '@nestjs/axios'
import { Global, Module } from '@nestjs/common'
import { PHONE_VALIDATOR_TOKEN } from '../../../domain/services/PhoneValidator'
import { AppProvider } from '../../../../AppProvider'
import { PhoneValidatorNeutrino } from './PhoneValidatorNeutrino'

@Global()
@Module({
exports: [PHONE_VALIDATOR_TOKEN],
exports: [AppProvider.PHONE_VALIDATOR],
imports: [HttpModule],
providers: [
{
provide: PHONE_VALIDATOR_TOKEN,
provide: AppProvider.PHONE_VALIDATOR,
useClass: PhoneValidatorNeutrino,
},
],
Expand Down
12 changes: 6 additions & 6 deletions test/api/health/get-health.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { HttpStatus } from '@nestjs/common'
import { DatabaseHealthIndicatorFake } from '../../../src/shared/infrastructure/database/DatabaseHealthIndicatorFake'
import { createClient } from '../../utils/createClient'
import { AppProvider } from '../../../src/AppProvider'
import { TestClient } from '../../utils/TestClient'

describe(`/health (GET)`, () => {
it('returns OK health status', async () => {
const client = await createClient()
const client = await TestClient.create()

await client.health().expect(HttpStatus.OK).run()
})

it('returns KO health status', async () => {
const client = await createClient({
databaseHealthIndicator: new DatabaseHealthIndicatorFake(true),
})
const client = await TestClient.create()
const healthIndicator = client.getProvider(AppProvider.HEALTH_INDICATOR)
healthIndicator.markAsDown()

await client.health().expect(HttpStatus.SERVICE_UNAVAILABLE).run()
})
Expand Down
4 changes: 2 additions & 2 deletions test/api/users/create-user-contacts.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { HttpStatus } from '@nestjs/common'
import { MICHAEL } from '../../../src/shared/fixtures/users'
import { createClient } from '../../utils/createClient'
import { TestClient } from '../../utils/TestClient'

describe(`POST /v1/users/:id/contacts`, () => {
it('creates or overrides the contacts of the user', async () => {
const client = await createClient()
const client = await TestClient.create()
const { body: michael } = await client.createUser(MICHAEL).expect(HttpStatus.CREATED).run()

await client
Expand Down
8 changes: 4 additions & 4 deletions test/api/users/create-user.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { HttpStatus } from '@nestjs/common'
import { MICHAEL, OLIVER } from '../../../src/shared/fixtures/users'
import { createClient } from '../../utils/createClient'
import { TestClient } from '../../utils/TestClient'

describe(`POST /v1/users`, () => {
it('creates the user', async () => {
const client = await createClient()
const client = await TestClient.create()

const { body } = await client.createUser(MICHAEL).expect(HttpStatus.CREATED).run()

Expand All @@ -15,7 +15,7 @@ describe(`POST /v1/users`, () => {
})

it('fails to create the user if the phone number is invalid', async () => {
const client = await createClient()
const client = await TestClient.create()

await client
.createUser({ ...MICHAEL, phone: 'invalid-phone' })
Expand All @@ -24,7 +24,7 @@ describe(`POST /v1/users`, () => {
})

it('fails to create the user if the phone is already in use by another user', async () => {
const client = await createClient()
const client = await TestClient.create()

await client.createUser(MICHAEL).expect(HttpStatus.CREATED).run()
await client
Expand Down
Loading