├── .nvmrc ├── .npmrc ├── .travis.yml ├── src ├── domain │ ├── type-aliases.ts │ ├── invalid-dummy.error.ts │ ├── dummy.repository.ts │ ├── dummy.ts │ └── __spec__ │ │ └── dummy.spec.ts ├── infrastructure │ ├── rest │ │ ├── models │ │ │ └── post-dummy-data-request.ts │ │ ├── rest.module.ts │ │ ├── filters │ │ │ ├── invalid-dummy-error.filter.ts │ │ │ └── __spec__ │ │ │ │ └── invalid-dummy-error.filter.spec.ts │ │ └── dummy.controller.ts │ ├── config │ │ ├── environment-config │ │ │ ├── environment-config.error.ts │ │ │ ├── environment-config.module.ts │ │ │ ├── environment-config.service.ts │ │ │ └── __spec__ │ │ │ │ └── environment-config.service.spec.ts │ │ └── typeorm │ │ │ └── typeorm-config.module.ts │ ├── use_cases_proxy │ │ ├── use-case-proxy.ts │ │ └── proxy-services-dynamic.module.ts │ ├── app.module.ts │ └── repositories │ │ ├── entities │ │ └── dummy.entity.ts │ │ ├── repositories.module.ts │ │ └── database-dummy.repository.ts ├── use_cases │ ├── get-all-dummy-data.ts │ ├── create-dummy-data.ts │ └── __spec__ │ │ ├── get-all-dummy-data.spec.ts │ │ └── create-dummy-data.spec.ts └── main.ts ├── nest-cli.json ├── jest-global-setup.js ├── .prettierrc ├── tsconfig.build.json ├── jest-mutation.config.js ├── e2e ├── jest-e2e.json ├── e2e-config.ts ├── repositories │ ├── database-e2e.utils.ts │ └── database-dummy.repository.e2e-spec.ts └── rest │ └── dummy.controller.e2e-spec.ts ├── jest.config.js ├── tslint.json ├── stryker.conf.js ├── .gitignore ├── tsconfig.json ├── database ├── write-ormconfig-for-migrations.ts └── migrations │ └── create-dummy-table-migration1573748588949.ts ├── tools └── check-clean-architecture.sh ├── README.md ├── tslint.config.json └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.15 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | -------------------------------------------------------------------------------- /src/domain/type-aliases.ts: -------------------------------------------------------------------------------- 1 | export type DummyId = number; 2 | -------------------------------------------------------------------------------- /src/domain/invalid-dummy.error.ts: -------------------------------------------------------------------------------- 1 | export class InvalidDummyError extends Error {} 2 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /jest-global-setup.js: -------------------------------------------------------------------------------- 1 | module.exports = async () => { 2 | process.env.TZ = 'Canada/Eastern'; 3 | }; 4 | -------------------------------------------------------------------------------- /src/infrastructure/rest/models/post-dummy-data-request.ts: -------------------------------------------------------------------------------- 1 | export interface PostDummyDataRequest { 2 | value: string; 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "printWidth": 150 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts", "e2e", "database", "tools"] 4 | } 5 | -------------------------------------------------------------------------------- /src/domain/dummy.repository.ts: -------------------------------------------------------------------------------- 1 | import { Dummy } from './dummy'; 2 | 3 | export interface DummyRepository { 4 | save(dummy: Dummy): Promise; 5 | findAll(): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /src/infrastructure/config/environment-config/environment-config.error.ts: -------------------------------------------------------------------------------- 1 | export class EnvironmentConfigError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/infrastructure/use_cases_proxy/use-case-proxy.ts: -------------------------------------------------------------------------------- 1 | export class UseCaseProxy { 2 | constructor(private readonly useCase: T) {} 3 | getInstance(): T { 4 | return this.useCase; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/infrastructure/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { RestModule } from './rest/rest.module'; 3 | 4 | @Module({ 5 | imports: [RestModule], 6 | }) 7 | export class AppModule {} 8 | -------------------------------------------------------------------------------- /jest-mutation.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'json', 'ts'], 3 | testRegex: '.*spec\\.ts$', // All tests (unit + e2e) 4 | transform: { 5 | '^.+\\.(t|j)s$': 'ts-jest', 6 | }, 7 | testEnvironment: 'node', 8 | testTimeout: 30000, 9 | }; 10 | -------------------------------------------------------------------------------- /e2e/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": "\\.e2e-spec\\.ts$", 6 | "transform": { 7 | "^.+\\.ts$": "ts-jest" 8 | }, 9 | "testTimeout": 30000, 10 | "globalSetup": "../jest-global-setup.js" 11 | } 12 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'json', 'ts'], 3 | testRegex: '\\.spec\\.ts$', 4 | transform: { 5 | '^.+\\.ts$': 'ts-jest', 6 | }, 7 | collectCoverageFrom: ['/src/**/*.ts'], 8 | testEnvironment: 'node', 9 | globalSetup: './jest-global-setup.js', 10 | }; 11 | -------------------------------------------------------------------------------- /src/infrastructure/config/environment-config/environment-config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EnvironmentConfigService } from './environment-config.service'; 3 | 4 | @Module({ 5 | providers: [EnvironmentConfigService], 6 | exports: [EnvironmentConfigService], 7 | }) 8 | export class EnvironmentConfigModule {} 9 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended", 4 | "tslint-config-prettier", 5 | "./tslint.config.json" 6 | ], 7 | "linterOptions": { 8 | "exclude": [ 9 | "node_modules/**", 10 | "dist/**", 11 | "dist.js/**" 12 | ] 13 | }, 14 | "rulesDirectory": [ 15 | "tslint-plugin-prettier" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/infrastructure/repositories/entities/dummy.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Entity({ name: 'dummy' }) 4 | export class DummyEntity { 5 | @PrimaryGeneratedColumn('increment', { name: 'id' }) 6 | id: number; 7 | 8 | @Column({ name: 'value', type: 'text', nullable: false }) 9 | value: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/use_cases/get-all-dummy-data.ts: -------------------------------------------------------------------------------- 1 | import { Dummy } from '../domain/dummy'; 2 | import { DummyRepository } from '../domain/dummy.repository'; 3 | 4 | export class GetAllDummyData { 5 | constructor(private readonly dummyRepository: DummyRepository) {} 6 | 7 | async execute(): Promise { 8 | return this.dummyRepository.findAll(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/use_cases/create-dummy-data.ts: -------------------------------------------------------------------------------- 1 | import { Dummy } from '../domain/dummy'; 2 | import { DummyRepository } from '../domain/dummy.repository'; 3 | 4 | export class CreateDummyData { 5 | constructor(private readonly dummyRepository: DummyRepository) {} 6 | 7 | async execute(value: string): Promise { 8 | const dummy: Dummy = new Dummy(value); 9 | 10 | return this.dummyRepository.save(dummy); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /e2e/e2e-config.ts: -------------------------------------------------------------------------------- 1 | import { EnvironmentConfigService } from '../src/infrastructure/config/environment-config/environment-config.service'; 2 | 3 | export const e2eEnvironmentConfigService: EnvironmentConfigService = { 4 | get(key: string): string { 5 | if (key === 'DATABASE_TYPE') return 'sqlite'; 6 | if (key === 'DATABASE_NAME') return 'e2e.sqlite'; 7 | 8 | return null; 9 | }, 10 | } as EnvironmentConfigService; 11 | -------------------------------------------------------------------------------- /src/domain/dummy.ts: -------------------------------------------------------------------------------- 1 | import { isEmpty } from 'lodash'; 2 | import { InvalidDummyError } from './invalid-dummy.error'; 3 | import { DummyId } from './type-aliases'; 4 | 5 | export class Dummy { 6 | id: DummyId; 7 | value: string; 8 | 9 | constructor(value: string) { 10 | if (isEmpty(value)) { 11 | throw new InvalidDummyError('value cannot be null or empty'); 12 | } 13 | this.value = value; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { AppModule } from './infrastructure/app.module'; 4 | import { EnvironmentConfigService } from './infrastructure/config/environment-config/environment-config.service'; 5 | 6 | async function bootstrap(): Promise { 7 | const app: INestApplication = await NestFactory.create(AppModule); 8 | const port: string = app.get(EnvironmentConfigService).get('PORT'); 9 | await app.listen(port); 10 | } 11 | bootstrap(); 12 | -------------------------------------------------------------------------------- /stryker.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | mutator: 'typescript', 4 | packageManager: 'npm', 5 | reporters: ['html', 'clear-text', 'progress'], 6 | testRunner: 'jest', 7 | transpilers: [], 8 | coverageAnalysis: 'off', 9 | tsconfigFile: 'tsconfig.json', 10 | mutate: ['src/**/*.ts', '!src/**/*.spec.ts', '!src/**/*-enums.ts', '!src/**/*.module.ts', '!src/**/*.swagger.ts'], 11 | maxConcurrentTestRunners: 4, 12 | jest: { 13 | config: require('./jest-mutation.config.js'), 14 | }, 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /src/infrastructure/rest/rest.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { APP_FILTER } from '@nestjs/core'; 3 | import { ProxyServicesDynamicModule } from '../use_cases_proxy/proxy-services-dynamic.module'; 4 | import { DummyController } from './dummy.controller'; 5 | import { InvalidDummyErrorFilter } from './filters/invalid-dummy-error.filter'; 6 | 7 | @Module({ 8 | imports: [ProxyServicesDynamicModule.register()], 9 | controllers: [DummyController], 10 | providers: [{ provide: APP_FILTER, useClass: InvalidDummyErrorFilter }], 11 | }) 12 | export class RestModule {} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /src/coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | *.iml 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # Database and TypeORM 39 | ormconfig.json 40 | *.sqlite 41 | 42 | # Stryker 43 | .stryker-tmp/ 44 | reports/ 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "esnext", 8 | "esnext.asynciterable", 9 | "dom" 10 | ], 11 | "esModuleInterop": true, 12 | "strict": false, 13 | "declaration": true, 14 | "removeComments": true, 15 | "emitDecoratorMetadata": true, 16 | "experimentalDecorators": true, 17 | "sourceMap": true, 18 | "outDir": "./dist", 19 | "baseUrl": "./", 20 | "incremental": true, 21 | "types": [ 22 | "@types/jest", 23 | "@types/supertest" 24 | ] 25 | }, 26 | "exclude": ["node_modules", "dist"] 27 | } 28 | -------------------------------------------------------------------------------- /e2e/repositories/database-e2e.utils.ts: -------------------------------------------------------------------------------- 1 | import { Connection, ConnectionOptions, createConnection } from 'typeorm'; 2 | import { EnvironmentConfigService } from '../../src/infrastructure/config/environment-config/environment-config.service'; 3 | import { getTypeOrmMigrationsOptions } from '../../src/infrastructure/config/typeorm/typeorm-config.module'; 4 | 5 | export const runDatabaseMigrations = async (environmentConfigService: EnvironmentConfigService) => { 6 | const connection: Connection = await createConnection({ 7 | ...getTypeOrmMigrationsOptions(environmentConfigService), 8 | name: 'e2e', 9 | } as ConnectionOptions); 10 | await connection.runMigrations(); 11 | await connection.close(); 12 | }; 13 | -------------------------------------------------------------------------------- /src/infrastructure/repositories/repositories.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { EnvironmentConfigModule } from '../config/environment-config/environment-config.module'; 4 | import { TypeOrmConfigModule } from '../config/typeorm/typeorm-config.module'; 5 | import { DatabaseDummyRepository } from './database-dummy.repository'; 6 | import { DummyEntity } from './entities/dummy.entity'; 7 | 8 | @Module({ 9 | imports: [TypeOrmConfigModule, TypeOrmModule.forFeature([DummyEntity]), EnvironmentConfigModule], 10 | providers: [DatabaseDummyRepository], 11 | exports: [DatabaseDummyRepository], 12 | }) 13 | export class RepositoriesModule {} 14 | -------------------------------------------------------------------------------- /database/write-ormconfig-for-migrations.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { PathLike } from 'fs'; 3 | import * as path from 'path'; 4 | import { EnvironmentConfigService } from '../src/infrastructure/config/environment-config/environment-config.service'; 5 | import { getTypeOrmMigrationsOptions } from '../src/infrastructure/config/typeorm/typeorm-config.module'; 6 | 7 | const typeOrmConfigFilePath: PathLike = path.join(__dirname, '../ormconfig.json'); 8 | const typeOrmMigrationsOptions: object = getTypeOrmMigrationsOptions(new EnvironmentConfigService()); 9 | try { 10 | fs.unlinkSync(typeOrmConfigFilePath); 11 | } catch (e) { 12 | // tslint:disable-next-line:no-console 13 | console.log(`Failed to delete file ${typeOrmConfigFilePath}. Probably because it does not exist.`); 14 | } 15 | fs.writeFileSync(typeOrmConfigFilePath, JSON.stringify([typeOrmMigrationsOptions], null, 2)); 16 | -------------------------------------------------------------------------------- /src/infrastructure/rest/filters/invalid-dummy-error.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common'; 2 | import { HttpArgumentsHost } from '@nestjs/common/interfaces'; 3 | import { Response } from 'express'; 4 | import { InvalidDummyError } from '../../../domain/invalid-dummy.error'; 5 | 6 | @Catch(InvalidDummyError) 7 | export class InvalidDummyErrorFilter implements ExceptionFilter { 8 | catch(exception: InvalidDummyError, host: ArgumentsHost): void { 9 | const ctx: HttpArgumentsHost = host.switchToHttp(); 10 | const response: Response = ctx.getResponse(); 11 | const status: HttpStatus = HttpStatus.BAD_REQUEST; 12 | 13 | response.status(status).json({ 14 | statusCode: status, 15 | timestamp: new Date().toISOString(), 16 | name: exception.name, 17 | message: exception.message, 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /database/migrations/create-dummy-table-migration1573748588949.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, Table } from 'typeorm'; 2 | 3 | export class CreateDummyTableMigration1573748588949 implements MigrationInterface { 4 | name: string = 'CreateDummyTableMigration1573748588949'; 5 | 6 | async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.createTable( 8 | new Table({ 9 | name: 'dummy', 10 | columns: [ 11 | { 12 | name: 'id', 13 | type: 'integer', 14 | isPrimary: true, 15 | isGenerated: true, 16 | generationStrategy: 'increment', 17 | }, 18 | { 19 | name: 'value', 20 | type: 'text', 21 | isNullable: false, 22 | }, 23 | ], 24 | }), 25 | true 26 | ); 27 | } 28 | 29 | async down(queryRunner: QueryRunner): Promise { 30 | await queryRunner.dropTable('dummy'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/domain/__spec__/dummy.spec.ts: -------------------------------------------------------------------------------- 1 | import { Dummy } from '../dummy'; 2 | import { InvalidDummyError } from '../invalid-dummy.error'; 3 | 4 | describe('domain/Dummy', () => { 5 | describe('constructor()', () => { 6 | it('should bind value', () => { 7 | // given 8 | const value: string = 'a value'; 9 | 10 | // when 11 | const result: Dummy = new Dummy(value); 12 | 13 | // then 14 | expect(result.value).toBe('a value'); 15 | }); 16 | 17 | it('should fail when value is null', () => { 18 | // given 19 | const value: string = null; 20 | 21 | // when 22 | const result = () => new Dummy(value); 23 | 24 | // then 25 | expect(result).toThrow(new InvalidDummyError('value cannot be null or empty')); 26 | }); 27 | 28 | it('should fail when value is empty', () => { 29 | // given 30 | const value: string = ''; 31 | 32 | // when 33 | const result = () => new Dummy(value); 34 | 35 | // then 36 | expect(result).toThrow(new InvalidDummyError('value cannot be null or empty')); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/use_cases/__spec__/get-all-dummy-data.spec.ts: -------------------------------------------------------------------------------- 1 | import { Dummy } from '../../domain/dummy'; 2 | import { DummyRepository } from '../../domain/dummy.repository'; 3 | import { GetAllDummyData } from '../get-all-dummy-data'; 4 | 5 | describe('uses_cases/GetAllDummyData', () => { 6 | let getAllDummyData: GetAllDummyData; 7 | let mockDummyRepository: DummyRepository; 8 | 9 | beforeEach(() => { 10 | mockDummyRepository = {} as DummyRepository; 11 | mockDummyRepository.findAll = jest.fn(); 12 | 13 | getAllDummyData = new GetAllDummyData(mockDummyRepository); 14 | }); 15 | 16 | describe('execute()', () => { 17 | it('should return found dummy data from repository', async () => { 18 | // given 19 | const dummy1: Dummy = new Dummy('some dummy value 1'); 20 | const dummy2: Dummy = new Dummy('some dummy value 2'); 21 | (mockDummyRepository.findAll as jest.Mock).mockReturnValue(Promise.resolve([dummy1, dummy2])); 22 | 23 | // when 24 | const result: Dummy[] = await getAllDummyData.execute(); 25 | 26 | // then 27 | expect(result).toStrictEqual([dummy1, dummy2]); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tools/check-clean-architecture.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | readonly SRC_PATH="src" 4 | 5 | readonly DOMAIN_DIRECTORY_PATH="${SRC_PATH}/domain" 6 | readonly USE_CASES_DIRECTORY_PATH="${SRC_PATH}/use_cases" 7 | 8 | readonly UNAUTHORIZED_IMPORTS_IN_USE_CASES="infrastructure|nestjs" 9 | readonly UNAUTHORIZED_IMPORTS_IN_DOMAIN="${UNAUTHORIZED_IMPORTS_IN_USE_CASES}|use_cases" 10 | 11 | readonly UNAUTHORIZED_IMPORTS_COUNT_IN_DOMAIN=$(find ${DOMAIN_DIRECTORY_PATH} -name "*.ts" -exec egrep -w ${UNAUTHORIZED_IMPORTS_IN_DOMAIN} {} \; | wc -l) 12 | readonly UNAUTHORIZED_IMPORTS_COUNT_IN_USE_CASES=$(find ${USE_CASES_DIRECTORY_PATH} -name "*.ts" -exec egrep -w ${UNAUTHORIZED_IMPORTS_IN_USE_CASES} {} \; | wc -l) 13 | 14 | if [[ "${UNAUTHORIZED_IMPORTS_COUNT_IN_DOMAIN}" -eq 0 ]] && [[ "${UNAUTHORIZED_IMPORTS_COUNT_IN_USE_CASES}" -eq 0 ]]; then 15 | exit 0 16 | fi 17 | 18 | echo "${UNAUTHORIZED_IMPORTS_COUNT_IN_DOMAIN} unauthorized imports in ${DOMAIN_DIRECTORY_PATH}:" 19 | find ${DOMAIN_DIRECTORY_PATH} -name "*.ts" -exec egrep -lw ${UNAUTHORIZED_IMPORTS_IN_DOMAIN} {} \; 20 | echo "" 21 | echo "${UNAUTHORIZED_IMPORTS_COUNT_IN_USE_CASES} unauthorized imports in ${USE_CASES_DIRECTORY_PATH}:" 22 | find ${USE_CASES_DIRECTORY_PATH} -name "*.ts" -exec egrep -lw ${UNAUTHORIZED_IMPORTS_IN_USE_CASES} {} \; 23 | exit 1 24 | -------------------------------------------------------------------------------- /src/infrastructure/rest/dummy.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Inject, Post } from '@nestjs/common'; 2 | import { Dummy } from '../../domain/dummy'; 3 | import { CreateDummyData } from '../../use_cases/create-dummy-data'; 4 | import { GetAllDummyData } from '../../use_cases/get-all-dummy-data'; 5 | import { ProxyServicesDynamicModule } from '../use_cases_proxy/proxy-services-dynamic.module'; 6 | import { UseCaseProxy } from '../use_cases_proxy/use-case-proxy'; 7 | import { PostDummyDataRequest } from './models/post-dummy-data-request'; 8 | 9 | @Controller('/api/dummy') 10 | export class DummyController { 11 | constructor( 12 | @Inject(ProxyServicesDynamicModule.GET_ALL_DUMMY_DATA_PROXY_SERVICE) private readonly getAllDummyDataProxyService: UseCaseProxy, 13 | @Inject(ProxyServicesDynamicModule.CREATE_DUMMY_DATA_PROXY_SERVICE) private readonly createDummyDataProxyService: UseCaseProxy 14 | ) {} 15 | 16 | @Get('/') 17 | async getAllDummyData(): Promise { 18 | return this.getAllDummyDataProxyService.getInstance().execute(); 19 | } 20 | 21 | @Post('/') 22 | async postDummyData(@Body() postDummyDataRequest: PostDummyDataRequest): Promise { 23 | return this.createDummyDataProxyService.getInstance().execute(postDummyDataRequest.value); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/infrastructure/repositories/database-dummy.repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { Dummy } from '../../domain/dummy'; 5 | import { DummyRepository } from '../../domain/dummy.repository'; 6 | import { DummyEntity } from './entities/dummy.entity'; 7 | 8 | @Injectable() 9 | export class DatabaseDummyRepository implements DummyRepository { 10 | constructor(@InjectRepository(DummyEntity) private readonly dummyEntityRepository: Repository) {} 11 | 12 | async findAll(): Promise { 13 | const foundDummyEntities: DummyEntity[] = await this.dummyEntityRepository.find(); 14 | 15 | return foundDummyEntities.map((dummyEntity: DummyEntity) => this.toDummy(dummyEntity)); 16 | } 17 | 18 | async save(dummy: Dummy): Promise { 19 | const dummyEntity: DummyEntity = this.toDummyEntity(dummy); 20 | 21 | const savedDummyEntity: DummyEntity = await this.dummyEntityRepository.save(dummyEntity); 22 | 23 | return this.toDummy(savedDummyEntity); 24 | } 25 | 26 | private toDummy(dummyEntity: DummyEntity): Dummy { 27 | const dummy: Dummy = new Dummy(dummyEntity.value); 28 | dummy.id = dummyEntity.id; 29 | 30 | return dummy; 31 | } 32 | 33 | private toDummyEntity(dummy: Dummy): DummyEntity { 34 | const dummyEntity: DummyEntity = new DummyEntity(); 35 | dummyEntity.id = dummy.id; 36 | dummyEntity.value = dummy.value; 37 | 38 | return dummyEntity; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/infrastructure/use_cases_proxy/proxy-services-dynamic.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module } from '@nestjs/common'; 2 | import { CreateDummyData } from '../../use_cases/create-dummy-data'; 3 | import { GetAllDummyData } from '../../use_cases/get-all-dummy-data'; 4 | import { DatabaseDummyRepository } from '../repositories/database-dummy.repository'; 5 | import { RepositoriesModule } from '../repositories/repositories.module'; 6 | import { UseCaseProxy } from './use-case-proxy'; 7 | 8 | @Module({ 9 | imports: [RepositoriesModule], 10 | }) 11 | export class ProxyServicesDynamicModule { 12 | static GET_ALL_DUMMY_DATA_PROXY_SERVICE: string = 'GetAllDummyDataProxyService'; 13 | static CREATE_DUMMY_DATA_PROXY_SERVICE: string = 'CreateDummyDataProxyService'; 14 | 15 | static register(): DynamicModule { 16 | return { 17 | module: ProxyServicesDynamicModule, 18 | providers: [ 19 | { 20 | inject: [DatabaseDummyRepository], 21 | provide: ProxyServicesDynamicModule.GET_ALL_DUMMY_DATA_PROXY_SERVICE, 22 | useFactory: (databaseDummyRepository: DatabaseDummyRepository) => new UseCaseProxy(new GetAllDummyData(databaseDummyRepository)), 23 | }, 24 | { 25 | inject: [DatabaseDummyRepository], 26 | provide: ProxyServicesDynamicModule.CREATE_DUMMY_DATA_PROXY_SERVICE, 27 | useFactory: (databaseDummyRepository: DatabaseDummyRepository) => new UseCaseProxy(new CreateDummyData(databaseDummyRepository)), 28 | }, 29 | ], 30 | exports: [ProxyServicesDynamicModule.GET_ALL_DUMMY_DATA_PROXY_SERVICE, ProxyServicesDynamicModule.CREATE_DUMMY_DATA_PROXY_SERVICE], 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/infrastructure/config/typeorm/typeorm-config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; 3 | import { DummyEntity } from '../../repositories/entities/dummy.entity'; 4 | import { EnvironmentConfigModule } from '../environment-config/environment-config.module'; 5 | import { EnvironmentConfigService } from '../environment-config/environment-config.service'; 6 | 7 | export const getTypeOrmModuleOptions = (environmentConfigService: EnvironmentConfigService): TypeOrmModuleOptions => 8 | ({ 9 | type: environmentConfigService.get('DATABASE_TYPE'), 10 | host: environmentConfigService.get('DATABASE_HOST'), 11 | port: parseInt(environmentConfigService.get('DATABASE_PORT'), 10), 12 | username: environmentConfigService.get('DATABASE_USERNAME'), 13 | password: environmentConfigService.get('DATABASE_PASSWORD'), 14 | database: environmentConfigService.get('DATABASE_NAME'), 15 | entities: [DummyEntity], 16 | ssl: true, 17 | } as TypeOrmModuleOptions); 18 | 19 | export const getTypeOrmMigrationsOptions = (environmentConfigService: EnvironmentConfigService) => ({ 20 | ...getTypeOrmModuleOptions(environmentConfigService), 21 | entities: ['dist/**/entities/*.entity{.ts,.js}'], 22 | migrationsTableName: 'typeorm_migrations', 23 | migrations: ['**/migrations/*migration*.ts'], 24 | name: 'schema', 25 | }); 26 | 27 | @Module({ 28 | imports: [ 29 | TypeOrmModule.forRootAsync({ 30 | imports: [EnvironmentConfigModule], 31 | inject: [EnvironmentConfigService], 32 | useFactory: getTypeOrmModuleOptions, 33 | }), 34 | ], 35 | }) 36 | export class TypeOrmConfigModule {} 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nestjs-clean-architecture-demo 2 | 3 | [![Build Status](https://travis-ci.org/damienbeaufils/nestjs-clean-architecture-demo.svg?branch=master)](https://travis-ci.org/damienbeaufils/nestjs-clean-architecture-demo) 4 | 5 | An example of clean architecture with NestJS 6 | 7 | ## Foreword 8 | 9 | This application is designed using a [Clean Architecture pattern](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) (also known as [Hexagonal Architecture](http://www.maximecolin.fr/uploads/2015/11/56570243d02c0_hexagonal-architecture.png)). 10 | Therefore [SOLID principles](https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)) are used in code, especially the [Dependency Inversion Principle](https://en.wikipedia.org/wiki/Dependency_inversion_principle) (do not mix up with the classic dependency injection in NestJS for example). 11 | 12 | Concretely, there are 3 main packages: `domain`, `use_cases` and `infrastructure`. These packages have to respect these rules: 13 | - `domain` contains the business code and its logic, and has no outward dependency: nor on frameworks (NestJS for example), nor on `use_cases` or `infrastructure` packages. 14 | - `use_cases` is like a conductor. It will depend only on `domain` package to execute business logic. `use_cases` should not have any dependencies on `infrastructure`. 15 | - `infrastructure` contains all the technical details, configuration, implementations (database, web services, etc.), and must not contain any business logic. `infrastructure` has dependencies on `domain`, `use_cases` and frameworks. 16 | 17 | ## Install 18 | 19 | ``` 20 | npm install 21 | ``` 22 | 23 | ## Test 24 | 25 | ``` 26 | npm test 27 | ``` 28 | 29 | ## Run 30 | 31 | ``` 32 | npm run typeorm:migration:run 33 | npm run start:dev 34 | ``` 35 | -------------------------------------------------------------------------------- /src/infrastructure/config/environment-config/environment-config.service.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from '@hapi/joi'; 2 | import { ValidationResult } from '@hapi/joi'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { EnvironmentConfigError } from './environment-config.error'; 5 | 6 | export interface EnvironmentConfig { 7 | [key: string]: string; 8 | } 9 | 10 | @Injectable() 11 | export class EnvironmentConfigService { 12 | private readonly environmentConfig: EnvironmentConfig; 13 | 14 | constructor() { 15 | this.environmentConfig = EnvironmentConfigService.validateInput({ ...process.env }); 16 | } 17 | 18 | private static validateInput(environmentConfig: EnvironmentConfig): EnvironmentConfig { 19 | const envVarsSchema: Joi.ObjectSchema = Joi.object({ 20 | PORT: Joi.number().default(3001), 21 | DATABASE_TYPE: Joi.string().default('sqlite'), 22 | DATABASE_NAME: Joi.string().default('local-db.sqlite'), 23 | DATABASE_HOST: Joi.string().when('DATABASE_TYPE', { is: 'sqlite', then: Joi.optional(), otherwise: Joi.required() }), 24 | DATABASE_PORT: Joi.number().when('DATABASE_TYPE', { is: 'sqlite', then: Joi.optional(), otherwise: Joi.required() }), 25 | DATABASE_USERNAME: Joi.string().when('DATABASE_TYPE', { is: 'sqlite', then: Joi.optional(), otherwise: Joi.required() }), 26 | DATABASE_PASSWORD: Joi.string().when('DATABASE_TYPE', { is: 'sqlite', then: Joi.optional(), otherwise: Joi.required() }), 27 | }).unknown(true); 28 | 29 | const { error, value: validatedEnvironmentConfig }: ValidationResult = envVarsSchema.validate(environmentConfig); 30 | if (error) { 31 | throw new EnvironmentConfigError(`Config validation error: ${error.message}`); 32 | } 33 | 34 | return validatedEnvironmentConfig; 35 | } 36 | 37 | get(key: string): string { 38 | return this.environmentConfig[key]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/use_cases/__spec__/create-dummy-data.spec.ts: -------------------------------------------------------------------------------- 1 | import { Dummy } from '../../domain/dummy'; 2 | import { DummyRepository } from '../../domain/dummy.repository'; 3 | import { CreateDummyData } from '../create-dummy-data'; 4 | 5 | jest.mock('../../domain/dummy'); 6 | 7 | describe('uses_cases/CreateDummyData', () => { 8 | let createDummyData: CreateDummyData; 9 | let mockDummyRepository: DummyRepository; 10 | 11 | beforeEach(() => { 12 | (Dummy as jest.Mock).mockClear(); 13 | 14 | mockDummyRepository = {} as DummyRepository; 15 | mockDummyRepository.save = jest.fn(); 16 | 17 | createDummyData = new CreateDummyData(mockDummyRepository); 18 | }); 19 | 20 | describe('execute()', () => { 21 | it('should create new dummy using given data', async () => { 22 | // given 23 | const value: string = 'some dummy value'; 24 | 25 | // when 26 | await createDummyData.execute(value); 27 | 28 | // then 29 | expect(Dummy).toHaveBeenCalledWith(value); 30 | }); 31 | 32 | it('should save created dummy using repository', async () => { 33 | // given 34 | const createdDummy: Dummy = new Dummy('some dummy value'); 35 | (Dummy as jest.Mock).mockImplementation(() => createdDummy); 36 | 37 | // when 38 | await createDummyData.execute('some dummy value'); 39 | 40 | // then 41 | expect(mockDummyRepository.save).toHaveBeenCalledWith(createdDummy); 42 | }); 43 | 44 | it('should return saved dummy from repository', async () => { 45 | // given 46 | const savedDummy: Dummy = { ...new Dummy('some dummy value'), id: 42 }; 47 | (mockDummyRepository.save as jest.Mock).mockReturnValue(Promise.resolve(savedDummy)); 48 | 49 | // when 50 | const result: Dummy = await createDummyData.execute('some dummy value'); 51 | 52 | // then 53 | expect(result).toBe(savedDummy); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/infrastructure/rest/filters/__spec__/invalid-dummy-error.filter.spec.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost } from '@nestjs/common'; 2 | import { InvalidDummyError } from '../../../../domain/invalid-dummy.error'; 3 | import { InvalidDummyErrorFilter } from '../invalid-dummy-error.filter'; 4 | 5 | describe('infrastructure/rest/filters/InvalidDummyErrorFilter', () => { 6 | let invalidDummyErrorFilter: InvalidDummyErrorFilter; 7 | let mockArgumentsHost: ArgumentsHost; 8 | let mockStatus: jest.Mock; 9 | let mockJson: jest.Mock; 10 | 11 | beforeEach(() => { 12 | mockStatus = jest.fn(); 13 | mockJson = jest.fn(); 14 | 15 | mockStatus.mockImplementation(() => { 16 | return { 17 | json: mockJson, 18 | }; 19 | }); 20 | 21 | mockArgumentsHost = { 22 | switchToHttp: () => ({ 23 | getResponse: () => ({ 24 | status: mockStatus, 25 | }), 26 | }), 27 | } as ArgumentsHost; 28 | 29 | invalidDummyErrorFilter = new InvalidDummyErrorFilter(); 30 | }); 31 | 32 | describe('catch()', () => { 33 | it('should call response status method with http bad request status code', () => { 34 | // given 35 | const invalidDummyError: InvalidDummyError = {} as InvalidDummyError; 36 | const expected: number = 400; 37 | 38 | // when 39 | invalidDummyErrorFilter.catch(invalidDummyError, mockArgumentsHost); 40 | 41 | // then 42 | expect(mockStatus).toHaveBeenCalledWith(expected); 43 | }); 44 | 45 | it('should call response status json method with body from invalid dummy error', () => { 46 | // given 47 | const fixedDate: Date = new Date('2017-06-13T04:41:20'); 48 | // @ts-ignore 49 | jest.spyOn(global, 'Date').mockImplementationOnce(() => fixedDate); 50 | 51 | const invalidDummyError: InvalidDummyError = { 52 | name: 'InvalidDummyError', 53 | message: 'A dummy validation error', 54 | } as InvalidDummyError; 55 | 56 | const expected: object = { 57 | statusCode: 400, 58 | timestamp: fixedDate.toISOString(), 59 | name: 'InvalidDummyError', 60 | message: 'A dummy validation error', 61 | }; 62 | 63 | // when 64 | invalidDummyErrorFilter.catch(invalidDummyError, mockArgumentsHost); 65 | 66 | // then 67 | expect(mockJson).toHaveBeenCalledWith(expected); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /tslint.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "jsRules": { 4 | "no-unused-expression": true, 5 | "object-literal-sort-keys": false, 6 | "ordered-imports": false, 7 | "no-console": false, 8 | "no-shadowed-variable": false, 9 | "no-empty": [ 10 | true, 11 | "allow-empty-functions" 12 | ], 13 | "curly": [ 14 | true, 15 | "ignore-same-line" 16 | ] 17 | }, 18 | "rules": { 19 | "prettier": true, 20 | "typedef": [ 21 | true, 22 | "call-signature", 23 | "parameter", 24 | "arrow-parameter", 25 | "property-declaration", 26 | "variable-declaration", 27 | "variable-declaration-ignore-function", 28 | "member-variable-declaration", 29 | "object-destructuring", 30 | "array-destructuring" 31 | ], 32 | "member-access": [ 33 | true, 34 | "no-public" 35 | ], 36 | "quotemark": [ 37 | true, 38 | "single", 39 | "avoid-escape" 40 | ], 41 | "ordered-imports": [ 42 | false 43 | ], 44 | "variable-name": [ 45 | true, 46 | "allow-leading-underscore" 47 | ], 48 | "member-ordering": [ 49 | true, 50 | { 51 | "order": [ 52 | "public-static-field", 53 | "protected-static-field", 54 | "private-static-field", 55 | "public-instance-field", 56 | "protected-instance-field", 57 | "private-instance-field", 58 | "public-constructor", 59 | "protected-constructor", 60 | "private-constructor", 61 | "public-static-method", 62 | "protected-static-method", 63 | "private-static-method", 64 | "public-instance-method", 65 | "protected-instance-method", 66 | "private-instance-method" 67 | ] 68 | } 69 | ], 70 | "interface-name": [ 71 | false 72 | ], 73 | "object-literal-sort-keys": false, 74 | "no-console": true, 75 | "no-empty": [ 76 | true, 77 | "allow-empty-functions" 78 | ], 79 | "arrow-parens": false, 80 | "curly": [ 81 | true, 82 | "ignore-same-line" 83 | ], 84 | "no-any": true, 85 | "no-bitwise": true, 86 | "trailing-comma": [ 87 | true, 88 | { 89 | "multiline": { 90 | "objects": "always", 91 | "arrays": "always", 92 | "functions": "never", 93 | "typeLiterals": "ignore" 94 | }, 95 | "esSpecCompliant": true 96 | } 97 | ], 98 | "newline-before-return": true 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-clean-architecture-demo", 3 | "version": "0.0.1", 4 | "description": "NestJS Clean Architecture demo", 5 | "private": true, 6 | "scripts": { 7 | "build": "rimraf dist && tsc -p tsconfig.build.json", 8 | "format": "prettier --write \"src/**/*.ts\"", 9 | "start": "npm run start:prod", 10 | "start:dev": "tsc-watch -p tsconfig.build.json --onSuccess \"node dist/main.js\"", 11 | "start:debug": "tsc-watch -p tsconfig.build.json --onSuccess \"node --inspect-brk dist/main.js\"", 12 | "start:prod": "node dist/main.js", 13 | "lint": "tslint -p tsconfig.json -c tslint.json", 14 | "lintfix": "npm run lint -- --fix", 15 | "pretest": "npm run test:cleanArchitecture && npm run lint", 16 | "pretest:e2e": "rimraf e2e.sqlite", 17 | "test": "npm run test:cov && npm run test:e2e", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config e2e/jest-e2e.json --runInBand", 22 | "test:cleanArchitecture": "./tools/check-clean-architecture.sh", 23 | "test:mutation": "stryker run", 24 | "pretypeorm": "ts-node -r tsconfig-paths/register database/write-ormconfig-for-migrations.ts", 25 | "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js", 26 | "typeorm:migration:run": "npm run typeorm -- migration:run -c schema" 27 | }, 28 | "dependencies": { 29 | "@hapi/joi": "17.1.0", 30 | "@nestjs/common": "6.11.6", 31 | "@nestjs/core": "6.11.6", 32 | "@nestjs/platform-express": "6.11.6", 33 | "@nestjs/typeorm": "6.2.0", 34 | "lodash": "4.17.21", 35 | "pg": "7.18.1", 36 | "rimraf": "3.0.2", 37 | "rxjs": "6.5.4", 38 | "sqlite3": "4.1.1", 39 | "typeorm": "0.2.25" 40 | }, 41 | "devDependencies": { 42 | "@nestjs/cli": "6.14.2", 43 | "@nestjs/schematics": "6.9.3", 44 | "@nestjs/testing": "6.11.6", 45 | "@stryker-mutator/core": "2.5.0", 46 | "@stryker-mutator/html-reporter": "2.5.0", 47 | "@stryker-mutator/jest-runner": "2.5.0", 48 | "@stryker-mutator/typescript": "2.5.0", 49 | "@types/express": "4.17.2", 50 | "@types/hapi__joi": "16.0.9", 51 | "@types/jest": "25.1.2", 52 | "@types/lodash": "4.14.149", 53 | "@types/node": "12.12.26", 54 | "@types/supertest": "2.0.8", 55 | "jest": "25.1.0", 56 | "prettier": "1.19.1", 57 | "supertest": "4.0.2", 58 | "ts-jest": "25.2.0", 59 | "ts-loader": "6.2.1", 60 | "ts-node": "8.6.2", 61 | "tsc-watch": "4.1.0", 62 | "tsconfig-paths": "3.9.0", 63 | "tslint": "5.20.1", 64 | "tslint-config-prettier": "1.18.0", 65 | "tslint-plugin-prettier": "2.1.0", 66 | "typescript": "3.7.5" 67 | }, 68 | "engines": { 69 | "node": "~12.15" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /e2e/repositories/database-dummy.repository.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { Repository } from 'typeorm'; 4 | import { Dummy } from '../../src/domain/dummy'; 5 | import { EnvironmentConfigService } from '../../src/infrastructure/config/environment-config/environment-config.service'; 6 | import { DatabaseDummyRepository } from '../../src/infrastructure/repositories/database-dummy.repository'; 7 | import { DummyEntity } from '../../src/infrastructure/repositories/entities/dummy.entity'; 8 | import { RepositoriesModule } from '../../src/infrastructure/repositories/repositories.module'; 9 | import { e2eEnvironmentConfigService } from '../e2e-config'; 10 | import { runDatabaseMigrations } from './database-e2e.utils'; 11 | 12 | describe('infrastructure/repositories/DatabaseDummyRepository', () => { 13 | let app: INestApplication; 14 | let databaseDummyRepository: DatabaseDummyRepository; 15 | let dummyEntityRepository: Repository; 16 | 17 | beforeAll(async () => { 18 | const moduleFixture: TestingModule = await Test.createTestingModule({ 19 | imports: [RepositoriesModule], 20 | }) 21 | .overrideProvider(EnvironmentConfigService) 22 | .useValue(e2eEnvironmentConfigService) 23 | .compile(); 24 | 25 | app = moduleFixture.createNestApplication(); 26 | await app.init(); 27 | 28 | await runDatabaseMigrations(app.get(EnvironmentConfigService)); 29 | 30 | databaseDummyRepository = app.get(DatabaseDummyRepository); 31 | // @ts-ignore 32 | dummyEntityRepository = databaseDummyRepository.dummyEntityRepository; 33 | }); 34 | 35 | beforeEach(async () => { 36 | await dummyEntityRepository.clear(); 37 | }); 38 | 39 | describe('save()', () => { 40 | it('should persist dummy in database', async () => { 41 | // given 42 | const dummyWithoutId: Dummy = { value: 'a-value' } as Dummy; 43 | 44 | // when 45 | await databaseDummyRepository.save(dummyWithoutId); 46 | 47 | // then 48 | const count: number = await dummyEntityRepository.count(); 49 | expect(count).toBe(1); 50 | }); 51 | 52 | it('should return saved dummy with an id', async () => { 53 | // given 54 | const dummyWithoutId: Dummy = { value: 'a-value' } as Dummy; 55 | 56 | // when 57 | const result: Dummy = await databaseDummyRepository.save(dummyWithoutId); 58 | 59 | // then 60 | expect(result.id).toBeDefined(); 61 | }); 62 | }); 63 | 64 | describe('findAll()', () => { 65 | it('should return all dummies when in database', async () => { 66 | // given 67 | await databaseDummyRepository.save({ value: 'a-value' } as Dummy); 68 | await databaseDummyRepository.save({ value: 'another-value' } as Dummy); 69 | 70 | // when 71 | const result: Dummy[] = await databaseDummyRepository.findAll(); 72 | 73 | // then 74 | expect(result).toHaveLength(2); 75 | }); 76 | 77 | it('should return empty array when no dummy found', async () => { 78 | // when 79 | const result: Dummy[] = await databaseDummyRepository.findAll(); 80 | 81 | // then 82 | expect(result).toHaveLength(0); 83 | }); 84 | }); 85 | 86 | afterAll(async () => { 87 | await app.close(); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /e2e/rest/dummy.controller.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import request, { Response } from 'supertest'; 4 | import { Dummy } from '../../src/domain/dummy'; 5 | import { InvalidDummyError } from '../../src/domain/invalid-dummy.error'; 6 | import { EnvironmentConfigService } from '../../src/infrastructure/config/environment-config/environment-config.service'; 7 | import { RestModule } from '../../src/infrastructure/rest/rest.module'; 8 | import { ProxyServicesDynamicModule } from '../../src/infrastructure/use_cases_proxy/proxy-services-dynamic.module'; 9 | import { UseCaseProxy } from '../../src/infrastructure/use_cases_proxy/use-case-proxy'; 10 | import { CreateDummyData } from '../../src/use_cases/create-dummy-data'; 11 | import { GetAllDummyData } from '../../src/use_cases/get-all-dummy-data'; 12 | import { e2eEnvironmentConfigService } from '../e2e-config'; 13 | 14 | describe('infrastructure/rest/DummyController (e2e)', () => { 15 | let app: INestApplication; 16 | let mockGetAllDummyData: GetAllDummyData; 17 | let mockCreateDummyData: CreateDummyData; 18 | 19 | beforeAll(async () => { 20 | mockGetAllDummyData = {} as GetAllDummyData; 21 | mockGetAllDummyData.execute = jest.fn(); 22 | const mockGetAllDummyDataProxyService: UseCaseProxy = { 23 | getInstance: () => mockGetAllDummyData, 24 | } as UseCaseProxy; 25 | 26 | mockCreateDummyData = {} as CreateDummyData; 27 | mockCreateDummyData.execute = jest.fn(); 28 | const mockCreateDummyDataProxyService: UseCaseProxy = { 29 | getInstance: () => mockCreateDummyData, 30 | } as UseCaseProxy; 31 | 32 | const moduleFixture: TestingModule = await Test.createTestingModule({ 33 | imports: [RestModule], 34 | }) 35 | .overrideProvider(ProxyServicesDynamicModule.GET_ALL_DUMMY_DATA_PROXY_SERVICE) 36 | .useValue(mockGetAllDummyDataProxyService) 37 | .overrideProvider(ProxyServicesDynamicModule.CREATE_DUMMY_DATA_PROXY_SERVICE) 38 | .useValue(mockCreateDummyDataProxyService) 39 | .overrideProvider(EnvironmentConfigService) 40 | .useValue(e2eEnvironmentConfigService) 41 | .compile(); 42 | 43 | app = moduleFixture.createNestApplication(); 44 | await app.init(); 45 | }); 46 | 47 | describe('GET /api/dummy', () => { 48 | it('should return http status code OK with found dummies', () => { 49 | // given 50 | const foundDummies: Dummy[] = [{ id: 1, value: 'value1' } as Dummy, { id: 2, value: 'value2' } as Dummy]; 51 | (mockGetAllDummyData.execute as jest.Mock).mockReturnValue(Promise.resolve(foundDummies)); 52 | 53 | // when 54 | const testRequest: request.Test = request(app.getHttpServer()).get('/api/dummy'); 55 | 56 | // then 57 | return testRequest.expect(200).expect(foundDummies); 58 | }); 59 | }); 60 | 61 | describe('POST /api/dummy', () => { 62 | it('should create body using value from body', () => { 63 | // given 64 | const value: string = 'a-value'; 65 | 66 | // when 67 | const testRequest: request.Test = request(app.getHttpServer()) 68 | .post('/api/dummy') 69 | .send({ value }); 70 | 71 | // then 72 | return testRequest.expect(201).expect((response: Response) => { 73 | expect(mockCreateDummyData.execute).toHaveBeenCalledWith(value); 74 | }); 75 | }); 76 | 77 | it('should return http status code CREATED with created dummy', () => { 78 | // given 79 | const createdDummy: Dummy = { id: 1, value: 'a-value' } as Dummy; 80 | (mockCreateDummyData.execute as jest.Mock).mockReturnValue(Promise.resolve(createdDummy)); 81 | 82 | // when 83 | const testRequest: request.Test = request(app.getHttpServer()) 84 | .post('/api/dummy') 85 | .send({ value: 'a-value' }); 86 | 87 | // then 88 | return testRequest.expect(201).expect(createdDummy); 89 | }); 90 | 91 | it('should return http status code BAD REQUEST when invalid dummy', () => { 92 | // given 93 | (mockCreateDummyData.execute as jest.Mock).mockImplementation(() => { 94 | throw new InvalidDummyError('value cannot be null or empty'); 95 | }); 96 | const fixedDate: Date = new Date('2017-06-13T04:41:20'); 97 | // @ts-ignore 98 | jest.spyOn(global, 'Date').mockImplementationOnce(() => fixedDate); 99 | 100 | // when 101 | const testRequest: request.Test = request(app.getHttpServer()) 102 | .post('/api/dummy') 103 | .send({ value: '' }); 104 | 105 | // then 106 | return testRequest.expect(400).expect({ 107 | statusCode: 400, 108 | timestamp: '2017-06-13T08:41:20.000Z', 109 | name: 'Error', 110 | message: 'value cannot be null or empty', 111 | }); 112 | }); 113 | }); 114 | 115 | afterAll(async () => { 116 | await app.close(); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /src/infrastructure/config/environment-config/__spec__/environment-config.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { EnvironmentConfigError } from '../environment-config.error'; 2 | import { EnvironmentConfigService } from '../environment-config.service'; 3 | import ProcessEnv = NodeJS.ProcessEnv; 4 | 5 | describe('infrastructure/config/environment-config/EnvironmentConfigService', () => { 6 | beforeEach(() => { 7 | const env: ProcessEnv = { 8 | PORT: '1111', 9 | DATABASE_TYPE: 'database-type', 10 | DATABASE_HOST: 'database-host', 11 | DATABASE_PORT: '1234', 12 | DATABASE_USERNAME: 'database-username', 13 | DATABASE_PASSWORD: 'database-password', 14 | DATABASE_NAME: 'database-name', 15 | } as ProcessEnv; 16 | 17 | process.env = { 18 | ...process.env, 19 | ...env, 20 | }; 21 | }); 22 | 23 | describe('constructor()', () => { 24 | it('should allow unknown variables', () => { 25 | // given 26 | process.env.UNKNOWN = 'any-value'; 27 | 28 | // when 29 | const result = () => new EnvironmentConfigService(); 30 | 31 | // then 32 | expect(result).not.toThrow(); 33 | }); 34 | 35 | describe('PORT', () => { 36 | it('should fail when not a number', () => { 37 | // given 38 | process.env.PORT = 'not-a-number'; 39 | 40 | // when 41 | const result = () => new EnvironmentConfigService(); 42 | 43 | // then 44 | expect(result).toThrow(new EnvironmentConfigError('Config validation error: "PORT" must be a number')); 45 | }); 46 | it('should be defaulted to 3001', () => { 47 | // given 48 | process.env.PORT = undefined; 49 | 50 | // when 51 | const result: EnvironmentConfigService = new EnvironmentConfigService(); 52 | 53 | // then 54 | expect(result.get('PORT')).toBe(3001); 55 | }); 56 | }); 57 | 58 | describe('DATABASE_TYPE', () => { 59 | it('should be defaulted to sqlite', () => { 60 | // given 61 | process.env.DATABASE_TYPE = undefined; 62 | 63 | // when 64 | const result: EnvironmentConfigService = new EnvironmentConfigService(); 65 | 66 | // then 67 | expect(result.get('DATABASE_TYPE')).toBe('sqlite'); 68 | }); 69 | }); 70 | 71 | describe('DATABASE_NAME', () => { 72 | it('should be defaulted to local-db.sqlite', () => { 73 | // given 74 | process.env.DATABASE_NAME = undefined; 75 | 76 | // when 77 | const result: EnvironmentConfigService = new EnvironmentConfigService(); 78 | 79 | // then 80 | expect(result.get('DATABASE_NAME')).toBe('local-db.sqlite'); 81 | }); 82 | }); 83 | 84 | describe('when DATABASE_TYPE is postgres', () => { 85 | beforeEach(() => { 86 | process.env.DATABASE_TYPE = 'postgres'; 87 | }); 88 | 89 | describe('DATABASE_HOST', () => { 90 | it('should fail when empty', () => { 91 | // given 92 | process.env.DATABASE_HOST = ''; 93 | 94 | // when 95 | const result = () => new EnvironmentConfigService(); 96 | 97 | // then 98 | expect(result).toThrow(new EnvironmentConfigError('Config validation error: "DATABASE_HOST" is not allowed to be empty')); 99 | }); 100 | }); 101 | 102 | describe('DATABASE_PORT', () => { 103 | it('should fail when not a number', () => { 104 | // given 105 | process.env.DATABASE_PORT = 'not-a-number'; 106 | 107 | // when 108 | const result = () => new EnvironmentConfigService(); 109 | 110 | // then 111 | expect(result).toThrow(new EnvironmentConfigError('Config validation error: "DATABASE_PORT" must be a number')); 112 | }); 113 | }); 114 | 115 | describe('DATABASE_USERNAME', () => { 116 | it('should fail when empty', () => { 117 | // given 118 | process.env.DATABASE_USERNAME = ''; 119 | 120 | // when 121 | const result = () => new EnvironmentConfigService(); 122 | 123 | // then 124 | expect(result).toThrow(new EnvironmentConfigError('Config validation error: "DATABASE_USERNAME" is not allowed to be empty')); 125 | }); 126 | }); 127 | 128 | describe('DATABASE_PASSWORD', () => { 129 | it('should fail when empty', () => { 130 | // given 131 | process.env.DATABASE_PASSWORD = ''; 132 | 133 | // when 134 | const result = () => new EnvironmentConfigService(); 135 | 136 | // then 137 | expect(result).toThrow(new EnvironmentConfigError('Config validation error: "DATABASE_PASSWORD" is not allowed to be empty')); 138 | }); 139 | }); 140 | }); 141 | 142 | describe('when DATABASE_TYPE is sqlite', () => { 143 | beforeEach(() => { 144 | process.env.DATABASE_TYPE = 'sqlite'; 145 | }); 146 | 147 | describe('DATABASE_HOST', () => { 148 | it('should be optional', () => { 149 | // given 150 | process.env.DATABASE_HOST = undefined; 151 | 152 | // when 153 | const result = () => new EnvironmentConfigService(); 154 | 155 | // then 156 | expect(result).not.toThrow(); 157 | }); 158 | }); 159 | 160 | describe('DATABASE_PORT', () => { 161 | it('should be optional', () => { 162 | // given 163 | process.env.DATABASE_PORT = undefined; 164 | 165 | // when 166 | const result = () => new EnvironmentConfigService(); 167 | 168 | // then 169 | expect(result).not.toThrow(); 170 | }); 171 | }); 172 | 173 | describe('DATABASE_USERNAME', () => { 174 | it('should be optional', () => { 175 | // given 176 | process.env.DATABASE_USERNAME = undefined; 177 | 178 | // when 179 | const result = () => new EnvironmentConfigService(); 180 | 181 | // then 182 | expect(result).not.toThrow(); 183 | }); 184 | }); 185 | 186 | describe('DATABASE_PASSWORD', () => { 187 | it('should be optional', () => { 188 | // given 189 | process.env.DATABASE_PASSWORD = undefined; 190 | 191 | // when 192 | const result = () => new EnvironmentConfigService(); 193 | 194 | // then 195 | expect(result).not.toThrow(); 196 | }); 197 | }); 198 | }); 199 | }); 200 | 201 | describe('get()', () => { 202 | it('should return variable from process.env', () => { 203 | // given 204 | const expected: string = 'any value'; 205 | process.env.TEST_ENV_VAR = expected; 206 | const environmentConfigService: EnvironmentConfigService = new EnvironmentConfigService(); 207 | 208 | // when 209 | const result: string = environmentConfigService.get('TEST_ENV_VAR'); 210 | 211 | // then 212 | expect(result).toBe(expected); 213 | }); 214 | }); 215 | }); 216 | --------------------------------------------------------------------------------