├── src ├── FrameworkAndDrivers │ ├── UI │ │ └── .gitkeep │ ├── Devices │ │ └── .gitkeep │ ├── ExternalInterfaces │ │ └── .gitkeep │ ├── Web │ │ └── express │ │ │ ├── config │ │ │ ├── .test.env │ │ │ ├── .development.env │ │ │ ├── app.conf.json │ │ │ ├── default.conf.json │ │ │ ├── production.conf.json │ │ │ ├── test.conf.json │ │ │ ├── staging.conf.json │ │ │ ├── development.conf.json │ │ │ ├── config.ts │ │ │ └── app-schema.conf.ts │ │ │ ├── app │ │ │ ├── _shared │ │ │ │ ├── action-view.interface.ts │ │ │ │ ├── router.interface.ts │ │ │ │ └── app-environment.enum.ts │ │ │ ├── server │ │ │ │ ├── server.interface.ts │ │ │ │ ├── server.router.class.ts │ │ │ │ ├── server.class.ts │ │ │ │ └── server.spec.ts │ │ │ ├── views │ │ │ │ └── base.njk │ │ │ └── UsesCases │ │ │ │ ├── User │ │ │ │ └── createUser │ │ │ │ │ ├── views │ │ │ │ │ ├── createUserView.ts │ │ │ │ │ └── createUserView.njk │ │ │ │ │ └── delivers │ │ │ │ │ └── createUserDeliver.ts │ │ │ │ └── usescases.router.class.ts │ │ │ ├── app.start.spec.inc.ts │ │ │ ├── app.spec.ts │ │ │ ├── app.ts │ │ │ ├── app.environments-server-options.spec.inc.ts │ │ │ └── app.server-start.spec.inc.ts │ └── DB │ │ └── MongoDB │ │ ├── User │ │ ├── user.model.ts │ │ ├── user.model.spec.ts │ │ ├── UserEntityRepository.ts │ │ └── UserEntityRepository.spec.ts │ │ └── db-connect.ts ├── ComponentInterfaces │ ├── Entity │ │ └── IEntity.ts │ ├── UseCase │ │ ├── IRequestModel.ts │ │ ├── IResponseModel.ts │ │ ├── IEntityGateway.ts │ │ ├── IPresenterOutputBoundary.ts │ │ └── IInteractor.ts │ └── InterfaceAdapters │ │ ├── IViewModel.ts │ │ ├── IControllerRequestModel.ts │ │ ├── IView.ts │ │ ├── IPresenterOuputBoundary.ts │ │ └── IControllerInputBoundary.ts ├── Components │ └── User │ │ ├── UseCases │ │ └── createUser │ │ │ ├── ICreateUserRequestModel.ts │ │ │ ├── ICreateUserResponseModel.ts │ │ │ ├── ICreateUserEntityGateway.ts │ │ │ ├── ICreateUserPresenterOutputBoundary.ts │ │ │ ├── ICreateUserControllerInputBoundary.ts │ │ │ ├── tests │ │ │ ├── CreateUserInteractor.spec.inc.ts │ │ │ └── CreateUserInteractor.spec.ts │ │ │ └── CreateUserInteractor.ts │ │ ├── InterfaceAdapters │ │ └── createUser │ │ │ ├── ICreateUserView.ts │ │ │ ├── ICreateUserViewModel.ts │ │ │ ├── ICreateUserControllerRequest.ts │ │ │ ├── tests │ │ │ ├── CreateUserPresenter.spec.inc.ts │ │ │ ├── CreateUserController.spec.inc.ts │ │ │ ├── CreateUserPresenter.spec.ts │ │ │ └── CreateUserController.spec.ts │ │ │ ├── CreateUserViewModelMapper.ts │ │ │ ├── CreateUserPresenter.ts │ │ │ └── CreateUserController.ts │ │ └── Entities │ │ ├── UserEntity.ts │ │ └── tests │ │ └── UserEntity.spec.ts └── index.ts ├── img ├── tests_results.png ├── folder_components_user.png ├── uncle_bob_implementation.png ├── uncle_bob_layers_schema.png ├── folder_componentinterfaces.png ├── folder_frameworkanddrivers_db.png ├── uncle_bob_interfaces_schema.png └── folder_frameworkanddrivers_web.png ├── mongodb-docker.sh ├── .prettierrc ├── scripts ├── copy_config.sh └── copy_views.sh ├── .vscode └── launch.json ├── tsconfig.json ├── tsconfig.prod.json ├── .gitignore ├── tslint.json ├── package.json └── README.md /src/FrameworkAndDrivers/UI/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Devices/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/ExternalInterfaces/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/config/.test.env: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/config/.development.env: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/config/app.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /src/ComponentInterfaces/Entity/IEntity.ts: -------------------------------------------------------------------------------- 1 | export default interface IEntity { 2 | // 3 | } 4 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/config/default.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": "development" 3 | } -------------------------------------------------------------------------------- /img/tests_results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stayfi/clean-architecture-ts/HEAD/img/tests_results.png -------------------------------------------------------------------------------- /src/ComponentInterfaces/UseCase/IRequestModel.ts: -------------------------------------------------------------------------------- 1 | export default interface IRequestModel { 2 | // 3 | } 4 | -------------------------------------------------------------------------------- /src/ComponentInterfaces/InterfaceAdapters/IViewModel.ts: -------------------------------------------------------------------------------- 1 | export default interface IViewModel { 2 | // 3 | } 4 | -------------------------------------------------------------------------------- /src/ComponentInterfaces/UseCase/IResponseModel.ts: -------------------------------------------------------------------------------- 1 | export default interface IResponseModel { 2 | // 3 | } 4 | -------------------------------------------------------------------------------- /mongodb-docker.sh: -------------------------------------------------------------------------------- 1 | docker volume create --name=mongodata 2 | docker run -d -p 27017:27017 -v mongodata:/data/db mongo 3 | -------------------------------------------------------------------------------- /img/folder_components_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stayfi/clean-architecture-ts/HEAD/img/folder_components_user.png -------------------------------------------------------------------------------- /img/uncle_bob_implementation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stayfi/clean-architecture-ts/HEAD/img/uncle_bob_implementation.png -------------------------------------------------------------------------------- /img/uncle_bob_layers_schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stayfi/clean-architecture-ts/HEAD/img/uncle_bob_layers_schema.png -------------------------------------------------------------------------------- /img/folder_componentinterfaces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stayfi/clean-architecture-ts/HEAD/img/folder_componentinterfaces.png -------------------------------------------------------------------------------- /img/folder_frameworkanddrivers_db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stayfi/clean-architecture-ts/HEAD/img/folder_frameworkanddrivers_db.png -------------------------------------------------------------------------------- /img/uncle_bob_interfaces_schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stayfi/clean-architecture-ts/HEAD/img/uncle_bob_interfaces_schema.png -------------------------------------------------------------------------------- /src/ComponentInterfaces/InterfaceAdapters/IControllerRequestModel.ts: -------------------------------------------------------------------------------- 1 | export default interface IControllerRequestModel { 2 | // 3 | } 4 | -------------------------------------------------------------------------------- /img/folder_frameworkanddrivers_web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stayfi/clean-architecture-ts/HEAD/img/folder_frameworkanddrivers_web.png -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/config/production.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 8080, 3 | "mongodbURI": "mongodb://192.168.99.100/login" 4 | } -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/config/test.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3100, 3 | "mongodbURI": "mongodb://192.168.99.100/login_test" 4 | } -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/config/staging.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 8080, 3 | "mongodbURI": "mongodb://192.168.99.100/login_stage" 4 | } -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/app/_shared/action-view.interface.ts: -------------------------------------------------------------------------------- 1 | export default interface ActionViewInterface { 2 | view: string; 3 | params: {}; 4 | } 5 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/config/development.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "logger": true, 3 | "port": 3000, 4 | "mongodbURI": "mongodb://192.168.99.100/login_dev" 5 | } -------------------------------------------------------------------------------- /src/ComponentInterfaces/InterfaceAdapters/IView.ts: -------------------------------------------------------------------------------- 1 | import IViewModel from './IViewModel'; 2 | 3 | export default interface IView { 4 | render(viewModel: IViewModel): void; 5 | } 6 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/app/_shared/router.interface.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | export default interface RouterInterface { 4 | getRouter(): express.Router; 5 | } 6 | -------------------------------------------------------------------------------- /src/ComponentInterfaces/UseCase/IEntityGateway.ts: -------------------------------------------------------------------------------- 1 | // import IEntity from '../Entity/IEntity'; 2 | 3 | export default interface IEntityGateway { 4 | // constructor(entity: IEntity): void; 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "printWidth": 80, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "trailingComma": "none", 7 | "jsxBracketSameLine": false, 8 | "parser": "typescript" 9 | } -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/app/_shared/app-environment.enum.ts: -------------------------------------------------------------------------------- 1 | export enum appEnvironment { 2 | DEV = 'development', 3 | TEST = 'test', 4 | STAGING = 'staging', 5 | PROD = 'production' 6 | } 7 | -------------------------------------------------------------------------------- /src/Components/User/UseCases/createUser/ICreateUserRequestModel.ts: -------------------------------------------------------------------------------- 1 | import IRequestModel from '../../../../ComponentInterfaces/UseCase/IRequestModel'; 2 | 3 | export default interface ICreateUserRequestModel extends IRequestModel { 4 | email: string; 5 | password: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/Components/User/UseCases/createUser/ICreateUserResponseModel.ts: -------------------------------------------------------------------------------- 1 | import IResponseModel from '../../../../ComponentInterfaces/UseCase/IResponseModel'; 2 | 3 | export default interface ICreateUserResponseModel extends IResponseModel { 4 | id?: string; 5 | email: string; 6 | } 7 | -------------------------------------------------------------------------------- /scripts/copy_config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mkdir -p ./dist/FrameworkAndDrivers/Web/express/config 3 | cd src/FrameworkAndDrivers/Web/express/config 4 | cp *.json ../../../../../dist/FrameworkAndDrivers/Web/express/config/ 5 | cp .*.env ../../../../../dist/FrameworkAndDrivers/Web/express/config/ 6 | -------------------------------------------------------------------------------- /src/ComponentInterfaces/UseCase/IPresenterOutputBoundary.ts: -------------------------------------------------------------------------------- 1 | // import IView from './IView'; 2 | import IResponseModel from './IResponseModel'; 3 | 4 | export default interface IPresenterOutputBoundary { 5 | // constructor(view: IView): void; 6 | 7 | presente(responseModel: IResponseModel): void; 8 | } 9 | -------------------------------------------------------------------------------- /src/ComponentInterfaces/InterfaceAdapters/IPresenterOuputBoundary.ts: -------------------------------------------------------------------------------- 1 | import IResponseModel from '../UseCase/IResponseModel'; 2 | // import IView from './IView'; 3 | 4 | export default interface IPresenterOuputBoundary { 5 | // constructor(view: IView): void; 6 | 7 | presente(responseModel: IResponseModel): void; 8 | } 9 | -------------------------------------------------------------------------------- /src/Components/User/InterfaceAdapters/createUser/ICreateUserView.ts: -------------------------------------------------------------------------------- 1 | import IView from '../../../../ComponentInterfaces/InterfaceAdapters/IView'; 2 | import ICreateUserViewModel from './ICreateUserViewModel'; 3 | 4 | export default interface ICreateUserView extends IView { 5 | render(viewModel: ICreateUserViewModel): void; 6 | } 7 | -------------------------------------------------------------------------------- /src/Components/User/InterfaceAdapters/createUser/ICreateUserViewModel.ts: -------------------------------------------------------------------------------- 1 | import IViewModel from '../../../../ComponentInterfaces/InterfaceAdapters/IViewModel'; 2 | 3 | export default interface ICreateUserViewModel extends IViewModel { 4 | userCreated: boolean; 5 | id?: string; 6 | email: string; 7 | errors?: Array; 8 | } 9 | -------------------------------------------------------------------------------- /src/Components/User/InterfaceAdapters/createUser/ICreateUserControllerRequest.ts: -------------------------------------------------------------------------------- 1 | import IControllerRequestModel from '../../../../ComponentInterfaces/InterfaceAdapters/IControllerRequestModel'; 2 | 3 | export default interface CreateUserControllerRequestModel 4 | extends IControllerRequestModel { 5 | email: string; 6 | password: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/app.start.spec.inc.ts: -------------------------------------------------------------------------------- 1 | import serverApp from './app'; 2 | 3 | export default function appServerStartTest() { 4 | describe('#AppStart: App Server tests ', () => { 5 | it('App Server should be an object', async () => { 6 | expect(typeof serverApp).toEqual('object'); 7 | }); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /scripts/copy_views.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mkdir -p ./dist/FrameworkAndDrivers/Web/express/app 3 | cd src/FrameworkAndDrivers/Web/express/app 4 | if [[ "$OSTYPE" == "darwin"* ]]; then 5 | rsync -R `find . -name *.njk` ../../../../../dist/FrameworkAndDrivers/Web/express/app/ 6 | else 7 | cp --parents `find . -name *.njk` ../../../../../dist/FrameworkAndDrivers/Web/express/app/ 8 | fi -------------------------------------------------------------------------------- /src/Components/User/UseCases/createUser/ICreateUserEntityGateway.ts: -------------------------------------------------------------------------------- 1 | import UserEntity from '../../Entities/UserEntity'; 2 | import IEntityGateway from '../../../../ComponentInterfaces/UseCase/IEntityGateway'; 3 | 4 | export default interface ICreateUserEntityGateway extends IEntityGateway { 5 | findByEmail(email: string): Promise; 6 | 7 | create(entity: UserEntity): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /src/Components/User/InterfaceAdapters/createUser/tests/CreateUserPresenter.spec.inc.ts: -------------------------------------------------------------------------------- 1 | import ICreateUserView from '../ICreateUserView'; 2 | import ICreateUserViewModel from '../ICreateUserViewModel'; 3 | 4 | export function getCreateUserViewModel(): ICreateUserView { 5 | return { 6 | render: (createUserViewModel: ICreateUserViewModel) => { 7 | console.info(createUserViewModel); 8 | } 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/app.spec.ts: -------------------------------------------------------------------------------- 1 | import appServerStartTest from './app.start.spec.inc'; 2 | import environmentsServerconfigTests from './app.environments-server-options.spec.inc'; 3 | import serverStartTests from './app.server-start.spec.inc'; 4 | 5 | describe('#App:', function() { 6 | 7 | appServerStartTest(); 8 | 9 | environmentsServerconfigTests(); 10 | 11 | serverStartTests() 12 | 13 | }); 14 | 15 | -------------------------------------------------------------------------------- /src/ComponentInterfaces/InterfaceAdapters/IControllerInputBoundary.ts: -------------------------------------------------------------------------------- 1 | // import IView from './IView'; 2 | // import IEntityGateway from '../UseCase/IEntityGateway'; 3 | import IControllerRequestModel from './IControllerRequestModel'; 4 | 5 | export default interface IControllerInputBoundary { 6 | // constructor(view: IView, entityRepository: IEntityGateway): void; 7 | 8 | execute(request: IControllerRequestModel): void; 9 | } 10 | -------------------------------------------------------------------------------- /src/ComponentInterfaces/UseCase/IInteractor.ts: -------------------------------------------------------------------------------- 1 | import IRequestModel from './IRequestModel'; 2 | // import IPresenterOuputBoundary from '../InterfaceAdapters/IPresenterOuputBoundary'; 3 | // import IEntityGateway from './IEntityGateway'; 4 | 5 | export default interface IInteractor { 6 | // constructor(entityRepository: IEntityGateway, presenter: IPresenterOuputBoundary): void; 7 | 8 | execute(requestModel: IRequestModel): void; 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Jest Tests", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeArgs": [ 9 | "--inspect-brk", 10 | "${workspaceRoot}/node_modules/jest/bin/jest.js", 11 | "--runInBand" 12 | ], 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "openOnSessionStart", 15 | "port": 9229 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "module": "commonjs", 5 | "noImplicitReturns": true, 6 | "noUnusedLocals": true, 7 | "outDir": "dist", 8 | "removeComments": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "target": "es2017" 12 | }, 13 | "compileOnSave": true, 14 | "include": [ 15 | "src" 16 | ], 17 | "exclude": [ 18 | "src/**/*.spec.ts", 19 | "src/**/*.spec.inc.ts" 20 | ] 21 | } -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "module": "commonjs", 5 | "noImplicitReturns": true, 6 | "noUnusedLocals": true, 7 | "outDir": "dist", 8 | "removeComments": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "target": "es2017" 12 | }, 13 | "compileOnSave": true, 14 | "include": [ 15 | "src" 16 | ], 17 | "exclude": [ 18 | "src/**/*.spec.ts", 19 | "src/**/*.spec.inc.ts" 20 | ] 21 | } -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/app.ts: -------------------------------------------------------------------------------- 1 | import Server from './app/server/server.class'; 2 | import { ServerOptionsInterface } from './app/server/server.interface'; 3 | import config from './config/config'; 4 | const appConfig = config(); 5 | 6 | const serverAppOptions: ServerOptionsInterface = { 7 | env: appConfig.get('env'), 8 | logger: { 9 | enabled: appConfig.get('logger') 10 | }, 11 | port: appConfig.get('port') 12 | }; 13 | 14 | const serverApp = new Server(serverAppOptions); 15 | 16 | export default serverApp; 17 | -------------------------------------------------------------------------------- /src/Components/User/InterfaceAdapters/createUser/CreateUserViewModelMapper.ts: -------------------------------------------------------------------------------- 1 | import ICreateUserResponseModel from '../../UseCases/createUser/ICreateUserResponseModel'; 2 | 3 | export default class CreateUserViewModelMapper { 4 | static mapFromCreateUserResponseModel( 5 | createUserResponseModel: ICreateUserResponseModel 6 | ) { 7 | return { 8 | userCreated: createUserResponseModel.id! ? true : false, 9 | id: createUserResponseModel.id!, 10 | email: createUserResponseModel.email 11 | }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/DB/MongoDB/User/user.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document } from 'mongoose'; 2 | import UserEntity from '../../../../Components/User/Entities/UserEntity'; 3 | 4 | export interface IUserModel extends Document, UserEntity { 5 | email: string; 6 | password: string; 7 | } 8 | 9 | const UserSchema: Schema = new Schema({ 10 | email: { type: String, required: true, unique: true }, 11 | password: { type: String, required: true } 12 | }); 13 | 14 | export default mongoose.model('User', UserSchema); 15 | -------------------------------------------------------------------------------- /src/Components/User/UseCases/createUser/ICreateUserPresenterOutputBoundary.ts: -------------------------------------------------------------------------------- 1 | // import ICreateUserView from './ICreateUserView'; 2 | import IPresenterOuputBoundary from '../../../../ComponentInterfaces/InterfaceAdapters/IPresenterOuputBoundary'; 3 | import ICreateUserResponseModel from './ICreateUserResponseModel'; 4 | 5 | export default interface ICreateUserPresenterOutputBoundary 6 | extends IPresenterOuputBoundary { 7 | // constructor(view: ICreateUserView): void; 8 | 9 | presente(createUserResponseModel: ICreateUserResponseModel): void; 10 | } 11 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import serverApp from './FrameworkAndDrivers/Web/express/app'; 2 | import * as db from './FrameworkAndDrivers/DB/MongoDB/db-connect'; 3 | import config from './FrameworkAndDrivers/Web/express/config/config'; 4 | const appConfig = config(); 5 | 6 | db.connect(appConfig.get('mongodbURI')); 7 | serverApp.start(); 8 | 9 | process.on('SIGINT', () => { 10 | console.info('\x1b[31m', 'SIGINT signal reveived', '\x1b[0m'); 11 | serverApp.close(async err => { 12 | if (err) { 13 | console.info('\x1b[31m', err, '\x1b[0m'); 14 | process.exit(1); 15 | } 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/app/server/server.interface.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { appEnvironment } from '../_shared/app-environment.enum'; 3 | 4 | export interface ServerInterface { 5 | readonly app: express.Application; 6 | readonly options: ServerOptionsInterface; 7 | start(): boolean; 8 | initServer(): void; 9 | close(callBack: IErrorCallback): void; 10 | } 11 | 12 | export interface ServerOptionsInterface { 13 | env: appEnvironment; 14 | logger?: { 15 | enabled?: boolean; 16 | }; 17 | port: number; 18 | } 19 | 20 | export interface IErrorCallback { 21 | (err?: Error | undefined): void; 22 | } 23 | -------------------------------------------------------------------------------- /src/Components/User/Entities/UserEntity.ts: -------------------------------------------------------------------------------- 1 | import IEntity from '../../../ComponentInterfaces/Entity/IEntity'; 2 | 3 | export default class UserEntity implements IEntity { 4 | id?: any; 5 | email: string; 6 | password: string; 7 | 8 | constructor(email: string, password: string, id?: string) { 9 | this.id = id ? id : undefined; 10 | this.email = email; 11 | this.password = password; 12 | } 13 | 14 | getId() { 15 | return this.id; 16 | } 17 | 18 | getEmail(): string { 19 | return this.email; 20 | } 21 | 22 | checkPassword(passwordToCheck: string) { 23 | return this.password === passwordToCheck; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/app/server/server.router.class.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import RouterInterface from '../_shared/router.interface'; 3 | import UsesCasesRouter from '../UsesCases/usescases.router.class'; 4 | const router = express.Router(); 5 | 6 | export default class ServerRouter implements RouterInterface { 7 | private router: express.Router = router; 8 | 9 | constructor() { 10 | this.initRouter(); 11 | } 12 | 13 | public getRouter(): express.Router { 14 | return this.router; 15 | } 16 | 17 | private initRouter(): void { 18 | this.router.use('/', new UsesCasesRouter().getRouter()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/app/views/base.njk: -------------------------------------------------------------------------------- 1 | {% block header %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ title }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% endblock %} 16 | 17 | {% block body %}{% endblock %} 18 | 19 | {% block footer %} 20 | 21 | 22 | 23 | {% endblock %} -------------------------------------------------------------------------------- /src/Components/User/UseCases/createUser/ICreateUserControllerInputBoundary.ts: -------------------------------------------------------------------------------- 1 | import CreateUserRequestModelType from './ICreateUserRequestModel'; 2 | import IControllerInputBoundary from '../../../../ComponentInterfaces/InterfaceAdapters/IControllerInputBoundary'; 3 | /* 4 | import ICreateUserEntityGateway from './ICreateUserEntityGateway'; 5 | import ICreateUserView from '../../InterfaceAdapters/ICreateUserView'; 6 | */ 7 | 8 | export default interface ICreateUserControllerInputBoundary 9 | extends IControllerInputBoundary { 10 | /* 11 | constructor( 12 | createUserEntityRepository: ICreateUserEntityGateway, 13 | createUserView: ICreateUserView 14 | ): void; 15 | */ 16 | 17 | execute(createUserRequestModelType: CreateUserRequestModelType): void; 18 | } 19 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/DB/MongoDB/db-connect.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | export const connect = (db: string) => { 4 | mongoose 5 | .connect(db, { 6 | useCreateIndex: true, 7 | useNewUrlParser: true, 8 | useUnifiedTopology: true 9 | }) 10 | .then(() => { 11 | return console.log( 12 | '\x1b[32m%s\x1b[0m', 13 | '√ Successfully connected to ' + db 14 | ); 15 | }) 16 | .catch(error => { 17 | console.error( 18 | '\x1b[31m', 19 | '[dbconnect]', 20 | error.name, 21 | error.message, 22 | '\x1b[0m' 23 | ); 24 | // return process.exit(1); 25 | }); 26 | 27 | mongoose.connection.on('disconnected', connect); 28 | }; 29 | 30 | export const close = async (callBack: () => void) => { 31 | await mongoose.disconnect().then(() => { 32 | callBack(); 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /src/Components/User/Entities/tests/UserEntity.spec.ts: -------------------------------------------------------------------------------- 1 | import UserEntity from '../UserEntity'; 2 | 3 | describe('#UserEntity', () => { 4 | 5 | const emailTest: string = 'UserEntity@mail.test'; 6 | const passwordTest: string = 'UserEntityPassword'; 7 | const idTest: string = 'idTest'; 8 | 9 | it('should return id setted', () => { 10 | const userEntity: UserEntity = new UserEntity(emailTest, passwordTest, idTest); 11 | expect(userEntity.getId()).toBe(idTest); 12 | }) 13 | 14 | it('should return email setted', () => { 15 | const userEntity: UserEntity = new UserEntity(emailTest, passwordTest); 16 | expect(userEntity.getEmail()).toBe(emailTest); 17 | }) 18 | 19 | 20 | it('should return password handled', () => { 21 | const userEntity: UserEntity = new UserEntity(emailTest, passwordTest); 22 | expect(userEntity.checkPassword(passwordTest)).toBe(true); 23 | }) 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/config/config.ts: -------------------------------------------------------------------------------- 1 | import convict = require('convict'); 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import { appSchemaConfig } from './app-schema.conf'; 5 | import dotenv from 'dotenv'; 6 | dotenv.config(); 7 | 8 | export default () => { 9 | const config = convict(appSchemaConfig); 10 | const env = config.get('env'); 11 | const defaultConfFiles = [ 12 | path.join(__dirname, `./${env}.conf.json`), 13 | path.join(__dirname, './app.conf.json') 14 | ]; 15 | try { 16 | const confFilesOk: string[] = []; 17 | defaultConfFiles.forEach(path => { 18 | if (fs.existsSync(path)) { 19 | confFilesOk.push(path); 20 | } 21 | }); 22 | config.loadFile(confFilesOk); 23 | } catch (err) { 24 | console.error('\x1b[31m', '[config]', err.message, '\x1b[0m'); 25 | } 26 | config.validate({ strict: true }); 27 | 28 | return config; 29 | }; 30 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/app/UsesCases/User/createUser/views/createUserView.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import IView from '../../../../../../../../ComponentInterfaces/InterfaceAdapters/IView'; 3 | import ActionViewInterface from '../../../../_shared/action-view.interface'; 4 | import IViewModel from '../../../../../../../../ComponentInterfaces/InterfaceAdapters/IViewModel'; 5 | 6 | export class View implements IView { 7 | private res: express.Response; 8 | constructor(res: express.Response) { 9 | this.res = res; 10 | } 11 | render(viewModel: IViewModel): void { 12 | const pageParams = { 13 | title: 'Clearn Architecture - Node.js', 14 | success_message: 'user.added' 15 | }; 16 | const viewParams: ActionViewInterface = { 17 | view: 'UsesCases/User/createUser/views/createUserView.njk', 18 | params: pageParams 19 | }; 20 | this.res.render(viewParams.view, viewParams.params); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/config/app-schema.conf.ts: -------------------------------------------------------------------------------- 1 | import { appEnvironment } from '../app/_shared/app-environment.enum'; 2 | 3 | export const appSchemaConfig = { 4 | env: { 5 | doc: 'The applicaton environment.', 6 | format: ([ 7 | 'development', 8 | 'test', 9 | 'staging', 10 | 'production' 11 | ] as unknown) as appEnvironment, 12 | default: appEnvironment.DEV, 13 | env: 'NODE_ENV', 14 | arg: 'node-env' 15 | }, 16 | port: { 17 | doc: 'The port to bind.', 18 | format: 'port', 19 | default: 3000, 20 | env: 'PORT', 21 | arg: 'port' 22 | }, 23 | logger: { 24 | doc: 'Logger enabled.', 25 | format: 'Boolean', 26 | default: false, 27 | env: 'LOGGER', 28 | arg: 'logger' 29 | }, 30 | mongodbURI: { 31 | doc: 'MongoDB URI.', 32 | format: String, 33 | default: 'mongodb://192.168.99.100/login_dev', 34 | env: 'MONGODB_URI', 35 | arg: 'mongodb-uri' 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/app/UsesCases/User/createUser/views/createUserView.njk: -------------------------------------------------------------------------------- 1 | {% extends "views/base.njk" %} 2 | 3 | {% block body %} 4 | 5 |
6 |
7 |
8 |

Welcome to Clean Architecture

9 |
With Typescript and Express server
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 |
{{ success_message }}
22 |
{{ error_message }}
23 |
24 |
25 |
26 | {% endblock %} -------------------------------------------------------------------------------- /src/Components/User/InterfaceAdapters/createUser/CreateUserPresenter.ts: -------------------------------------------------------------------------------- 1 | import ICreateUserPresenterOutputBoundary from '../../UseCases/createUser/ICreateUserPresenterOutputBoundary'; 2 | import ICreateUserView from './ICreateUserView'; 3 | import ICreateUserResponseModel from '../../UseCases/createUser/ICreateUserResponseModel'; 4 | import ICreateUserViewModel from './ICreateUserViewModel'; 5 | import CreateUserViewModelMapper from './CreateUserViewModelMapper'; 6 | 7 | export default class CreateUserPresenter 8 | implements ICreateUserPresenterOutputBoundary { 9 | private createUserView: ICreateUserView; 10 | 11 | constructor(createUserView: ICreateUserView) { 12 | this.createUserView = createUserView; 13 | } 14 | 15 | presente(createUserResponseModel: ICreateUserResponseModel): void { 16 | const createUserViewModel: ICreateUserViewModel = 17 | CreateUserViewModelMapper.mapFromCreateUserResponseModel( 18 | createUserResponseModel 19 | ); 20 | if (!createUserViewModel.id) { 21 | createUserViewModel.errors = []; 22 | createUserViewModel.errors.push('CreateUserPresenter.id.undefined'); 23 | } 24 | this.createUserView.render(createUserViewModel); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/DB/MongoDB/User/user.model.spec.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import UserModel, { IUserModel } from './user.model'; 3 | 4 | const mongoDBTest = 'mongodb://192.168.99.100/login_test'; 5 | 6 | describe('#UserModel', () => { 7 | beforeAll(async () => { 8 | await mongoose.connect(mongoDBTest, { 9 | useCreateIndex: true, 10 | useNewUrlParser: true, 11 | useUnifiedTopology: true 12 | }); 13 | }); 14 | 15 | afterAll(async () => { 16 | mongoose.connection.close(() => { }); 17 | }); 18 | 19 | it('Should throw validation errors', () => { 20 | const user: IUserModel = new UserModel(); 21 | 22 | expect(user.validate).toThrow(); 23 | }); 24 | 25 | it('Should save a user', async () => { 26 | expect.assertions(3); 27 | 28 | const user: IUserModel = new UserModel({ 29 | email: 'test@example.com', 30 | password: 'Test password' 31 | }); 32 | const spy = jest.spyOn(user, 'save'); 33 | await user.save(); 34 | 35 | expect(spy).toHaveBeenCalled(); 36 | 37 | expect(user).toMatchObject({ 38 | email: expect.any(String), 39 | password: expect.any(String) 40 | }); 41 | 42 | expect(user.email).toBe('test@example.com'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/Components/User/InterfaceAdapters/createUser/tests/CreateUserController.spec.inc.ts: -------------------------------------------------------------------------------- 1 | import UserEntity from '../../../Entities/UserEntity'; 2 | import ICreateUserEntityGateway from '../../../UseCases/createUser/ICreateUserEntityGateway'; 3 | import ICreateUserView from '../ICreateUserView'; 4 | import ICreateUserViewModel from '../ICreateUserViewModel'; 5 | 6 | const userTestExisting: UserEntity = new UserEntity( 7 | 'existingUser@mail.test', 8 | 'CreateUserEntityRepositoryPassword', 9 | '1' 10 | ); 11 | 12 | export class CreateUserEntityRepositoryTest 13 | implements ICreateUserEntityGateway { 14 | findByEmail(email: string): Promise { 15 | if (email !== userTestExisting.getEmail()) { 16 | throw new Error('user.notexist'); 17 | } 18 | return new Promise(resolve => { 19 | resolve(userTestExisting); 20 | }); 21 | } 22 | 23 | create(userEntity: UserEntity): Promise { 24 | return new Promise(resolve => { 25 | resolve(userEntity); 26 | }); 27 | } 28 | } 29 | 30 | export class CreateUserViewTest implements ICreateUserView { 31 | render(viewModel: ICreateUserViewModel): void { 32 | throw new Error('CreateUserViewTest.render not implemented (test only)'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/DB/MongoDB/User/UserEntityRepository.ts: -------------------------------------------------------------------------------- 1 | import ICreateUserEntityGateway from '../../../../Components/User/UseCases/createUser/ICreateUserEntityGateway'; 2 | import UserEntity from '../../../../Components/User/Entities/UserEntity'; 3 | import UserModel, { IUserModel } from './user.model'; 4 | 5 | export default class UserEntityRepository implements ICreateUserEntityGateway { 6 | async findByEmail(email: string): Promise { 7 | let userFindedByEmail: IUserModel | null = null; 8 | userFindedByEmail = await UserModel.findOne({ 9 | email: email 10 | }) 11 | .exec() 12 | .catch(err => { 13 | throw new Error('UserEntityRepository.findByEmail.err'); 14 | }); 15 | if (!userFindedByEmail) { 16 | throw new Error('user.notexist'); 17 | } 18 | return userFindedByEmail!; 19 | } 20 | async create(newUserEntity: UserEntity): Promise { 21 | const newUser: IUserModel = new UserModel(newUserEntity); 22 | await newUser.save((err, user) => { 23 | if (err || !user) { 24 | throw new Error('UserEntityRepository.save.err'); 25 | } 26 | }); 27 | const userSaved = new UserEntity( 28 | newUser.email, 29 | newUser.password, 30 | newUser._id 31 | ); 32 | return userSaved; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Compiled JavaScript files 2 | **/*.js 3 | **/*.js.map 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | config/*.env 64 | 65 | # next.js build output 66 | .next 67 | 68 | .jscpd 69 | /.coveralls.yml 70 | /dist 71 | /.VSCodeCounter 72 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/app/UsesCases/usescases.router.class.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import RouterInterface from '../_shared/router.interface'; 3 | import CreateUserDeliver from './User/createUser/delivers/createUserDeliver'; 4 | import ActionViewInterface from '../_shared/action-view.interface'; 5 | const router = express.Router(); 6 | 7 | export default class UseCaseRouter implements RouterInterface { 8 | private router: express.Router = router; 9 | 10 | constructor() { 11 | this.initRouter(); 12 | } 13 | 14 | public getRouter(): express.Router { 15 | return this.router; 16 | } 17 | 18 | private initRouter(): void { 19 | this.router.post( 20 | '/', 21 | async (request: express.Request, response: express.Response) => { 22 | const createUserDeliver = new CreateUserDeliver(request, response); 23 | await createUserDeliver.IndexActionView(); 24 | } 25 | ); 26 | this.router.get( 27 | '/', 28 | (request: express.Request, response: express.Response) => { 29 | const pageParams = { 30 | title: 'Clearn Architecture - Node.js', 31 | message: '' 32 | }; 33 | const viewParams: ActionViewInterface = { 34 | view: 'UsesCases/User/createUser/views/createUserView.njk', 35 | params: pageParams 36 | }; 37 | response.render(viewParams.view, viewParams.params); 38 | } 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Components/User/InterfaceAdapters/createUser/CreateUserController.ts: -------------------------------------------------------------------------------- 1 | import ICreateUserRequestModel from '../../UseCases/createUser/ICreateUserRequestModel'; 2 | import CreateUserControllerRequest from './ICreateUserControllerRequest'; 3 | import ICreateUserEntityGateway from '../../UseCases/createUser/ICreateUserEntityGateway'; 4 | import CreateUserInteractor from '../../UseCases/createUser/CreateUserInteractor'; 5 | import ICreateUserView from './ICreateUserView'; 6 | import CreateUserPresenter from './CreateUserPresenter'; 7 | 8 | export default class CreateUserController { 9 | private createUserEntityRepository: ICreateUserEntityGateway; 10 | private createUserPresenter: CreateUserPresenter; 11 | 12 | constructor( 13 | createUserEntityRepository: ICreateUserEntityGateway, 14 | createUserView: ICreateUserView 15 | ) { 16 | this.createUserEntityRepository = createUserEntityRepository; 17 | this.createUserPresenter = new CreateUserPresenter(createUserView); 18 | } 19 | 20 | async execute( 21 | createUserControllerRequest: CreateUserControllerRequest 22 | ): Promise { 23 | await this.getCreateUserInteractor().execute( 24 | this.getCreateUserRequestModelFromRequest(createUserControllerRequest) 25 | ); 26 | } 27 | 28 | private getCreateUserInteractor(): CreateUserInteractor { 29 | const createUserInteractor: CreateUserInteractor = new CreateUserInteractor( 30 | this.createUserEntityRepository, 31 | this.createUserPresenter 32 | ); 33 | return createUserInteractor; 34 | } 35 | 36 | private getCreateUserRequestModelFromRequest( 37 | createUserControllerRequest: CreateUserControllerRequest 38 | ): ICreateUserRequestModel { 39 | const createUserRequestModel: ICreateUserRequestModel = { 40 | email: createUserControllerRequest.email, 41 | password: createUserControllerRequest.password 42 | }; 43 | return createUserRequestModel; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "tslint-config-prettier", 4 | "tslint-config-security", 5 | "tslint-plugin-prettier" 6 | ], 7 | "rules": { 8 | "prettier": true, 9 | "array-type": [ 10 | false 11 | ], 12 | "arrow-parens": false, 13 | "class-name": true, 14 | "label-position": true, 15 | "member-ordering": [ 16 | true, 17 | { 18 | "order": [ 19 | "public-instance-field", 20 | "private-instance-field", 21 | "public-constructor", 22 | "public-instance-method", 23 | "protected-instance-method", 24 | "private-instance-method" 25 | ] 26 | } 27 | ], 28 | "max-line-length": [ 29 | true, 30 | { 31 | "limit": 80, 32 | "ignore-pattern": "^import [^,]+ from |^export | implements" 33 | } 34 | ], 35 | "no-arg": true, 36 | "no-console": false, 37 | "no-debugger": true, 38 | "no-empty": true, 39 | "no-eval": true, 40 | "no-non-null-assertion": false, 41 | "no-string-literal": false, 42 | "no-unsafe-finally": true, 43 | "no-var-keyword": true, 44 | "no-var-requires": false, 45 | "only-arrow-functions": [ 46 | true, 47 | "allow-declarations", 48 | "allow-named-functions" 49 | ], 50 | "ordered-imports": false, 51 | "prefer-for-of": true, 52 | "quotemark": [ 53 | true, 54 | "single" 55 | ], 56 | "radix": true, 57 | "switch-default": true, 58 | "triple-equals": [ 59 | true, 60 | "allow-null-check" 61 | ], 62 | "unified-signatures": true, 63 | "variable-name": false, 64 | "whitespace": [ 65 | true, 66 | "check-branch", 67 | "check-decl", 68 | "check-operator", 69 | "check-separator", 70 | "check-type" 71 | ] 72 | }, 73 | "linterOptions": { 74 | "exclude": [ 75 | "./src/**/*.spec.ts" 76 | ] 77 | } 78 | } -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/app/UsesCases/User/createUser/delivers/createUserDeliver.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import ActionViewInterface from '../../../../_shared/action-view.interface'; 3 | import CreateUserController from '../../../../../../../../Components/User/InterfaceAdapters/createUser/CreateUserController'; 4 | import ICreateUserEntityGateway from '../../../../../../../../Components/User/UseCases/createUser/ICreateUserEntityGateway'; 5 | import IView from '../../../../../../../../ComponentInterfaces/InterfaceAdapters/IView'; 6 | import CreateUserControllerRequestModel from '../../../../../../../../Components/User/InterfaceAdapters/createUser/ICreateUserControllerRequest'; 7 | import UserEntityRepository from '../../../../../../../DB/MongoDB/User/UserEntityRepository'; 8 | import { View } from '../views/createUserView'; 9 | 10 | export default class CreateUserDeliver { 11 | private req: express.Request; 12 | private res: express.Response; 13 | 14 | constructor(req: express.Request, res: express.Response) { 15 | this.req = req; 16 | this.res = res; 17 | } 18 | 19 | public async IndexActionView(): Promise { 20 | const userRepository: UserEntityRepository = new UserEntityRepository(); 21 | const view: IView = new View(this.res); 22 | const createUserController: CreateUserController = new CreateUserController( 23 | userRepository as ICreateUserEntityGateway, 24 | view 25 | ); 26 | try { 27 | await createUserController.execute({ 28 | email: this.req.body.email, 29 | password: this.req.body.password 30 | } as CreateUserControllerRequestModel); 31 | } catch (err) { 32 | const pageParams = { 33 | title: 'Clearn Architecture - Node.js', 34 | error_message: err.message 35 | }; 36 | const viewParams: ActionViewInterface = { 37 | view: 'UsesCases/User/createUser/views/createUserView.njk', 38 | params: pageParams 39 | }; 40 | this.res.render(viewParams.view, viewParams.params); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Components/User/InterfaceAdapters/createUser/tests/CreateUserPresenter.spec.ts: -------------------------------------------------------------------------------- 1 | import CreateUserPresenter from '../CreateUserPresenter'; 2 | import ICreateUserView from '../ICreateUserView'; 3 | import ICreateUserResponseModel from '../../../UseCases/createUser/ICreateUserResponseModel'; 4 | import ICreateUserViewModel from '../ICreateUserViewModel'; 5 | import * as testIncludes from './CreateUserPresenter.spec.inc'; 6 | 7 | describe('#CreateUserPresenter', () => { 8 | 9 | let consoleInfos: Array = new Array(); 10 | console.info = function(info: string) { consoleInfos.push(info); }; 11 | const createUserView: ICreateUserView = testIncludes.getCreateUserViewModel(); 12 | 13 | beforeEach(function() { 14 | consoleInfos = []; 15 | }); 16 | 17 | it("Should create new CreateUserPresenter succeed", () => { 18 | const createUserPresenter: CreateUserPresenter = new CreateUserPresenter(createUserView); 19 | expect(createUserPresenter).toMatchObject(new CreateUserPresenter(createUserView)); 20 | }); 21 | 22 | it("Should presente result to userCreated true when valid CreateUserResponseModel", () => { 23 | const createUserPresenter: CreateUserPresenter = new CreateUserPresenter(createUserView); 24 | const createUserResponseModel: ICreateUserResponseModel = { id: '1', email: 'CreateUserPresenter@mail.test' }; 25 | createUserPresenter.presente(createUserResponseModel); 26 | expect(((consoleInfos[0]) as ICreateUserViewModel).userCreated).toBeTruthy(); 27 | }); 28 | 29 | it("Should presente result to userCreated false when invalid CreateUserResponseModel", () => { 30 | const createUserPresenter: CreateUserPresenter = new CreateUserPresenter(createUserView); 31 | const createUserResponseModel: ICreateUserResponseModel = { email: 'CreateUserPresenter@mail.test' }; 32 | createUserPresenter.presente(createUserResponseModel); 33 | expect(((consoleInfos[0]) as ICreateUserViewModel).userCreated).toBeFalsy(); 34 | }); 35 | 36 | it("Should presente result return error message 'CreateUserPresenter.id.undefined' when id is undefined", () => { 37 | const createUserPresenter: CreateUserPresenter = new CreateUserPresenter(createUserView); 38 | const createUserResponseModel: ICreateUserResponseModel = { email: 'CreateUserPresenter@mail.test' }; 39 | createUserPresenter.presente(createUserResponseModel); 40 | expect(((consoleInfos[0]) as ICreateUserViewModel).errors).toContain('CreateUserPresenter.id.undefined'); 41 | }); 42 | 43 | }); 44 | -------------------------------------------------------------------------------- /src/Components/User/UseCases/createUser/tests/CreateUserInteractor.spec.inc.ts: -------------------------------------------------------------------------------- 1 | import ICreateUserRequestModel from '../ICreateUserRequestModel'; 2 | import ICreateUserResponseModel from '../ICreateUserResponseModel'; 3 | import IPresenterOuputBoundary from '../../../../../ComponentInterfaces/InterfaceAdapters/IPresenterOuputBoundary'; 4 | import UserEntity from '../../../Entities/UserEntity'; 5 | import IView from '../../../../../ComponentInterfaces/InterfaceAdapters/IView'; 6 | import ICreateUserViewModel from '../../../InterfaceAdapters/createUser/ICreateUserViewModel'; 7 | 8 | export function getCreateUserRequestModelTest(): ICreateUserRequestModel { 9 | return { 10 | email: 'CreateUserRequestModel@mail.test', 11 | password: 'myPasswordTest' 12 | }; 13 | } 14 | 15 | export function getCreateUserExistRequestModel(): ICreateUserRequestModel { 16 | return { 17 | email: 'getCreateUserExistRequestModel@mail.test', 18 | password: 'CreateUserRequestModelPassword' 19 | }; 20 | } 21 | 22 | function findByEmailMocked(email: string): Promise { 23 | const userTestExisting: UserEntity = new UserEntity( 24 | 'getCreateUserExistRequestModel@mail.test', 25 | 'getCreateUserExistRequestModel', 26 | '1' 27 | ); 28 | if (email !== userTestExisting.getEmail()) { 29 | throw new Error('user.notexist'); 30 | } 31 | return new Promise(resolve => { 32 | resolve(userTestExisting); 33 | }); 34 | } 35 | 36 | export function getCreateUserEntityRepository( 37 | createUserRequestModel: ICreateUserRequestModel 38 | ) { 39 | const newUserTestSaved: UserEntity = new UserEntity( 40 | createUserRequestModel.email, 41 | createUserRequestModel.password, 42 | '2' 43 | ); 44 | return { 45 | findByEmail: findByEmailMocked, 46 | 47 | create: (): Promise => { 48 | return new Promise(resolve => { 49 | resolve(newUserTestSaved); 50 | }); 51 | } 52 | }; 53 | } 54 | 55 | export function getCreateUserEntityRepositorySaveFailing( 56 | createUserRequestModel: ICreateUserRequestModel 57 | ) { 58 | return { 59 | findByEmail: findByEmailMocked, 60 | 61 | create: (): Promise => { 62 | return new Promise(() => { 63 | throw new Error('getTestEntityRepositorySaveFailing'); 64 | }); 65 | } 66 | }; 67 | } 68 | 69 | export function getTestView(): IView { 70 | return { 71 | render: (viewModel: ICreateUserViewModel) => { 72 | console.info(viewModel); 73 | } 74 | }; 75 | } 76 | 77 | export function getTestPresenter(view: IView): IPresenterOuputBoundary { 78 | return { 79 | presente: (responseModel: ICreateUserResponseModel): void => { 80 | const viewModel: ICreateUserViewModel = { 81 | userCreated: true, 82 | id: responseModel.id, 83 | email: responseModel.email 84 | }; 85 | view.render(viewModel); 86 | } 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /src/Components/User/UseCases/createUser/CreateUserInteractor.ts: -------------------------------------------------------------------------------- 1 | import ICreateUserRequestModel from './ICreateUserRequestModel'; 2 | import ICreateUserPresenterOutputBoundary from './ICreateUserPresenterOutputBoundary'; 3 | import ICreateUserEntityGateway from './ICreateUserEntityGateway'; 4 | import ICreateUserResponseModel from './ICreateUserResponseModel'; 5 | import UserEntity from '../../Entities/UserEntity'; 6 | import ICreateUserControllerInputBoundary from './ICreateUserControllerInputBoundary'; 7 | 8 | export default class CreateUserInteractor 9 | implements ICreateUserControllerInputBoundary { 10 | private userEntity: UserEntity | undefined; 11 | private entityRepository: ICreateUserEntityGateway; 12 | private presenter: ICreateUserPresenterOutputBoundary; 13 | 14 | constructor( 15 | entityRepository: ICreateUserEntityGateway, 16 | presenter: ICreateUserPresenterOutputBoundary 17 | ) { 18 | this.entityRepository = entityRepository; 19 | this.presenter = presenter; 20 | } 21 | 22 | async execute(requestModel: ICreateUserRequestModel) { 23 | this.userEntity = new UserEntity(requestModel.email, requestModel.password); 24 | await this.validateRequestModel(requestModel); 25 | let newUserEntitySaved; 26 | 27 | try { 28 | newUserEntitySaved = await this.entityRepository.create(this.userEntity); 29 | } catch (error) { 30 | throw new Error('CreateUserInteractor.repository.create.error'); 31 | } 32 | 33 | // tslint:disable-next-line: max-line-length 34 | const createUserResponse: ICreateUserResponseModel = this.getCreateUserResponseModelFromUser( 35 | newUserEntitySaved 36 | ); 37 | this.presenter.presente(createUserResponse); 38 | } 39 | 40 | private async validateRequestModel(requestModel: ICreateUserRequestModel) { 41 | if (!requestModel.email && !requestModel.password) { 42 | throw new Error('CreateUserInteractor.email_and_password.empty'); 43 | } else if (!requestModel.email) { 44 | throw new Error('CreateUserInteractor.email.empty'); 45 | } else if (!requestModel.password) { 46 | throw new Error('CreateUserInteractor.password.empty'); 47 | } 48 | const userEmail: string = await this.userEntity!.getEmail(); 49 | const isUserExist: boolean = await this.checkNewUserExist(userEmail); 50 | if (isUserExist) { 51 | throw new Error('CreateUserInteractor.newuser.exist'); 52 | } 53 | } 54 | 55 | private async checkNewUserExist(email: string): Promise { 56 | try { 57 | const isUserExist = await this.entityRepository.findByEmail(email); 58 | return !!isUserExist; 59 | } catch (error) { 60 | return false; 61 | } 62 | } 63 | 64 | private getCreateUserResponseModelFromUser(newUserEntity: UserEntity) { 65 | const createUserResponse: ICreateUserResponseModel = { 66 | id: newUserEntity.getId()!, 67 | email: newUserEntity.getEmail() 68 | }; 69 | return createUserResponse; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/app.environments-server-options.spec.inc.ts: -------------------------------------------------------------------------------- 1 | import * as sinon from 'sinon'; 2 | import { appEnvironment } from './app/_shared/app-environment.enum'; 3 | import config from './config/config'; 4 | const defaultConf = require('./config/default.conf.json'); 5 | const developmentConf = require('./config/development.conf.json'); 6 | const testConf = require('./config/test.conf.json'); 7 | const stagingConf = require('./config/staging.conf.json'); 8 | const productionConf = require('./config/production.conf.json'); 9 | const sandbox = sinon.createSandbox(); 10 | 11 | export default function environmentsServerconfigTests() { 12 | describe('#Config: Environments server config test ', function() { 13 | const loggerIsEnabled = true; 14 | const loggerIsDisabled = false; 15 | const serverPortDevelopment = developmentConf.port; 16 | const serverPortTest = testConf.port; 17 | const serverPortStaging = stagingConf.port; 18 | const serverPortProduction = productionConf.port; 19 | 20 | describe('#Env:Default server env is "development"', function() { 21 | expect(defaultConf.env).toEqual(appEnvironment.DEV); 22 | }); 23 | 24 | describe('#Env:Development server config test ', function() { 25 | serverConfigTest( 26 | appEnvironment.DEV, 27 | loggerIsEnabled, 28 | serverPortDevelopment 29 | ); 30 | }); 31 | 32 | describe('#Env:Test server config test ', function() { 33 | serverConfigTest(appEnvironment.TEST, loggerIsDisabled, serverPortTest); 34 | }); 35 | 36 | describe('#Env:Staging server config test ', function() { 37 | serverConfigTest( 38 | appEnvironment.STAGING, 39 | loggerIsDisabled, 40 | serverPortStaging 41 | ); 42 | }); 43 | 44 | describe('#Env:Production server config test ', function() { 45 | serverConfigTest( 46 | appEnvironment.PROD, 47 | loggerIsDisabled, 48 | serverPortProduction 49 | ); 50 | }); 51 | }); 52 | } 53 | 54 | function serverConfigTest( 55 | appEnvToTest: appEnvironment, 56 | loggerStatutExpected: Boolean, 57 | serverPortExpected: number 58 | ) { 59 | it(`${appEnvToTest} config env should be setted`, async function() { 60 | sandbox.stub(process, 'env').value({ NODE_ENV: appEnvToTest }); 61 | const configTest = config(); 62 | expect(configTest.get('env')).toEqual(appEnvToTest); 63 | sandbox.restore(); 64 | }); 65 | 66 | it(`${appEnvToTest} config logger should be ${loggerStatutExpected}`, async function() { 67 | sandbox.stub(process, 'env').value({ NODE_ENV: appEnvToTest }); 68 | const configTest = config(); 69 | expect(configTest.get('logger')).toEqual(loggerStatutExpected); 70 | sandbox.restore(); 71 | }); 72 | 73 | it(`${appEnvToTest} config port should be ${serverPortExpected}`, async function() { 74 | sandbox.stub(process, 'env').value({ NODE_ENV: appEnvToTest }); 75 | const configTest = config(); 76 | expect(configTest.get('port')).toEqual(serverPortExpected); 77 | sandbox.restore(); 78 | }); 79 | } -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/app/server/server.class.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import cors from 'cors'; 4 | import http from 'http'; 5 | import morgan from 'morgan'; 6 | import nunjucks from 'nunjucks'; 7 | import { 8 | ServerInterface, 9 | ServerOptionsInterface, 10 | IErrorCallback 11 | } from './server.interface'; 12 | import ServerRouter from './server.router.class'; 13 | 14 | export default class Server implements ServerInterface { 15 | public readonly app: express.Application; 16 | public readonly options: ServerOptionsInterface; 17 | private serverInitialized: boolean = false; 18 | private serverListener: http.Server | undefined = undefined; 19 | 20 | constructor(options: ServerOptionsInterface) { 21 | this.options = options; 22 | this.app = express(); 23 | } 24 | 25 | public start(): boolean { 26 | let serverStarted: boolean = false; 27 | try { 28 | this.initServer(); 29 | this.serverListener = this.app.listen(this.options.port, () => { 30 | this.appListeningHandler(); 31 | }); 32 | } finally { 33 | serverStarted = true; 34 | } 35 | 36 | return serverStarted; 37 | } 38 | 39 | public initServer(): void { 40 | if (!this.serverInitialized) { 41 | this.setAppSettings(); 42 | this.setAppViewEngine(); 43 | this.setAppMiddlewars(); 44 | this.setAppRouter(); 45 | this.serverInitialized = true; 46 | } 47 | } 48 | 49 | public close(callBack: IErrorCallback): void { 50 | this.serverListener!.close(callBack); 51 | } 52 | 53 | private setAppSettings(): void { 54 | this.app.disable('x-powered-by'); 55 | this.app.use(cors()); 56 | this.app.use(express.json()); 57 | this.app.use(express.urlencoded({ extended: false })); 58 | this.app.use(express.static('public')); 59 | } 60 | 61 | private setAppViewEngine(): void { 62 | nunjucks.configure(__dirname + '/..', { 63 | autoescape: true, 64 | express: this.app 65 | }); 66 | } 67 | 68 | private setAppMiddlewars(): void { 69 | if (this.options.logger && this.options.logger!.enabled) { 70 | this.app.use(morgan('dev')); 71 | } 72 | this.app.use(bodyParser.urlencoded({ extended: true })); 73 | this.app.use(express.urlencoded({ extended: false })); 74 | } 75 | 76 | private setAppRouter(): void { 77 | this.app.use('/', new ServerRouter().getRouter()); 78 | } 79 | 80 | private appListeningHandler(): void { 81 | this.nodeEnvInformation(); 82 | this.loggerInformation(); 83 | console.log( 84 | '\x1b[32m%s\x1b[0m', 85 | '√ Server is running on port ' + this.options.port 86 | ); 87 | } 88 | 89 | private loggerInformation(): boolean { 90 | let loggerEnabled: boolean = false; 91 | if (this.options.logger && this.options.logger!.enabled) { 92 | console.log('\x1b[33m%s\x1b[0m', '√ Logger enabled'); 93 | loggerEnabled = true; 94 | } 95 | return loggerEnabled; 96 | } 97 | 98 | private nodeEnvInformation(): void { 99 | console.log( 100 | '\x1b[36m%s\x1b[0m', 101 | `√ ${this.options.env[0].toUpperCase()}${this.options.env.slice(1)} mode` 102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clean-architecture-typescript", 3 | "version": "1.0.0", 4 | "description": "Uncle Bob's clean architecture implementation in Typescript, with Express as Web Detail", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "assets:config": "sh scripts/copy_config.sh", 8 | "assets:views": "sh scripts/copy_views.sh", 9 | "clean": "rimraf dist", 10 | "test": "jest", 11 | "coverage": "jest --coverage", 12 | "jscpd": "jscpd . --ignore \"package.json,tsconfig.json,dist,**/node_modules,**/coverage\"", 13 | "tslint": "tslint -c tslint.json -p tsconfig.json", 14 | "tslint:fix": "npm run tslint -- --fix", 15 | "install:dev": "npm i", 16 | "build:dev": "tsc --watch --preserveWatchOutput", 17 | "start:dev": "nodemon dist/index.js --node-env development", 18 | "install:prod": "npm i --production", 19 | "build:prod": "tsc --build tsconfig.prod.json", 20 | "start:prod": "node dist/index.js --node-env production", 21 | "dev": "concurrently -k -n \"Build,Start\" -p \"[{name}]\" -c \"blue,green\" \"npm:build:dev\" \"npm:start:dev\"", 22 | "start": "npm run build:prod && npm run start:prod --" 23 | }, 24 | "keywords": [ 25 | "Clean Architecture", 26 | "Clean", 27 | "Architecture", 28 | "Uncle Bob", 29 | "Typescript", 30 | "Node.js", 31 | "Express", 32 | "Server", 33 | "Node", 34 | "Clean-code", 35 | "Craftsmanship", 36 | "TDD", 37 | "test", 38 | "convict", 39 | "jest" 40 | ], 41 | "author": "", 42 | "license": "ISC", 43 | "dependencies": { 44 | "@types/convict": "^4.2.1", 45 | "@types/cors": "^2.8.6", 46 | "@types/express": "^4.17.3", 47 | "@types/mongoose": "^5.7.7", 48 | "@types/morgan": "^1.9.0", 49 | "@types/nunjucks": "^3.1.3", 50 | "body-parser": "^1.19.0", 51 | "convict": "^5.2.0", 52 | "cors": "^2.8.5", 53 | "dotenv": "^8.2.0", 54 | "express": "^4.17.1", 55 | "mongoose": "^5.9.5", 56 | "morgan": "^1.10.0", 57 | "nunjucks": "^3.2.1", 58 | "rimraf": "^3.0.2" 59 | }, 60 | "devDependencies": { 61 | "@types/dotenv": "^8.2.0", 62 | "@types/jest": "^25.1.4", 63 | "@types/sinon": "^7.5.2", 64 | "@types/supertest": "^2.0.8", 65 | "concurrently": "^5.1.0", 66 | "jest": "^25.1.0", 67 | "jscpd": "^2.0.16", 68 | "nodemon": "^2.0.2", 69 | "prettier": "^1.19.1", 70 | "sinon": "^9.0.1", 71 | "supertest": "^4.0.2", 72 | "ts-jest": "^25.2.1", 73 | "ts-node": "^8.8.1", 74 | "tslint": "^6.1.0", 75 | "tslint-config-prettier": "^1.18.0", 76 | "tslint-config-security": "^1.16.0", 77 | "tslint-plugin-prettier": "^2.2.0", 78 | "typescript": "^3.8.3" 79 | }, 80 | "jest": { 81 | "moduleFileExtensions": [ 82 | "js", 83 | "json", 84 | "ts" 85 | ], 86 | "rootDir": "src", 87 | "testRegex": ".spec.ts$", 88 | "testPathIgnorePatterns": [ 89 | "node_modules" 90 | ], 91 | "coveragePathIgnorePatterns": [ 92 | "node_modules", 93 | ".spec.inc.ts$", 94 | ".spec.ts$" 95 | ], 96 | "transform": { 97 | "^.+\\.(t|j)s$": "ts-jest" 98 | }, 99 | "coverageDirectory": "../coverage", 100 | "testEnvironment": "node", 101 | "moduleDirectories": [ 102 | "node_modules", 103 | "src", 104 | "src/FrameworkAndDrivers/Web/express" 105 | ] 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/DB/MongoDB/User/UserEntityRepository.spec.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import UserEntityRepository from './UserEntityRepository'; 3 | import UserEntity from '../../../../Components/User/Entities/UserEntity'; 4 | 5 | 6 | const mongoDBTest = 'mongodb://192.168.99.100/login_test'; 7 | 8 | describe('#UserEntityRepository without MongoDB connexion', () => { 9 | const emailTest = 'stayfi@test.test'; 10 | const emailTestNotSaved = 'stayfi-not-found@test.test'; 11 | const passwordTest = 'Stayfi Password'; 12 | 13 | it('Should create new user', async () => { 14 | const newUserEntity: UserEntity = new UserEntity(emailTest, passwordTest); 15 | const userEntityRepository: UserEntityRepository = new UserEntityRepository(); 16 | const spy = jest.spyOn(userEntityRepository, 'create'); 17 | //const newUserSaved = 18 | await userEntityRepository.create(newUserEntity); 19 | 20 | expect(spy).toHaveBeenCalled(); 21 | }); 22 | 23 | 24 | it(`Should not find user by mail ${emailTestNotSaved}`, async done => { 25 | expect.assertions(1); 26 | const userEntityRepository: UserEntityRepository = new UserEntityRepository(); 27 | let isDone = false; 28 | jest.setTimeout(10000); 29 | setTimeout(() => { 30 | expect(isDone).toBeFalsy(); 31 | done(); 32 | }, 5000); 33 | try { 34 | await userEntityRepository.findByEmail(emailTestNotSaved); 35 | isDone = true; 36 | } catch (e) { 37 | expect(e.message).toEqual('user.notexist'); 38 | } 39 | }); 40 | }); 41 | 42 | describe('#UserEntityRepository with MongoDB connexion', () => { 43 | const emailTest = 'stayfi@test.test'; 44 | const emailTestNotSaved = 'stayfi-not-found@test.test'; 45 | const passwordTest = 'Stayfi Password'; 46 | 47 | beforeAll(async () => { 48 | await mongoose.connect(mongoDBTest, { 49 | useCreateIndex: true, 50 | useNewUrlParser: true, 51 | useUnifiedTopology: true 52 | }); 53 | }); 54 | 55 | afterAll(async () => { 56 | mongoose.connection.close(() => { }); 57 | }); 58 | 59 | it('Should create new user', async () => { 60 | expect.assertions(4); 61 | 62 | const newUserEntity: UserEntity = new UserEntity(emailTest, passwordTest); 63 | const userEntityRepository: UserEntityRepository = new UserEntityRepository(); 64 | const spy = jest.spyOn(userEntityRepository, 'create'); 65 | const newUserSaved = await userEntityRepository.create(newUserEntity); 66 | 67 | expect(spy).toHaveBeenCalled(); 68 | 69 | expect(newUserSaved).toMatchObject({ 70 | email: expect.any(String), 71 | password: expect.any(String) 72 | }); 73 | 74 | expect(newUserSaved.email).toBe(emailTest); 75 | expect(newUserSaved.password).toBe(passwordTest); 76 | }); 77 | 78 | it(`Should find user by mail ${emailTest}`, async () => { 79 | const userEntityRepository: UserEntityRepository = new UserEntityRepository(); 80 | const userTest = await userEntityRepository.findByEmail(emailTest); 81 | 82 | expect(userTest.email).toBe(emailTest); 83 | expect(userTest.password).toBe(passwordTest); 84 | }); 85 | 86 | it(`Should not find user by mail ${emailTestNotSaved}`, async () => { 87 | jest.setTimeout(10000); 88 | const userEntityRepository: UserEntityRepository = new UserEntityRepository(); 89 | try { 90 | await userEntityRepository.findByEmail(emailTestNotSaved); 91 | } catch (e) { 92 | expect(e.message).toEqual('user.notexist'); 93 | } 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /src/Components/User/InterfaceAdapters/createUser/tests/CreateUserController.spec.ts: -------------------------------------------------------------------------------- 1 | import CreateUserController from '../CreateUserController'; 2 | import CreateUserInteractor from '../../../UseCases/createUser/CreateUserInteractor'; 3 | import CreateUserControllerRequest from '../ICreateUserControllerRequest'; 4 | import CreateUserPresenter from '../CreateUserPresenter'; 5 | import ICreateUserRequestModel from '../../../UseCases/createUser/ICreateUserRequestModel'; 6 | import * as testIncludes from './CreateUserController.spec.inc'; 7 | 8 | describe('#CreateUserController', () => { 9 | 10 | let consoleInfos: Array = new Array(); 11 | console.info = function(info: string) { consoleInfos.push(info); }; 12 | 13 | beforeEach(function() { 14 | consoleInfos = []; 15 | }); 16 | 17 | it("Should create new CreateUserController succeed", () => { 18 | const createUserEntityRepository = new testIncludes.CreateUserEntityRepositoryTest(); 19 | const createUserView = new testIncludes.CreateUserViewTest(); 20 | const createUserController: CreateUserController = new CreateUserController( 21 | createUserEntityRepository, 22 | createUserView 23 | ); 24 | expect(createUserController).toMatchObject(new CreateUserController( 25 | createUserEntityRepository, 26 | createUserView 27 | )); 28 | }); 29 | 30 | it("Should getCreateUserInteractor return CreateUserInteractor", async () => { 31 | expect.assertions(2); 32 | const createUserEntityRepository = new testIncludes.CreateUserEntityRepositoryTest(); 33 | const createUserView = new testIncludes.CreateUserViewTest(); 34 | const createUserController: CreateUserController = new CreateUserController( 35 | createUserEntityRepository, 36 | createUserView 37 | ); 38 | const createUserControllerRequest: CreateUserControllerRequest = { 39 | email: 'email@email.test', 40 | password: 'password-test' 41 | } 42 | const spy = jest.spyOn(createUserController, 'execute'); 43 | try { 44 | await createUserController.execute(createUserControllerRequest); 45 | } catch (e) { 46 | expect(e.message).toBe('CreateUserViewTest.render not implemented (test only)'); 47 | } 48 | expect(spy).toHaveBeenCalled(); 49 | 50 | }); 51 | 52 | it("Should getCreateUserInteractor return CreateUserInteractor", () => { 53 | const createUserEntityRepository = new testIncludes.CreateUserEntityRepositoryTest(); 54 | const createUserView = new testIncludes.CreateUserViewTest(); 55 | const createUserController: CreateUserController = new CreateUserController( 56 | createUserEntityRepository, 57 | createUserView 58 | ); 59 | expect(createUserController['getCreateUserInteractor']()).toMatchObject(new CreateUserInteractor( 60 | createUserEntityRepository, 61 | new CreateUserPresenter(createUserView) 62 | )); 63 | }); 64 | 65 | it("Should getCreateUserRequestModelFromRequest return CreateUserControllerRequest", () => { 66 | const createUserEntityRepository = new testIncludes.CreateUserEntityRepositoryTest(); 67 | const createUserView = new testIncludes.CreateUserViewTest(); 68 | const createUserController: CreateUserController = new CreateUserController( 69 | createUserEntityRepository, 70 | createUserView 71 | ); 72 | const createUserRequestModel: ICreateUserRequestModel = { 73 | email: 'email@email.test', 74 | password: 'password-test' 75 | }; 76 | const createUserControllerRequest: CreateUserControllerRequest = { 77 | email: 'email@email.test', 78 | password: 'password-test' 79 | } 80 | expect(createUserController['getCreateUserRequestModelFromRequest'](createUserControllerRequest)).toMatchObject( 81 | createUserRequestModel 82 | ); 83 | }); 84 | 85 | }); 86 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/app/server/server.spec.ts: -------------------------------------------------------------------------------- 1 | import * as sinon from 'sinon'; 2 | import request from 'supertest'; 3 | import Server from './server.class'; 4 | import { appEnvironment } from '../_shared/app-environment.enum'; 5 | import { ServerOptionsInterface } from './server.interface'; 6 | import config from '../../config/config'; 7 | 8 | const sandbox = sinon.createSandbox(); 9 | 10 | describe('#Server config test', function() { 11 | let serverConsoleLogStub: sinon.SinonStub<[any?, ...any[]], void>; 12 | beforeEach(function() { 13 | serverConsoleLogStub = sinon.stub(console, 'log'); 14 | }); 15 | 16 | it('Should test port setted to 3100', async function() { 17 | const serverAppOptions = getServerAppOptions(); 18 | const serverTest: Server = getInitializedServer(serverAppOptions); 19 | expect(serverTest.options.port).toEqual(3100); 20 | }); 21 | 22 | it(`Should console.log('√ Logger enabled') when logger enabled`, async function() { 23 | sandbox.stub(process, 'env').value({ LOGGER: true }); 24 | const serverAppOptions = getServerAppOptions(); 25 | const serverTest: Server = getInitializedServer(serverAppOptions); 26 | const loggerEnabled = serverTest['loggerInformation'](); 27 | // @ts-ignore 28 | expect(console.log.calledWith('\x1b[33m%s\x1b[0m', '√ Logger enabled')).toBeTruthy(); 29 | expect(loggerEnabled).toBeTruthy(); 30 | }); 31 | 32 | it(`Should NO console.log('√ Logger enabled') when logger isabled`, async function() { 33 | sandbox.stub(process, 'env').value({ LOGGER: false }); 34 | const serverAppOptions = getServerAppOptions(); 35 | const serverTest: Server = getInitializedServer(serverAppOptions); 36 | const loggerEnabled = serverTest['loggerInformation'](); 37 | // @ts-ignore 38 | expect(console.log.calledWith('\x1b[33m%s\x1b[0m', '√ Logger enabled')).toBeFalsy(); 39 | expect(loggerEnabled).toBeFalsy(); 40 | }); 41 | 42 | it(`Should console.log('√ Staging mode') when staging environment`, async function() { 43 | sandbox.stub(process, 'env').value({ NODE_ENV: appEnvironment.STAGING }); 44 | const serverAppOptions = getServerAppOptions(); 45 | const serverTest: Server = getInitializedServer(serverAppOptions); 46 | serverTest['nodeEnvInformation'](); 47 | // @ts-ignore 48 | expect(console.log.calledWith('\x1b[36m%s\x1b[0m', '√ Staging mode')).toBeTruthy(); 49 | }); 50 | 51 | it(`Should console.log('√ Production mode') when production environment`, async function() { 52 | sandbox.stub(process, 'env').value({ NODE_ENV: appEnvironment.PROD }); 53 | const serverAppOptions = getServerAppOptions(); 54 | const serverTest: Server = getInitializedServer(serverAppOptions); 55 | serverTest['nodeEnvInformation'](); 56 | // @ts-ignore 57 | expect(console.log.calledWith('\x1b[36m%s\x1b[0m', '√ Production mode')).toBeTruthy(); 58 | }); 59 | 60 | it(`When started, should index return status 200`, async function() { 61 | sandbox.stub(process, 'env').value({ NODE_ENV: appEnvironment.TEST }); 62 | const serverAppOptions = getServerAppOptions(); 63 | const serverTest: Server = getInitializedServer(serverAppOptions); 64 | serverTest.start(); 65 | const res = await request( 66 | `http://localhost:${serverTest.options.port}` 67 | ).get('/'); 68 | expect(res.status).toEqual(200); 69 | serverTest.close(() => { }); 70 | }); 71 | 72 | afterEach(function() { 73 | serverConsoleLogStub.restore(); 74 | }); 75 | }); 76 | 77 | function getInitializedServer(options: ServerOptionsInterface): Server { 78 | const serverTest = new Server(options); 79 | serverTest.initServer(); 80 | return serverTest; 81 | } 82 | 83 | function getServerAppOptions(): ServerOptionsInterface { 84 | const appConfig = config(); 85 | const serverAppOptions: ServerOptionsInterface = { 86 | env: appConfig.get('env') as appEnvironment, 87 | logger: { 88 | enabled: appConfig.get('logger') 89 | }, 90 | port: appConfig.get('port') 91 | }; 92 | 93 | return serverAppOptions; 94 | } -------------------------------------------------------------------------------- /src/Components/User/UseCases/createUser/tests/CreateUserInteractor.spec.ts: -------------------------------------------------------------------------------- 1 | import CreateUserInteractor from '../CreateUserInteractor'; 2 | import ICreateUserEntityGateway from '../ICreateUserEntityGateway'; 3 | import ICreateUserRequestModel from '../ICreateUserRequestModel'; 4 | import IPresenterOuputBoundary from '../../../../../ComponentInterfaces/InterfaceAdapters/IPresenterOuputBoundary'; 5 | import IView from '../../../../../ComponentInterfaces/InterfaceAdapters/IView'; 6 | import * as testIncludes from './CreateUserInteractor.spec.inc'; 7 | import ICreateUserViewModel from '../../../InterfaceAdapters/createUser/ICreateUserViewModel'; 8 | 9 | describe('#CreateUserInteractor', () => { 10 | 11 | let consoleInfos: Array = new Array(); 12 | console.info = function(info: string) { consoleInfos.push(info); }; 13 | const createUserRequestModel: ICreateUserRequestModel = testIncludes.getCreateUserRequestModelTest(); 14 | const createUserExistRequestModel: ICreateUserRequestModel = testIncludes.getCreateUserExistRequestModel(); 15 | const entityRepository: ICreateUserEntityGateway = testIncludes.getCreateUserEntityRepository(createUserRequestModel); 16 | const entityRepositorySaveFailing: ICreateUserEntityGateway = testIncludes.getCreateUserEntityRepositorySaveFailing(createUserRequestModel); 17 | const view: IView = testIncludes.getTestView(); 18 | const presenter: IPresenterOuputBoundary = testIncludes.getTestPresenter(view); 19 | 20 | beforeEach(function() { 21 | consoleInfos = []; 22 | }); 23 | 24 | it('should new user execute responseModel userCreated being true', async () => { 25 | const userInteractor: CreateUserInteractor = new CreateUserInteractor(entityRepository, presenter); 26 | await userInteractor.execute(createUserRequestModel); 27 | expect(((consoleInfos[0]) as ICreateUserViewModel).userCreated).toBe(true); 28 | }) 29 | 30 | it('should return error when email and password is empty', async () => { 31 | expect.assertions(1); 32 | try { 33 | const userInteractor: CreateUserInteractor = new CreateUserInteractor(entityRepository, presenter); 34 | await userInteractor.execute({ 35 | email: '', 36 | password: '' 37 | }); 38 | } catch (e) { 39 | expect(e.message).toBe('CreateUserInteractor.email_and_password.empty'); 40 | } 41 | }) 42 | 43 | it('should return error when email is empty', async () => { 44 | expect.assertions(1); 45 | try { 46 | const userInteractor: CreateUserInteractor = new CreateUserInteractor(entityRepository, presenter); 47 | await userInteractor.execute({ 48 | email: '', 49 | password: 'passwordtest' 50 | }); 51 | } catch (e) { 52 | expect(e.message).toBe('CreateUserInteractor.email.empty'); 53 | } 54 | }) 55 | 56 | it('should return error when password is empty', async () => { 57 | expect.assertions(1); 58 | try { 59 | const userInteractor: CreateUserInteractor = new CreateUserInteractor(entityRepository, presenter); 60 | await userInteractor.execute({ 61 | email: 'email@mail.test', 62 | password: '' 63 | }); 64 | } catch (e) { 65 | expect(e.message).toBe('CreateUserInteractor.password.empty'); 66 | } 67 | }) 68 | 69 | it('should new user throw error when email exist', async () => { 70 | expect.assertions(1); 71 | try { 72 | const userInteractor: CreateUserInteractor = new CreateUserInteractor(entityRepository, presenter); 73 | await userInteractor.execute(createUserExistRequestModel); 74 | } catch (e) { 75 | expect(e.message).toBe('CreateUserInteractor.newuser.exist'); 76 | } 77 | }) 78 | 79 | it('should new user throw error when repository save failed', async () => { 80 | expect.assertions(1); 81 | try { 82 | const userInteractor: CreateUserInteractor = new CreateUserInteractor(entityRepositorySaveFailing, presenter); 83 | await userInteractor.execute(createUserRequestModel); 84 | } catch (e) { 85 | expect(e.message).toBe('CreateUserInteractor.repository.create.error'); 86 | } 87 | }) 88 | }); 89 | -------------------------------------------------------------------------------- /src/FrameworkAndDrivers/Web/express/app.server-start.spec.inc.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import treeKill from 'tree-kill'; 3 | 4 | export default function serverStartTests() { 5 | describe('#Start: Server start test ', () => { 6 | const serverTestPort = 3003; 7 | const serverTestLoggerPort = 3004; 8 | const serverTestProductionDisabledPort = 3005; 9 | const serverTestProductionEnabledPort = 3006; 10 | let serverTestToBeKill: any; 11 | 12 | beforeAll(() => { 13 | const NPM_CMD: string = getNpmCmd(); 14 | require('child_process').spawn(NPM_CMD, ['run', `build:prod`]); 15 | require('child_process').spawn(NPM_CMD, ['run', `assets:view`]); 16 | }); 17 | 18 | it(`Start Server should log on console 'Server is running on port ${serverTestPort}'`, done => { 19 | let goDone = true; 20 | const serverTest = getServerRunning(serverTestPort, 'prod'); 21 | serverTestToBeKill = serverTest; 22 | 23 | serverTest.stdout.on('data', (data: {}) => { 24 | if ( 25 | typeof data === 'string' && 26 | data.indexOf(`Server is running on port ${serverTestPort}`) >= 0 && 27 | goDone 28 | ) { 29 | goDone = false; 30 | return done(); 31 | } 32 | }); 33 | }); 34 | 35 | it(`When started, should http://localhost:${serverTestPort} return status 200`, async () => { 36 | const res = await request(`http://localhost:${serverTestPort}`).get('/'); 37 | 38 | expect(res.status).toEqual(200); 39 | }); 40 | 41 | it('Server should be killed', done => { 42 | treeKill(serverTestToBeKill.pid, 'SIGTERM', done); 43 | }); 44 | 45 | // tslint:disable-next-line: max-line-length 46 | it(`Start Server without logger should not log 'Logger enabled'`, done => { 47 | const serverTestLoggerDisabled = getServerRunning( 48 | serverTestLoggerPort, 49 | 'prod' 50 | ); 51 | 52 | serverTestLoggerDisabled.stdout.on('data', (data: {}) => { 53 | if (typeof data === 'string' && data.indexOf(`Logger enabled`) >= 0) { 54 | throw new Error('Logger not disabled'); 55 | } 56 | if ( 57 | typeof data === 'string' && 58 | data.indexOf(`Server is running on port ${serverTestLoggerPort}`) >= 0 59 | ) { 60 | treeKill(serverTestLoggerDisabled.pid, 'SIGTERM', done); 61 | } 62 | }); 63 | }); 64 | 65 | it(`Start:dev should not log 'Production mode'`, done => { 66 | const serverTestProductionDisabled = getServerRunning( 67 | serverTestProductionDisabledPort, 68 | 'dev' 69 | ); 70 | 71 | serverTestProductionDisabled.stdout.on('data', (data: {}) => { 72 | if (typeof data === 'string' && data.indexOf(`Production mode`) >= 0) { 73 | throw new Error('Production mode activated'); 74 | } 75 | if ( 76 | typeof data === 'string' && 77 | data.indexOf( 78 | `Server is running on port ${serverTestProductionDisabledPort}` 79 | ) >= 0 80 | ) { 81 | treeKill(serverTestProductionDisabled.pid, 'SIGTERM', done); 82 | } 83 | }); 84 | }); 85 | 86 | it(`Start:prod should log 'Production mode'`, done => { 87 | jest.setTimeout(30000); 88 | let isDone = false; 89 | setTimeout(() => { 90 | if (!isDone) done(new Error('Production mode test: Timeout')); 91 | }, 10000); 92 | const serverTestProductionEnabled = getServerRunning( 93 | serverTestProductionEnabledPort, 94 | 'prod' 95 | ); 96 | let isProduction = false; 97 | 98 | serverTestProductionEnabled.stdout.on('data', (data: {}) => { 99 | if (typeof data === 'string' && data.indexOf(`Production mode`) >= 0) { 100 | isProduction = true; 101 | } 102 | if ( 103 | typeof data === 'string' && 104 | data.indexOf( 105 | `Server is running on port ${serverTestProductionEnabledPort}` 106 | ) >= 0 107 | ) { 108 | expect(isProduction).toBeTruthy(); 109 | isDone = true; 110 | treeKill(serverTestProductionEnabled.pid, 'SIGTERM', done); 111 | } 112 | }); 113 | }); 114 | }); 115 | } 116 | 117 | function getServerRunning(port: number, env: string) { 118 | const NPM_CMD: string = getNpmCmd(); 119 | const serverRunning = require('child_process').spawn(NPM_CMD, [ 120 | 'run', 121 | `start:${env}`, 122 | '--', 123 | '--port', 124 | port 125 | ]); 126 | serverRunning.stdout.setEncoding('utf8'); 127 | serverRunning.stderr.setEncoding('utf8'); 128 | 129 | return serverRunning; 130 | } 131 | function getNpmCmd(): string { 132 | let NPM_CMD: string = 'npm'; 133 | if (process.platform === 'win32') { 134 | NPM_CMD = 'npm.cmd'; 135 | } 136 | return NPM_CMD; 137 | } 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Uncle Bob's clean architecture implementation in Typescript, with Express as Web Detail 2 | 3 | version: 'v1.0.0' 4 | 5 | ## Description 6 | 7 | Based on Uncle Bob's Clean Architecture, i tried to an implementation in Typescript with "screaming architecture" in folders and files. 8 | 9 | First, i use separated components with an example : 10 | 11 | ``` 12 | ./src/Components/User 13 | ``` 14 | 15 | As Web Detail, i used my own "Node.js server with express framework, craftsmanship way" : 16 | 17 | - Clean project structure 18 | 19 | - Tests Driven Development 20 | 21 | - Test Coverage 22 | 23 | - Copy/paste detection 24 | 25 | ``` 26 | ./src/FrameworkAndDrivers/Web/express 27 | ``` 28 | 29 | https://github.com/Stayfi/nodejs-server 30 | 31 | ## Project structure 32 | 33 | ./scripts: Scripts used by package.json 34 | 35 | ./src: Application source code 36 | 37 | --- 38 | 39 | ./src/ComponentInterfaces: Clean architecture Interfaces, to represent the Uncle Bob's interfaces schema 40 | 41 | ![Uncle Bob's schema](https://raw.githubusercontent.com/Stayfi/clean-architecture-ts/develop/img/uncle_bob_interfaces_schema.png) 42 | 43 | ![Folder ComponentInterfaces](https://raw.githubusercontent.com/Stayfi/clean-architecture-ts/develop/img/folder_componentinterfaces.png) 44 | 45 | --- 46 | 47 | ./src/Components: Components, as "Packages by feature" with all feature use cases 48 | 49 | ./src/Components/User/Entities 50 | 51 | ./src/Components/User/InterfaceAdapters 52 | 53 | ./src/Components/UseCases/User 54 | 55 | ./src/Components/UseCases/createUser: "create user" use case for "User" feature 56 | 57 | ![Folder ComponentInterfaces](https://raw.githubusercontent.com/Stayfi/clean-architecture-ts/develop/img/folder_components_user.png) 58 | 59 | --- 60 | 61 | ./src/FrameworkAndDrivers 62 | 63 | ./src/FrameworkAndDrivers/DB/MongoDB: MongoDB implementatin with connexion and repositories 64 | 65 | ![Folder ComponentInterfaces](https://raw.githubusercontent.com/Stayfi/clean-architecture-ts/develop/img/folder_frameworkanddrivers_db.png) 66 | 67 | --- 68 | 69 | ./src/FrameworkAndDrivers/Web/express: Node.js server with express ( https://github.com/Stayfi/nodejs-server ) 70 | 71 | ![Folder ComponentInterfaces](https://raw.githubusercontent.com/Stayfi/clean-architecture-ts/develop/img/folder_frameworkanddrivers_web.png) 72 | 73 | --- 74 | 75 | ## Implementation schema 76 | 77 | ![Folder ComponentInterfaces](https://raw.githubusercontent.com/Stayfi/clean-architecture-ts/develop/img/uncle_bob_layers_schema.png) 78 | 79 | * Initialize the view to render viewModel datas with nunjucks template 80 | ``` 81 | ./src/FrameworkAndDrivers/Web/express/app/UsesCases/User/createUser/views/createUserView.ts 82 | ``` 83 | 84 | * Entity gateway implementation with MongoDB 85 | ``` 86 | ./src/FrameworkAndDrivers/DB/MongoDB/User/user.model.ts 87 | ``` 88 | 89 | * Deliver initialize controller with entity gateway and view, then execute controller with controllerRequestModel sended by Web implementation (express) 90 | ``` 91 | ./src/FrameworkAndDrivers/Web/express/app/UsesCases/User/createUser/delivers/createUserDeliver.ts 92 | ``` 93 | 94 | * Controller initialize presenter with view received, initialize interactor with entity gateway and presenter, then execute interactor with controllerRequest datas mapped to requestUserModel 95 | ``` 96 | ./src/Components/User/InterfaceAdapters/createUserController.ts 97 | ``` 98 | 99 | * Interactor execution do the business job with entity, entity gateway implementation, datas and then presente responseModel data 100 | ``` 101 | ./src/Components/User/UseCases/createUser/CreateUserInteractor.ts 102 | ``` 103 | 104 | * Presenter, execute view renderer with responseModel data mapped to viewModel data 105 | ``` 106 | ./src/Components/User/InterfaceAdapters/createUserPresenter.ts 107 | ``` 108 | 109 | ![Folder ComponentInterfaces](https://raw.githubusercontent.com/Stayfi/clean-architecture-ts/develop/img/uncle_bob_implementation.png) 110 | 111 | ## Install 112 | 113 | ### Dev installation 114 | 115 | Go to the project directory and do 116 | 117 | ```bash 118 | $ npm run install:dev 119 | ``` 120 | 121 | ### Production installation 122 | 123 | Go to the project directory and do 124 | 125 | ```bash 126 | $ npm run install:prod 127 | ``` 128 | 129 | ## Start the project 130 | 131 | ### Dev running 132 | 133 | ```bash 134 | $ npm run dev 135 | ``` 136 | 137 | Open your browser to : http://localhost:3000 138 | 139 | ### Prod running 140 | 141 | ```bash 142 | $ npm run start 143 | ``` 144 | 145 | Open your browser to : http://localhost:8080 146 | 147 | ## Running tests 148 | 149 | ```bash 150 | $ npm run test 151 | ``` 152 | 153 | Or simply : 154 | 155 | ```bash 156 | $ jest 157 | ``` 158 | 159 | ![Tests results](https://raw.githubusercontent.com/Stayfi/clean-architecture-ts/develop/img/tests_results.png) 160 | 161 | ## Running coverage 162 | 163 | ```bash 164 | $ npm run coverage 165 | ``` 166 | 167 | ## Running copy/paste detection 168 | 169 | ```bash 170 | $ npm run jscpd 171 | ``` 172 | 173 | ## Running tslint 174 | 175 | ```bash 176 | $ npm run tslint 177 | ``` 178 | 179 | With fix : 180 | 181 | ```bash 182 | $ npm run tslint:fix 183 | ``` 184 | 185 | ## Clean the project 186 | 187 | It remove all folders generated by build, test and coverage. 188 | 189 | ```bash 190 | $ npm run clean 191 | ``` 192 | 193 | ## Package.json scripts 194 | 195 | - "assets:config": copy config files to dist folder 196 | 197 | - "assets:views": copy views assets to dist folder (nunjunks views) 198 | 199 | - "install:dev": install all dependencies and dev dependencies 200 | 201 | - "build:dev": run "assets:dist" and typescript compiler with watching 202 | 203 | - "start:dev": running nodemon to monitor dist/app.js 204 | 205 | - "install:prod": install only dependencies without dev dependencies 206 | --------------------------------------------------------------------------------