├── .env.example ├── .eslintrc.json ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── application ├── CreateUser.js ├── DeleteUser.js ├── GetUser.js ├── ListUsers.js └── UpdateUser.js ├── architecture.jpg ├── coverage ├── base.css ├── block-navigation.js ├── favicon.png ├── index.html ├── node-clean-architecture │ ├── application │ │ ├── CreateUser.js.html │ │ ├── DeleteUser.js.html │ │ ├── GetUser.js.html │ │ ├── ListUsers.js.html │ │ ├── UpdateUser.js.html │ │ └── index.html │ ├── domain │ │ ├── User.js.html │ │ └── index.html │ ├── index.html │ ├── index.js.html │ ├── infrastructure │ │ ├── config │ │ │ ├── container.js.html │ │ │ └── index.html │ │ ├── repositories │ │ │ ├── UserRepositoryInMemory.js.html │ │ │ └── index.html │ │ └── webserver │ │ │ ├── index.html │ │ │ └── server.js.html │ └── ports │ │ └── http │ │ ├── UsersController.js.html │ │ ├── errors.js.html │ │ ├── index.html │ │ └── routes.js.html ├── prettify.css ├── prettify.js ├── sort-arrow-sprite.png └── sorter.js ├── docker-compose.yml ├── docs ├── index.html └── openapi.json ├── domain └── User.js ├── index.js ├── infrastructure ├── bootstrap.js ├── bootstrap │ ├── container.js │ ├── database.js │ └── logger.js ├── database │ ├── mongo.js │ └── schemas │ │ └── User.js ├── repositories │ ├── UserRepositoryInMemory.js │ └── UserRepositoryMongo.js └── webserver │ ├── errors.js │ └── fastify.js ├── jest.config.js ├── mongo-init.js ├── package.json ├── ports └── http │ ├── UsersController.js │ └── routes.js ├── preview.png ├── tests ├── integration │ ├── users.integration.test.js │ └── users.test.js └── setup.js └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | DB_DRIVER=mongo 2 | DB_HOST=localhost 3 | DB_PORT=27017 4 | DB_DATABASE=users 5 | DB_USER=myuser 6 | DB_PASS=mypass 7 | 8 | LOG_LEVEL=info 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2021": true, 6 | "jest": true 7 | }, 8 | "extends": [ 9 | "standard" 10 | ], 11 | "parserOptions": { 12 | "ecmaVersion": 12 13 | }, 14 | "rules": { 15 | }, 16 | "overrides": [ 17 | { 18 | "files": "*.test.js", 19 | "rules": { 20 | "no-unused-expressions": "off" 21 | } 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .nyc_output/ 3 | coverage/ 4 | .env -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "conventionalCommits.scopes": [ 3 | "dependency-injection" 4 | ] 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Code Coverage](https://img.shields.io/badge/coverage-96%25-green?style=flat-square) 2 | 3 | # Node Clean Architecture 4 | 5 | ![Preview](preview.png) 6 | 7 | ## Table of Contents 8 | - [Node Clean Architecture](#node-clean-architecture) 9 | - [Table of Contents](#table-of-contents) 10 | - [Libs](#libs) 11 | - [Installation](#installation) 12 | - [Testing](#testing) 13 | - [Clean Architecture](#clean-architecture) 14 | - [Folder structure](#folder-structure) 15 | - [The Dependency Rule](#the-dependency-rule) 16 | - [Typical Request](#typical-request) 17 | - [Troubleshooting](#troubleshooting) 18 | - [Log `connected to MongoDB database!` doesn't appear](#log-connected-to-mongodb-database-doesnt-appear) 19 | - [I'm getting `EADDRINUSE` upon application start](#im-getting-eaddrinuse-upon-application-start) 20 | 21 | 22 | This backend implements a [RESTful](https://restfulapi.net/) CRUD interface for users and complies with Eric Evan's [DDD](https://en.wikipedia.org/wiki/Domain-driven_design) and Uncle Bob's [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) which is briefly explained here in the document. 23 | 24 | It also exposes a `/docs/` endpoint for further reference and `/coverage/` for test coverage. 25 | 26 | ## Libs 27 | * [Restify](http://restify.com/) 28 | * [Mongoose](https://mongoosejs.com/) 29 | * [Lodash FP](https://github.com/lodash/lodash/wiki/FP-Guide) Functional Programming version 30 | * [Awilix](https://github.com/jeffijoe/awilix) as Dependency Injection container 31 | * [dotenv](https://www.npmjs.com/package/dotenv) 32 | 33 | ## Installation 34 | 35 | ``` 36 | docker-compose up -d 37 | cp .env.example .env 38 | npm start 39 | ``` 40 | You should get 41 | ``` 42 | restify listening at http://[::]:8080 43 | connected to MongoDB database! 44 | ``` 45 | Access http://localhost:8080/docs/ and http://localhost:8080/coverage/ 46 | 47 | ## Testing 48 | 49 | ``` 50 | npm test 51 | ``` 52 | It uses an in-memory DB to run tests so you don't need to have mongodb up and running 53 | 54 | ## Clean Architecture 55 | 56 | ![Cleab Architecture](https://blog.cleancoder.com/uncle-bob/images/2012-08-13-the-clean-architecture/CleanArchitecture.jpg) 57 | 58 | ### Folder structure 59 | ``` 60 | 61 | 62 | └ application → Application services layer 63 | └ use_cases → Application business rules 64 | └ domain → Enterprise core business layer such as domain model objects (Aggregates, Entities, Value Objects) and repository interfaces 65 | └ infrastructure → Frameworks, drivers and tools such as Database, the Web Framework, mailing/logging/glue code etc. 66 | └ config → Application configuration files, modules and services 67 | └ container.js → Module that manage service implementations by environment 68 | └ database → Database ORMs middleware 69 | └ schemas → Mongoose schemas 70 | └ repositories → Implementation of domain repository interfaces 71 | └ webserver → Restify Web server configuration (server, routes, plugins, etc.) 72 | └ server.js → Restify server definition 73 | └ ports/http → Adapters and formatters for use cases and entities to external agency such as Database or the Web 74 | └ UserController.js → Restify route handlers 75 | └ routes.js → Restify route definitions 76 | └ errors.js → Standard errors for the whole application 77 | └ index.js → Main application entry point 78 | ``` 79 | 80 | ### The Dependency Rule 81 | 82 | >The overriding rule that makes this architecture work is The Dependency Rule. This rule says that source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle. In particular, the name of something declared in an outer circle must not be mentioned by the code in the an inner circle. That includes, functions, classes. variables, or any other named software entity. 83 | 84 | Extracted from https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html#the-dependency-rule 85 | 86 | ### Typical Request 87 | 88 | ![Request](architecture.jpg) 89 | 90 | ## Troubleshooting 91 | 92 | ### Log `connected to MongoDB database!` doesn't appear 93 | The backend uses its own database (`users`) to run its business logic, so you need to ensure this database is created with proper user credentials. The script `mongo-init.js` is run when `docker-compose up` runs for the first time. 94 | 95 | Check in `docker-compose logs mongo` to see if something unusual is happening 96 | 97 | ### I'm getting `EADDRINUSE` upon application start 98 | You need port `8080` to be free in order to boot up the application. Check if it's already in use and shut the application down before you `npm start` again 99 | 100 |
101 | 102 | # Stack Report 103 | ![](https://img.stackshare.io/repo.svg "repo") [rasouza/node-clean-architecture](https://github.com/rasouza/node-clean-architecture)![](https://img.stackshare.io/public_badge.svg "public") 104 |

