├── .gitignore ├── src ├── server.js ├── controllers │ ├── BanksController.js │ └── RatingsController.js ├── app.js ├── config │ └── database.js ├── database │ ├── seeders │ │ ├── 20191124155251-ratings.js │ │ └── 20191122185532-banks.js │ └── migrations │ │ ├── 20190420125737-create-banks.js │ │ └── 20191124114714-create-ratings.js ├── routes │ └── index.js └── models │ ├── index.js │ ├── Bank.js │ └── Rating.js ├── design ├── banks.xd └── banks.png ├── .env.test ├── .editorconfig ├── __tests__ ├── factories │ ├── fakes │ │ ├── FakeRating.js │ │ └── FakeBank.js │ └── index.js ├── helpers │ └── TruncateHelper.js └── integration │ ├── ratings.test.js │ └── banks.test.js ├── .sequelizerc ├── docker-compose.yml ├── .eslintrc ├── package.json ├── insomnia.json ├── jest.config.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /database 3 | *.env 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | const app = require('./app') 2 | 3 | app.listen(process.env.PORT || 3333) 4 | -------------------------------------------------------------------------------- /design/banks.xd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diogocezar/rsxp-neon-api-nodejs/HEAD/design/banks.xd -------------------------------------------------------------------------------- /design/banks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diogocezar/rsxp-neon-api-nodejs/HEAD/design/banks.png -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # DATABASE 2 | DB_USER=root 3 | DB_PASSWORD=rsxp2019@@ 4 | DB_DATABASE=rsxp-neon-api-nodejs-test 5 | DB_HOST=localhost 6 | DB_DIALECT=mysql 7 | # PORT 8 | PORT=3333 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /src/controllers/BanksController.js: -------------------------------------------------------------------------------- 1 | const { Bank, Rating } = require('../models') 2 | 3 | class BanksController { 4 | async show(req, res) { 5 | return res.status(200).json() 6 | } 7 | } 8 | 9 | module.exports = new BanksController() 10 | -------------------------------------------------------------------------------- /src/controllers/RatingsController.js: -------------------------------------------------------------------------------- 1 | const { Rating } = require('../models') 2 | 3 | class RatingsController { 4 | async create(req, res) { 5 | return res.status(200).json() 6 | } 7 | } 8 | 9 | module.exports = new RatingsController() 10 | -------------------------------------------------------------------------------- /__tests__/factories/fakes/FakeRating.js: -------------------------------------------------------------------------------- 1 | const faker = require('faker') 2 | 3 | const FakeRating = () => ({ 4 | idBank: 1, 5 | idUser: faker.random.word() + faker.random.uuid(), 6 | rating: Math.floor(Math.random() * 5) + 1, 7 | }) 8 | 9 | module.exports = FakeRating 10 | -------------------------------------------------------------------------------- /__tests__/factories/fakes/FakeBank.js: -------------------------------------------------------------------------------- 1 | const faker = require('faker') 2 | 3 | const FakeBank = () => ({ 4 | code: faker.random.number(), 5 | name: faker.name.findName(), 6 | icon: faker.random.word(), 7 | rating: faker.random.number(), 8 | }) 9 | 10 | module.exports = FakeBank 11 | -------------------------------------------------------------------------------- /__tests__/helpers/TruncateHelper.js: -------------------------------------------------------------------------------- 1 | const { sequelize } = require('../../src/models') 2 | 3 | const models = Object.values(sequelize.models).reverse() 4 | 5 | module.exports = async () => Promise.all( 6 | Object.keys(models).map(key => models[key].destroy({ truncate: { cascade: true }, force: true })), 7 | ) 8 | -------------------------------------------------------------------------------- /.sequelizerc: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | 'config': path.resolve('src', 'config', 'database.js'), 5 | 'models-path': path.resolve('src', 'models'), 6 | 'seeders-path': path.resolve('src', 'database', 'seeders'), 7 | 'migrations-path': path.resolve('src', 'database', 'migrations'), 8 | }; 9 | -------------------------------------------------------------------------------- /__tests__/factories/index.js: -------------------------------------------------------------------------------- 1 | const { factory } = require('factory-girl') 2 | const { 3 | Bank, 4 | Rating, 5 | } = require('../../src/models') 6 | 7 | const FakeBank = require('../factories/fakes/FakeBank') 8 | const FakeRating = require('../factories/fakes/FakeRating') 9 | 10 | factory.define('Bank', Bank, FakeBank) 11 | factory.define('Rating', Rating, FakeRating) 12 | 13 | module.exports = factory 14 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | db: 4 | image: mysql:5.6 5 | ports: 6 | - "3306:3306" 7 | volumes: 8 | - ./database:/var/lib/mysql 9 | environment: 10 | - MYSQL_ROOT_PASSWORD=rsxp2019@@ 11 | - MYSQL_DATABASE=rsxp-neon-api-nodejs 12 | app: 13 | image: phpmyadmin/phpmyadmin:latest 14 | links: 15 | - db 16 | ports: 17 | - 8888:80 18 | environment: 19 | - PMA_ARBITRARY=1 20 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ 2 | path: process.env.NODE_ENV === 'test' ? '.env.test' : '.env', 3 | }) 4 | 5 | const express = require('express') 6 | const cors = require('cors') 7 | const routes = require('./routes') 8 | 9 | class App { 10 | constructor() { 11 | this.app = express() 12 | this.app.use(cors()) 13 | this.app.use(express.json()) 14 | this.routes() 15 | } 16 | 17 | routes() { 18 | this.app.use(routes) 19 | } 20 | } 21 | 22 | module.exports = new App().app 23 | -------------------------------------------------------------------------------- /src/config/database.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ 2 | path: process.env.NODE_ENV === 'test' ? '.env.test' : '.env', 3 | }) 4 | 5 | module.exports = { 6 | username: process.env.DB_USER, 7 | password: process.env.DB_PASSWORD, 8 | database: process.env.DB_DATABASE, 9 | host: process.env.DB_HOST, 10 | dialect: process.env.DB_DIALECT, 11 | operatorAlises: false, 12 | logging: false, 13 | define: { 14 | timestamps: true, 15 | underscored: true, 16 | underscoredAll: true, 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /src/database/seeders/20191124155251-ratings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.bulkInsert( 3 | 'ratings', 4 | [ 5 | { 6 | id_bank: 1, 7 | id_user: 1, 8 | rating: 5, 9 | }, 10 | { 11 | id_bank: 1, 12 | id_user: 2, 13 | rating: 4, 14 | }, 15 | { 16 | id_bank: 1, 17 | id_user: 3, 18 | rating: 1, 19 | }, 20 | ], 21 | {}, 22 | ), 23 | down: (queryInterface, Sequelize) => queryInterface.bulkDelete('ratings', null, {}), 24 | } 25 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | 3 | const routes = express.Router() 4 | 5 | const BanksController = require('../controllers/BanksController') 6 | const RatingsController = require('../controllers/RatingsController') 7 | 8 | class Routes { 9 | constructor() { 10 | this.router = routes 11 | this.setRoutes() 12 | } 13 | 14 | setRoutes() { 15 | this.router.get('/banks', BanksController.show) 16 | this.router.post('/rate', RatingsController.create) 17 | } 18 | 19 | getRouter() { 20 | return this.router 21 | } 22 | } 23 | 24 | module.exports = new Routes().getRouter() 25 | -------------------------------------------------------------------------------- /src/models/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const Sequelize = require('sequelize') 4 | const config = require('../config/database') 5 | 6 | const sequelize = new Sequelize( 7 | config.database, 8 | config.username, 9 | config.password, 10 | config, 11 | ) 12 | 13 | const models = { 14 | Bank: require('../models/Bank'), 15 | Rating: require('../models/Rating'), 16 | } 17 | 18 | Object.values(models).forEach(model => model.init(sequelize)) 19 | 20 | Object.values(models) 21 | .filter(model => typeof model.associate === 'function') 22 | .forEach(model => model.associate(models)) 23 | 24 | const db = { 25 | ...models, 26 | sequelize, 27 | } 28 | 29 | module.exports = db 30 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "airbnb-base", 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly" 11 | }, 12 | "parserOptions": { 13 | "ecmaVersion": 2018, 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "semi": [ 18 | 2, 19 | "never" 20 | ], 21 | "class-methods-use-this": 0, 22 | "func-names": [ 23 | "error", 24 | "never" 25 | ], 26 | "camelcase": 0, 27 | "no-param-reassign": 0, 28 | "max-len": 0, 29 | "eqeqeq": 0, 30 | "no-unsafe-finally": 0, 31 | "no-console": 0, 32 | "no-plusplus": 0, 33 | "no-restricted-imports": [ 34 | "error", 35 | "never" 36 | ] 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /src/models/Bank.js: -------------------------------------------------------------------------------- 1 | const { Model, DataTypes } = require('sequelize') 2 | 3 | class Bank extends Model { 4 | static init(sequelize) { 5 | super.init( 6 | { 7 | id: { 8 | type: DataTypes.INTEGER, 9 | allowNull: false, 10 | primaryKey: true, 11 | autoIncrement: true, 12 | }, 13 | name: { 14 | type: DataTypes.STRING, 15 | allowNull: false, 16 | }, 17 | code: { 18 | type: DataTypes.INTEGER, 19 | allowNull: false, 20 | }, 21 | icon: { 22 | type: DataTypes.STRING, 23 | allowNull: false, 24 | }, 25 | }, 26 | { 27 | sequelize, 28 | tableName: 'banks', 29 | }, 30 | ) 31 | } 32 | } 33 | 34 | module.exports = Bank 35 | -------------------------------------------------------------------------------- /__tests__/integration/ratings.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest') 2 | const app = require('../../src/app') 3 | const truncate = require('../helpers/TruncateHelper') 4 | const factory = require('../factories') 5 | 6 | describe('Ratings Controller', () => { 7 | let defaultBank 8 | beforeEach(async () => { 9 | await truncate() 10 | defaultBank = await factory.create('Bank') 11 | }) 12 | 13 | describe('POST /rate', () => { 14 | it('should create a rating', async (done) => { 15 | const rating = { 16 | idBank: defaultBank.id, 17 | rating: 4, 18 | } 19 | const response = await request(app) 20 | .post('/rate') 21 | .set('id_user', 'A1B2C3D4') 22 | .send(rating) 23 | 24 | expect(response.status).toBe(200) 25 | 26 | const data = response.body 27 | expect(data).toHaveProperty('success', true) 28 | done() 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/models/Rating.js: -------------------------------------------------------------------------------- 1 | const { Model, DataTypes } = require('sequelize') 2 | 3 | class Rating extends Model { 4 | static init(sequelize) { 5 | super.init( 6 | { 7 | id: { 8 | type: DataTypes.INTEGER, 9 | allowNull: false, 10 | primaryKey: true, 11 | autoIncrement: true, 12 | }, 13 | idBank: { 14 | type: DataTypes.INTEGER, 15 | field: 'id_bank', 16 | allowNull: false, 17 | references: { 18 | model: 'banks', 19 | key: 'id', 20 | }, 21 | }, 22 | idUser: { 23 | type: DataTypes.INTEGER, 24 | allowNull: false, 25 | }, 26 | rating: { 27 | type: DataTypes.INTEGER, 28 | allowNull: false, 29 | }, 30 | }, 31 | { 32 | sequelize, 33 | tableName: 'ratings', 34 | }, 35 | ) 36 | } 37 | } 38 | 39 | module.exports = Rating 40 | -------------------------------------------------------------------------------- /src/database/migrations/20190420125737-create-banks.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => 3 | queryInterface.createTable("banks", { 4 | id: { 5 | allowNull: false, 6 | autoIncrement: true, 7 | primaryKey: true, 8 | type: DataTypes.INTEGER 9 | }, 10 | name: { 11 | allowNull: false, 12 | type: DataTypes.STRING 13 | }, 14 | code: { 15 | allowNull: false, 16 | type: DataTypes.INTEGER 17 | }, 18 | icon: { 19 | allowNull: false, 20 | type: DataTypes.STRING 21 | }, 22 | created_at: { 23 | allowNull: false, 24 | type: DataTypes.DATE, 25 | defaultValue: DataTypes.literal("NOW()") 26 | }, 27 | updated_at: { 28 | allowNull: true, 29 | type: DataTypes.DATE, 30 | defaultValue: DataTypes.literal("NOW()") 31 | } 32 | }), 33 | down: queryInterface => queryInterface.dropTable("banks") 34 | }; 35 | -------------------------------------------------------------------------------- /src/database/seeders/20191122185532-banks.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.bulkInsert( 3 | 'banks', 4 | [ 5 | { 6 | name: 'Neon', 7 | code: 655, 8 | icon: 'neon.png', 9 | }, 10 | { 11 | name: 'Banco Vestido', 12 | code: 213, 13 | icon: 'banco-vestido.png', 14 | }, 15 | { 16 | name: 'D7', 17 | code: 122, 18 | icon: 'd7.png', 19 | }, 20 | { 21 | name: 'Banco Grêmio', 22 | code: 123, 23 | icon: 'banco-gremio.png', 24 | }, 25 | { 26 | name: 'Box Bank', 27 | code: 425, 28 | icon: 'box-bank.png', 29 | }, 30 | { 31 | name: 'Lento Bank', 32 | code: 345, 33 | icon: 'lento-bank.png', 34 | }, 35 | { 36 | name: 'Lento Bank', 37 | code: 345, 38 | icon: 'lento-bank.png', 39 | }, 40 | ], 41 | {}, 42 | ), 43 | down: (queryInterface, Sequelize) => queryInterface.bulkDelete('banks', null, {}), 44 | } 45 | -------------------------------------------------------------------------------- /src/database/migrations/20191124114714-create-ratings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => 3 | queryInterface.createTable("ratings", { 4 | id: { 5 | allowNull: false, 6 | autoIncrement: true, 7 | primaryKey: true, 8 | type: DataTypes.INTEGER 9 | }, 10 | id_bank: { 11 | type: DataTypes.INTEGER, 12 | references: { 13 | model: { 14 | tableName: "banks" 15 | }, 16 | key: "id" 17 | }, 18 | allowNull: false 19 | }, 20 | id_user: { 21 | allowNull: false, 22 | type: DataTypes.STRING 23 | }, 24 | rating: { 25 | allowNull: false, 26 | type: DataTypes.INTEGER 27 | }, 28 | created_at: { 29 | allowNull: false, 30 | type: DataTypes.DATE, 31 | defaultValue: DataTypes.literal("NOW()") 32 | }, 33 | updated_at: { 34 | allowNull: true, 35 | type: DataTypes.DATE, 36 | defaultValue: DataTypes.literal("NOW()") 37 | } 38 | }), 39 | down: queryInterface => queryInterface.dropTable("ratings") 40 | }; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rscp-neon-api-nodejs", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:diogocezar/rscp-neon-api-nodejs.git", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "node src/server.js", 9 | "dev": "nodemon src/server.js --ignore tests", 10 | "pretest": "NODE_ENV=test sequelize db:migrate", 11 | "test": "NODE_ENV=test jest --detectOpenHandles", 12 | "posttest": "NODE_ENV=test sequelize db:migrate:undo:all", 13 | "migrate": "sequelize db:migrate", 14 | "seed": "sequelize db:seed:all" 15 | }, 16 | "devDependencies": { 17 | "eslint": "^5.16.0", 18 | "eslint-config-airbnb": "^17.1.0", 19 | "eslint-plugin-import": "^2.18.2", 20 | "eslint-plugin-jsx-a11y": "^6.2.1", 21 | "eslint-plugin-react": "^7.12.4", 22 | "factory-girl": "^5.0.4", 23 | "faker": "^4.1.0", 24 | "jest": "24.9", 25 | "nodemon": "1.19.2", 26 | "supertest": "^4.0.2" 27 | }, 28 | "dependencies": { 29 | "cors": "^2.8.5", 30 | "dotenv": "^7.0.0", 31 | "express": "^4.16.4", 32 | "mysql2": "^1.6.5", 33 | "sequelize": "^5.7.1", 34 | "sequelize-auto": "^0.4.29", 35 | "sequelize-cli": "^5.5.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /__tests__/integration/banks.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest') 2 | const app = require('../../src/app') 3 | const truncate = require('../helpers/TruncateHelper') 4 | const factory = require('../factories') 5 | 6 | describe('Banks Controller', () => { 7 | let defaultBank 8 | beforeEach(async () => { 9 | await truncate() 10 | defaultBank = await factory.create('Bank') 11 | await factory.create('Rating', { idBank: defaultBank.id }) 12 | }) 13 | 14 | describe('GET /banks', () => { 15 | it('should get all banks', async (done) => { 16 | const response = await request(app) 17 | .get('/banks') 18 | .set('id_user', 'A1B2C3D4') 19 | .send() 20 | 21 | expect(response.status).toBe(200) 22 | 23 | const data = response.body 24 | expect(data).toBeInstanceOf(Array) 25 | expect(data.length).toBeGreaterThanOrEqual(1) 26 | 27 | const [firstBank] = data 28 | expect(firstBank).toHaveProperty('id', defaultBank.id) 29 | expect(firstBank).toHaveProperty('name', defaultBank.name) 30 | expect(firstBank).toHaveProperty('code', defaultBank.code) 31 | expect(firstBank).toHaveProperty('icon', defaultBank.icon) 32 | expect(firstBank).toHaveProperty('generalRating') 33 | expect(firstBank).toHaveProperty('myRating') 34 | done() 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /insomnia.json: -------------------------------------------------------------------------------- 1 | {"_type":"export","__export_format":4,"__export_date":"2019-11-22T20:14:25.456Z","__export_source":"insomnia.desktop.app:v7.0.3","resources":[{"_id":"req_339a267102bd4b7c9d73ecf080ca0d7c","authentication":{},"body":{"mimeType":"application/json","text":"{\n\t\"rating\" : 5\n}"},"created":1574450840740,"description":"","headers":[{"id":"pair_aa466a1efbb241389803c7d3cadc4d0d","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1574450840740,"method":"POST","modified":1574452354633,"name":"Banks","parameters":[],"parentId":"wrk_45f696cb200648bd8403778c05672368","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:3333/banks/1","_type":"request"},{"_id":"wrk_45f696cb200648bd8403778c05672368","created":1574437521966,"description":"","modified":1574437521966,"name":"RS/XP - Neon","parentId":null,"_type":"workspace"},{"_id":"req_5b88c455ebbc4a068c06bd8ac8cfc32a","authentication":{},"body":{},"created":1574437556242,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1574437556242,"method":"GET","modified":1574437563301,"name":"Banks","parameters":[],"parentId":"wrk_45f696cb200648bd8403778c05672368","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:3333/banks","_type":"request"},{"_id":"env_feec2c7b755a844b93c5e0f23541f124878e4476","color":null,"created":1574437522166,"data":{},"dataPropertyOrder":null,"isPrivate":false,"metaSortKey":1574437522166,"modified":1574437522166,"name":"Base Environment","parentId":"wrk_45f696cb200648bd8403778c05672368","_type":"environment"},{"_id":"jar_feec2c7b755a844b93c5e0f23541f124878e4476","cookies":[],"created":1574437522168,"modified":1574437522168,"name":"Default Jar","parentId":"wrk_45f696cb200648bd8403778c05672368","_type":"cookie_jar"}]} -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | bail: true, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/r_/96tl0ftj12z6fzzvj2h_t3vw0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | collectCoverageFrom: ['src/**', '!src/config', '!src/server.js', '!src/database/**'], 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: '__tests__/coverage', 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // An array of directory names to be searched recursively up from the requiring module's location 64 | // moduleDirectories: [ 65 | // "node_modules" 66 | // ], 67 | 68 | // An array of file extensions your modules use 69 | // moduleFileExtensions: [ 70 | // "js", 71 | // "json", 72 | // "jsx", 73 | // "ts", 74 | // "tsx", 75 | // "node" 76 | // ], 77 | 78 | // A map from regular expressions to module names that allow to stub out resources with a single module 79 | // moduleNameMapper: {}, 80 | 81 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 82 | // modulePathIgnorePatterns: [], 83 | 84 | // Activates notifications for test results 85 | // notify: false, 86 | 87 | // An enum that specifies notification mode. Requires { notify: true } 88 | // notifyMode: "failure-change", 89 | 90 | // A preset that is used as a base for Jest's configuration 91 | // preset: null, 92 | 93 | // Run tests from one or more projects 94 | // projects: null, 95 | 96 | // Use this configuration option to add custom reporters to Jest 97 | // reporters: undefined, 98 | 99 | // Automatically reset mock state between every test 100 | // resetMocks: false, 101 | 102 | // Reset the module registry before running each individual test 103 | // resetModules: false, 104 | 105 | // A path to a custom resolver 106 | // resolver: null, 107 | 108 | // Automatically restore mock state between every test 109 | // restoreMocks: false, 110 | 111 | // The root directory that Jest should scan for tests and modules within 112 | // rootDir: null, 113 | 114 | // A list of paths to directories that Jest should use to search for files in 115 | // roots: [ 116 | // "" 117 | // ], 118 | 119 | // Allows you to use a custom runner instead of Jest's default test runner 120 | // runner: "jest-runner", 121 | 122 | // The paths to modules that run some code to configure or set up the testing environment before each test 123 | // setupFiles: [], 124 | 125 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 126 | // setupFilesAfterEnv: [], 127 | 128 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 129 | // snapshotSerializers: [], 130 | 131 | // The test environment that will be used for testing 132 | testEnvironment: 'node', 133 | 134 | // Options that will be passed to the testEnvironment 135 | // testEnvironmentOptions: {}, 136 | 137 | // Adds a location field to test results 138 | // testLocationInResults: false, 139 | 140 | // The glob patterns Jest uses to detect test files 141 | testMatch: ['**/__tests__/**/*.test.js?(x)'], 142 | 143 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 144 | // testPathIgnorePatterns: [ 145 | // "/node_modules/" 146 | // ], 147 | 148 | // The regexp pattern or array of patterns that Jest uses to detect test files 149 | // testRegex: [], 150 | 151 | // This option allows the use of a custom results processor 152 | // testResultsProcessor: null, 153 | 154 | // This option allows use of a custom test runner 155 | // testRunner: "jasmine2", 156 | 157 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 158 | // testURL: "http://localhost", 159 | 160 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 161 | // timers: "real", 162 | 163 | // A map from regular expressions to paths to transformers 164 | // transform: null, 165 | 166 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 167 | // transformIgnorePatterns: [ 168 | // "/node_modules/" 169 | // ], 170 | 171 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 172 | // unmockedModulePathPatterns: undefined, 173 | 174 | // Indicates whether each individual test should be reported during the run 175 | // verbose: null, 176 | 177 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 178 | // watchPathIgnorePatterns: [], 179 | 180 | // Whether to use watchman for file crawling 181 | // watchman: true, 182 | } 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Neon NodeJS API 2 | 3 | Fala deeevv, bem vindo ao nosso workshop de NodeJS, uma parceria muito bacana entre a Rocketseat e o Banco Neon. 4 | 5 | Hoje, nós vamos focar em algumas coisas que as vezes passam desapercebidas no dia a dia. 6 | 7 | Sabe? Aquele problema que aparece do nada? Um método que não funciona? Então... Vamos mostrar algumas coisas por aqui! 8 | 9 | A ideia é explorar essas dificudades em um exemplo prático, com muita mão na massa! 10 | 11 | ## O que vamos fazer? 12 | 13 | Vamos criar neste projeto um Back-End simples, para fornecer *Bancos* e seus respectivos *Ratings*. 14 | 15 | A idéia é simples: um usuário vê uma lista de bancos, e ele escolher uma nota para cada um ou mais bancos. Com estrelas. de 1 a 5. 16 | 17 | Uma boa maneira de começar qualquer projeto é ter em mente mais do que uma ideia, um desenho. 18 | 19 | E é assim que é o nosso processo na NEON. Claro, que dentro de um processo bem mais complexo... mas... o começo é um desenho. 20 | 21 | Para isso, nossa queria parceira Rebbeca (que é a designer da nossa squad) nos ajudou construindo a imagem que está disponível em /design 22 | 23 | Bom, ela usa o AdobeXD, mas você também pode ser seu PNG a seguir: 24 | 25 | Vamos estudar essa imagem com mais calma! 26 | 27 | Aqui estão os arquivos: 28 | 29 | - https://github.com/diogocezar/rsxp-neon-api-nodejs/ 30 | 31 | ## Quais tecnologias nós vamos utilizar? 32 | 33 | Temos MUITAS tecnologias para escolher, mas... qual vamos utilizar? 34 | 35 | Aqui, vamos escolher uma _stack_ que jah conhecemos e trabalhamos. 36 | 37 | E não muito coincidentemente, é bem parecida com o pessoal da _RocketSeat_. 38 | 39 | Vamos lah! 40 | 41 | ``` 42 | "dependencies": { 43 | "cors": "^2.8.5", 44 | "dotenv": "^7.0.0", 45 | "express": "^4.16.4", 46 | "mysql2": "^1.6.5", 47 | "sequelize": "^5.7.1", 48 | "sequelize-auto": "^0.4.29", 49 | "sequelize-cli": "^5.5.1" 50 | } 51 | ``` 52 | 53 | - vamos usar o cors para permitir os requests de fora da máquina que estiver rodando o projeto; 54 | - o dotenv nos ajuda a obter as variáveis de ambiente; 55 | - express é o nosso framework para desenvolvimento web; 56 | - mysql2 é utilizado pelo sequelize para conexão ao banco; 57 | - o sequelize será o nosso orm; 58 | 59 | E é claro, como já vimos em MUITOS vídeos, também vamos seguir todo um guia de estilos com o ESLint e bla bla bla... 60 | 61 | ``` 62 | "devDependencies": { 63 | "eslint": "^5.16.0", 64 | "eslint-config-airbnb": "^17.1.0", 65 | "eslint-plugin-import": "^2.18.2", 66 | "eslint-plugin-jsx-a11y": "^6.2.1", 67 | "eslint-plugin-react": "^7.12.4", 68 | "factory-girl": "^5.0.4", 69 | "faker": "^4.1.0", 70 | "jest": "24.9", 71 | "nodemon": "1.19.2", 72 | "supertest": "^4.0.2" 73 | }, 74 | ``` 75 | 76 | Aqui estamos incluindo algumas bibliotecas que servirão para executar os testes: 77 | 78 | - faker; 79 | - factory-girl; 80 | - supertest; 81 | 82 | E algumas outras já conhecidas ;) nodemon e eslint*. 83 | 84 | Para nosso ambiente local, vamos usar também a tecnologia docker, em um container com uma imagem que levanta um banco de dados MySQL e um PhpMyAdmin. 85 | 86 | O código do docker-compose: 87 | 88 | ``` 89 | version: "3.3" 90 | services: 91 | db: 92 | image: mysql:5.6 93 | ports: 94 | - "3306:3306" 95 | volumes: 96 | - ./database:/var/lib/mysql 97 | environment: 98 | - MYSQL_ROOT_PASSWORD=rsxp2019@@ 99 | - MYSQL_DATABASE=rsxp-neon-api-nodejs 100 | app: 101 | image: phpmyadmin/phpmyadmin:latest 102 | links: 103 | - db 104 | ports: 105 | - 8888:80 106 | environment: 107 | - PMA_ARBITRARY=1 108 | ``` 109 | 110 | Note que... aqui nós definimos já a senha do banco de dados, e também criamos um volume local, para que os dados não se percam a cada vez que o container precisar ser reiniciado. 111 | 112 | Ainda temos mais alguns arquivos adicionais de configuração, que podem ser exploradores depois, com mais calma: 113 | 114 | - `.editorconfig` - são as definições de estilo para diferentes editores; 115 | - `.eslintrc` - definições de estilo de codificação; 116 | - `.gitignore` - remove os arquivos que não devem ir para o repositório; 117 | - `.sequelizerc` - definições de configuração do sequelize; 118 | - `insomnia.json` - endpoints que podem ser importados no insomnia para testes da API; 119 | - `jest.config.js` - as configurações do jest; 120 | 121 | ## Como está organizado o projeto? 122 | 123 | O projeto está organizado na seguinte estrutura: 124 | 125 | - `__tests__` - é a pasta onde os testes devem ser organizados; 126 | - `factories`- é a pasta para organizar as os fakes da aplicação; 127 | - `helpers` - são ajudantes para trabalhar com o banco de dados; 128 | - `integration` - onde estão os testes de integração; 129 | - `database` - é uma pasta que irá conter os arquivos persistidos do banco de dados; 130 | - `design` - é uma pasta com coisas de design; (AdobeXD) 131 | - `src` - é onde está toda a implementação; 132 | - `config` - é o arquivo no qual configuramos a conexão com o banco de dados; 133 | - `controllers` - é onde estão as nossas controllers; 134 | - `database` - possuem nossas migrations e seeders; 135 | - `models` - são os models da aplicação; 136 | - `routes` - são as rotas; 137 | - `app.js` - é a configuração principal do aplicativo; 138 | - `server.js` - é a configuração do servidor que irá rodar a aplicação principal; 139 | 140 | ## Exemplos dos Envs 141 | 142 | Subir um `.env` nunca é uma coisa muito legal, mas nesse caso, precisamos que todos tenham os 2 ambientes: 143 | 144 | ### .env 145 | 146 | São as variáveis que serão utilizadas em ambientes de desenvolvimento e produção. 147 | 148 | ``` 149 | # DATABASE 150 | DB_USER=root 151 | DB_PASSWORD=rsxp2019@@ 152 | DB_DATABASE=rsxp-neon-api-nodejs 153 | DB_HOST=localhost 154 | DB_DIALECT=mysql 155 | # PORT 156 | PORT=3333 157 | ``` 158 | 159 | ### .env.test 160 | 161 | É um env dedicado aos testes, ele serve apenas para subir um banco de dados temporário para realizar os testes. 162 | 163 | ``` 164 | # DATABASE 165 | DB_USER=root 166 | DB_PASSWORD=rsxp2019@@ 167 | DB_DATABASE=rsxp-neon-api-nodejs-test 168 | DB_HOST=localhost 169 | DB_DIALECT=mysql 170 | # PORT 171 | PORT=3333 172 | ``` 173 | 174 | Notamos que estamos trabalhando com 2 ambientes diferentes, isso para que seja possível subir um banco de dados "estragável" apenas para testar as funcionalidades com o Jest. 175 | 176 | ## Tá... mas o que vamos vamos fazer? 177 | 178 | A idéia é criar a uma funcionalidade simples, para que seja possível listar os bancos e classificá-los. 179 | 180 | ### Listando os bancos 181 | 182 | Precisaremos criar uma rota `/banks` que deverá obter uma estrutura parecida com o a seguinte: 183 | 184 | ``` 185 | [ 186 | { 187 | "id": 1, 188 | "name": "Neon", 189 | "icon": "neon.png", 190 | "code": 655, 191 | "generalRating": 3, 192 | "myRating": 5 193 | }, 194 | { 195 | "id": 2, 196 | "name": "Banco Vestido", 197 | "icon": "banco-vestido.png", 198 | "code": 213, 199 | "generalRating": 0, 200 | "myRating": 0 201 | }, 202 | { 203 | "id": 3, 204 | "name": "D7", 205 | "icon": "d7.png", 206 | "code": 122, 207 | "generalRating": 0, 208 | "myRating": 0 209 | }, 210 | { 211 | "id": 4, 212 | "name": "Banco Grêmio", 213 | "icon": "banco-gremio.png", 214 | "code": 123, 215 | "generalRating": 3, 216 | "myRating": 3 217 | }, 218 | { 219 | "id": 5, 220 | "name": "Box Bank", 221 | "icon": "box-bank.png", 222 | "code": 425, 223 | "generalRating": 0, 224 | "myRating": 0 225 | }, 226 | { 227 | "id": 6, 228 | "name": "Lento Bank", 229 | "icon": "lento-bank.png", 230 | "code": 345, 231 | "generalRating": 0, 232 | "myRating": 0 233 | }, 234 | { 235 | "id": 7, 236 | "name": "Lento Bank", 237 | "icon": "lento-bank.png", 238 | "code": 345, 239 | "generalRating": 0, 240 | "myRating": 0 241 | } 242 | ] 243 | ``` 244 | 245 | - `id` - é o id do banco; 246 | - `name` - é o nome do banco; 247 | - `icon` - é o ícone do banco; 248 | - `code` - é o código do banco; 249 | - `generalRating` - é um campo que deve ser calculado, ou seja, deverá pegar a média de todas as classificações dos bancos e retonar qual é a média daquele banco; 250 | - `myRating` - é a minha classificação para aquele banco, deve-se trazer sempre a última classificação! 251 | 252 | É importante neste caso, enviar pelo `HEADER` uma variável `id_user` para que seja possível saber a sua própria classificação para a consulta. 253 | 254 | ### Salvando o ranking 255 | 256 | Este é um processo mais simples, e para isso vamos apenas receber um corpo de um POST com: 257 | 258 | ``` 259 | { 260 | "idUser" : 1, 261 | "idBank" : 4, 262 | "rating" : 3 263 | } 264 | ``` 265 | 266 | E retornar 267 | 268 | ``` 269 | { 270 | "success": true 271 | } 272 | ``` 273 | 274 | ou 275 | 276 | ``` 277 | { 278 | "error": true 279 | } 280 | ``` 281 | 282 | ## E os testes? 283 | 284 | Sempre que possível, pode ser interessante aplicar a técnica de TDD, ou seja, criar os testes primeiro, antes mesmo da sua aplicação existir. 285 | 286 | O nosso cérebro está preparado para criar coisas complexas a partir de coisas mais simples. 287 | 288 | E isso pode ser um ótimo caminho para ser trilhado também no desenvolvimento. 289 | 290 | Inicie criando os testes da sua aplicação. 291 | 292 | E faça com que a sua aplicação, resolva apenas o que for necessário para passar nos testes! 293 | 294 | Em seguida, começe e implementar cada uma das funcionalidades. Até que todo o sistema esteja pronto. 295 | 296 | Prontos? 297 | 298 | Então... 299 | 300 | Vamos criar um teste, e estudar o que já temos pronto. 301 | 302 | ## Precisamos fazer os testes passarem 303 | 304 | Rodando os testes, é fácil perceber que eles não vão passar. 305 | 306 | Mas podemos burlar estes sistema. 307 | 308 | Para isso, podemos implementar as funcionalidades específicas para que os testes passem! 309 | 310 | E é isso que vamos fazer. 311 | 312 | Criar mocks para os testes passarem! 313 | 314 | ## Agora é a hora de por a mão na massa! 315 | 316 | Com os testes definidos e os mocks passando, precisamos realizar as implementações! 317 | 318 | Notem que o objetivo aqui não é ser o mais performático ou elegante, e sim mostrar os conceitos reais das implementações do nosso dia a dia. 319 | 320 | Vamos nessa! 321 | 322 | ## Tá... mas e quais são as sacadas? 323 | 324 | Tah kras! Até ai tudo bem! E o que tem de diferentes? 325 | 326 | Pode parecer um exemplo simples, mas... nós já passamos por MUITA coisa. 327 | 328 | Vamos discutir alguns dos pontos que nós tivemos que aprender na raça! 329 | 330 | ### Onde ficam as regras de negócios? 331 | 332 | Essa é uma confusão clássica da comunidade, a regra fica nas models ou nas controllers? 333 | 334 | Neste caso, não poderíamos trabalhar com alguns campos virtuais calculados? 335 | 336 | Mas afinal? Onde deixar a regra de negócios? 337 | 338 | Model? 339 | 340 | Controller? 341 | 342 | Pois então, na nossa solução. Isso também foi difícil de entender. E acabamos chegando em um modelo um pouco mais complexo, que trabalha com mais algumas camadas: Service e Repository. 343 | 344 | Dessa forma desacoplamos a controller da regra de negócios, e a regra de negócios do banco de dados. 345 | 346 | #### Assumindo que ela fique no controller, como fazer para acessar um método interno? 347 | 348 | Quando trabalhamos com o `express`, em uma versão com POO, (classes) quando expomos uma função temos um problema: não é possível assumir o contexto this! 349 | 350 | Então como separar possíveis funções? 351 | 352 | Para nós, uma solução foi usar métodos estáticos! 353 | 354 | #### Quais problemas tivemos com a regra de negócios na controller? 355 | 356 | Mas por que deixar as regras de negócio na própria controller não foi uma opção interessante? 357 | 358 | O reaproveitamento de código se tornea muito ineficiente. 359 | 360 | #### Como executar um map que possui awaits? 361 | 362 | Em nossa solução implementada temos um trecho de código interessante: 363 | 364 | ```js 365 | const banks = await Bank.findAll() 366 | const promises = banks.map(async item => ({ 367 | id: item.id, 368 | name: item.name, 369 | icon: item.icon, 370 | code: item.code, 371 | generalRating: await BanksController.extractRating(item.id), 372 | myRating: await BanksController.extractMyRating(idUser, item.id), 373 | } 374 | )) 375 | const jsonReturn = await Promise.all(promises) 376 | return res.status(200).json(jsonReturn) 377 | ``` 378 | 379 | Notemos que promises retorna uma coleção. E como fazer para retornar só quando todos estiverem prontos? Utilizando o `Promise.all` 380 | 381 | #### Sempre previsa-se dos erros 382 | 383 | Use e abude do `try` e `catch`` 384 | 385 | Mas isso foi uma jornada! 386 | 387 | Achar um padrão para retornar os erros, e a forma de como tratar isso no código foi difícil. 388 | 389 | Na nossa solução: 390 | 391 | - Criamos um middleware que trata os erros; 392 | - Criamos dois tipos de erros: Erros de Domínio e Erros Internos; 393 | - Com isso conseguimos separar e direcionar ações e mensagens para o front-end; 394 | 395 | #### Padronizações das formas de retorno (erro, sucesso, etc...) 396 | 397 | A sua API deve seguir um padrão. Mas nem tente fazer isso a cada método! 398 | 399 | A dica é: Crie uma camada que irá cuidar de tratar os erros, mensagens e API's. 400 | 401 | #### Não ignore o poder dos códigos HTTP 402 | 403 | Utilize corretamente um set de códigos de erros HTTP, eles ajudam não só o seu Front-End, mas também sistemas de monitoramento. 404 | 405 | #### Existem várias formas de testar, o importante é que ela exista! 406 | 407 | No caso deste exemplo, usamos um banco `fake` para estragar. Mas... nem sempre isso é possível. 408 | 409 | Os testes unitários devem estar mocados, para que suas funcionalidades sejam testadas independentes do do banco de dados; 410 | 411 | Os testes de integração, devem testar o máximo possível do sistema, de preferência realizando operações em banco, caches, recursos e etc; 412 | 413 | #### Não reinvente a roda! 414 | 415 | Existem várias libs prontas! Boas, e testadas. 416 | 417 | Utilize com sabedoria, entenda a necessidade do projeto e procure por um set de libs que atendam as funcionalidades. 418 | 419 | ## Conclusão 420 | 421 | É isso pessoal, muito obrigado de coração pela oportunidade e pelo espaço! 422 | 423 | Estamos contratando ;) 424 | --------------------------------------------------------------------------------