├── .gitignore ├── README.md ├── nest-cli.json ├── package-lock.json ├── package.json ├── src ├── app.module.ts ├── configuration │ └── index.ts ├── controllers │ ├── app.controller.ts │ ├── author.controller.ts │ ├── book.controller.ts │ ├── genre.controller.ts │ └── index.ts ├── core │ ├── abstracts │ │ ├── crm-services.service.ts │ │ ├── data-services.abstract.ts │ │ ├── generic-repository.abstract.ts │ │ └── index.ts │ ├── dtos │ │ ├── author.dto.ts │ │ ├── book.dto.ts │ │ ├── create-book-response.dto.ts │ │ ├── genre.dto.ts │ │ └── index.ts │ ├── entities │ │ ├── author.entity.ts │ │ ├── book.entity.ts │ │ ├── genre.entity.ts │ │ └── index.ts │ └── index.ts ├── frameworks │ ├── crm-services │ │ └── salesforce │ │ │ ├── salesforce-service.module.ts │ │ │ └── salesforce-service.service.ts │ └── data-services │ │ └── mongo │ │ ├── model │ │ ├── author.model.ts │ │ ├── book.model.ts │ │ ├── genre.model.ts │ │ └── index.ts │ │ ├── mongo-data-services.module.ts │ │ ├── mongo-data-services.service.ts │ │ └── mongo-generic-repository.ts ├── main.ts ├── services │ ├── crm-services │ │ └── crm-services.module.ts │ ├── data-services │ │ └── data-services.module.ts │ └── index.ts └── use-cases │ ├── author │ ├── author-factory.service.ts │ ├── author-use-cases.module.ts │ └── author.use-case.ts │ ├── book │ ├── book-factory.service.ts │ ├── book-use-cases.module.ts │ ├── book.use-case.ts │ └── index.ts │ └── genre │ ├── genre-factory.service.ts │ ├── genre-use-cases.module.ts │ └── genre.use-case.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clean Architecture With NestJS 2 | ## Description 3 | It's been a while since my last article on [how to implement clean architecture on Node.js applications](https://betterprogramming.pub/node-clean-architecture-deep-dive-ab68e523554b), [git repo](https://github.com/royib/clean-architecture-node). 4 | After working with NestJS and TypeScript I thought it was a good idea to come back and write a new article on the subject. This time we are going to take the super power of typescript and the methodologies and tools of NestJS and harness them to our benefits. 5 | 6 | Coming from a background of object-oriented languages, it was natural that we 7 | wanted to keep all our [SOLID](https://en.wikipedia.org/wiki/SOLID) principles 8 | in our new and shiny node API. 9 | 10 | Like any other architecture, we had to make different trade-offs in the 11 | implementation. 12 | 13 | We had to be careful not to over-engineer or over-abstract our layers, but 14 | rather keep it as flexible as needed. 15 | 16 | In recent years, we have implemented [clean 17 | architecture](http://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) 18 | by Robert C. Martin (Uncle Bob) on our API projects. This architecture attempts 19 | to integrate some of the leading modern architecture like [Hexagonal 20 | Architecture](http://alistair.cockburn.us/Hexagonal+architecture), [Onion 21 | Architecture](http://jeffreypalermo.com/blog/the-onion-architecture-part-1/), 22 | [Screaming 23 | Architecture](http://blog.cleancoders.com/2011-09-30-Screaming-Architecture) 24 | into one main architecture. It aims to achieve good separation of concerns. Like 25 | most architecture, it also aims to make the application more flexible to 26 | inevitable changes in client requirements (which always happens). 27 | 28 | ![](https://fullstackroyhome.files.wordpress.com/2019/03/cleanarchitecture.jpg) 29 | 30 | clean architecture diagram - dependencies direction are from outside in. 31 | [source](http://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) 32 | 33 | This diagram is taken from the [official 34 | article](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) 35 | by Robert C. Martin. I recommend reading his article before diving into the node 36 | implementation. This is the best source knowledge about this architecture. 37 | 38 | Few words about this diagram and how to read it: 39 | 40 | - Dependency - the dependency direction is from the outside in. meaning that 41 | the Entities layer is independent and the Frameworks layer depend on all the 42 | other layers. 43 | 44 | - Entities - contains all the business entities that construct our 45 | application. 46 | 47 | - Use Cases - This is where we centralize our logic. Each use case 48 | orchestrates all of the logic for a specific business use case. 49 | 50 | - Controllers and Presenters - Our controller, presenters, and gateways are 51 | intermediate layers. You can think of them as an entry and exit gates to the 52 | use cases . 53 | 54 | - Frameworks - This layer has all the specific implementations. The database, 55 | the web frameworks, error handling etc. 56 | Robert C. Martin describes this layer : 57 | *“This layer is where all the details go. The Web is a detail. The database 58 | is a detail. We keep these things on the outside where they can do little 59 | harm.”* 60 | 61 | In this point you will probably say to yourself “database is in outer layer, 62 | database is a detail ???” database is supposed to be my core layer. 63 | 64 | I love this architecture because it has a smart motivation behind it. Instead of 65 | focusing on frameworks and tools, it focuses on the business logic of the 66 | application. This architecture is framework independent (or as much as it can 67 | be). This means it doesn’t matter which database, frameworks, UI, external 68 | services you are using, the entities and the business logic of the application 69 | will always stay the same. We can change all of the above without changing our 70 | logic. This is what makes it so easy to test applications built on this 71 | architecture. Don’t worry if you don’t understand this yet, we will explore it 72 | step-by-step. 73 | ## Getting Started 74 | ### Dependencies 75 | * mongoDb - you need to provide a valid mongDb connection string. 76 | add a new environment variable named CLEAN_NEST_MONGO_CONNECTION_STRING 77 | ``` 78 | export CLEAN_NEST_MONGO_CONNECTION_STRING='valid mongoDB connection string' 79 | ``` 80 | ### Installing 81 | ``` 82 | npm install 83 | ``` 84 | ### Executing program 85 | 86 | ``` 87 | npm start 88 | ``` 89 | ## Authors 90 | Royi Benita 91 | ## Version History 92 | * 1.0 93 | ## License 94 | This project is licensed under the [NAME HERE] License - see the LICENSE.md file for details 95 | ## Acknowledgments 96 | Inspiration, code snippets, etc. 97 | * [clean-architecture-node](https://github.com/royib/clean-architecture-node) 98 | * [Node Clean Architecture — Deep Dive](https://betterprogramming.pub/node-clean-architecture-deep-dive-ab68e523554b) 99 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clean-nest", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^8.0.0", 25 | "@nestjs/core": "^8.0.0", 26 | "@nestjs/mapped-types": "^1.0.1", 27 | "@nestjs/mongoose": "^9.0.2", 28 | "@nestjs/platform-express": "^8.0.0", 29 | "class-validator": "^0.13.2", 30 | "mongoose": "^6.1.3", 31 | "mongoose-autopopulate": "^0.16.0", 32 | "reflect-metadata": "^0.1.13", 33 | "rimraf": "^3.0.2", 34 | "rxjs": "^7.2.0" 35 | }, 36 | "devDependencies": { 37 | "@nestjs/cli": "^8.0.0", 38 | "@nestjs/schematics": "^8.0.0", 39 | "@nestjs/testing": "^8.0.0", 40 | "@types/express": "^4.17.13", 41 | "@types/jest": "27.0.2", 42 | "@types/node": "^16.11.17", 43 | "@types/supertest": "^2.0.11", 44 | "@typescript-eslint/eslint-plugin": "^5.0.0", 45 | "@typescript-eslint/parser": "^5.0.0", 46 | "eslint": "^8.0.1", 47 | "eslint-config-prettier": "^8.3.0", 48 | "eslint-plugin-prettier": "^4.0.0", 49 | "jest": "^27.2.5", 50 | "prettier": "^2.3.2", 51 | "source-map-support": "^0.5.20", 52 | "supertest": "^6.1.3", 53 | "ts-jest": "^27.0.3", 54 | "ts-loader": "^9.2.3", 55 | "ts-node": "^10.0.0", 56 | "tsconfig-paths": "^3.10.1", 57 | "typescript": "^4.3.5" 58 | }, 59 | "jest": { 60 | "moduleFileExtensions": [ 61 | "js", 62 | "json", 63 | "ts" 64 | ], 65 | "rootDir": "src", 66 | "testRegex": ".*\\.spec\\.ts$", 67 | "transform": { 68 | "^.+\\.(t|j)s$": "ts-jest" 69 | }, 70 | "collectCoverageFrom": [ 71 | "**/*.(t|j)s" 72 | ], 73 | "coverageDirectory": "../coverage", 74 | "testEnvironment": "node" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { 3 | AppController, 4 | BookController, 5 | AuthorController, 6 | GenreController, 7 | } from './controllers'; 8 | import { DataServicesModule } from './services/data-services/data-services.module'; 9 | import { BookUseCasesModule } from './use-cases/book/book-use-cases.module'; 10 | import { AuthorUseCasesModule } from './use-cases/author/author-use-cases.module'; 11 | import { GenreUseCasesModule } from './use-cases/genre/genre-use-cases.module'; 12 | import { CrmServicesModule } from './services/crm-services/crm-services.module'; 13 | 14 | @Module({ 15 | imports: [ 16 | DataServicesModule, 17 | BookUseCasesModule, 18 | AuthorUseCasesModule, 19 | GenreUseCasesModule, 20 | CrmServicesModule, 21 | ], 22 | controllers: [ 23 | AppController, 24 | BookController, 25 | AuthorController, 26 | GenreController, 27 | ], 28 | providers: [], 29 | }) 30 | export class AppModule {} 31 | -------------------------------------------------------------------------------- /src/configuration/index.ts: -------------------------------------------------------------------------------- 1 | export const DATA_BASE_CONFIGURATION = { 2 | mongoConnectionString: process.env 3 | .CLEAN_NEST_MONGO_CONNECTION_STRING as string, 4 | }; 5 | -------------------------------------------------------------------------------- /src/controllers/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | 3 | @Controller() 4 | export class AppController { 5 | constructor() {} 6 | 7 | @Get() 8 | getHello(): string { 9 | return 'hello'; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/controllers/author.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param, Post, Body, Put } from '@nestjs/common'; 2 | import { CreateAuthorDto, UpdateAuthorDto } from '../core/dtos'; 3 | import { AuthorUseCases } from '../use-cases/author/author.use-case'; 4 | 5 | @Controller('api/author') 6 | export class AuthorController { 7 | constructor(private authorUseCases: AuthorUseCases) {} 8 | 9 | @Get() 10 | async getAll() { 11 | return this.authorUseCases.getAllAuthors(); 12 | } 13 | 14 | @Get(':id') 15 | async getById(@Param('id') id: any) { 16 | return this.authorUseCases.getAuthorById(id); 17 | } 18 | 19 | @Post() 20 | createAuthor(@Body() authorDto: CreateAuthorDto) { 21 | return this.authorUseCases.createAuthor(authorDto); 22 | } 23 | 24 | @Put(':id') 25 | updateAuthor( 26 | @Param('id') authorId: string, 27 | @Body() updateAuthorDto: UpdateAuthorDto, 28 | ) { 29 | return this.authorUseCases.updateAuthor(authorId, updateAuthorDto); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/controllers/book.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param, Post, Body, Put } from '@nestjs/common'; 2 | import { CreateBookDto, UpdateBookDto, CreateBookResponseDto } from '../core/dtos'; 3 | import { BookUseCases, BookFactoryService } from '../use-cases/book'; 4 | 5 | @Controller('api/book') 6 | export class BookController { 7 | constructor( 8 | private bookUseCases: BookUseCases, 9 | private bookFactoryService: BookFactoryService, 10 | ) {} 11 | 12 | @Get() 13 | async getAll() { 14 | return this.bookUseCases.getAllBooks(); 15 | } 16 | 17 | @Get(':id') 18 | async getById(@Param('id') id: any) { 19 | return this.bookUseCases.getBookById(id); 20 | } 21 | 22 | @Post() 23 | async createBook(@Body() bookDto: CreateBookDto) : Promise { 24 | const createBookResponse = new CreateBookResponseDto(); 25 | try { 26 | const book = this.bookFactoryService.createNewBook(bookDto); 27 | const createdBook = await this.bookUseCases.createBook(book); 28 | 29 | createBookResponse.success = true; 30 | createBookResponse.createdBook = createdBook; 31 | } catch (error) { 32 | // report and log error 33 | createBookResponse.success = false; 34 | } 35 | 36 | return createBookResponse; 37 | } 38 | 39 | @Put(':id') 40 | updateBook( 41 | @Param('id') bookId: string, 42 | @Body() updateBookDto: UpdateBookDto, 43 | ) { 44 | const book = this.bookFactoryService.updateBook(updateBookDto); 45 | return this.bookUseCases.updateBook(bookId, book); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/controllers/genre.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param, Post, Body, Put } from '@nestjs/common'; 2 | import { CreateGenreDto, UpdateGenreDto } from '../core/dtos'; 3 | import { GenreUseCases } from '../use-cases/genre/genre.use-case'; 4 | 5 | @Controller('api/genre') 6 | export class GenreController { 7 | constructor(private genreUseCases: GenreUseCases) {} 8 | 9 | @Get() 10 | async getAll() { 11 | return this.genreUseCases.getAllGenres(); 12 | } 13 | 14 | @Get(':id') 15 | async getById(@Param('id') id: any) { 16 | return this.genreUseCases.getGenreById(id); 17 | } 18 | 19 | @Post() 20 | createGenre(@Body() genreDto: CreateGenreDto) { 21 | return this.genreUseCases.createGenre(genreDto); 22 | } 23 | 24 | @Put(':id') 25 | updateGenre( 26 | @Param('id') genreId: string, 27 | @Body() updateGenreDto: UpdateGenreDto, 28 | ) { 29 | return this.genreUseCases.updateGenre(genreId, updateGenreDto); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.controller'; 2 | export * from './book.controller'; 3 | export * from './author.controller'; 4 | export * from './genre.controller'; 5 | -------------------------------------------------------------------------------- /src/core/abstracts/crm-services.service.ts: -------------------------------------------------------------------------------- 1 | import { Book } from '../entities'; 2 | 3 | export abstract class ICrmServices { 4 | abstract bookAdded(book: Book): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /src/core/abstracts/data-services.abstract.ts: -------------------------------------------------------------------------------- 1 | import { Author, Book, Genre } from '../entities'; 2 | import { IGenericRepository } from './generic-repository.abstract'; 3 | 4 | export abstract class IDataServices { 5 | abstract authors: IGenericRepository; 6 | 7 | abstract books: IGenericRepository; 8 | 9 | abstract genres: IGenericRepository; 10 | } 11 | -------------------------------------------------------------------------------- /src/core/abstracts/generic-repository.abstract.ts: -------------------------------------------------------------------------------- 1 | export abstract class IGenericRepository { 2 | abstract getAll(): Promise; 3 | 4 | abstract get(id: string): Promise; 5 | 6 | abstract create(item: T): Promise; 7 | 8 | abstract update(id: string, item: T); 9 | } 10 | -------------------------------------------------------------------------------- /src/core/abstracts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './data-services.abstract'; 2 | export * from './generic-repository.abstract'; 3 | export * from './crm-services.service'; 4 | -------------------------------------------------------------------------------- /src/core/dtos/author.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsNotEmpty } from 'class-validator'; 2 | import { PartialType } from '@nestjs/mapped-types'; 3 | 4 | export class CreateAuthorDto { 5 | @IsString() 6 | @IsNotEmpty() 7 | firstName: string; 8 | 9 | @IsString() 10 | @IsNotEmpty() 11 | lastName: string; 12 | } 13 | 14 | export class UpdateAuthorDto extends PartialType(CreateAuthorDto) {} 15 | -------------------------------------------------------------------------------- /src/core/dtos/book.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsNotEmpty, IsDate } from 'class-validator'; 2 | import { PartialType } from '@nestjs/mapped-types'; 3 | 4 | export class CreateBookDto { 5 | @IsString() 6 | @IsNotEmpty() 7 | title: string; 8 | 9 | @IsNotEmpty() 10 | authorId: any; 11 | 12 | @IsNotEmpty() 13 | genreId: any; 14 | 15 | @IsDate() 16 | publishDate: Date; 17 | } 18 | 19 | export class UpdateBookDto extends PartialType(CreateBookDto) {} 20 | -------------------------------------------------------------------------------- /src/core/dtos/create-book-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { Book } from '../entities'; 2 | 3 | export class CreateBookResponseDto { 4 | success: boolean; 5 | 6 | createdBook: Book; 7 | } 8 | -------------------------------------------------------------------------------- /src/core/dtos/genre.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsNotEmpty } from 'class-validator'; 2 | import { PartialType } from '@nestjs/mapped-types'; 3 | 4 | export class CreateGenreDto { 5 | @IsString() 6 | @IsNotEmpty() 7 | name: string; 8 | } 9 | 10 | export class UpdateGenreDto extends PartialType(CreateGenreDto) {} 11 | -------------------------------------------------------------------------------- /src/core/dtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './book.dto'; 2 | export * from './author.dto'; 3 | export * from './genre.dto'; 4 | export * from './create-book-response.dto'; 5 | -------------------------------------------------------------------------------- /src/core/entities/author.entity.ts: -------------------------------------------------------------------------------- 1 | export class Author { 2 | firstName: string; 3 | 4 | lastName: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/core/entities/book.entity.ts: -------------------------------------------------------------------------------- 1 | import { Author, Genre } from './'; 2 | 3 | export class Book { 4 | 5 | title: string; 6 | 7 | author: Author; 8 | 9 | genre: Genre; 10 | 11 | publishDate: Date; 12 | } 13 | -------------------------------------------------------------------------------- /src/core/entities/genre.entity.ts: -------------------------------------------------------------------------------- 1 | export class Genre { 2 | name: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/core/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './author.entity'; 2 | export * from './book.entity'; 3 | export * from './genre.entity'; 4 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './abstracts'; 2 | export * from './entities'; 3 | -------------------------------------------------------------------------------- /src/frameworks/crm-services/salesforce/salesforce-service.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ICrmServices } from '../../../core'; 3 | 4 | import { SalesforceService } from './salesforce-service.service'; 5 | 6 | @Module({ 7 | providers: [ 8 | { 9 | provide: ICrmServices, 10 | useClass: SalesforceService, 11 | }, 12 | ], 13 | exports: [ICrmServices], 14 | }) 15 | export class SalesforceServicesModule {} 16 | -------------------------------------------------------------------------------- /src/frameworks/crm-services/salesforce/salesforce-service.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Book } from '../../../core/entities'; 3 | import { ICrmServices } from '../../../core/abstracts'; 4 | 5 | @Injectable() 6 | export class SalesforceService implements ICrmServices { 7 | bookAdded(book: Book): Promise { 8 | // Implement salesforce api logic 9 | 10 | return Promise.resolve(true); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/frameworks/data-services/mongo/model/author.model.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | 3 | export type AuthorDocument = Author & Document; 4 | 5 | @Schema() 6 | export class Author { 7 | @Prop() 8 | firstName: string; 9 | 10 | @Prop() 11 | lastName: string; 12 | } 13 | 14 | export const AuthorSchema = SchemaFactory.createForClass(Author); 15 | -------------------------------------------------------------------------------- /src/frameworks/data-services/mongo/model/book.model.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import * as mongoose from 'mongoose'; 3 | import { Author, Genre } from './'; 4 | 5 | export type BookDocument = Book & Document; 6 | 7 | @Schema() 8 | export class Book { 9 | @Prop({ required: true, unique: true }) 10 | title: string; 11 | 12 | @Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Author', required: true }) 13 | author: Author; 14 | 15 | @Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Genre', required: true }) 16 | genre: Genre; 17 | 18 | @Prop() 19 | publishDate: Date; 20 | } 21 | 22 | export const BookSchema = SchemaFactory.createForClass(Book); 23 | -------------------------------------------------------------------------------- /src/frameworks/data-services/mongo/model/genre.model.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | 3 | export type GenreDocument = Genre & Document; 4 | 5 | @Schema() 6 | export class Genre { 7 | @Prop({ required: true, unique: true }) 8 | name: string; 9 | } 10 | 11 | export const GenreSchema = SchemaFactory.createForClass(Genre); 12 | -------------------------------------------------------------------------------- /src/frameworks/data-services/mongo/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './author.model'; 2 | export * from './book.model'; 3 | export * from './genre.model'; 4 | -------------------------------------------------------------------------------- /src/frameworks/data-services/mongo/mongo-data-services.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | import { IDataServices } from '../../../core'; 4 | import { DATA_BASE_CONFIGURATION } from '../../../configuration'; 5 | import { 6 | Author, 7 | AuthorSchema, 8 | Book, 9 | BookSchema, 10 | Genre, 11 | GenreSchema, 12 | } from './model'; 13 | import { MongoDataServices } from './mongo-data-services.service'; 14 | 15 | @Module({ 16 | imports: [ 17 | MongooseModule.forFeature([ 18 | { name: Author.name, schema: AuthorSchema }, 19 | { name: Book.name, schema: BookSchema }, 20 | { name: Genre.name, schema: GenreSchema }, 21 | ]), 22 | MongooseModule.forRoot(DATA_BASE_CONFIGURATION.mongoConnectionString), 23 | ], 24 | providers: [ 25 | { 26 | provide: IDataServices, 27 | useClass: MongoDataServices, 28 | }, 29 | ], 30 | exports: [IDataServices], 31 | }) 32 | export class MongoDataServicesModule {} 33 | -------------------------------------------------------------------------------- /src/frameworks/data-services/mongo/mongo-data-services.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnApplicationBootstrap } from '@nestjs/common'; 2 | import { InjectModel } from '@nestjs/mongoose'; 3 | import { Model } from 'mongoose'; 4 | import { IDataServices } from '../../../core'; 5 | import { MongoGenericRepository } from './mongo-generic-repository'; 6 | import { 7 | Author, 8 | AuthorDocument, 9 | Book, 10 | BookDocument, 11 | Genre, 12 | GenreDocument, 13 | } from './model'; 14 | 15 | @Injectable() 16 | export class MongoDataServices 17 | implements IDataServices, OnApplicationBootstrap 18 | { 19 | authors: MongoGenericRepository; 20 | books: MongoGenericRepository; 21 | genres: MongoGenericRepository; 22 | 23 | constructor( 24 | @InjectModel(Author.name) 25 | private AuthorRepository: Model, 26 | @InjectModel(Book.name) 27 | private BookRepository: Model, 28 | @InjectModel(Genre.name) 29 | private GenreRepository: Model, 30 | ) {} 31 | 32 | onApplicationBootstrap() { 33 | this.authors = new MongoGenericRepository(this.AuthorRepository); 34 | this.books = new MongoGenericRepository(this.BookRepository, [ 35 | 'author', 36 | 'genre', 37 | ]); 38 | this.genres = new MongoGenericRepository(this.GenreRepository); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/frameworks/data-services/mongo/mongo-generic-repository.ts: -------------------------------------------------------------------------------- 1 | import { Model } from 'mongoose'; 2 | import { IGenericRepository } from '../../../core'; 3 | 4 | export class MongoGenericRepository implements IGenericRepository { 5 | private _repository: Model; 6 | private _populateOnFind: string[]; 7 | 8 | constructor(repository: Model, populateOnFind: string[] = []) { 9 | this._repository = repository; 10 | this._populateOnFind = populateOnFind; 11 | } 12 | 13 | getAll(): Promise { 14 | return this._repository.find().populate(this._populateOnFind).exec(); 15 | } 16 | 17 | get(id: any): Promise { 18 | return this._repository.findById(id).populate(this._populateOnFind).exec(); 19 | } 20 | 21 | create(item: T): Promise { 22 | return this._repository.create(item); 23 | } 24 | 25 | update(id: string, item: T) { 26 | return this._repository.findByIdAndUpdate(id, item); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /src/services/crm-services/crm-services.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { SalesforceServicesModule } from '../../frameworks/crm-services/salesforce/salesforce-service.module'; 3 | 4 | @Module({ 5 | imports: [SalesforceServicesModule], 6 | exports: [SalesforceServicesModule], 7 | }) 8 | export class CrmServicesModule {} 9 | -------------------------------------------------------------------------------- /src/services/data-services/data-services.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongoDataServicesModule } from '../../frameworks/data-services/mongo/mongo-data-services.module'; 3 | 4 | @Module({ 5 | imports: [MongoDataServicesModule], 6 | exports: [MongoDataServicesModule], 7 | }) 8 | export class DataServicesModule {} 9 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | // export * from './use-cases/book/book-services.use-case'; 2 | -------------------------------------------------------------------------------- /src/use-cases/author/author-factory.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Author } from '../../core/entities'; 3 | import { CreateAuthorDto, UpdateAuthorDto } from '../../core/dtos'; 4 | 5 | @Injectable() 6 | export class AuthorFactoryService { 7 | createNewAuthor(createAuthorDto: CreateAuthorDto) { 8 | const newAuthor = new Author(); 9 | newAuthor.firstName = createAuthorDto.firstName; 10 | newAuthor.lastName = createAuthorDto.lastName; 11 | 12 | return newAuthor; 13 | } 14 | 15 | updateAuthor(updateAuthorDto: UpdateAuthorDto) { 16 | const newAuthor = new Author(); 17 | newAuthor.firstName = updateAuthorDto.firstName; 18 | newAuthor.lastName = updateAuthorDto.lastName; 19 | 20 | return newAuthor; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/use-cases/author/author-use-cases.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { DataServicesModule } from '../../services/data-services/data-services.module'; 3 | import { AuthorFactoryService } from './author-factory.service'; 4 | import { AuthorUseCases } from './author.use-case'; 5 | 6 | @Module({ 7 | imports: [DataServicesModule], 8 | providers: [AuthorFactoryService, AuthorUseCases], 9 | exports: [AuthorFactoryService, AuthorUseCases], 10 | }) 11 | export class AuthorUseCasesModule {} 12 | -------------------------------------------------------------------------------- /src/use-cases/author/author.use-case.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Author } from '../../core/entities'; 3 | import { IDataServices } from '../../core/abstracts'; 4 | import { CreateAuthorDto, UpdateAuthorDto } from '../../core/dtos'; 5 | import { AuthorFactoryService } from './author-factory.service'; 6 | 7 | @Injectable() 8 | export class AuthorUseCases { 9 | constructor( 10 | private dataServices: IDataServices, 11 | private authorFactoryService: AuthorFactoryService, 12 | ) {} 13 | 14 | getAllAuthors(): Promise { 15 | return this.dataServices.authors.getAll(); 16 | } 17 | 18 | getAuthorById(id: any): Promise { 19 | return this.dataServices.authors.get(id); 20 | } 21 | 22 | createAuthor(createAuthorDto: CreateAuthorDto): Promise { 23 | const author = this.authorFactoryService.createNewAuthor(createAuthorDto); 24 | return this.dataServices.authors.create(author); 25 | } 26 | 27 | updateAuthor( 28 | authorId: string, 29 | updateAuthorDto: UpdateAuthorDto, 30 | ): Promise { 31 | const author = this.authorFactoryService.updateAuthor(updateAuthorDto); 32 | return this.dataServices.authors.update(authorId, author); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/use-cases/book/book-factory.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Book } from '../../core/entities'; 3 | import { CreateBookDto, UpdateBookDto } from '../../core/dtos'; 4 | 5 | @Injectable() 6 | export class BookFactoryService { 7 | createNewBook(createBookDto: CreateBookDto) { 8 | const newBook = new Book(); 9 | newBook.title = createBookDto.title; 10 | newBook.author = createBookDto.authorId; 11 | newBook.genre = createBookDto.genreId; 12 | newBook.publishDate = createBookDto.publishDate; 13 | 14 | return newBook; 15 | } 16 | 17 | updateBook(updateBookDto: UpdateBookDto) { 18 | const newBook = new Book(); 19 | newBook.title = updateBookDto.title; 20 | newBook.author = updateBookDto.authorId; 21 | newBook.genre = updateBookDto.genreId; 22 | newBook.publishDate = updateBookDto.publishDate; 23 | 24 | return newBook; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/use-cases/book/book-use-cases.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { DataServicesModule } from '../../services/data-services/data-services.module'; 3 | import { CrmServicesModule } from '../../services/crm-services/crm-services.module'; 4 | import { BookFactoryService } from './book-factory.service'; 5 | import { BookUseCases } from './book.use-case'; 6 | 7 | @Module({ 8 | imports: [DataServicesModule, CrmServicesModule], 9 | providers: [BookFactoryService, BookUseCases], 10 | exports: [BookFactoryService, BookUseCases], 11 | }) 12 | export class BookUseCasesModule {} 13 | -------------------------------------------------------------------------------- /src/use-cases/book/book.use-case.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Book } from '../../core/entities'; 3 | import { IDataServices, ICrmServices } from '../../core/abstracts'; 4 | 5 | @Injectable() 6 | export class BookUseCases { 7 | constructor( 8 | private dataServices: IDataServices, 9 | private crmServices: ICrmServices, 10 | ) {} 11 | 12 | getAllBooks(): Promise { 13 | return this.dataServices.books.getAll(); 14 | } 15 | 16 | getBookById(id: any): Promise { 17 | return this.dataServices.books.get(id); 18 | } 19 | 20 | async createBook(book: Book): Promise { 21 | try { 22 | // call to our dependencies 23 | const createdBook = await this.dataServices.books.create(book); 24 | await this.crmServices.bookAdded(createdBook); 25 | 26 | return createdBook; 27 | } catch (error) { 28 | throw error; 29 | } 30 | } 31 | 32 | updateBook(bookId: string, book: Book): Promise { 33 | return this.dataServices.books.update(bookId, book); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/use-cases/book/index.ts: -------------------------------------------------------------------------------- 1 | export * from './book-factory.service'; 2 | export * from './book.use-case'; 3 | export * from './book-use-cases.module'; 4 | -------------------------------------------------------------------------------- /src/use-cases/genre/genre-factory.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Genre } from '../../core/entities'; 3 | import { CreateGenreDto, UpdateGenreDto } from '../../core/dtos'; 4 | 5 | @Injectable() 6 | export class GenreFactoryService { 7 | createNewGenre(createGenreDto: CreateGenreDto) { 8 | const newGenre = new Genre(); 9 | newGenre.name = createGenreDto.name; 10 | 11 | return newGenre; 12 | } 13 | 14 | updateGenre(updateGenreDto: UpdateGenreDto) { 15 | const newGenre = new Genre(); 16 | newGenre.name = updateGenreDto.name; 17 | 18 | return newGenre; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/use-cases/genre/genre-use-cases.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { DataServicesModule } from '../../services/data-services/data-services.module'; 3 | import { GenreFactoryService } from './genre-factory.service'; 4 | import { GenreUseCases } from './genre.use-case'; 5 | 6 | @Module({ 7 | imports: [DataServicesModule], 8 | providers: [GenreFactoryService, GenreUseCases], 9 | exports: [GenreFactoryService, GenreUseCases], 10 | }) 11 | export class GenreUseCasesModule {} 12 | -------------------------------------------------------------------------------- /src/use-cases/genre/genre.use-case.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Genre } from '../../core/entities'; 3 | import { IDataServices } from '../../core/abstracts'; 4 | import { CreateGenreDto, UpdateGenreDto } from '../../core/dtos'; 5 | import { GenreFactoryService } from './genre-factory.service'; 6 | 7 | @Injectable() 8 | export class GenreUseCases { 9 | constructor( 10 | private dataServices: IDataServices, 11 | private genreFactoryService: GenreFactoryService, 12 | ) {} 13 | 14 | getAllGenres(): Promise { 15 | return this.dataServices.genres.getAll(); 16 | } 17 | 18 | getGenreById(id: any): Promise { 19 | return this.dataServices.genres.get(id); 20 | } 21 | 22 | createGenre(createGenreDto: CreateGenreDto): Promise { 23 | const genre = this.genreFactoryService.createNewGenre(createGenreDto); 24 | return this.dataServices.genres.create(genre); 25 | } 26 | 27 | updateGenre(genreId: string, updateGenreDto: UpdateGenreDto): Promise { 28 | const genre = this.genreFactoryService.updateGenre(updateGenreDto); 29 | return this.dataServices.genres.update(genreId, genre); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | --------------------------------------------------------------------------------