105 | |28
Tools used|3
Contributors|10/11/23
Report generated|06/06/22
Last commit date| 106 | |------|------|------|------| 107 |
108 | 109 | ## Languages (2) 110 | 111 | 118 | 119 | 126 | 127 | 128 |
112 | JavaScript 113 |
114 | JavaScript 115 |
116 | 117 |
120 | CSS 3 121 |
122 | CSS 3 123 |
124 | 125 |
129 | 130 | ## Frameworks (1) 131 | 132 | 139 | 140 | 141 |
133 | Fastify 134 |
135 | Fastify 136 |
137 | v3.9.2 138 |
142 | 143 | ## Data (1) 144 | 145 | 152 | 153 | 154 |
146 | Mongoose 147 |
148 | Mongoose 149 |
150 | v5.10.3 151 |
155 | 156 | ## DevOps (7) 157 | 158 | 165 | 166 | 173 | 174 | 181 | 182 | 189 | 190 | 197 | 198 | 205 | 206 | 213 | 214 | 215 |
159 | Dotenv 160 |
161 | Dotenv 162 |
163 | 164 |
167 | ESLint 168 |
169 | ESLint 170 |
171 | 172 |
175 | Git 176 |
177 | Git 178 |
179 | 180 |
183 | Jest 184 |
185 | Jest 186 |
187 | v28.1.0 188 |
191 | Yarn 192 |
193 | Yarn 194 |
195 | 196 |
199 | nodemon 200 |
201 | nodemon 202 |
203 | v2.0.4 204 |
207 | npm 208 |
209 | npm 210 |
211 | 212 |
216 | 217 | ## Other (2) 218 | 219 | 226 | 227 | 234 | 235 | 236 |
220 | Shell 221 |
222 | Shell 223 |
224 | 225 |
228 | semantic-release 229 |
230 | semantic-release 231 |
232 | 233 |
237 | 238 | 239 | ## Open source packages (15) 240 | 241 | ## npm (15) 242 | 243 | |NAME|VERSION|SOURCE FILE| 244 | |------|------|------| 245 | |[@semantic-release/git](https://github.com/semantic-release/git)|v10.0.1|[yarn.lock](https://github.com/rasouza/node-clean-architecture/blob/master/yarn.lock)| 246 | |[@types/jest](http://definitelytyped.org/)|v28.1.1|[yarn.lock](https://github.com/rasouza/node-clean-architecture/blob/master/yarn.lock)| 247 | |[cross-env](https://github.com/kentcdodds/cross-env)|v7.0.3|[yarn.lock](https://github.com/rasouza/node-clean-architecture/blob/master/yarn.lock)| 248 | |[dotenv](https://github.com/motdotla/dotenv)|v8.2.0|[yarn.lock](https://github.com/rasouza/node-clean-architecture/blob/master/yarn.lock)| 249 | |[eslint-config-standard](https://github.com/standard/eslint-config-standard)|v16.0.2|[yarn.lock](https://github.com/rasouza/node-clean-architecture/blob/master/yarn.lock)| 250 | |[eslint-plugin-import](https://github.com/benmosher/eslint-plugin-import)|v2.22.1|[yarn.lock](https://github.com/rasouza/node-clean-architecture/blob/master/yarn.lock)| 251 | |[eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest)|v26.5.3|[yarn.lock](https://github.com/rasouza/node-clean-architecture/blob/master/yarn.lock)| 252 | |[eslint-plugin-node](https://github.com/mysticatea/eslint-plugin-node)|v11.1.0|[yarn.lock](https://github.com/rasouza/node-clean-architecture/blob/master/yarn.lock)| 253 | |[eslint-plugin-promise](https://github.com/xjamundx/eslint-plugin-promise)|v4.2.1|[yarn.lock](https://github.com/rasouza/node-clean-architecture/blob/master/yarn.lock)| 254 | |[eslint-plugin-standard](https://github.com/standard/eslint-plugin-standard)|v5.0.0|[yarn.lock](https://github.com/rasouza/node-clean-architecture/blob/master/yarn.lock)| 255 | |[mongoose](https://mongoosejs.com)|v5.10.3|[yarn.lock](https://github.com/rasouza/node-clean-architecture/blob/master/yarn.lock)| 256 | |[nyc](https://github.com/istanbuljs/nyc)|v15.1.0|[yarn.lock](https://github.com/rasouza/node-clean-architecture/blob/master/yarn.lock)| 257 | |[pino](http://getpino.io)|v6.9.0|[yarn.lock](https://github.com/rasouza/node-clean-architecture/blob/master/yarn.lock)| 258 | |[pino-pretty](https://github.com/pinojs/pino-pretty)|v4.3.0|[yarn.lock](https://github.com/rasouza/node-clean-architecture/blob/master/yarn.lock)| 259 | |[semantic-release](https://github.com/semantic-release/semantic-release)|v19.0.2|[yarn.lock](https://github.com/rasouza/node-clean-architecture/blob/master/yarn.lock)| 260 | 261 |
262 |
263 | 264 | Generated via [Stack Reports](https://stackshare.io/stack-report) 265 | -------------------------------------------------------------------------------- /application/CreateUser.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ UserRepository, User }) => 2 | (name, cpf, birthdate, subscription, dependents) => { 3 | const user = new User(null, name, cpf, birthdate, subscription, dependents) 4 | return UserRepository.persist(user) 5 | } 6 | -------------------------------------------------------------------------------- /application/DeleteUser.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ UserRepository }) => id => UserRepository.remove(id) 2 | -------------------------------------------------------------------------------- /application/GetUser.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ UserRepository }) => id => UserRepository.get(id) 2 | -------------------------------------------------------------------------------- /application/ListUsers.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ UserRepository }) => () => UserRepository.find() 2 | -------------------------------------------------------------------------------- /application/UpdateUser.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ UserRepository }) => 2 | (id, data) => UserRepository.merge(id, data) 3 | -------------------------------------------------------------------------------- /architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rasouza/node-clean-architecture/b99b4b213e6d18bea8b2aaa148e23029f3370004/architecture.jpg -------------------------------------------------------------------------------- /coverage/base.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin:0; padding: 0; 3 | height: 100%; 4 | } 5 | body { 6 | font-family: Helvetica Neue, Helvetica, Arial; 7 | font-size: 14px; 8 | color:#333; 9 | } 10 | .small { font-size: 12px; } 11 | *, *:after, *:before { 12 | -webkit-box-sizing:border-box; 13 | -moz-box-sizing:border-box; 14 | box-sizing:border-box; 15 | } 16 | h1 { font-size: 20px; margin: 0;} 17 | h2 { font-size: 14px; } 18 | pre { 19 | font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; 20 | margin: 0; 21 | padding: 0; 22 | -moz-tab-size: 2; 23 | -o-tab-size: 2; 24 | tab-size: 2; 25 | } 26 | a { color:#0074D9; text-decoration:none; } 27 | a:hover { text-decoration:underline; } 28 | .strong { font-weight: bold; } 29 | .space-top1 { padding: 10px 0 0 0; } 30 | .pad2y { padding: 20px 0; } 31 | .pad1y { padding: 10px 0; } 32 | .pad2x { padding: 0 20px; } 33 | .pad2 { padding: 20px; } 34 | .pad1 { padding: 10px; } 35 | .space-left2 { padding-left:55px; } 36 | .space-right2 { padding-right:20px; } 37 | .center { text-align:center; } 38 | .clearfix { display:block; } 39 | .clearfix:after { 40 | content:''; 41 | display:block; 42 | height:0; 43 | clear:both; 44 | visibility:hidden; 45 | } 46 | .fl { float: left; } 47 | @media only screen and (max-width:640px) { 48 | .col3 { width:100%; max-width:100%; } 49 | .hide-mobile { display:none!important; } 50 | } 51 | 52 | .quiet { 53 | color: #7f7f7f; 54 | color: rgba(0,0,0,0.5); 55 | } 56 | .quiet a { opacity: 0.7; } 57 | 58 | .fraction { 59 | font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; 60 | font-size: 10px; 61 | color: #555; 62 | background: #E8E8E8; 63 | padding: 4px 5px; 64 | border-radius: 3px; 65 | vertical-align: middle; 66 | } 67 | 68 | div.path a:link, div.path a:visited { color: #333; } 69 | table.coverage { 70 | border-collapse: collapse; 71 | margin: 10px 0 0 0; 72 | padding: 0; 73 | } 74 | 75 | table.coverage td { 76 | margin: 0; 77 | padding: 0; 78 | vertical-align: top; 79 | } 80 | table.coverage td.line-count { 81 | text-align: right; 82 | padding: 0 5px 0 20px; 83 | } 84 | table.coverage td.line-coverage { 85 | text-align: right; 86 | padding-right: 10px; 87 | min-width:20px; 88 | } 89 | 90 | table.coverage td span.cline-any { 91 | display: inline-block; 92 | padding: 0 5px; 93 | width: 100%; 94 | } 95 | .missing-if-branch { 96 | display: inline-block; 97 | margin-right: 5px; 98 | border-radius: 3px; 99 | position: relative; 100 | padding: 0 4px; 101 | background: #333; 102 | color: yellow; 103 | } 104 | 105 | .skip-if-branch { 106 | display: none; 107 | margin-right: 10px; 108 | position: relative; 109 | padding: 0 4px; 110 | background: #ccc; 111 | color: white; 112 | } 113 | .missing-if-branch .typ, .skip-if-branch .typ { 114 | color: inherit !important; 115 | } 116 | .coverage-summary { 117 | border-collapse: collapse; 118 | width: 100%; 119 | } 120 | .coverage-summary tr { border-bottom: 1px solid #bbb; } 121 | .keyline-all { border: 1px solid #ddd; } 122 | .coverage-summary td, .coverage-summary th { padding: 10px; } 123 | .coverage-summary tbody { border: 1px solid #bbb; } 124 | .coverage-summary td { border-right: 1px solid #bbb; } 125 | .coverage-summary td:last-child { border-right: none; } 126 | .coverage-summary th { 127 | text-align: left; 128 | font-weight: normal; 129 | white-space: nowrap; 130 | } 131 | .coverage-summary th.file { border-right: none !important; } 132 | .coverage-summary th.pct { } 133 | .coverage-summary th.pic, 134 | .coverage-summary th.abs, 135 | .coverage-summary td.pct, 136 | .coverage-summary td.abs { text-align: right; } 137 | .coverage-summary td.file { white-space: nowrap; } 138 | .coverage-summary td.pic { min-width: 120px !important; } 139 | .coverage-summary tfoot td { } 140 | 141 | .coverage-summary .sorter { 142 | height: 10px; 143 | width: 7px; 144 | display: inline-block; 145 | margin-left: 0.5em; 146 | background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; 147 | } 148 | .coverage-summary .sorted .sorter { 149 | background-position: 0 -20px; 150 | } 151 | .coverage-summary .sorted-desc .sorter { 152 | background-position: 0 -10px; 153 | } 154 | .status-line { height: 10px; } 155 | /* yellow */ 156 | .cbranch-no { background: yellow !important; color: #111; } 157 | /* dark red */ 158 | .red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } 159 | .low .chart { border:1px solid #C21F39 } 160 | .highlighted, 161 | .highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ 162 | background: #C21F39 !important; 163 | } 164 | /* medium red */ 165 | .cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } 166 | /* light red */ 167 | .low, .cline-no { background:#FCE1E5 } 168 | /* light green */ 169 | .high, .cline-yes { background:rgb(230,245,208) } 170 | /* medium green */ 171 | .cstat-yes { background:rgb(161,215,106) } 172 | /* dark green */ 173 | .status-line.high, .high .cover-fill { background:rgb(77,146,33) } 174 | .high .chart { border:1px solid rgb(77,146,33) } 175 | /* dark yellow (gold) */ 176 | .status-line.medium, .medium .cover-fill { background: #f9cd0b; } 177 | .medium .chart { border:1px solid #f9cd0b; } 178 | /* light yellow */ 179 | .medium { background: #fff4c2; } 180 | 181 | .cstat-skip { background: #ddd; color: #111; } 182 | .fstat-skip { background: #ddd; color: #111 !important; } 183 | .cbranch-skip { background: #ddd !important; color: #111; } 184 | 185 | span.cline-neutral { background: #eaeaea; } 186 | 187 | .coverage-summary td.empty { 188 | opacity: .5; 189 | padding-top: 4px; 190 | padding-bottom: 4px; 191 | line-height: 1; 192 | color: #888; 193 | } 194 | 195 | .cover-fill, .cover-empty { 196 | display:inline-block; 197 | height: 12px; 198 | } 199 | .chart { 200 | line-height: 0; 201 | } 202 | .cover-empty { 203 | background: white; 204 | } 205 | .cover-full { 206 | border-right: none !important; 207 | } 208 | pre.prettyprint { 209 | border: none !important; 210 | padding: 0 !important; 211 | margin: 0 !important; 212 | } 213 | .com { color: #999 !important; } 214 | .ignore-none { color: #999; font-weight: normal; } 215 | 216 | .wrapper { 217 | min-height: 100%; 218 | height: auto !important; 219 | height: 100%; 220 | margin: 0 auto -48px; 221 | } 222 | .footer, .push { 223 | height: 48px; 224 | } 225 | -------------------------------------------------------------------------------- /coverage/block-navigation.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var jumpToCode = (function init() { 3 | // Classes of code we would like to highlight in the file view 4 | var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; 5 | 6 | // Elements to highlight in the file listing view 7 | var fileListingElements = ['td.pct.low']; 8 | 9 | // We don't want to select elements that are direct descendants of another match 10 | var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` 11 | 12 | // Selecter that finds elements on the page to which we can jump 13 | var selector = 14 | fileListingElements.join(', ') + 15 | ', ' + 16 | notSelector + 17 | missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` 18 | 19 | // The NodeList of matching elements 20 | var missingCoverageElements = document.querySelectorAll(selector); 21 | 22 | var currentIndex; 23 | 24 | function toggleClass(index) { 25 | missingCoverageElements 26 | .item(currentIndex) 27 | .classList.remove('highlighted'); 28 | missingCoverageElements.item(index).classList.add('highlighted'); 29 | } 30 | 31 | function makeCurrent(index) { 32 | toggleClass(index); 33 | currentIndex = index; 34 | missingCoverageElements.item(index).scrollIntoView({ 35 | behavior: 'smooth', 36 | block: 'center', 37 | inline: 'center' 38 | }); 39 | } 40 | 41 | function goToPrevious() { 42 | var nextIndex = 0; 43 | if (typeof currentIndex !== 'number' || currentIndex === 0) { 44 | nextIndex = missingCoverageElements.length - 1; 45 | } else if (missingCoverageElements.length > 1) { 46 | nextIndex = currentIndex - 1; 47 | } 48 | 49 | makeCurrent(nextIndex); 50 | } 51 | 52 | function goToNext() { 53 | var nextIndex = 0; 54 | 55 | if ( 56 | typeof currentIndex === 'number' && 57 | currentIndex < missingCoverageElements.length - 1 58 | ) { 59 | nextIndex = currentIndex + 1; 60 | } 61 | 62 | makeCurrent(nextIndex); 63 | } 64 | 65 | return function jump(event) { 66 | switch (event.which) { 67 | case 78: // n 68 | case 74: // j 69 | goToNext(); 70 | break; 71 | case 66: // b 72 | case 75: // k 73 | case 80: // p 74 | goToPrevious(); 75 | break; 76 | } 77 | }; 78 | })(); 79 | window.addEventListener('keydown', jumpToCode); 80 | -------------------------------------------------------------------------------- /coverage/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rasouza/node-clean-architecture/b99b4b213e6d18bea8b2aaa148e23029f3370004/coverage/favicon.png -------------------------------------------------------------------------------- /coverage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for All files 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files

23 |
24 | 25 |
26 | 96.12% 27 | Statements 28 | 124/129 29 |
30 | 31 | 32 |
33 | 61.54% 34 | Branches 35 | 8/13 36 |
37 | 38 | 39 |
40 | 97.06% 41 | Functions 42 | 33/34 43 |
44 | 45 | 46 |
47 | 96.49% 48 | Lines 49 | 110/114 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 |
FileStatementsBranchesFunctionsLines
application 78 |
79 |
100%16/16100%0/0100%10/10100%9/9
domain 93 |
94 |
100%8/80%0/1100%2/2100%7/7
infrastructure/config 108 |
109 |
84.85%28/3350%4/880%4/586.67%26/30
infrastructure/repositories 123 |
124 |
100%25/25100%4/4100%6/6100%23/23
infrastructure/webserver 138 |
139 |
100%24/24100%0/0100%4/4100%24/24
ports/http 153 |
154 |
100%23/23100%0/0100%7/7100%21/21
167 |
168 |
169 |
170 | 175 |
176 | 177 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/application/CreateUser.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture/application/CreateUser.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / node-clean-architecture/application CreateUser.js

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 4/4 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 2/2 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 4/4 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 61x 66 | 2x 67 | 2x 68 | 2x 69 |   70 |  
module.exports = ({ UserRepository, User }) =>
71 |   (name, cpf, birthdate, subscription, dependents) => {
72 |     const user = new User(null, name, cpf, birthdate, subscription, dependents)
73 |     return UserRepository.persist(user)
74 |   }
75 |  
76 | 77 |
78 |
79 | 84 | 85 | 86 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/application/DeleteUser.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture/application/DeleteUser.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / node-clean-architecture/application DeleteUser.js

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 3/3 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 2/2 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 1/1 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

60 | 
1 61 | 22x 62 |  
module.exports = ({ UserRepository }) => id => UserRepository.remove(id)
63 |  
64 | 65 |
66 |
67 | 72 | 73 | 74 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/application/GetUser.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture/application/GetUser.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / node-clean-architecture/application GetUser.js

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 3/3 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 2/2 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 1/1 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

60 | 
1 61 | 22x 62 |  
module.exports = ({ UserRepository }) => id => UserRepository.get(id)
63 |  
64 | 65 |
66 |
67 | 72 | 73 | 74 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/application/ListUsers.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture/application/ListUsers.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / node-clean-architecture/application ListUsers.js

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 3/3 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 2/2 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 1/1 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

60 | 
1 61 | 22x 62 |  
module.exports = ({ UserRepository }) => () => UserRepository.find()
63 |  
64 | 65 |
66 |
67 | 72 | 73 | 74 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/application/UpdateUser.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture/application/UpdateUser.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / node-clean-architecture/application UpdateUser.js

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 3/3 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 2/2 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 2/2 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

60 | 
1 61 | 2 62 | 31x 63 | 2x 64 |  
module.exports = ({ UserRepository }) =>
65 |   (id, data) => UserRepository.merge(id, data)
66 |  
67 | 68 |
69 |
70 | 75 | 76 | 77 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/application/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture/application 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files node-clean-architecture/application

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 16/16 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 10/10 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 9/9 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 |
FileStatementsBranchesFunctionsLines
CreateUser.js 78 |
79 |
100%4/4100%0/0100%2/2100%4/4
DeleteUser.js 93 |
94 |
100%3/3100%0/0100%2/2100%1/1
GetUser.js 108 |
109 |
100%3/3100%0/0100%2/2100%1/1
ListUsers.js 123 |
124 |
100%3/3100%0/0100%2/2100%1/1
UpdateUser.js 138 |
139 |
100%3/3100%0/0100%2/2100%2/2
152 |
153 |
154 |
155 | 160 | 161 | 162 | 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/domain/User.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture/domain/User.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / node-clean-architecture/domain User.js

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 8/8 29 |
30 | 31 | 32 |
33 | 0% 34 | Branches 35 | 0/1 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 2/2 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 7/7 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

 60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 6 66 | 7 67 | 8 68 | 9 69 | 10 70 | 112x 71 |   72 | 2x 73 | 2x 74 | 2x 75 | 2x 76 | 2x 77 | 2x 78 |   79 |   80 |  
module.exports = () => class {
 81 |   constructor (id = null, name, cpf, birthdate, subscription, dependents) {
 82 |     this.id = id
 83 |     this.name = name
 84 |     this.cpf = cpf
 85 |     this.birthdate = birthdate
 86 |     this.subscription = subscription
 87 |     this.dependents = dependents
 88 |   }
 89 | }
 90 |  
91 | 92 |
93 |
94 | 99 | 100 | 101 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/domain/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture/domain 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files node-clean-architecture/domain

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 8/8 29 |
30 | 31 | 32 |
33 | 0% 34 | Branches 35 | 0/1 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 2/2 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 7/7 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
FileStatementsBranchesFunctionsLines
User.js 78 |
79 |
100%8/80%0/1100%2/2100%7/7
92 |
93 |
94 |
95 | 100 | 101 | 102 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files node-clean-architecture

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 4/4 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 0/0 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 4/4 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
FileStatementsBranchesFunctionsLines
index.js 78 |
79 |
100%4/4100%0/0100%0/0100%4/4
92 |
93 |
94 |
95 | 100 | 101 | 102 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/index.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture/index.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / node-clean-architecture index.js

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 4/4 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 0/0 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 4/4 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 6 66 | 71x 67 |   68 | 1x 69 | 1x 70 |   71 | 1x 72 |  
require('dotenv').config()
73 |  
74 | const container = require('./infrastructure/config/container')()
75 | const server = require('./infrastructure/webserver/server')
76 |  
77 | module.exports = server(container.cradle)
78 |  
79 | 80 |
81 |
82 | 87 | 88 | 89 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/infrastructure/config/container.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture/infrastructure/config/container.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / node-clean-architecture/infrastructure/config container.js

23 |
24 | 25 |
26 | 75% 27 | Statements 28 | 15/20 29 |
30 | 31 | 32 |
33 | 33.33% 34 | Branches 35 | 2/6 36 |
37 | 38 | 39 |
40 | 75% 41 | Functions 42 | 3/4 43 |
44 | 45 | 46 |
47 | 76.47% 48 | Lines 49 | 13/17 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

 60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 6 66 | 7 67 | 8 68 | 9 69 | 10 70 | 11 71 | 12 72 | 13 73 | 14 74 | 15 75 | 16 76 | 17 77 | 18 78 | 19 79 | 20 80 | 21 81 | 22 82 | 23 83 | 24 84 | 25 85 | 26 86 | 27 87 | 28 88 | 29 89 | 30 90 | 31 91 | 32 92 | 33 93 | 34 94 | 35 95 | 36 96 | 37 97 | 38 98 | 39 99 | 40 100 | 41 101 | 42 102 | 43 103 | 441x 104 |   105 | 1x 106 |   107 | 1x 108 | 1x 109 |   110 | 1x 111 |   112 |   113 |   114 | 1x 115 | 1x 116 |   117 | 1x 118 |   119 |   120 |   121 |   122 | 1x 123 |   124 |   125 |   126 |   127 |   128 |   129 |   130 |   131 |   132 |   133 |   134 |   135 | 1x 136 | 1x 137 |   138 |   139 |   140 |   141 |   142 | 1x 143 |   144 | 1x 145 |   146 |  
const { createContainer, asFunction } = require('awilix')
147 |  
148 | const container = createContainer()
149 |  
150 | const resolveDB = ({ DB_DRIVER, NODE_ENV }) => {
151 |   Eif (NODE_ENV === 'test') DB_DRIVER = 'in-memory'
152 |  
153 |   Eif (DB_DRIVER === 'in-memory') inMemoryDB()
154 |   else if (DB_DRIVER === 'mongo') mongoDB()
155 | }
156 |  
157 | const inMemoryDB = () => {
158 |   const UserRepositoryInMemory = require('../repositories/UserRepositoryInMemory')
159 |  
160 |   container.register({
161 |     UserRepository: asFunction(UserRepositoryInMemory).singleton()
162 |   })
163 | }
164 |  
165 | const mongoDB = () => {
166 |   const UserRepositoryMongo = require('../repositories/UserRepositoryMongo')
167 |  
168 |   // Load Database and Schemas
169 |   container.loadModules([
170 |     'infrastructure/database/**/*.js'
171 |   ])
172 |  
173 |   container.register({
174 |     UserRepository: asFunction(UserRepositoryMongo)
175 |   })
176 | }
177 |  
178 | module.exports = () => {
179 |   container.loadModules([
180 |     'ports/**/*.js',
181 |     'application/**/*.js',
182 |     'domain/**/*.js'
183 |   ])
184 |  
185 |   resolveDB(process.env)
186 |  
187 |   return container
188 | }
189 |  
190 | 191 |
192 |
193 | 198 | 199 | 200 | 205 | 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/infrastructure/config/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture/infrastructure/config 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files node-clean-architecture/infrastructure/config

23 |
24 | 25 |
26 | 75% 27 | Statements 28 | 15/20 29 |
30 | 31 | 32 |
33 | 33.33% 34 | Branches 35 | 2/6 36 |
37 | 38 | 39 |
40 | 75% 41 | Functions 42 | 3/4 43 |
44 | 45 | 46 |
47 | 76.47% 48 | Lines 49 | 13/17 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
FileStatementsBranchesFunctionsLines
container.js 78 |
79 |
75%15/2033.33%2/675%3/476.47%13/17
92 |
93 |
94 |
95 | 100 | 101 | 102 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/infrastructure/repositories/UserRepositoryInMemory.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture/infrastructure/repositories/UserRepositoryInMemory.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / node-clean-architecture/infrastructure/repositories UserRepositoryInMemory.js

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 25/25 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 4/4 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 6/6 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 23/23 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

 60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 6 66 | 7 67 | 8 68 | 9 69 | 10 70 | 11 71 | 12 72 | 13 73 | 14 74 | 15 75 | 16 76 | 17 77 | 18 78 | 19 79 | 20 80 | 21 81 | 22 82 | 23 83 | 24 84 | 25 85 | 26 86 | 27 87 | 28 88 | 29 89 | 30 90 | 31 91 | 32 92 | 33 93 | 34 94 | 35 95 | 36 96 | 37 97 | 38 98 | 39 99 | 40 100 | 41 101 | 42 102 | 43 103 | 44 104 | 45 105 | 46 106 | 471x 107 | 1x 108 | 1x 109 | 1x 110 |   111 | 1x 112 |   113 | 1x 114 | 1x 115 |   116 |   117 |   118 | 2x 119 |   120 |   121 |   122 | 2x 123 | 2x 124 |   125 | 1x 126 | 1x 127 | 1x 128 |   129 |   130 |   131 | 6x 132 | 6x 133 | 3x 134 |   135 |   136 |   137 | 2x 138 | 1x 139 |   140 | 1x 141 | 1x 142 |   143 |   144 |   145 | 4x 146 | 2x 147 |   148 | 2x 149 |   150 |   151 |   152 |  
const filter = require('lodash/fp/filter')
153 | const first = require('lodash/fp/first')
154 | const remove = require('lodash/fp/remove')
155 | const merge = require('lodash/fp/merge')
156 |  
157 | const { NotFoundError, ForbiddenError } = require('restify-errors')
158 |  
159 | module.exports = () => {
160 |   return {
161 |     lastId: 0,
162 |     db: [],
163 |     find () {
164 |       return this.db
165 |     },
166 |  
167 |     persist (user) {
168 |       const duplicated = filter({ cpf: user.cpf }, this.db)
169 |       if (duplicated.length > 0) throw new ForbiddenError('This CPF already exists')
170 |  
171 |       user.id = `${++this.lastId}`
172 |       this.db.push(user)
173 |       return user
174 |     },
175 |  
176 |     get (id) {
177 |       const user = first(filter({ id }, this.db))
178 |       if (!user) throw new NotFoundError('User not found')
179 |       return user
180 |     },
181 |  
182 |     merge (id, data) {
183 |       let user = this.remove(id)
184 |       user = merge(user, data)
185 |  
186 |       this.db.push(user)
187 |       return user
188 |     },
189 |  
190 |     remove (id) {
191 |       const user = this.get(id)
192 |       this.db = remove({ id }, this.db)
193 |  
194 |       return user
195 |     }
196 |   }
197 | }
198 |  
199 | 200 |
201 |
202 | 207 | 208 | 209 | 214 | 215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/infrastructure/repositories/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture/infrastructure/repositories 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files node-clean-architecture/infrastructure/repositories

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 25/25 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 4/4 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 6/6 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 23/23 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
FileStatementsBranchesFunctionsLines
UserRepositoryInMemory.js 78 |
79 |
100%25/25100%4/4100%6/6100%23/23
92 |
93 |
94 |
95 | 100 | 101 | 102 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/infrastructure/webserver/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture/infrastructure/webserver 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files node-clean-architecture/infrastructure/webserver

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 10/10 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 3/3 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 10/10 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
FileStatementsBranchesFunctionsLines
server.js 78 |
79 |
100%10/10100%0/0100%3/3100%10/10
92 |
93 |
94 |
95 | 100 | 101 | 102 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/infrastructure/webserver/server.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture/infrastructure/webserver/server.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / node-clean-architecture/infrastructure/webserver server.js

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 10/10 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 3/3 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 10/10 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

 60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 6 66 | 7 67 | 8 68 | 9 69 | 10 70 | 11 71 | 12 72 | 13 73 | 14 74 | 15 75 | 16 76 | 17 77 | 181x 78 | 1x 79 |   80 | 1x 81 | 1x 82 | 1x 83 |   84 | 1x 85 | 8x 86 |   87 |   88 | 1x 89 | 1x 90 |   91 |   92 | 1x 93 |   94 |  
const each = require('lodash/fp/each')
 95 | const restify = require('restify')
 96 |  
 97 | module.exports = ({ routes }) => {
 98 |   const server = restify.createServer()
 99 |   server.use(restify.plugins.bodyParser())
100 |  
101 |   each(route => {
102 |     server[route.method](route.path, route.handler)
103 |   })(routes)
104 |  
105 |   server.listen(8080, () => {
106 |     console.log(`${server.name} listening at ${server.url}`)
107 |   })
108 |  
109 |   return server
110 | }
111 |  
112 | 113 |
114 |
115 | 120 | 121 | 122 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/ports/http/UsersController.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture/ports/http/UsersController.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / node-clean-architecture/ports/http UsersController.js

23 |
24 | 25 |
26 | 96.77% 27 | Statements 28 | 30/31 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 6/6 43 |
44 | 45 | 46 |
47 | 96.67% 48 | Lines 49 | 29/30 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

 60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 6 66 | 7 67 | 8 68 | 9 69 | 10 70 | 11 71 | 12 72 | 13 73 | 14 74 | 15 75 | 16 76 | 17 77 | 18 78 | 19 79 | 20 80 | 21 81 | 22 82 | 23 83 | 24 84 | 25 85 | 26 86 | 27 87 | 28 88 | 29 89 | 30 90 | 31 91 | 32 92 | 33 93 | 34 94 | 35 95 | 36 96 | 37 97 | 38 98 | 39 99 | 40 100 | 41 101 | 42 102 | 43 103 | 44 104 | 45 105 | 46 106 | 47 107 | 48 108 | 49 109 | 50 110 | 51 111 | 52 112 | 53 113 | 54 114 | 55 115 | 56 116 | 57 117 | 58 118 | 59 119 | 60 120 | 61 121 | 62 122 | 63 123 | 64 124 | 65 125 | 66 126 | 67 127 | 68 128 | 69 129 | 70 130 | 71 131 | 72 132 | 73 133 | 74 134 | 75 135 | 76 136 | 771x 137 |   138 | 1x 139 |   140 | 2x 141 |   142 | 2x 143 | 2x 144 | 2x 145 |   146 |   147 |   148 |   149 |   150 |   151 | 2x 152 | 2x 153 |   154 | 2x 155 | 2x 156 |   157 |   158 |   159 |   160 |   161 |   162 |   163 | 1x 164 |   165 | 1x 166 |   167 |   168 |   169 |   170 | 2x 171 |   172 | 2x 173 | 2x 174 | 1x 175 |   176 | 1x 177 |   178 |   179 |   180 |   181 | 2x 182 |   183 | 2x 184 | 2x 185 | 1x 186 |   187 | 1x 188 |   189 |   190 |   191 |   192 | 2x 193 |   194 | 2x 195 | 2x 196 |   197 |   198 |   199 |   200 |   201 |   202 |   203 | 2x 204 | 1x 205 |   206 | 1x 207 |   208 |   209 |   210 |   211 | 1x 212 |  
const pick = require('lodash/fp/pick')
213 |  
214 | const UsersController = (container) => ({
215 |   listUsers: async (req, res, next) => {
216 |     const { ListUsers } = container
217 |  
218 |     try {
219 |       const users = await ListUsers()
220 |       res.send(users)
221 |     } catch (err) {
222 |       next(err)
223 |     }
224 |   },
225 |  
226 |   createUser: async (req, res, next) => {
227 |     const { CreateUser } = container
228 |     const { name, cpf, birthdate, subscription, dependents } = req.body
229 |  
230 |     try {
231 |       const user = await CreateUser(
232 |         name,
233 |         cpf,
234 |         birthdate,
235 |         subscription,
236 |         dependents
237 |       )
238 |  
239 |       res.send(201, user)
240 |     } catch (err) {
241 |       next(err)
242 |     }
243 |   },
244 |  
245 |   findUser: async (req, res, next) => {
246 |     const { GetUser } = container
247 |  
248 |     try {
249 |       const user = await GetUser(req.params.id)
250 |       res.send(user)
251 |     } catch (err) {
252 |       next(err)
253 |     }
254 |   },
255 |  
256 |   deleteUser: async (req, res, next) => {
257 |     const { DeleteUser } = container
258 |  
259 |     try {
260 |       await DeleteUser(req.params.id)
261 |       res.send(204, {})
262 |     } catch (err) {
263 |       next(err)
264 |     }
265 |   },
266 |  
267 |   updateUser: async (req, res, next) => {
268 |     const { UpdateUser } = container
269 |  
270 |     try {
271 |       const permitted = [
272 |         'name',
273 |         'cpf',
274 |         'birthdate',
275 |         'subscription',
276 |         'dependents'
277 |       ]
278 |  
279 |       const user = await UpdateUser(req.params.id, pick(permitted, req.body))
280 |       res.send(user)
281 |     } catch (err) {
282 |       next(err)
283 |     }
284 |   }
285 | })
286 |  
287 | module.exports = UsersController
288 |  
289 | 290 |
291 |
292 | 297 | 298 | 299 | 304 | 305 | 306 | 307 | 308 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/ports/http/errors.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture/ports/http/errors.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / node-clean-architecture/ports/http errors.js

23 |
24 | 25 |
26 | 66.67% 27 | Statements 28 | 2/3 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 0% 41 | Functions 42 | 0/1 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 2/2 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 6 66 | 71x 67 |   68 |   69 |   70 |   71 | 1x 72 |  
const errors = require('restify-errors')
73 |  
74 | class NotFound extends errors.NotFoundError {}
75 | class AlreadyExists extends errors.ForbiddenError {}
76 |  
77 | module.exports = () => ({ NotFound, AlreadyExists })
78 |  
79 | 80 |
81 |
82 | 87 | 88 | 89 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/ports/http/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture/ports/http 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files node-clean-architecture/ports/http

23 |
24 | 25 |
26 | 94.59% 27 | Statements 28 | 35/37 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 87.5% 41 | Functions 42 | 7/8 43 |
44 | 45 | 46 |
47 | 97.06% 48 | Lines 49 | 33/34 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 |
FileStatementsBranchesFunctionsLines
UsersController.js 78 |
79 |
96.77%30/31100%0/0100%6/696.67%29/30
errors.js 93 |
94 |
66.67%2/3100%0/00%0/1100%2/2
routes.js 108 |
109 |
100%3/3100%0/0100%1/1100%2/2
122 |
123 |
124 |
125 | 130 | 131 | 132 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /coverage/node-clean-architecture/ports/http/routes.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for node-clean-architecture/ports/http/routes.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / node-clean-architecture/ports/http routes.js

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 3/3 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 1/1 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 2/2 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

 60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 6 66 | 7 67 | 8 68 | 9 69 | 10 70 | 11 71 | 12 72 | 13 73 | 14 74 | 15 75 | 16 76 | 17 77 | 18 78 | 19 79 | 20 80 | 21 81 | 22 82 | 23 83 | 24 84 | 25 85 | 26 86 | 27 87 | 28 88 | 29 89 | 30 90 | 31 91 | 32 92 | 33 93 | 34 94 | 35 95 | 36 96 | 37 97 | 38 98 | 39 99 | 40 100 | 41 101 | 42 102 | 43 103 | 44 104 | 45 105 | 46 106 | 47 107 | 48 108 | 49 109 | 50 110 | 511x 111 |   112 | 1x 113 |   114 |   115 |   116 |   117 |   118 |   119 |   120 |   121 |   122 |   123 |   124 |   125 |   126 |   127 |   128 |   129 |   130 |   131 |   132 |   133 |   134 |   135 |   136 |   137 |   138 |   139 |   140 |   141 |   142 |   143 |   144 |   145 |   146 |   147 |   148 |   149 |   150 |   151 |   152 |   153 |   154 |   155 |   156 |   157 |   158 |   159 |   160 |  
const restify = require('restify')
161 |  
162 | module.exports = ({ UsersController }) => [
163 |   {
164 |     method: 'get',
165 |     path: '/users',
166 |     handler: UsersController.listUsers
167 |   },
168 |   {
169 |     method: 'post',
170 |     path: '/users',
171 |     handler: UsersController.createUser
172 |   },
173 |   {
174 |     method: 'get',
175 |     path: '/users/:id',
176 |     handler: UsersController.findUser
177 |   },
178 |   {
179 |     method: 'del',
180 |     path: '/users/:id',
181 |     handler: UsersController.deleteUser
182 |   },
183 |   {
184 |     method: 'patch',
185 |     path: '/users/:id',
186 |     handler: UsersController.updateUser
187 |   },
188 |   {
189 |     method: 'put',
190 |     path: '/users/:id',
191 |     handler: UsersController.updateUser
192 |   },
193 |   {
194 |     method: 'get',
195 |     path: '/docs/*',
196 |     handler: restify.plugins.serveStatic({
197 |       directory: `${__dirname}../../../`,
198 |       default: 'index.html'
199 |     })
200 |   },
201 |   {
202 |     method: 'get',
203 |     path: '/coverage/*',
204 |     handler: restify.plugins.serveStatic({
205 |       directory: `${__dirname}../../../`,
206 |       default: 'index.html'
207 |     })
208 |   }
209 | ]
210 |  
211 | 212 |
213 |
214 | 219 | 220 | 221 | 226 | 227 | 228 | 229 | 230 | -------------------------------------------------------------------------------- /coverage/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} 2 | -------------------------------------------------------------------------------- /coverage/prettify.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /coverage/sort-arrow-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rasouza/node-clean-architecture/b99b4b213e6d18bea8b2aaa148e23029f3370004/coverage/sort-arrow-sprite.png -------------------------------------------------------------------------------- /coverage/sorter.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var addSorting = (function() { 3 | 'use strict'; 4 | var cols, 5 | currentSort = { 6 | index: 0, 7 | desc: false 8 | }; 9 | 10 | // returns the summary table element 11 | function getTable() { 12 | return document.querySelector('.coverage-summary'); 13 | } 14 | // returns the thead element of the summary table 15 | function getTableHeader() { 16 | return getTable().querySelector('thead tr'); 17 | } 18 | // returns the tbody element of the summary table 19 | function getTableBody() { 20 | return getTable().querySelector('tbody'); 21 | } 22 | // returns the th element for nth column 23 | function getNthColumn(n) { 24 | return getTableHeader().querySelectorAll('th')[n]; 25 | } 26 | 27 | // loads all columns 28 | function loadColumns() { 29 | var colNodes = getTableHeader().querySelectorAll('th'), 30 | colNode, 31 | cols = [], 32 | col, 33 | i; 34 | 35 | for (i = 0; i < colNodes.length; i += 1) { 36 | colNode = colNodes[i]; 37 | col = { 38 | key: colNode.getAttribute('data-col'), 39 | sortable: !colNode.getAttribute('data-nosort'), 40 | type: colNode.getAttribute('data-type') || 'string' 41 | }; 42 | cols.push(col); 43 | if (col.sortable) { 44 | col.defaultDescSort = col.type === 'number'; 45 | colNode.innerHTML = 46 | colNode.innerHTML + ''; 47 | } 48 | } 49 | return cols; 50 | } 51 | // attaches a data attribute to every tr element with an object 52 | // of data values keyed by column name 53 | function loadRowData(tableRow) { 54 | var tableCols = tableRow.querySelectorAll('td'), 55 | colNode, 56 | col, 57 | data = {}, 58 | i, 59 | val; 60 | for (i = 0; i < tableCols.length; i += 1) { 61 | colNode = tableCols[i]; 62 | col = cols[i]; 63 | val = colNode.getAttribute('data-value'); 64 | if (col.type === 'number') { 65 | val = Number(val); 66 | } 67 | data[col.key] = val; 68 | } 69 | return data; 70 | } 71 | // loads all row data 72 | function loadData() { 73 | var rows = getTableBody().querySelectorAll('tr'), 74 | i; 75 | 76 | for (i = 0; i < rows.length; i += 1) { 77 | rows[i].data = loadRowData(rows[i]); 78 | } 79 | } 80 | // sorts the table using the data for the ith column 81 | function sortByIndex(index, desc) { 82 | var key = cols[index].key, 83 | sorter = function(a, b) { 84 | a = a.data[key]; 85 | b = b.data[key]; 86 | return a < b ? -1 : a > b ? 1 : 0; 87 | }, 88 | finalSorter = sorter, 89 | tableBody = document.querySelector('.coverage-summary tbody'), 90 | rowNodes = tableBody.querySelectorAll('tr'), 91 | rows = [], 92 | i; 93 | 94 | if (desc) { 95 | finalSorter = function(a, b) { 96 | return -1 * sorter(a, b); 97 | }; 98 | } 99 | 100 | for (i = 0; i < rowNodes.length; i += 1) { 101 | rows.push(rowNodes[i]); 102 | tableBody.removeChild(rowNodes[i]); 103 | } 104 | 105 | rows.sort(finalSorter); 106 | 107 | for (i = 0; i < rows.length; i += 1) { 108 | tableBody.appendChild(rows[i]); 109 | } 110 | } 111 | // removes sort indicators for current column being sorted 112 | function removeSortIndicators() { 113 | var col = getNthColumn(currentSort.index), 114 | cls = col.className; 115 | 116 | cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); 117 | col.className = cls; 118 | } 119 | // adds sort indicators for current column being sorted 120 | function addSortIndicators() { 121 | getNthColumn(currentSort.index).className += currentSort.desc 122 | ? ' sorted-desc' 123 | : ' sorted'; 124 | } 125 | // adds event listeners for all sorter widgets 126 | function enableUI() { 127 | var i, 128 | el, 129 | ithSorter = function ithSorter(i) { 130 | var col = cols[i]; 131 | 132 | return function() { 133 | var desc = col.defaultDescSort; 134 | 135 | if (currentSort.index === i) { 136 | desc = !currentSort.desc; 137 | } 138 | sortByIndex(i, desc); 139 | removeSortIndicators(); 140 | currentSort.index = i; 141 | currentSort.desc = desc; 142 | addSortIndicators(); 143 | }; 144 | }; 145 | for (i = 0; i < cols.length; i += 1) { 146 | if (cols[i].sortable) { 147 | // add the click event handler on the th so users 148 | // dont have to click on those tiny arrows 149 | el = getNthColumn(i).querySelector('.sorter').parentElement; 150 | if (el.addEventListener) { 151 | el.addEventListener('click', ithSorter(i)); 152 | } else { 153 | el.attachEvent('onclick', ithSorter(i)); 154 | } 155 | } 156 | } 157 | } 158 | // adds sorting functionality to the UI 159 | return function() { 160 | if (!getTable()) { 161 | return; 162 | } 163 | cols = loadColumns(); 164 | loadData(); 165 | addSortIndicators(); 166 | enableUI(); 167 | }; 168 | })(); 169 | 170 | window.addEventListener('load', addSorting); 171 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | mongo: 4 | image: mongo 5 | restart: always 6 | ports: 7 | - "27017:27017" 8 | volumes: 9 | - ${PWD}/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js 10 | environment: 11 | MONGO_INITDB_DATABASE: users 12 | MONGO_INITDB_ROOT_USERNAME: root 13 | MONGO_INITDB_ROOT_PASSWORD: root -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ReDoc 5 | 6 | 7 | 8 | 9 | 10 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/openapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "description": "CRUD for users", 5 | "version": "1.0.0", 6 | "title": "Node Clean Architecture API", 7 | "contact": { 8 | "email": "alves.wm@gmail.com" 9 | }, 10 | "license": { 11 | "name": "Apache 2.0", 12 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 13 | } 14 | }, 15 | "servers": [ 16 | { 17 | "url": "http://localhost:3000", 18 | "description": "localhost" 19 | } 20 | ], 21 | "paths": { 22 | "/users/{id}": { 23 | "get": { 24 | "tags": ["Users"], 25 | "summary": "Retrieve a single user by ID", 26 | "parameters": [ 27 | { 28 | "name": "id", 29 | "in": "path", 30 | "description": "User ID stored in database", 31 | "required": true, 32 | "schema": { 33 | "type": "integer" 34 | } 35 | } 36 | ], 37 | "responses": { 38 | "201": { 39 | "description": "Success", 40 | "content": { 41 | "application/json": { 42 | "schema": { 43 | "$ref": "#/components/schemas/User" 44 | } 45 | } 46 | } 47 | } 48 | } 49 | }, 50 | "patch": { 51 | "tags": ["Users"], 52 | "summary": "Update user profile", 53 | "parameters": [ 54 | { 55 | "name": "id", 56 | "in": "path", 57 | "description": "User ID stored in database", 58 | "required": true, 59 | "schema": { 60 | "type": "integer" 61 | } 62 | } 63 | ], 64 | "requestBody": { 65 | "required": true, 66 | "content": { 67 | "application/json": { 68 | "schema": { 69 | "type": "object", 70 | "allOf": [ 71 | { 72 | "$ref": "#/components/schemas/UserRequest" 73 | } 74 | ] 75 | } 76 | } 77 | } 78 | }, 79 | "responses": { 80 | "200": { 81 | "description": "Success", 82 | "content": { 83 | "application/json": { 84 | "schema": { 85 | "$ref": "#/components/schemas/User" 86 | } 87 | } 88 | } 89 | }, 90 | "403": { 91 | "description": "CPF already exists in database" 92 | } 93 | } 94 | }, 95 | "delete": { 96 | "tags": ["Users"], 97 | "summary": "Delete user", 98 | "parameters": [ 99 | { 100 | "name": "id", 101 | "in": "path", 102 | "description": "User ID stored in database", 103 | "required": true, 104 | "schema": { 105 | "type": "integer" 106 | } 107 | } 108 | ], 109 | "responses": { 110 | "204": { 111 | "description": "No response" 112 | } 113 | } 114 | } 115 | }, 116 | "/users": { 117 | "get": { 118 | "tags": ["Users"], 119 | "summary": "List all users", 120 | "responses": { 121 | "200": { 122 | "description": "Success", 123 | "content": { 124 | "application/json": { 125 | "schema": { 126 | "type": "array", 127 | "items": { 128 | "$ref": "#/components/schemas/User" 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } 135 | }, 136 | "post": { 137 | "tags": ["Users"], 138 | "summary": "Create a new user", 139 | "requestBody": { 140 | "required": true, 141 | "content": { 142 | "application/json": { 143 | "schema": { 144 | "type": "object", 145 | "allOf": [ 146 | { 147 | "$ref": "#/components/schemas/UserRequest" 148 | } 149 | ] 150 | } 151 | } 152 | } 153 | }, 154 | "responses": { 155 | "201": { 156 | "description": "Created", 157 | "content": { 158 | "application/json": { 159 | "schema": { 160 | "$ref": "#/components/schemas/User" 161 | } 162 | } 163 | } 164 | }, 165 | "403": { 166 | "description": "CPF already exists in database" 167 | } 168 | } 169 | } 170 | } 171 | }, 172 | "components": { 173 | "schemas": { 174 | "User": { 175 | "allOf": [ 176 | { 177 | "$ref": "#/components/schemas/UserRequest" 178 | } 179 | ], 180 | "properties": { 181 | "id": { 182 | "type": "string" 183 | } 184 | } 185 | }, 186 | "UserRequest": { 187 | "properties": { 188 | "name": { 189 | "type": "string" 190 | }, 191 | "cpf": { 192 | "type": "string" 193 | }, 194 | "birthdate": { 195 | "type": "string" 196 | }, 197 | "subscription": { 198 | "type": "string", 199 | "enum": ["Basic", "Standard", "Premium"] 200 | }, 201 | "dependents": { 202 | "type": "number" 203 | } 204 | } 205 | } 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /domain/User.js: -------------------------------------------------------------------------------- 1 | module.exports = () => class { 2 | constructor (id = null, name, cpf, birthdate, subscription, dependents) { 3 | this.id = id 4 | this.name = name 5 | this.cpf = cpf 6 | this.birthdate = birthdate 7 | this.subscription = subscription 8 | this.dependents = dependents 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | require('make-promises-safe') 3 | 4 | const startHttp = require('./infrastructure/webserver/fastify') 5 | const bootstrap = require('./infrastructure/bootstrap') 6 | 7 | const container = bootstrap() 8 | const app = startHttp(container) 9 | 10 | app.listen(3000, (err, address) => { 11 | if (err) { 12 | app.log.error(err) 13 | process.exit(1) 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /infrastructure/bootstrap.js: -------------------------------------------------------------------------------- 1 | const container = require('./bootstrap/container')() 2 | const registerLogger = require('./bootstrap/logger') 3 | const registerDB = require('./bootstrap/database') 4 | 5 | module.exports = () => { 6 | const { DB_DRIVER } = process.env 7 | // Logger 8 | registerLogger(container) 9 | 10 | // Database 11 | switch (DB_DRIVER) { 12 | case 'in-memory': 13 | registerDB.inMemory(container) 14 | break 15 | 16 | case 'mongo': 17 | registerDB.mongo(container) 18 | break 19 | } 20 | 21 | return container.cradle 22 | } 23 | -------------------------------------------------------------------------------- /infrastructure/bootstrap/container.js: -------------------------------------------------------------------------------- 1 | const { createContainer } = require('awilix') 2 | 3 | const container = createContainer() 4 | 5 | module.exports = () => { 6 | container.loadModules([ 7 | 'ports/**/*.js', 8 | 'application/**/*.js', 9 | 'domain/**/*.js' 10 | ]) 11 | 12 | return container 13 | } 14 | -------------------------------------------------------------------------------- /infrastructure/bootstrap/database.js: -------------------------------------------------------------------------------- 1 | const { asValue } = require('awilix') 2 | const connectMongoDB = require('../database/mongo') 3 | 4 | module.exports = { 5 | inMemory (container) { 6 | container.loadModules([ 7 | 'infrastructure/repositories/*InMemory.js' 8 | ]) 9 | }, 10 | 11 | mongo (container) { 12 | const mongoose = connectMongoDB() 13 | 14 | container.register({ 15 | database: asValue(mongoose) 16 | }) 17 | 18 | container.loadModules([ 19 | 'infrastructure/database/schemas/*.js', 20 | 'infrastructure/repositories/*Mongo.js' 21 | ]) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /infrastructure/bootstrap/logger.js: -------------------------------------------------------------------------------- 1 | const { asValue } = require('awilix') 2 | 3 | const resolveLogger = container => { 4 | const isDev = process.env.NODE_ENV === 'development' 5 | const config = { 6 | name: 'app', 7 | prettyPrint: isDev, 8 | level: process.env.LOG_LEVEL || 'info' 9 | } 10 | 11 | const Logger = require('pino')(config) 12 | 13 | container.register({ 14 | Logger: asValue(Logger) 15 | }) 16 | 17 | container.register({ 18 | LoggerConfig: asValue(config) 19 | }) 20 | } 21 | 22 | module.exports = resolveLogger 23 | -------------------------------------------------------------------------------- /infrastructure/database/mongo.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const connect = () => { 4 | const { DB_HOST, DB_PORT, DB_DATABASE, DB_USER, DB_PASS } = process.env 5 | const connectionString = `mongodb://${DB_HOST}:${DB_PORT}/${DB_DATABASE}` 6 | 7 | mongoose.connect(connectionString, { 8 | useNewUrlParser: true, 9 | useUnifiedTopology: true, 10 | useCreateIndex: true, 11 | user: DB_USER, 12 | pass: DB_PASS 13 | }) 14 | 15 | const db = mongoose.connection 16 | db.on('error', console.error.bind(console, 'connection error:')) 17 | db.once('open', () => { 18 | console.log('connected to MongoDB database!') 19 | }) 20 | 21 | return mongoose 22 | } 23 | 24 | module.exports = connect 25 | -------------------------------------------------------------------------------- /infrastructure/database/schemas/User.js: -------------------------------------------------------------------------------- 1 | const { RESOLVER } = require('awilix') 2 | const { Schema } = require('mongoose') 3 | 4 | const userSchema = new Schema({ 5 | name: String, 6 | cpf: { 7 | type: Number, 8 | index: true, 9 | unique: true 10 | }, 11 | birthdate: Date, 12 | subscription: { 13 | type: String, 14 | enum: ['Basic', 'Standard', 'Premium'] 15 | }, 16 | dependents: Number 17 | }) 18 | 19 | const userModel = ({ database: mongoose }) => mongoose.model('User', userSchema) 20 | module.exports = userModel 21 | 22 | userModel[RESOLVER] = { 23 | name: 'UserSchema' 24 | } 25 | -------------------------------------------------------------------------------- /infrastructure/repositories/UserRepositoryInMemory.js: -------------------------------------------------------------------------------- 1 | const { RESOLVER, Lifetime } = require('awilix') 2 | 3 | const filter = require('lodash/fp/filter') 4 | const first = require('lodash/fp/first') 5 | const remove = require('lodash/fp/remove') 6 | const merge = require('lodash/fp/merge') 7 | 8 | const { NotFoundError, AlreadyExistsError } = require('../webserver/errors') 9 | 10 | const UserRepository = () => { 11 | return { 12 | lastId: 0, 13 | db: [], 14 | find () { 15 | return this.db 16 | }, 17 | 18 | persist (user) { 19 | const duplicated = filter({ cpf: user.cpf }, this.db) 20 | if (duplicated.length > 0) throw new AlreadyExistsError('This CPF already exists') 21 | 22 | user.id = `${++this.lastId}` 23 | this.db.push(user) 24 | return user 25 | }, 26 | 27 | get (id) { 28 | const user = first(filter({ id }, this.db)) 29 | if (!user) throw new NotFoundError('User not found') 30 | return user 31 | }, 32 | 33 | merge (id, data) { 34 | let user = this.remove(id) 35 | user = merge(user, data) 36 | 37 | this.db.push(user) 38 | return user 39 | }, 40 | 41 | remove (id) { 42 | const user = this.get(id) 43 | this.db = remove({ id }, this.db) 44 | 45 | return user 46 | } 47 | } 48 | } 49 | 50 | module.exports = UserRepository 51 | UserRepository[RESOLVER] = { 52 | name: 'UserRepository', 53 | lifetime: Lifetime.SINGLETON 54 | // register: asFunction 55 | } 56 | -------------------------------------------------------------------------------- /infrastructure/repositories/UserRepositoryMongo.js: -------------------------------------------------------------------------------- 1 | const { RESOLVER } = require('awilix') 2 | 3 | const { NotFoundError, AlreadyExistsError } = require('../webserver/errors') 4 | 5 | const MONGO_ALREADY_EXISTS = 11000 6 | 7 | const UserRepository = ({ User, UserSchema }) => ({ 8 | find: async () => await UserSchema.find(), 9 | 10 | persist: async user => { 11 | const { name, cpf, birthdate, subscription, dependents } = user 12 | const mongooseUser = new UserSchema({ 13 | name, 14 | cpf, 15 | birthdate, 16 | subscription, 17 | dependents 18 | }) 19 | 20 | try { 21 | await mongooseUser.save() 22 | return new User( 23 | mongooseUser.id, 24 | mongooseUser.name, 25 | mongooseUser.cpf, 26 | mongooseUser.birthdate, 27 | mongooseUser.subscription, 28 | mongooseUser.dependents 29 | ) 30 | } catch (err) { 31 | if (err.code === MONGO_ALREADY_EXISTS) { 32 | throw new AlreadyExistsError('This CPF already exists') 33 | } 34 | throw err 35 | } 36 | }, 37 | 38 | get: async id => { 39 | const mongooseUser = await UserSchema.findById(id) 40 | if (!mongooseUser) throw new NotFoundError('User not found') 41 | 42 | return new User( 43 | mongooseUser.id, 44 | mongooseUser.name, 45 | mongooseUser.cpf, 46 | mongooseUser.birthdate, 47 | mongooseUser.subscription, 48 | mongooseUser.dependents 49 | ) 50 | }, 51 | 52 | merge: async (id, data) => { 53 | try { 54 | const user = await UserSchema 55 | .findByIdAndUpdate(id, data, { new: true }) 56 | 57 | return new User( 58 | user.id, 59 | user.name, 60 | user.cpf, 61 | user.birthdate, 62 | user.subscription, 63 | user.dependents 64 | ) 65 | } catch (err) { 66 | if (err.name === 'CastError') { 67 | throw new NotFoundError('User not found') 68 | } else if (err.code === MONGO_ALREADY_EXISTS) { 69 | throw new AlreadyExistsError('This CPF already exists') 70 | } else { 71 | throw err 72 | } 73 | } 74 | }, 75 | 76 | remove: async (id) => { 77 | const mongooseUser = await UserSchema.findOneAndDelete({ _id: id }) 78 | if (!mongooseUser) { 79 | throw new NotFoundError('User not found') 80 | } 81 | 82 | return mongooseUser 83 | } 84 | }) 85 | 86 | module.exports = UserRepository 87 | UserRepository[RESOLVER] = { 88 | name: 'UserRepository' 89 | // register: asFunction 90 | } 91 | -------------------------------------------------------------------------------- /infrastructure/webserver/errors.js: -------------------------------------------------------------------------------- 1 | function NotFoundError (description) { 2 | Error.call(this) 3 | Error.captureStackTrace(this) 4 | this.name = 'NOT_FOUND' 5 | this.statusCode = 404 6 | this.description = description 7 | this.isOperational = true 8 | } 9 | NotFoundError.prototype = Object.create(Error.prototype) 10 | NotFoundError.prototype.constructor = NotFoundError 11 | 12 | function AlreadyExistsError (description) { 13 | Error.call(this) 14 | Error.captureStackTrace(this) 15 | this.name = 'ALREADY_EXISTS' 16 | this.statusCode = 409 17 | this.description = description 18 | this.isOperational = true 19 | } 20 | AlreadyExistsError.prototype = Object.create(Error.prototype) 21 | AlreadyExistsError.prototype.constructor = AlreadyExistsError 22 | 23 | module.exports = { 24 | NotFoundError, 25 | AlreadyExistsError 26 | } 27 | -------------------------------------------------------------------------------- /infrastructure/webserver/fastify.js: -------------------------------------------------------------------------------- 1 | const each = require('lodash/fp/each') 2 | const fastify = require('fastify') 3 | const fastifyStatic = require('fastify-static') 4 | const path = require('path') 5 | 6 | module.exports = ({ routes, LoggerConfig }) => { 7 | const server = fastify({ 8 | logger: LoggerConfig 9 | }) 10 | 11 | each(path => { 12 | server.route(path) 13 | })(routes) 14 | 15 | /** 16 | * Setup Docs and Coverage static file serving 17 | */ 18 | server.register(fastifyStatic, { 19 | root: path.join(__dirname, '../..'), 20 | redirect: true 21 | }) 22 | server.get('/docs/', (req, res) => { res.sendFile('docs/') }) 23 | server.get('/coverage/', (req, res) => { res.sendFile('coverage/') }) 24 | 25 | return server 26 | } 27 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | module.exports = { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/yl/h579xygd2nx0xzpwl3bcn08w0000gn/T/jest_dx", 15 | 16 | // Automatically clear mock calls, instances, contexts and results before every test 17 | // clearMocks: false, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | collectCoverage: true, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | coverageDirectory: 'coverage', 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | coverageProvider: 'v8', 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // The default configuration for fake timers 54 | // fakeTimers: { 55 | // "enableGlobally": false 56 | // }, 57 | 58 | // Force coverage collection from ignored files using an array of glob patterns 59 | // forceCoverageMatch: [], 60 | 61 | // A path to a module which exports an async function that is triggered once before all test suites 62 | // globalSetup: undefined, 63 | 64 | // A path to a module which exports an async function that is triggered once after all test suites 65 | // globalTeardown: undefined, 66 | 67 | // A set of global variables that need to be available in all test environments 68 | // globals: {}, 69 | 70 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 71 | // maxWorkers: "50%", 72 | 73 | // An array of directory names to be searched recursively up from the requiring module's location 74 | // moduleDirectories: [ 75 | // "node_modules" 76 | // ], 77 | 78 | // An array of file extensions your modules use 79 | // moduleFileExtensions: [ 80 | // "js", 81 | // "mjs", 82 | // "cjs", 83 | // "jsx", 84 | // "ts", 85 | // "tsx", 86 | // "json", 87 | // "node" 88 | // ], 89 | 90 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 91 | // moduleNameMapper: {}, 92 | 93 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 94 | // modulePathIgnorePatterns: [], 95 | 96 | // Activates notifications for test results 97 | // notify: false, 98 | 99 | // An enum that specifies notification mode. Requires { notify: true } 100 | // notifyMode: "failure-change", 101 | 102 | // A preset that is used as a base for Jest's configuration 103 | // preset: undefined, 104 | 105 | // Run tests from one or more projects 106 | // projects: undefined, 107 | 108 | // Use this configuration option to add custom reporters to Jest 109 | // reporters: undefined, 110 | 111 | // Automatically reset mock state before every test 112 | // resetMocks: false, 113 | 114 | // Reset the module registry before running each individual test 115 | // resetModules: false, 116 | 117 | // A path to a custom resolver 118 | // resolver: undefined, 119 | 120 | // Automatically restore mock state and implementation before every test 121 | // restoreMocks: false, 122 | 123 | // The root directory that Jest should scan for tests and modules within 124 | // rootDir: undefined, 125 | 126 | // A list of paths to directories that Jest should use to search for files in 127 | // roots: [ 128 | // "" 129 | // ], 130 | 131 | // Allows you to use a custom runner instead of Jest's default test runner 132 | // runner: "jest-runner", 133 | 134 | // The paths to modules that run some code to configure or set up the testing environment before each test 135 | setupFiles: [ 136 | './tests/setup.js' 137 | ] 138 | 139 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 140 | // setupFilesAfterEnv: [], 141 | 142 | // The number of seconds after which a test is considered as slow and reported as such in the results. 143 | // slowTestThreshold: 5, 144 | 145 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 146 | // snapshotSerializers: [], 147 | 148 | // The test environment that will be used for testing 149 | // testEnvironment: "jest-environment-node", 150 | 151 | // Options that will be passed to the testEnvironment 152 | // testEnvironmentOptions: {}, 153 | 154 | // Adds a location field to test results 155 | // testLocationInResults: false, 156 | 157 | // The glob patterns Jest uses to detect test files 158 | // testMatch: [ 159 | // "**/__tests__/**/*.[jt]s?(x)", 160 | // "**/?(*.)+(spec|test).[tj]s?(x)" 161 | // ], 162 | 163 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 164 | // testPathIgnorePatterns: [ 165 | // "/node_modules/" 166 | // ], 167 | 168 | // The regexp pattern or array of patterns that Jest uses to detect test files 169 | // testRegex: [], 170 | 171 | // This option allows the use of a custom results processor 172 | // testResultsProcessor: undefined, 173 | 174 | // This option allows use of a custom test runner 175 | // testRunner: "jest-circus/runner", 176 | 177 | // A map from regular expressions to paths to transformers 178 | // transform: undefined, 179 | 180 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 181 | // transformIgnorePatterns: [ 182 | // "/node_modules/", 183 | // "\\.pnp\\.[^\\/]+$" 184 | // ], 185 | 186 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 187 | // unmockedModulePathPatterns: undefined, 188 | 189 | // Indicates whether each individual test should be reported during the run 190 | // verbose: undefined, 191 | 192 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 193 | // watchPathIgnorePatterns: [], 194 | 195 | // Whether to use watchman for file crawling 196 | // watchman: true, 197 | } 198 | -------------------------------------------------------------------------------- /mongo-init.js: -------------------------------------------------------------------------------- 1 | db.createUser({ 2 | user: 'myuser', 3 | pwd: 'mypass', 4 | roles: [ 5 | { 6 | role: 'readWrite', 7 | db: 'users' 8 | } 9 | ] 10 | }) 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.3.0", 4 | "private": true, 5 | "description": "User management using Clean Architecture", 6 | "main": "index.js", 7 | "scripts": { 8 | "dev": "NODE_ENV=development nodemon", 9 | "start": "node index.js", 10 | "test": "cross-env NODE_ENV=test jest", 11 | "test:watch": "NODE_ENV=test nyc --reporter=html --report-dir=coverage mocha --require test/setup.js --recursive --parallel --watch" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/rasouza/node-clean-architecture.git" 16 | }, 17 | "author": "R. A. Souza", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/rasouza/node-clean-architecture/issues" 21 | }, 22 | "homepage": "https://github.com/rasouza/node-clean-architecture#readme", 23 | "dependencies": { 24 | "awilix": "^4.2.6", 25 | "dotenv": "^8.2.0", 26 | "fastify": "^3.9.2", 27 | "fastify-static": "^3.3.1", 28 | "lodash-fp": "^0.10.4", 29 | "make-promises-safe": "^5.1.0", 30 | "mongoose": "^5.9.18", 31 | "pino": "^6.9.0" 32 | }, 33 | "devDependencies": { 34 | "@semantic-release/git": "^10.0.1", 35 | "@types/jest": "^28.1.1", 36 | "cross-env": "^7.0.3", 37 | "eslint": "^7.12.1", 38 | "eslint-config-standard": "^16.0.2", 39 | "eslint-plugin-import": "^2.22.1", 40 | "eslint-plugin-jest": "^26.5.3", 41 | "eslint-plugin-node": "^11.1.0", 42 | "eslint-plugin-promise": "^4.2.1", 43 | "eslint-plugin-standard": "^5.0.0", 44 | "jest": "^28.1.0", 45 | "nodemon": "^2.0.4", 46 | "nyc": "^15.1.0", 47 | "pino-pretty": "^4.3.0", 48 | "semantic-release": "^19.0.2" 49 | }, 50 | "release": { 51 | "plugins": [ 52 | "@semantic-release/commit-analyzer", 53 | "@semantic-release/release-notes-generator", 54 | "@semantic-release/github", 55 | "@semantic-release/npm", 56 | "@semantic-release/git" 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ports/http/UsersController.js: -------------------------------------------------------------------------------- 1 | const pick = require('lodash/fp/pick') 2 | 3 | const UsersController = (container) => ({ 4 | listUsers: async (req, res) => { 5 | const { ListUsers } = container 6 | 7 | const users = await ListUsers() 8 | return users 9 | }, 10 | 11 | createUser: async (req, res) => { 12 | const { CreateUser } = container 13 | const { name, cpf, birthdate, subscription, dependents } = req.body 14 | 15 | const user = await CreateUser( 16 | name, 17 | cpf, 18 | birthdate, 19 | subscription, 20 | dependents 21 | ) 22 | 23 | res.code(201).send(user) 24 | }, 25 | 26 | findUser: async (req, res) => { 27 | const { GetUser } = container 28 | 29 | const user = await GetUser(req.params.id) 30 | return user 31 | }, 32 | 33 | deleteUser: async (req, res) => { 34 | const { DeleteUser } = container 35 | 36 | const user = await DeleteUser(req.params.id) 37 | return user 38 | }, 39 | 40 | updateUser: async (req, res) => { 41 | const { UpdateUser } = container 42 | 43 | const permitted = [ 44 | 'name', 45 | 'cpf', 46 | 'birthdate', 47 | 'subscription', 48 | 'dependents' 49 | ] 50 | 51 | const user = await UpdateUser(req.params.id, pick(permitted, req.body)) 52 | return user 53 | } 54 | }) 55 | 56 | module.exports = UsersController 57 | -------------------------------------------------------------------------------- /ports/http/routes.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ UsersController }) => [ 2 | { 3 | method: 'GET', 4 | path: '/users', 5 | handler: UsersController.listUsers 6 | }, 7 | { 8 | method: 'POST', 9 | path: '/users', 10 | handler: UsersController.createUser 11 | }, 12 | { 13 | method: 'GET', 14 | path: '/users/:id', 15 | handler: UsersController.findUser 16 | }, 17 | { 18 | method: 'DELETE', 19 | path: '/users/:id', 20 | handler: UsersController.deleteUser 21 | }, 22 | { 23 | method: 'PATCH', 24 | path: '/users/:id', 25 | handler: UsersController.updateUser 26 | }, 27 | { 28 | method: 'PUT', 29 | path: '/users/:id', 30 | handler: UsersController.updateUser 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rasouza/node-clean-architecture/b99b4b213e6d18bea8b2aaa148e23029f3370004/preview.png -------------------------------------------------------------------------------- /tests/integration/users.integration.test.js: -------------------------------------------------------------------------------- 1 | const startHttp = require('../../infrastructure/webserver/fastify') 2 | const bootstrap = require('../../infrastructure/bootstrap') 3 | 4 | const container = bootstrap() 5 | const app = startHttp(container) 6 | 7 | afterEach(() => { 8 | const { UserRepository } = container 9 | UserRepository.db = [] 10 | }) 11 | 12 | describe('Users API', () => { 13 | describe('When there is no user in database', () => { 14 | describe('GET /users', () => { 15 | it('returns empty list', async () => { 16 | const res = await app.inject() 17 | .get('/users') 18 | const body = res.json() 19 | 20 | expect(res.statusCode).toBe(200) 21 | expect(body).toMatchObject([]) 22 | expect(body).toHaveLength(0) 23 | }) 24 | }) 25 | 26 | describe('POST /users', () => { 27 | it('returns a successful response', async () => { 28 | const user = { 29 | name: 'fake user', 30 | cpf: '111' 31 | } 32 | 33 | const res = await app.inject() 34 | .post('/users') 35 | .payload(user) 36 | 37 | expect(res.statusCode).toBe(201) 38 | expect(res.json()).toEqual({ id: '1', name: 'fake user', cpf: '111' }) 39 | }) 40 | }) 41 | }) 42 | 43 | describe('When a valid user already exists', () => { 44 | beforeEach(() => { 45 | const { UserRepository } = container 46 | 47 | const user = { 48 | name: 'fake user', 49 | cpf: '111', 50 | id: '1' 51 | } 52 | 53 | UserRepository.persist(user) 54 | }) 55 | 56 | describe('GET /users', () => { 57 | it('returns a list with 1 user', async () => { 58 | const res = await app.inject() 59 | .get('/users') 60 | 61 | expect(res.statusCode).toBe(200) 62 | expect(res.json()).toHaveLength(1) 63 | }) 64 | }) 65 | describe('POST /users', () => { 66 | it('returns 409 Conflict when inserting the same CPF', async () => { 67 | const user = { 68 | name: 'another fake user', 69 | cpf: '111' 70 | } 71 | const res = await app.inject() 72 | .post('/users') 73 | .payload(user) 74 | 75 | expect(res.statusCode).toBe(409) 76 | }) 77 | }) 78 | 79 | describe('GET /users/:id', () => { 80 | it('returns 404 when an user not exists', async () => { 81 | const res = await app.inject() 82 | .get('/users/0') 83 | 84 | expect(res.statusCode).toBe(404) 85 | }) 86 | it('returns 200 when an user exists', async () => { 87 | const res = await app.inject() 88 | .get('/users') 89 | 90 | expect(res.statusCode).toBe(200) 91 | expect(res.json()).toMatchObject([{ name: 'fake user' }]) 92 | }) 93 | }) 94 | 95 | describe('PATCH /users/:id', () => { 96 | it('returns 404 when an user not exists', async () => { 97 | const res = await app.inject() 98 | .patch('/users/0') 99 | .payload({ 100 | subscription: 'Basic' 101 | }) 102 | 103 | expect(res.statusCode).toBe(404) 104 | }) 105 | it('returns 200 when an update went successful', async () => { 106 | const user = { 107 | name: 'fake user', 108 | cpf: '222' 109 | } 110 | 111 | const { id } = (await app.inject() 112 | .post('/users') 113 | .payload(user)) 114 | .json() 115 | 116 | const res = await app.inject() 117 | .patch(`/users/${id}`) 118 | .payload({ 119 | subscription: 'Basic', 120 | name: 'Laura' 121 | }) 122 | 123 | expect(res.statusCode).toBe(200) 124 | expect(res.json()).toMatchObject({ id, name: 'Laura', subscription: 'Basic' }) 125 | }) 126 | }) 127 | 128 | describe('DELETE /users/:id ', () => { 129 | it('return 404 when user not exists', async () => { 130 | const res = await app.inject() 131 | .delete('/users/0') 132 | 133 | expect(res.statusCode).toBe(404) 134 | }) 135 | it('return 204 when succesfully deleting user', async () => { 136 | const user = { 137 | name: 'fake user', 138 | cpf: '222' 139 | } 140 | 141 | const { id } = (await app.inject() 142 | .post('/users') 143 | .payload(user)) 144 | .json() 145 | 146 | const res = await app.inject() 147 | .delete(`/users/${id}`) 148 | 149 | expect(res.statusCode).toBe(200) 150 | }) 151 | }) 152 | }) 153 | }) 154 | -------------------------------------------------------------------------------- /tests/integration/users.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | const chaiHttp = require('chai-http') 3 | 4 | chai.use(chaiHttp) 5 | const expect = chai.expect 6 | 7 | describe('Users API', async () => { 8 | describe('When there is no user in database', () => { 9 | it('GET /users returns empty list', async () => { 10 | await chai.request('http://localhost:3000') 11 | .get('/users') 12 | .then(res => { 13 | expect(res).to.be.json 14 | expect(res).to.have.status(200) 15 | expect(res.body).to.be.empty 16 | }) 17 | }) 18 | }) 19 | 20 | describe('When a valid user is inserted', () => { 21 | it('POST /users returns a successful response', async () => { 22 | const user = { 23 | name: 'Rodrigo' 24 | } 25 | 26 | await chai.request('http://localhost:3000') 27 | .post('/users') 28 | .send(user) 29 | .then(res => { 30 | expect(res).to.have.status(201) 31 | expect(res.body).to.eql({ id: '1', name: 'Rodrigo' }) 32 | }) 33 | }) 34 | 35 | it('GET /users returns a list with 1 user', async () => { 36 | await chai.request('http://localhost:3000') 37 | .get('/users') 38 | .then(res => { 39 | expect(res) 40 | expect(res).to.be.json 41 | expect(res).to.have.status(200) 42 | expect(res.body).to.have.lengthOf(1) 43 | }) 44 | }) 45 | 46 | describe('but a CPF already exists', () => { 47 | it('POST /users returns 409 Conflict', async () => { 48 | const user = { 49 | name: 'Rodrigo' 50 | } 51 | await chai.request('http://localhost:3000') 52 | .post('/users') 53 | .send(user) 54 | .catch(err => { 55 | expect(err).to.have.status(409) 56 | }) 57 | }) 58 | }) 59 | }) 60 | 61 | describe('When there are users in database', () => { 62 | describe('GET /users/:id', () => { 63 | it('returns 404 when an user not exists', async () => { 64 | await chai.request('http://localhost:3000') 65 | .get('/users/0') 66 | .catch(err => { 67 | expect(err).to.be.json 68 | expect(err).to.have.status(404) 69 | }) 70 | }) 71 | it('returns 200 when an user exists', async () => { 72 | await chai.request('http://localhost:3000') 73 | .get('/users/1') 74 | .then(res => { 75 | expect(res).to.be.json 76 | expect(res).to.have.status(200) 77 | expect(res.body).to.eql({ id: '1', name: 'Rodrigo' }) 78 | }) 79 | }) 80 | }) 81 | describe('PATCH /users/:id', () => { 82 | it('returns 404 when an user not exists', async () => { 83 | await chai.request('http://localhost:3000') 84 | .patch('/users/0') 85 | .send({ 86 | subscription: 'Basic' 87 | }) 88 | .catch(err => { 89 | expect(err).to.be.json 90 | expect(err).to.have.status(404) 91 | }) 92 | }) 93 | it('returns 200 when an update went successful', async () => { 94 | await chai.request('http://localhost:3000') 95 | .patch('/users/1') 96 | .send({ 97 | subscription: 'Basic', 98 | name: 'Laura' 99 | }) 100 | .then(res => { 101 | expect(res).to.be.json 102 | expect(res).to.have.status(200) 103 | expect(res.body).to.eql({ id: '1', name: 'Laura', subscription: 'Basic' }) 104 | }) 105 | }) 106 | }) 107 | describe('DELETE /users/:id ', () => { 108 | it('return 404 when user not exists', async () => { 109 | await chai.request('http://localhost:3000') 110 | .delete('/users/0') 111 | .catch(err => { 112 | expect(err).to.have.status(404) 113 | }) 114 | }) 115 | it('return 204 when succesfully deleting user', async () => { 116 | await chai.request('http://localhost:3000') 117 | .delete('/users/1') 118 | .then(res => { 119 | expect(res).to.have.status(200) 120 | }) 121 | }) 122 | }) 123 | }) 124 | }) 125 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | process.env.DB_DRIVER = 'in-memory' 2 | process.env.LOG_LEVEL = 'error' 3 | --------------------------------------------------------------------------------