├── .babelrc ├── .dockerignore ├── .eslintignore ├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .sequelizerc ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── jsconfig.json ├── nginx ├── Dockerfile └── default.conf ├── package.json ├── src ├── config │ └── index.js ├── data │ ├── models │ │ ├── comment.model.js │ │ ├── index.js │ │ ├── person.model.js │ │ └── todo.model.js │ └── repositories │ │ ├── comment.repository.js │ │ ├── createPerson.repository.js │ │ ├── index.js │ │ ├── person.repository.js │ │ └── todo.repository.js ├── server │ ├── app.js │ ├── controllers │ │ ├── comment.controller.js │ │ ├── createPerson.controller.js │ │ ├── index.js │ │ ├── person.controller.js │ │ └── todo.controller.js │ ├── index.js │ ├── middlewares │ │ ├── errorHandler.js │ │ ├── index.js │ │ ├── initResLocalsHandler.js │ │ ├── methodNotAllowedHandler.js │ │ ├── pageNotFoundHandler.js │ │ └── responseHandler.js │ ├── routes │ │ ├── comment.route.js │ │ ├── createPerson.route.js │ │ └── todo.route.js │ ├── services │ │ ├── comment.service.js │ │ ├── createPerson.service.js │ │ ├── index.js │ │ ├── person.service.js │ │ └── todo.service.js │ ├── tests │ │ ├── comment.test.js │ │ ├── createPerson.test.js │ │ ├── factories │ │ │ ├── comment.factory.js │ │ │ ├── index.js │ │ │ ├── person.factory.js │ │ │ └── todo.factory.js │ │ ├── todo.test.js │ │ └── utils.js │ ├── utils │ │ ├── constants │ │ │ ├── errors.js │ │ │ └── fieldChoices.js │ │ ├── errors │ │ │ ├── BadRequest.js │ │ │ ├── BaseError.js │ │ │ ├── Forbidden.js │ │ │ ├── MethodNotAllowed.js │ │ │ ├── NotAcceptable.js │ │ │ ├── NotFound.js │ │ │ ├── Throttled.js │ │ │ ├── Unauthorized.js │ │ │ ├── UnsupportedMediaType.js │ │ │ └── index.js │ │ └── functions.js │ └── validations │ │ ├── comment.validation.js │ │ ├── createPerson.validation.js │ │ ├── index.js │ │ ├── person.validation.js │ │ └── todo.validation.js └── tests │ ├── comment.test.js │ ├── createPerson.test.js │ ├── factories │ ├── comment.factory.js │ ├── index.js │ ├── person.factory.js │ └── todo.factory.js │ ├── todo.test.js │ └── utils.js └── todoapp.im /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": [ 13 | [ 14 | "module-resolver", 15 | { 16 | "root": [ 17 | "./src" 18 | ], 19 | "alias": { 20 | "utils": "./src/server/utils" 21 | } 22 | } 23 | ] 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | bin 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "jest": true 6 | }, 7 | "plugins": ["prettier"], 8 | "extends": ["airbnb", "plugin:prettier/recommended"], 9 | "parserOptions": { 10 | "ecmaVersion": 12, 11 | "sourceType": "module" 12 | }, 13 | "rules": { 14 | "import/no-extraneous-dependencies": [ 15 | "error", 16 | { 17 | "devDependencies": true 18 | } 19 | ], 20 | "no-unused-vars": "warn", 21 | "import/prefer-default-export": "off" 22 | }, 23 | "settings": { 24 | "import/resolver": { 25 | "babel-module": {} 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | workflow_dispatch: 16 | schedule: 17 | - cron: '0 16 * * *' 18 | 19 | jobs: 20 | analyze: 21 | name: Analyze 22 | runs-on: ubuntu-latest 23 | 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | language: [ 'javascript' ] 28 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 29 | # Learn more: 30 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 31 | 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@v2 35 | 36 | # Initializes the CodeQL tools for scanning. 37 | - name: Initialize CodeQL 38 | uses: github/codeql-action/init@v1 39 | with: 40 | languages: ${{ matrix.language }} 41 | # If you wish to specify custom queries, you can do so here or in a config file. 42 | # By default, queries listed here will override any specified in a config file. 43 | # Prefix the list here with "+" to use these queries and those in the config file. 44 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 45 | 46 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 47 | # If this step fails, then you should remove it and run the build manually (see below) 48 | - name: Autobuild 49 | uses: github/codeql-action/autobuild@v1 50 | 51 | # ℹ️ Command-line programs to run using the OS shell. 52 | # 📚 https://git.io/JvXDl 53 | 54 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 55 | # and modify them (or add more) to build your code if your project 56 | # uses a compiled language 57 | 58 | #- run: | 59 | # make bootstrap 60 | # make release 61 | 62 | - name: Perform CodeQL Analysis 63 | uses: github/codeql-action/analyze@v1 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables folder and file 72 | .env 73 | 74 | # parcel-bundler cache (https://parceljs.org/) 75 | .cache 76 | 77 | # Next.js build output 78 | .next 79 | 80 | # Nuxt.js build / generate output 81 | .nuxt 82 | dist 83 | 84 | # Gatsby files 85 | .cache/ 86 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 87 | # https://nextjs.org/blog/next-9-1#public-directory-support 88 | # public 89 | 90 | # vuepress build output 91 | .vuepress/dist 92 | 93 | # Serverless directories 94 | .serverless/ 95 | 96 | # FuseBox cache 97 | .fusebox/ 98 | 99 | # DynamoDB Local files 100 | .dynamodb/ 101 | 102 | # TernJS port file 103 | .tern-port 104 | 105 | # Ignore databases 106 | *.sqlite 107 | *.db 108 | 109 | # Ignore editor folder 110 | .vscode 111 | .idea 112 | 113 | # Imagine Stuff 114 | .imagine 115 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "arrowParens": "always", 7 | "proseWrap": "preserve" 8 | } 9 | -------------------------------------------------------------------------------- /.sequelizerc: -------------------------------------------------------------------------------- 1 | require("@babel/register"); 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | config: path.resolve('src', 'config', 'index.js'), 7 | 'migrations-path': path.resolve('src', 'data', 'migrations'), 8 | 'models-path': path.resolve('src', 'data', 'models'), 9 | 'seeders-path': path.resolve('src', 'data', 'seeders'), 10 | }; 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | 3 | # Create app directory 4 | WORKDIR /usr/src/app 5 | 6 | # Install app dependencies 7 | # Both package.json AND yarn.lock are copied 8 | # where available (npm@5+) 9 | COPY package.json ./ 10 | COPY yarn.lock ./ 11 | 12 | RUN yarn install 13 | # If you are building your code for production 14 | # RUN npm ci --only=production 15 | 16 | # Bundle app source 17 | COPY . . 18 | 19 | EXPOSE 3000 20 | 21 | CMD [ "yarn", "start-dev" ] 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 imagine.ai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CodeQL](https://github.com/imagineai/create-node-app/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/imagineai/create-node-app/actions/workflows/codeql-analysis.yml) 2 | 3 | 4 | 5 |

Create Node App 💛

6 | 7 | > We're a Node.js project starter on steroids! 8 | 9 |
10 | 11 | **One-line command** to create a Node.js app with all the **dependencies auto-installed** 12 |
13 | AND 14 |
15 | **Easy config (alpha release) to generate Node.js source code** for: 16 | - connecting to different databases (**MySQL, PostgreSQL, SQLite3**) 17 | - data model creation 18 | - CRUD API creation (**REST, GraphQL**) 19 | - unit tests and test coverage reporting 20 | - autogenerated test factories 21 | - linting and code formatting (**eslint, prettier**) 22 | - autogenerated API documentation (**Swagger, ReDoc**) 23 | 24 |
25 | 26 |

Quick start

27 | 28 | - **Run the following command** to create your new Node app: 29 | ``` 30 | npm install -g imagine && imagine create -f node -n myapp 31 | ``` 32 | If you don't have `npm` installed, you'll need to [install this first](https://docs.npmjs.com/cli/v7/commands/npm-install). 33 | 34 |
35 | 36 | - You should see this: 37 | 38 | ``` 39 | $ npm install -g imagine && imagine create -f node -n myapp 40 | 41 | changed 214 packages, and audited 215 packages in 5s 42 | .... 43 | found 0 vulnerabilities 44 | 32 files written 45 | You have successfully created a new project! 46 | Now you can run "cd myapp && imagine run" to install the dependencies and open a web server running at 47 | http://127.0.0.1:8000/ 48 | ``` 49 |
50 | 51 | - Run `cd myapp && imagine run` to run your new Node app, and open http://127.0.0.1:8000/ to see that the install worked successfully. 52 | 53 | - **Congrats! Your Node app is up and running!** 54 | 55 | - Now that you've created your new app, **check out the `myapp.im` in your app directory** - using this you can: 56 | - easily change your app settings such as Node server, package manager, API format, database etc. 57 | - generate code for data models, CRUD APIs etc using our simple config spec. 58 | 59 | - Continue reading to learn more, or check out www.imagine.ai. 60 | 61 |
62 |

Learn more

63 | 64 |

Easy to create

65 | 66 | - Our one-line command allows your to get started with your Node.js app immediately, without worrying about installing dependencies - we take care of those, so you can focusing on writing business logic. 67 | 68 | 69 | - Our default settings when we create your Node app are as follows: 70 | - API format: REST 71 | - Database: sqlite3 72 | - Database name: myapp-db 73 | 74 | - These aren't the exact settings you want? No sweat, you can always change the settings as per your preferences - read on to see how to do this. 75 | 76 |
77 | 78 |

Easy to customize

79 | 80 | - If you want to change any of the above defaults for your app, its a piece of cake. 81 | 82 | - Go to the myapp.im file in your directory, your should see the basic default settings here: 83 | 84 |
85 | 86 | ``` 87 | settings 88 | 89 | app: 90 | # your application name 91 | name: myapp 92 | # choose one: [django, node] 93 | framework: node 94 | 95 | api: 96 | # choose one: [rest, graphql] 97 | format: rest 98 | 99 | end settings 100 | 101 | # database 102 | database myapp-db sqlite3 103 | 104 | ``` 105 | 106 | - You can replace the default settings with your preferences (based on the options allowed), and then run `imagine compile myapp.im` in your terminal. Your app will be updated with the new settings. 107 | 108 | 109 |
110 | 111 |

Easy to add app functionality

112 | 113 | - Not only can you change your app settings easily, you can also generated production-ready code using the `myapp.im` file. 114 | 115 | 116 | - Use Imagine's simple syntax to generate code for [data models](https://www.imagine.ai/docs/model) and [CRUD APIs](https://www.imagine.ai/docs/api) to your Node app. 117 | 118 | 119 | - Run `imagine compile myapp.im` to see the generated code. 120 | 121 | - PS - all our generated code has: 122 | - unit tests and test coverage reporting 123 | - autogenerated test factories (using FactoryBoy) 124 | - linting and code formatting (you can select autopep8 or isort) 125 | - autogenerated API documentation (using Swagger and ReDoc) 126 | 127 | - PPS - in this repository, we have included an example to-do app that we created and generated using the `todoapp.im` file. You can check out the `todoapp.im` file that we used to create the Node.js app and express the specifications for the data models and APIs, as well as all the Node.js code files that are generated when you run `imagine compile todoapp.im`. 128 | 129 | - Have fun! 💛 130 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | version: "3.8" 3 | services: 4 | nodeserver: 5 | build: 6 | context: ./ 7 | ports: 8 | - "3000:3000" 9 | nginx: 10 | restart: always 11 | build: 12 | context: ./nginx 13 | ports: 14 | - "80:80" 15 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "*": ["src/*"], 6 | "utils/*": ["src/server/utils/*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | COPY default.conf /etc/nginx/conf.d/default.conf 3 | -------------------------------------------------------------------------------- /nginx/default.conf: -------------------------------------------------------------------------------- 1 | upstream noderest { 2 | server nodeserver:3000; 3 | } 4 | 5 | server { 6 | listen 80; 7 | 8 | location / { 9 | proxy_set_header Host $host; 10 | proxy_set_header X-Real-IP $remote_addr; 11 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 12 | proxy_set_header X-Forwarded-Proto $scheme; 13 | 14 | proxy_pass http://noderest; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "babel src -d dist", 7 | "start": "yarn run build && node dist/server/index.js", 8 | "start-dev": "nodemon src/server --exec babel-node", 9 | "docker:build": "docker-compose up --build", 10 | "docker:up": "docker-compose up", 11 | "format": "prettier --write \"**/*.{js,jsx,json,md}\"", 12 | "lint": "eslint src/ --fix", 13 | "test": "NODE_ENV=test jest --runInBand --verbose", 14 | "coverage": "NODE_ENV=test jest --runInBand --verbose --collect-coverage", 15 | "create-db": "sequelize-cli db:create", 16 | "drop-db": "sequelize-cli db:drop", 17 | "apply-migrations": "sequelize-cli db:migrate", 18 | "revert-migrations": "sequelize-cli db:migrate:undo:all", 19 | "apply-seeders": "sequelize-cli db:seed:all", 20 | "revert-seeders": "sequelize-cli db:seed:undo:all" 21 | }, 22 | "dependencies": { 23 | "cookie-parser": "~1.4.4", 24 | "dayjs": "^1.10.4", 25 | "dotenv": "^8.2.0", 26 | "express": "~4.16.1", 27 | "express-validation": "^3.0.6", 28 | "http-errors": "~1.6.3", 29 | "http-status": "^1.5.0", 30 | "jade": "~1.11.0", 31 | "morgan": "~1.9.1", 32 | "mysql2": "^2.2.5", 33 | "pg": "^8.5.1", 34 | "pg-hstore": "^2.3.3", 35 | "sequelize": "^6.3.5", 36 | "sqlite3": "^5.0.1", 37 | "swagger-ui-express": "^4.1.6" 38 | }, 39 | "devDependencies": { 40 | "@babel/cli": "^7.12.10", 41 | "@babel/core": "^7.12.10", 42 | "@babel/node": "^7.12.10", 43 | "@babel/preset-env": "^7.12.11", 44 | "@babel/register": "^7.12.13", 45 | "babel-jest": "^26.6.3", 46 | "babel-plugin-module-resolver": "^4.1.0", 47 | "debug": "^4.3.1", 48 | "eslint": "^7.17.0", 49 | "eslint-config-airbnb": "^18.2.1", 50 | "eslint-config-airbnb-base": "^14.2.1", 51 | "eslint-config-prettier": "^7.1.0", 52 | "eslint-import-resolver-babel-module": "^5.2.0", 53 | "eslint-plugin-import": "^2.22.1", 54 | "eslint-plugin-prettier": "^3.3.1", 55 | "eslint-plugin-security": "^1.4.0", 56 | "faker": "^5.1.0", 57 | "husky": "^4.3.7", 58 | "jest": "^26.6.3", 59 | "nodemon": "^2.0.7", 60 | "prettier": "^2.2.1", 61 | "sequelize-cli": "^6.2.0", 62 | "supertest": "^6.1.1" 63 | }, 64 | "husky": { 65 | "hooks": { 66 | "pre-commit": "yarn format && yarn lint" 67 | } 68 | }, 69 | "jest": { 70 | "collectCoverageFrom": [ 71 | "src/**", 72 | "!**/tests/**", 73 | "!**/utils/**", 74 | "!**/**test**", 75 | "!**/middlewares/**" 76 | ], 77 | "testPathIgnorePatterns": [ 78 | "/.imagine/" 79 | ] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | dotenv.config(); 4 | 5 | module.exports = { 6 | development: { 7 | username: process.env.DB_USER, 8 | password: process.env.DB_PASSWORD, 9 | database: process.env.DB_DATABASE, 10 | dialect: process.env.DB_DIALECT, 11 | params: { 12 | host: process.env.DB_HOST, 13 | port: process.env.DB_PORT, 14 | storage: process.env.DB_STORAGE, 15 | define: { 16 | underscore: true, 17 | }, 18 | logging: false, 19 | }, 20 | }, 21 | test: { 22 | username: process.env.TEST_DB_USER, 23 | password: process.env.TEST_DB_PASSWORD, 24 | database: process.env.TEST_DB_DATABASE, 25 | dialect: process.env.DB_DIALECT, 26 | params: { 27 | host: process.env.TEST_DB_HOST, 28 | port: process.env.TEST_DB_PORT, 29 | storage: process.env.TEST_DB_STORAGE, 30 | define: { 31 | underscore: true, 32 | }, 33 | logging: false, 34 | }, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /src/data/models/comment.model.js: -------------------------------------------------------------------------------- 1 | import { DataTypes, Sequelize } from 'sequelize'; 2 | import { commentStatusChoices } from 'server/utils/constants/fieldChoices'; 3 | 4 | const commentModel = (sequelize) => { 5 | const Comment = sequelize.define( 6 | 'Comment', 7 | { 8 | id: { 9 | type: DataTypes.INTEGER, 10 | primaryKey: true, 11 | autoIncrement: true, 12 | }, 13 | message: { 14 | type: DataTypes.STRING, 15 | validate: { 16 | len: [0, 512], 17 | }, 18 | }, 19 | submitted: { 20 | type: DataTypes.DATE, 21 | defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), 22 | validate: { 23 | isDate: true, 24 | }, 25 | }, 26 | status: { 27 | type: DataTypes.STRING, 28 | validate: { 29 | isIn: [commentStatusChoices], 30 | }, 31 | }, 32 | }, 33 | { 34 | freezeTableName: true, 35 | } 36 | ); 37 | Comment.associate = (models) => { 38 | Comment.belongsTo(models.Todo, { foreignKey: { name: 'todo', allowNull: false }, as: 'todo_' }); 39 | }; 40 | }; 41 | 42 | export { commentModel }; 43 | -------------------------------------------------------------------------------- /src/data/models/index.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | import config from 'config'; 3 | 4 | import { commentModel } from './comment.model'; 5 | import { personModel } from './person.model'; 6 | import { todoModel } from './todo.model'; 7 | 8 | const env = process.env.NODE_ENV || 'development'; 9 | 10 | const { test, development } = config; 11 | 12 | let database = {}; 13 | 14 | if (env === 'test') { 15 | database = test; 16 | } else database = development; 17 | 18 | const sequelize = new Sequelize(database.database, database.username, database.password, { 19 | dialect: database.dialect, 20 | ...database.params, 21 | }); 22 | 23 | todoModel(sequelize); 24 | commentModel(sequelize); 25 | personModel(sequelize); 26 | 27 | const { Todo, Comment, Person } = sequelize.models; 28 | 29 | Todo.associate(sequelize.models); 30 | Comment.associate(sequelize.models); 31 | Person.associate(sequelize.models); 32 | 33 | sequelize.sync(); 34 | 35 | export { sequelize, Todo, Comment, Person }; 36 | -------------------------------------------------------------------------------- /src/data/models/person.model.js: -------------------------------------------------------------------------------- 1 | import { DataTypes, Sequelize } from 'sequelize'; 2 | 3 | const personModel = (sequelize) => { 4 | const Person = sequelize.define( 5 | 'Person', 6 | { 7 | id: { 8 | type: DataTypes.INTEGER, 9 | primaryKey: true, 10 | autoIncrement: true, 11 | }, 12 | email: { 13 | type: DataTypes.STRING, 14 | validate: { 15 | len: [0, 100], 16 | }, 17 | }, 18 | firstname: { 19 | type: DataTypes.STRING, 20 | validate: { 21 | len: [0, 100], 22 | }, 23 | }, 24 | lastname: { 25 | type: DataTypes.STRING, 26 | validate: { 27 | len: [0, 100], 28 | }, 29 | }, 30 | lastLogin: { 31 | type: DataTypes.DATE, 32 | defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), 33 | validate: { 34 | isDate: true, 35 | }, 36 | }, 37 | }, 38 | { 39 | freezeTableName: true, 40 | } 41 | ); 42 | Person.associate = (models) => { 43 | Person.hasMany(models.Todo, { 44 | foreignKey: { name: 'assignee', allowNull: false }, 45 | as: 'todos', 46 | }); 47 | }; 48 | }; 49 | 50 | export { personModel }; 51 | -------------------------------------------------------------------------------- /src/data/models/todo.model.js: -------------------------------------------------------------------------------- 1 | import { DataTypes, Sequelize } from 'sequelize'; 2 | 3 | const todoModel = (sequelize) => { 4 | const Todo = sequelize.define( 5 | 'Todo', 6 | { 7 | id: { 8 | type: DataTypes.INTEGER, 9 | primaryKey: true, 10 | autoIncrement: true, 11 | }, 12 | title: { 13 | type: DataTypes.STRING, 14 | allowNull: false, 15 | validate: { 16 | len: [0, 255], 17 | }, 18 | }, 19 | description: { 20 | type: DataTypes.STRING, 21 | validate: { 22 | len: [0, 1024], 23 | }, 24 | }, 25 | dueDate: { 26 | type: DataTypes.DATE, 27 | defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), 28 | validate: { 29 | isDate: true, 30 | }, 31 | }, 32 | done: { 33 | type: DataTypes.BOOLEAN, 34 | }, 35 | }, 36 | { 37 | freezeTableName: true, 38 | } 39 | ); 40 | Todo.associate = (models) => { 41 | Todo.hasMany(models.Comment, { 42 | foreignKey: { name: 'todo', allowNull: false }, 43 | as: 'comments', 44 | }); 45 | Todo.belongsTo(models.Person, { 46 | foreignKey: { name: 'assignee', allowNull: false }, 47 | as: 'assignee_', 48 | }); 49 | }; 50 | }; 51 | 52 | export { todoModel }; 53 | -------------------------------------------------------------------------------- /src/data/repositories/comment.repository.js: -------------------------------------------------------------------------------- 1 | import { Comment } from 'data/models'; 2 | import { NotFound } from 'server/utils/errors'; 3 | 4 | class CommentRepository { 5 | static async create(message, submitted, status, todo) { 6 | const comment = await Comment.create({ 7 | message, 8 | submitted, 9 | status, 10 | todo, 11 | }); 12 | 13 | return comment; 14 | } 15 | 16 | static get(id) { 17 | return Comment.findByPk(id, { include: ['todo_'] }); 18 | } 19 | 20 | static getAll(filters) { 21 | return Comment.findAll({ 22 | where: filters, 23 | include: ['todo_'], 24 | }); 25 | } 26 | 27 | static async update(id, message, submitted, status, todo) { 28 | return this.partialUpdate({ 29 | id, 30 | message, 31 | submitted, 32 | status, 33 | todo, 34 | }); 35 | } 36 | 37 | static async partialUpdate({ id, message, submitted, status, todo }) { 38 | const comment = await Comment.findByPk(id); 39 | if (!comment) throw new NotFound(`Comment with primary key ${id} not found`); 40 | if (message !== undefined) comment.message = message; 41 | if (submitted !== undefined) comment.submitted = submitted; 42 | if (status !== undefined) comment.status = status; 43 | if (todo !== undefined) comment.todo = todo; 44 | await comment.save(); 45 | return comment.reload(); 46 | } 47 | 48 | static async destroy(id) { 49 | const comment = await Comment.findByPk(id); 50 | if (!comment) throw new NotFound(`Comment with primary key ${id} not found`); 51 | await comment.destroy(); 52 | return comment; 53 | } 54 | } 55 | 56 | export { CommentRepository }; 57 | -------------------------------------------------------------------------------- /src/data/repositories/createPerson.repository.js: -------------------------------------------------------------------------------- 1 | import { Person } from 'data/models'; 2 | import { NotFound } from 'server/utils/errors'; 3 | 4 | class CreatePersonRepository { 5 | static async create(email, firstname, lastname, lastLogin) { 6 | const person = await Person.create({ 7 | email, 8 | firstname, 9 | lastname, 10 | lastLogin, 11 | }); 12 | 13 | return person; 14 | } 15 | } 16 | 17 | export { CreatePersonRepository }; 18 | -------------------------------------------------------------------------------- /src/data/repositories/index.js: -------------------------------------------------------------------------------- 1 | import { CommentRepository } from './comment.repository'; 2 | import { CreatePersonRepository } from './createPerson.repository'; 3 | import { PersonRepository } from './person.repository'; 4 | import { TodoRepository } from './todo.repository'; 5 | 6 | export { TodoRepository, CommentRepository, PersonRepository, CreatePersonRepository }; 7 | -------------------------------------------------------------------------------- /src/data/repositories/person.repository.js: -------------------------------------------------------------------------------- 1 | import { Person } from 'data/models'; 2 | import { NotFound } from 'server/utils/errors'; 3 | 4 | class PersonRepository { 5 | static async create(email, firstname, lastname, lastLogin) { 6 | const person = await Person.create({ 7 | email, 8 | firstname, 9 | lastname, 10 | lastLogin, 11 | }); 12 | 13 | return person; 14 | } 15 | 16 | static get(id) { 17 | return Person.findByPk(id, { include: ['todos'] }); 18 | } 19 | 20 | static getAll(filters) { 21 | return Person.findAll({ 22 | where: filters, 23 | include: ['todos'], 24 | }); 25 | } 26 | 27 | static async update(id, email, firstname, lastname, lastLogin) { 28 | return this.partialUpdate({ 29 | id, 30 | email, 31 | firstname, 32 | lastname, 33 | lastLogin, 34 | }); 35 | } 36 | 37 | static async partialUpdate({ id, email, firstname, lastname, lastLogin }) { 38 | const person = await Person.findByPk(id); 39 | if (!person) throw new NotFound(`Person with primary key ${id} not found`); 40 | if (email !== undefined) person.email = email; 41 | if (firstname !== undefined) person.firstname = firstname; 42 | if (lastname !== undefined) person.lastname = lastname; 43 | if (lastLogin !== undefined) person.lastLogin = lastLogin; 44 | await person.save(); 45 | return person.reload(); 46 | } 47 | 48 | static async destroy(id) { 49 | const person = await Person.findByPk(id); 50 | if (!person) throw new NotFound(`Person with primary key ${id} not found`); 51 | await person.destroy(); 52 | return person; 53 | } 54 | } 55 | 56 | export { PersonRepository }; 57 | -------------------------------------------------------------------------------- /src/data/repositories/todo.repository.js: -------------------------------------------------------------------------------- 1 | import { Todo } from 'data/models'; 2 | import { NotFound } from 'server/utils/errors'; 3 | 4 | class TodoRepository { 5 | static async create(title, description, dueDate, done, assignee) { 6 | const todo = await Todo.create({ 7 | title, 8 | description, 9 | dueDate, 10 | done, 11 | assignee, 12 | }); 13 | 14 | return todo; 15 | } 16 | 17 | static get(id) { 18 | return Todo.findByPk(id, { include: ['comments', 'assignee_'] }); 19 | } 20 | 21 | static getAll(filters) { 22 | return Todo.findAll({ 23 | where: filters, 24 | include: ['comments', 'assignee_'], 25 | }); 26 | } 27 | 28 | static async update(id, title, description, dueDate, done, assignee) { 29 | return this.partialUpdate({ 30 | id, 31 | title, 32 | description, 33 | dueDate, 34 | done, 35 | assignee, 36 | }); 37 | } 38 | 39 | static async partialUpdate({ id, title, description, dueDate, done, assignee }) { 40 | const todo = await Todo.findByPk(id); 41 | if (!todo) throw new NotFound(`Todo with primary key ${id} not found`); 42 | if (title !== undefined) todo.title = title; 43 | if (description !== undefined) todo.description = description; 44 | if (dueDate !== undefined) todo.dueDate = dueDate; 45 | if (done !== undefined) todo.done = done; 46 | if (assignee !== undefined) todo.assignee = assignee; 47 | await todo.save(); 48 | return todo.reload(); 49 | } 50 | 51 | static async destroy(id) { 52 | const todo = await Todo.findByPk(id); 53 | if (!todo) throw new NotFound(`Todo with primary key ${id} not found`); 54 | await todo.destroy(); 55 | return todo; 56 | } 57 | } 58 | 59 | export { TodoRepository }; 60 | -------------------------------------------------------------------------------- /src/server/app.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | import cookieParser from 'cookie-parser'; 4 | import logger from 'morgan'; 5 | 6 | import { commentRouter } from './routes/comment.route'; 7 | import { createPersonRouter } from './routes/createPerson.route'; 8 | import { todoRouter } from './routes/todo.route'; 9 | 10 | import { 11 | errorHandler, 12 | responseHandler, 13 | pageNotFoundHandler, 14 | initResLocalsHandler, 15 | } from './middlewares'; 16 | 17 | const app = express(); 18 | 19 | // Middlewares 20 | app.use(logger('dev')); 21 | app.use(express.json()); 22 | app.use(express.urlencoded({ extended: false })); 23 | app.use(cookieParser()); 24 | app.use(express.static(path.join(__dirname, 'public'))); 25 | app.use(initResLocalsHandler); 26 | 27 | app.use('/todo', todoRouter); 28 | 29 | app.use('/comment', commentRouter); 30 | 31 | app.use('/create_person', createPersonRouter); 32 | 33 | // Use custom response handler 34 | app.use(responseHandler); 35 | 36 | // Use custom error handler 37 | app.use(errorHandler); 38 | 39 | // Page not found 40 | app.use(pageNotFoundHandler); 41 | 42 | export { app }; 43 | -------------------------------------------------------------------------------- /src/server/controllers/comment.controller.js: -------------------------------------------------------------------------------- 1 | import { CREATED } from 'http-status'; 2 | import { CommentService, TodoService } from 'server/services'; 3 | import { NotFound } from 'utils/errors/NotFound'; 4 | 5 | class CommentController { 6 | static async create(req, res, next) { 7 | try { 8 | const { message, submitted, status, todo } = req.body; 9 | if (todo !== null && typeof todo !== 'undefined') { 10 | const dbtodo = await TodoService.get(todo); 11 | if (!dbtodo) { 12 | throw new NotFound(`Todo ${todo} not found`); 13 | } 14 | } 15 | const newComment = await CommentService.create(message, submitted, status, todo); 16 | res.locals.status = CREATED; 17 | res.locals.data = newComment; 18 | return next(); 19 | } catch (error) { 20 | return next(error); 21 | } 22 | } 23 | 24 | static async get(req, res, next) { 25 | try { 26 | const { id } = req.params; 27 | const comment = await CommentService.get(id); 28 | if (!comment) { 29 | throw new NotFound(`Comment with primary key ${id} not found`); 30 | } 31 | res.locals.data = comment; 32 | return next(); 33 | } catch (error) { 34 | return next(error); 35 | } 36 | } 37 | 38 | static async getAll(req, res, next) { 39 | try { 40 | const filters = { ...req.query }; 41 | const allComments = await CommentService.getAll(filters); 42 | res.locals.data = allComments; 43 | return next(); 44 | } catch (error) { 45 | return next(error); 46 | } 47 | } 48 | 49 | static async update(req, res, next) { 50 | try { 51 | const { id } = req.params; 52 | const { message, submitted, status, todo } = req.body; 53 | if (todo !== null && typeof todo !== 'undefined') { 54 | if (!(await TodoService.get(todo))) { 55 | throw new NotFound(`Todo ${todo} not found`); 56 | } 57 | } 58 | 59 | const updatedComment = await CommentService.update(id, message, submitted, status, todo); 60 | if (!updatedComment) { 61 | throw new NotFound(`Comment with primary key ${id} not found`); 62 | } 63 | 64 | res.locals.data = updatedComment; 65 | return next(); 66 | } catch (error) { 67 | return next(error); 68 | } 69 | } 70 | 71 | static async partialUpdate(req, res, next) { 72 | try { 73 | const { id } = req.params; 74 | const { message, submitted, status, todo } = req.body; 75 | if (todo !== null && typeof todo !== 'undefined') { 76 | if (!(await TodoService.get(todo))) { 77 | throw new NotFound(`Todo ${todo} not found`); 78 | } 79 | } 80 | 81 | const updatedComment = await CommentService.partialUpdate( 82 | id, 83 | message, 84 | submitted, 85 | status, 86 | todo 87 | ); 88 | if (!updatedComment) { 89 | throw new NotFound(`Comment with primary key ${id} not found`); 90 | } 91 | 92 | res.locals.data = updatedComment; 93 | return next(); 94 | } catch (error) { 95 | return next(error); 96 | } 97 | } 98 | 99 | static async destroy(req, res, next) { 100 | try { 101 | const { id } = req.params; 102 | const commentDelete = await CommentService.destroy(id); 103 | if (!commentDelete) { 104 | throw new NotFound(`Comment with primary key ${id} not found`); 105 | } 106 | res.locals.data = commentDelete; 107 | return next(); 108 | } catch (error) { 109 | return next(error); 110 | } 111 | } 112 | } 113 | 114 | export { CommentController }; 115 | -------------------------------------------------------------------------------- /src/server/controllers/createPerson.controller.js: -------------------------------------------------------------------------------- 1 | import { CREATED } from 'http-status'; 2 | import { CreatePersonService } from 'server/services'; 3 | import { NotFound } from 'utils/errors/NotFound'; 4 | 5 | class CreatePersonController { 6 | static async create(req, res, next) { 7 | try { 8 | const { email, firstname, lastname, lastLogin } = req.body; 9 | const newCreatePerson = await CreatePersonService.create( 10 | email, 11 | firstname, 12 | lastname, 13 | lastLogin 14 | ); 15 | res.locals.status = CREATED; 16 | res.locals.data = newCreatePerson; 17 | return next(); 18 | } catch (error) { 19 | return next(error); 20 | } 21 | } 22 | } 23 | 24 | export { CreatePersonController }; 25 | -------------------------------------------------------------------------------- /src/server/controllers/index.js: -------------------------------------------------------------------------------- 1 | import { CommentController } from './comment.controller'; 2 | import { CreatePersonController } from './createPerson.controller'; 3 | import { PersonController } from './person.controller'; 4 | import { TodoController } from './todo.controller'; 5 | 6 | export { TodoController, CommentController, PersonController, CreatePersonController }; 7 | -------------------------------------------------------------------------------- /src/server/controllers/person.controller.js: -------------------------------------------------------------------------------- 1 | import { CREATED } from 'http-status'; 2 | import { PersonService } from 'server/services'; 3 | import { NotFound } from 'utils/errors/NotFound'; 4 | 5 | class PersonController { 6 | static async create(req, res, next) { 7 | try { 8 | const { email, firstname, lastname, lastLogin } = req.body; 9 | const newPerson = await PersonService.create(email, firstname, lastname, lastLogin); 10 | res.locals.status = CREATED; 11 | res.locals.data = newPerson; 12 | return next(); 13 | } catch (error) { 14 | return next(error); 15 | } 16 | } 17 | 18 | static async get(req, res, next) { 19 | try { 20 | const { id } = req.params; 21 | const person = await PersonService.get(id); 22 | if (!person) { 23 | throw new NotFound(`Person with primary key ${id} not found`); 24 | } 25 | res.locals.data = person; 26 | return next(); 27 | } catch (error) { 28 | return next(error); 29 | } 30 | } 31 | 32 | static async getAll(req, res, next) { 33 | try { 34 | const filters = { ...req.query }; 35 | const allPersons = await PersonService.getAll(filters); 36 | res.locals.data = allPersons; 37 | return next(); 38 | } catch (error) { 39 | return next(error); 40 | } 41 | } 42 | 43 | static async update(req, res, next) { 44 | try { 45 | const { id } = req.params; 46 | const { email, firstname, lastname, lastLogin } = req.body; 47 | 48 | const updatedPerson = await PersonService.update(id, email, firstname, lastname, lastLogin); 49 | if (!updatedPerson) { 50 | throw new NotFound(`Person with primary key ${id} not found`); 51 | } 52 | 53 | res.locals.data = updatedPerson; 54 | return next(); 55 | } catch (error) { 56 | return next(error); 57 | } 58 | } 59 | 60 | static async partialUpdate(req, res, next) { 61 | try { 62 | const { id } = req.params; 63 | const { email, firstname, lastname, lastLogin } = req.body; 64 | 65 | const updatedPerson = await PersonService.partialUpdate( 66 | id, 67 | email, 68 | firstname, 69 | lastname, 70 | lastLogin 71 | ); 72 | if (!updatedPerson) { 73 | throw new NotFound(`Person with primary key ${id} not found`); 74 | } 75 | 76 | res.locals.data = updatedPerson; 77 | return next(); 78 | } catch (error) { 79 | return next(error); 80 | } 81 | } 82 | 83 | static async destroy(req, res, next) { 84 | try { 85 | const { id } = req.params; 86 | const personDelete = await PersonService.destroy(id); 87 | if (!personDelete) { 88 | throw new NotFound(`Person with primary key ${id} not found`); 89 | } 90 | res.locals.data = personDelete; 91 | return next(); 92 | } catch (error) { 93 | return next(error); 94 | } 95 | } 96 | } 97 | 98 | export { PersonController }; 99 | -------------------------------------------------------------------------------- /src/server/controllers/todo.controller.js: -------------------------------------------------------------------------------- 1 | import { CREATED } from 'http-status'; 2 | import { TodoService, PersonService } from 'server/services'; 3 | import { NotFound } from 'utils/errors/NotFound'; 4 | 5 | class TodoController { 6 | static async create(req, res, next) { 7 | try { 8 | const { title, description, dueDate, done, assignee } = req.body; 9 | if (assignee !== null && typeof assignee !== 'undefined') { 10 | const dbassignee = await PersonService.get(assignee); 11 | if (!dbassignee) { 12 | throw new NotFound(`Person ${assignee} not found`); 13 | } 14 | } 15 | const newTodo = await TodoService.create(title, description, dueDate, done, assignee); 16 | res.locals.status = CREATED; 17 | res.locals.data = newTodo; 18 | return next(); 19 | } catch (error) { 20 | return next(error); 21 | } 22 | } 23 | 24 | static async get(req, res, next) { 25 | try { 26 | const { id } = req.params; 27 | const todo = await TodoService.get(id); 28 | if (!todo) { 29 | throw new NotFound(`Todo with primary key ${id} not found`); 30 | } 31 | res.locals.data = todo; 32 | return next(); 33 | } catch (error) { 34 | return next(error); 35 | } 36 | } 37 | 38 | static async getAll(req, res, next) { 39 | try { 40 | const filters = { ...req.query }; 41 | const allTodos = await TodoService.getAll(filters); 42 | res.locals.data = allTodos; 43 | return next(); 44 | } catch (error) { 45 | return next(error); 46 | } 47 | } 48 | 49 | static async update(req, res, next) { 50 | try { 51 | const { id } = req.params; 52 | const { title, description, dueDate, done, assignee } = req.body; 53 | if (assignee !== null && typeof assignee !== 'undefined') { 54 | if (!(await PersonService.get(assignee))) { 55 | throw new NotFound(`Person ${assignee} not found`); 56 | } 57 | } 58 | 59 | const updatedTodo = await TodoService.update(id, title, description, dueDate, done, assignee); 60 | if (!updatedTodo) { 61 | throw new NotFound(`Todo with primary key ${id} not found`); 62 | } 63 | 64 | res.locals.data = updatedTodo; 65 | return next(); 66 | } catch (error) { 67 | return next(error); 68 | } 69 | } 70 | 71 | static async partialUpdate(req, res, next) { 72 | try { 73 | const { id } = req.params; 74 | const { title, description, dueDate, done, assignee } = req.body; 75 | if (assignee !== null && typeof assignee !== 'undefined') { 76 | if (!(await PersonService.get(assignee))) { 77 | throw new NotFound(`Person ${assignee} not found`); 78 | } 79 | } 80 | 81 | const updatedTodo = await TodoService.partialUpdate( 82 | id, 83 | title, 84 | description, 85 | dueDate, 86 | done, 87 | assignee 88 | ); 89 | if (!updatedTodo) { 90 | throw new NotFound(`Todo with primary key ${id} not found`); 91 | } 92 | 93 | res.locals.data = updatedTodo; 94 | return next(); 95 | } catch (error) { 96 | return next(error); 97 | } 98 | } 99 | 100 | static async destroy(req, res, next) { 101 | try { 102 | const { id } = req.params; 103 | const todoDelete = await TodoService.destroy(id); 104 | if (!todoDelete) { 105 | throw new NotFound(`Todo with primary key ${id} not found`); 106 | } 107 | res.locals.data = todoDelete; 108 | return next(); 109 | } catch (error) { 110 | return next(error); 111 | } 112 | } 113 | } 114 | 115 | export { TodoController }; 116 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | import { app } from './app'; 2 | 3 | const PORT = process.env.PORT || 3000; 4 | app.listen(PORT, () => { 5 | console.log(`Express server listening on port ${PORT}`); 6 | }); 7 | -------------------------------------------------------------------------------- /src/server/middlewares/errorHandler.js: -------------------------------------------------------------------------------- 1 | import { BAD_REQUEST, INTERNAL_SERVER_ERROR } from 'http-status'; 2 | import { ValidationError as ExpressValidationError } from 'express-validation'; 3 | import { ValidationError as SequelizeValidationError } from 'sequelize'; 4 | import { BaseError } from 'utils/errors/BaseError'; 5 | import { createErrorResponse } from 'utils/functions'; 6 | import { errors } from 'utils/constants/errors'; 7 | 8 | const errorHandler = (err, req, res, next) => { 9 | if (err instanceof BaseError) { 10 | return res 11 | .status(err.statusCode) 12 | .json(createErrorResponse(err.statusCode, err.type, undefined, err.message)); 13 | } 14 | 15 | if (err instanceof ExpressValidationError) { 16 | const param = Object.keys(err.details[0])[0]; 17 | const msg = err.details[0][param]; 18 | return res 19 | .status(err.statusCode) 20 | .json(createErrorResponse(err.statusCode, errors.validation, param, msg)); 21 | } 22 | 23 | if (err instanceof SyntaxError) { 24 | return res 25 | .status(err.statusCode) 26 | .json(createErrorResponse(err.statusCode, errors.parse, undefined, err.message)); 27 | } 28 | 29 | if (err instanceof SequelizeValidationError) { 30 | const param = err.fields[0]; 31 | const msg = err.errors[0].message; 32 | return res 33 | .status(BAD_REQUEST) 34 | .json(createErrorResponse(BAD_REQUEST, errors.validation, param, msg)); 35 | } 36 | 37 | return res 38 | .status(INTERNAL_SERVER_ERROR) 39 | .json(createErrorResponse(INTERNAL_SERVER_ERROR, errors.server, undefined, err.message)); 40 | }; 41 | 42 | export { errorHandler }; 43 | -------------------------------------------------------------------------------- /src/server/middlewares/index.js: -------------------------------------------------------------------------------- 1 | import { errorHandler } from './errorHandler'; 2 | import { responseHandler } from './responseHandler'; 3 | import { methodNotAllowedHandler } from './methodNotAllowedHandler'; 4 | import { pageNotFoundHandler } from './pageNotFoundHandler'; 5 | import { initResLocalsHandler } from './initResLocalsHandler'; 6 | 7 | export { 8 | errorHandler, 9 | responseHandler, 10 | methodNotAllowedHandler, 11 | pageNotFoundHandler, 12 | initResLocalsHandler, 13 | }; 14 | -------------------------------------------------------------------------------- /src/server/middlewares/initResLocalsHandler.js: -------------------------------------------------------------------------------- 1 | import { OK } from 'http-status'; 2 | 3 | const initResLocalsHandler = (req, res, next) => { 4 | res.locals.status = OK; 5 | res.locals.data = null; 6 | return next(); 7 | }; 8 | 9 | export { initResLocalsHandler }; 10 | -------------------------------------------------------------------------------- /src/server/middlewares/methodNotAllowedHandler.js: -------------------------------------------------------------------------------- 1 | import { MethodNotAllowed } from '../utils/errors'; 2 | 3 | const methodNotAllowedHandler = () => { 4 | throw new MethodNotAllowed(); 5 | }; 6 | 7 | export { methodNotAllowedHandler }; 8 | -------------------------------------------------------------------------------- /src/server/middlewares/pageNotFoundHandler.js: -------------------------------------------------------------------------------- 1 | import { NOT_FOUND } from 'http-status'; 2 | import { errors } from 'utils/constants/errors'; 3 | import { createErrorResponse } from 'utils/functions'; 4 | 5 | const pageNotFoundHandler = (req, res) => 6 | res 7 | .status(NOT_FOUND) 8 | .json(createErrorResponse(NOT_FOUND, errors.not_found, undefined, '404 - Page not found')); 9 | 10 | export { pageNotFoundHandler }; 11 | -------------------------------------------------------------------------------- /src/server/middlewares/responseHandler.js: -------------------------------------------------------------------------------- 1 | import { createSuccessResponse } from 'utils/functions'; 2 | 3 | const responseHandler = (req, res, next) => { 4 | if (res.locals.data) { 5 | return res 6 | .status(res.locals.status) 7 | .json(createSuccessResponse(res.locals.status, res.locals.data)); 8 | } 9 | return next(); 10 | }; 11 | 12 | export { responseHandler }; 13 | -------------------------------------------------------------------------------- /src/server/routes/comment.route.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { validate } from 'express-validation'; 3 | import { CommentController } from 'server/controllers'; 4 | import { commentValidation, options } from 'server/validations'; 5 | 6 | const router = Router(); 7 | 8 | router.get('/', validate(commentValidation.getAll, options), CommentController.getAll); 9 | 10 | router.get('/:id', CommentController.get); 11 | 12 | router.post('/', validate(commentValidation.create, options), CommentController.create); 13 | 14 | router.put('/:id', validate(commentValidation.update, options), CommentController.update); 15 | 16 | router.patch( 17 | '/:id', 18 | validate(commentValidation.partialUpdate, options), 19 | CommentController.partialUpdate 20 | ); 21 | 22 | router.delete('/:id', validate(commentValidation.destroy, options), CommentController.destroy); 23 | 24 | export { router as commentRouter }; 25 | -------------------------------------------------------------------------------- /src/server/routes/createPerson.route.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { validate } from 'express-validation'; 3 | import { CreatePersonController } from 'server/controllers'; 4 | import { createPersonValidation, options } from 'server/validations'; 5 | 6 | const router = Router(); 7 | 8 | router.post('/', validate(createPersonValidation.create, options), CreatePersonController.create); 9 | 10 | export { router as createPersonRouter }; 11 | -------------------------------------------------------------------------------- /src/server/routes/todo.route.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { validate } from 'express-validation'; 3 | import { TodoController } from 'server/controllers'; 4 | import { todoValidation, options } from 'server/validations'; 5 | 6 | const router = Router(); 7 | 8 | router.get('/', validate(todoValidation.getAll, options), TodoController.getAll); 9 | 10 | router.get('/:id', TodoController.get); 11 | 12 | router.post('/', validate(todoValidation.create, options), TodoController.create); 13 | 14 | router.put('/:id', validate(todoValidation.update, options), TodoController.update); 15 | 16 | router.patch('/:id', validate(todoValidation.partialUpdate, options), TodoController.partialUpdate); 17 | 18 | router.delete('/:id', validate(todoValidation.destroy, options), TodoController.destroy); 19 | 20 | export { router as todoRouter }; 21 | -------------------------------------------------------------------------------- /src/server/services/comment.service.js: -------------------------------------------------------------------------------- 1 | import { CommentRepository } from 'data/repositories'; 2 | 3 | class CommentService { 4 | static create(message, submitted, status, todo) { 5 | return CommentRepository.create(message, submitted, status, todo); 6 | } 7 | 8 | static get(id) { 9 | return CommentRepository.get(id); 10 | } 11 | 12 | static getAll(args) { 13 | return CommentRepository.getAll(args); 14 | } 15 | 16 | static update(id, message, submitted, status, todo) { 17 | return CommentRepository.update(id, message, submitted, status, todo); 18 | } 19 | 20 | static partialUpdate(id, message, submitted, status, todo) { 21 | return CommentRepository.partialUpdate({ id, message, submitted, status, todo }); 22 | } 23 | 24 | static destroy(id) { 25 | return CommentRepository.destroy(id); 26 | } 27 | } 28 | 29 | export { CommentService }; 30 | -------------------------------------------------------------------------------- /src/server/services/createPerson.service.js: -------------------------------------------------------------------------------- 1 | import { CreatePersonRepository } from 'data/repositories'; 2 | 3 | class CreatePersonService { 4 | static create(email, firstname, lastname, lastLogin) { 5 | return CreatePersonRepository.create(email, firstname, lastname, lastLogin); 6 | } 7 | } 8 | 9 | export { CreatePersonService }; 10 | -------------------------------------------------------------------------------- /src/server/services/index.js: -------------------------------------------------------------------------------- 1 | import { CommentService } from './comment.service'; 2 | import { CreatePersonService } from './createPerson.service'; 3 | import { PersonService } from './person.service'; 4 | import { TodoService } from './todo.service'; 5 | 6 | export { TodoService, CommentService, PersonService, CreatePersonService }; 7 | -------------------------------------------------------------------------------- /src/server/services/person.service.js: -------------------------------------------------------------------------------- 1 | import { PersonRepository } from 'data/repositories'; 2 | 3 | class PersonService { 4 | static create(email, firstname, lastname, lastLogin) { 5 | return PersonRepository.create(email, firstname, lastname, lastLogin); 6 | } 7 | 8 | static get(id) { 9 | return PersonRepository.get(id); 10 | } 11 | 12 | static getAll(args) { 13 | return PersonRepository.getAll(args); 14 | } 15 | 16 | static update(id, email, firstname, lastname, lastLogin) { 17 | return PersonRepository.update(id, email, firstname, lastname, lastLogin); 18 | } 19 | 20 | static partialUpdate(id, email, firstname, lastname, lastLogin) { 21 | return PersonRepository.partialUpdate({ id, email, firstname, lastname, lastLogin }); 22 | } 23 | 24 | static destroy(id) { 25 | return PersonRepository.destroy(id); 26 | } 27 | } 28 | 29 | export { PersonService }; 30 | -------------------------------------------------------------------------------- /src/server/services/todo.service.js: -------------------------------------------------------------------------------- 1 | import { TodoRepository } from 'data/repositories'; 2 | 3 | class TodoService { 4 | static create(title, description, dueDate, done, assignee) { 5 | return TodoRepository.create(title, description, dueDate, done, assignee); 6 | } 7 | 8 | static get(id) { 9 | return TodoRepository.get(id); 10 | } 11 | 12 | static getAll(args) { 13 | return TodoRepository.getAll(args); 14 | } 15 | 16 | static update(id, title, description, dueDate, done, assignee) { 17 | return TodoRepository.update(id, title, description, dueDate, done, assignee); 18 | } 19 | 20 | static partialUpdate(id, title, description, dueDate, done, assignee) { 21 | return TodoRepository.partialUpdate({ id, title, description, dueDate, done, assignee }); 22 | } 23 | 24 | static destroy(id) { 25 | return TodoRepository.destroy(id); 26 | } 27 | } 28 | 29 | export { TodoService }; 30 | -------------------------------------------------------------------------------- /src/server/tests/comment.test.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { buildComment, buildTodo, createComment, createTodo } from './factories'; 3 | import { startDatabase } from './utils'; 4 | import { Comment, Todo } from 'data/models'; 5 | import { app } from 'server/app'; 6 | 7 | const ENDPOINT = '/comment'; 8 | 9 | describe('Comment tests', () => { 10 | beforeEach(async () => { 11 | await startDatabase(); 12 | }); 13 | 14 | afterAll(async () => { 15 | await app.close(); 16 | }); 17 | 18 | test('/POST - Response with a new created comment', async () => { 19 | const { comment: fakeComment, todoDict } = buildComment(); 20 | 21 | createTodo(todoDict); 22 | 23 | const response = await request(app).post(ENDPOINT).send(fakeComment); 24 | 25 | expect(response.status).toBe(201); 26 | expect(response.statusCode).toBe(201); 27 | 28 | const comment = await Comment.findByPk(fakeComment.id); 29 | 30 | expect(comment.id).toBe(fakeComment.id); 31 | expect(comment.message).toBe(fakeComment.message); 32 | expect(comment.status).toBe(fakeComment.status); 33 | expect(new Date(comment.submitted).toUTCString()).toEqual(fakeComment.submitted.toUTCString()); 34 | expect(comment.todo).toBe(fakeComment.todo); 35 | }); 36 | 37 | test('/POST - does not exists, comment cant be created', async () => { 38 | const { comment: fakeComment } = buildComment(); 39 | 40 | const response = await request(app).post(ENDPOINT).send(fakeComment); 41 | 42 | const { statusCode } = response; 43 | expect(statusCode).toBe(404); 44 | }); 45 | 46 | test('/GET - Response with a comment', async () => { 47 | const commentDict = buildComment(); 48 | await createComment(commentDict); 49 | const { comment: fakeComment } = commentDict; 50 | 51 | const response = await request(app).get(`${ENDPOINT}/${fakeComment.id}`); 52 | 53 | const { statusCode, status } = response; 54 | const { data } = response.body; 55 | 56 | expect(status).toBe(200); 57 | expect(statusCode).toBe(200); 58 | 59 | expect(data.id).toBe(fakeComment.id); 60 | expect(data.message).toBe(fakeComment.message); 61 | expect(data.status).toBe(fakeComment.status); 62 | expect(new Date(data.submitted).toUTCString()).toEqual(fakeComment.submitted.toUTCString()); 63 | expect(data.todo).toBe(fakeComment.todo); 64 | }); 65 | 66 | test('/GET - Response with a comment not found', async () => { 67 | const { id } = await buildComment(); 68 | const response = await request(app).get(`${ENDPOINT}/${id}`); 69 | const { statusCode } = response; 70 | expect(statusCode).toBe(404); 71 | }); 72 | 73 | test('/GET - Response with a list of comments', async () => { 74 | const commentDict = buildComment(); 75 | await createComment(commentDict); 76 | const { comment: fakeComment } = commentDict; 77 | 78 | const response = await request(app).get(ENDPOINT); 79 | 80 | const { statusCode, status } = response; 81 | const { data } = response.body; 82 | 83 | expect(status).toBe(200); 84 | expect(statusCode).toBe(200); 85 | 86 | expect(data.length).toBe(1); 87 | 88 | expect(data[0].id).toBe(fakeComment.id); 89 | expect(data[0].message).toBe(fakeComment.message); 90 | expect(data[0].status).toBe(fakeComment.status); 91 | expect(new Date(data[0].submitted).toUTCString()).toEqual(fakeComment.submitted.toUTCString()); 92 | expect(data[0].todo).toBe(fakeComment.todo); 93 | }); 94 | 95 | test('/PUT - Response with an updated comment', async () => { 96 | const commentDict = buildComment(); 97 | await createComment(commentDict); 98 | const { comment: fakeComment } = commentDict; 99 | 100 | const { comment: otherFakeComment } = buildComment(); 101 | 102 | const response = await request(app).put(`${ENDPOINT}/${fakeComment.id}`).send({ 103 | message: otherFakeComment.message, 104 | submitted: otherFakeComment.submitted, 105 | status: otherFakeComment.status, 106 | todo: fakeComment.todo, 107 | }); 108 | 109 | const { status } = response; 110 | const { data } = response.body; 111 | 112 | expect(status).toBe(200); 113 | expect(response.statusCode).toBe(200); 114 | 115 | expect(data.message).toBe(otherFakeComment.message); 116 | expect(data.status).toBe(otherFakeComment.status); 117 | expect(new Date(data.submitted).toUTCString()).toEqual( 118 | otherFakeComment.submitted.toUTCString() 119 | ); 120 | 121 | const updatedComment = await Comment.findByPk(fakeComment.id); 122 | 123 | expect(updatedComment.message).toBe(otherFakeComment.message); 124 | expect(updatedComment.status).toBe(otherFakeComment.status); 125 | expect(new Date(updatedComment.submitted).toUTCString()).toEqual( 126 | otherFakeComment.submitted.toUTCString() 127 | ); 128 | }); 129 | 130 | test('/PUT - does not exists, comment cant be updated', async () => { 131 | const commentDict = buildComment(); 132 | await createComment(commentDict); 133 | const { comment: fakeComment } = commentDict; 134 | 135 | const { todo: anotherFakeTodo } = buildTodo(); 136 | const { id: todo } = anotherFakeTodo; 137 | 138 | const response = await request(app).put(`${ENDPOINT}/${fakeComment.id}`).send({ 139 | message: fakeComment.message, 140 | submitted: fakeComment.submitted, 141 | status: fakeComment.status, 142 | todo, 143 | }); 144 | 145 | const { statusCode } = response; 146 | expect(statusCode).toBe(404); 147 | }); 148 | 149 | test('/PUT - Comment does not exists, comment cant be updated', async () => { 150 | const { comment: fakeComment } = buildComment(); 151 | 152 | const response = await request(app).put(`${ENDPOINT}/${fakeComment.id}`).send({ 153 | message: fakeComment.message, 154 | submitted: fakeComment.submitted, 155 | status: fakeComment.status, 156 | todo: fakeComment.todo, 157 | }); 158 | 159 | const { statusCode } = response; 160 | expect(statusCode).toBe(404); 161 | }); 162 | 163 | test('/PATCH - Response with an updated comment', async () => { 164 | const commentDict = buildComment(); 165 | await createComment(commentDict); 166 | const { comment: fakeComment } = commentDict; 167 | 168 | const { comment: anotherfakeComment } = buildComment(); 169 | const { message } = anotherfakeComment; 170 | 171 | const response = await request(app).patch(`${ENDPOINT}/${fakeComment.id}`).send({ message }); 172 | 173 | const { status } = response; 174 | const { data } = response.body; 175 | 176 | expect(status).toBe(200); 177 | expect(response.statusCode).toBe(200); 178 | 179 | expect(data.message).toBe(message); 180 | 181 | const updatedComment = await Comment.findByPk(fakeComment.id); 182 | 183 | expect(updatedComment.message).toBe(anotherfakeComment.message); 184 | }); 185 | 186 | test('/PATCH - todo does not exists, comment cant be updated', async () => { 187 | const commentDict = buildComment(); 188 | await createComment(commentDict); 189 | const { comment: fakeComment } = commentDict; 190 | 191 | const { todo: anotherFakeTodo } = buildTodo(); 192 | const { id: todo } = anotherFakeTodo; 193 | 194 | const response = await request(app).patch(`${ENDPOINT}/${fakeComment.id}`).send({ todo }); 195 | const { statusCode } = response; 196 | expect(statusCode).toBe(404); 197 | }); 198 | 199 | test('/PATCH - Comment does not exists, comment cant be updated', async () => { 200 | const { comment: fakeComment } = buildComment(); 201 | 202 | const response = await request(app) 203 | .patch(`${ENDPOINT}/${fakeComment.id}`) 204 | .send({ message: fakeComment.message }); 205 | 206 | const { statusCode } = response; 207 | expect(statusCode).toBe(404); 208 | }); 209 | 210 | test('/DELETE - Response with a deleted comment', async () => { 211 | const commentDict = buildComment(); 212 | await createComment(commentDict); 213 | const { comment: fakeComment } = commentDict; 214 | 215 | const response = await request(app).delete(`${ENDPOINT}/${fakeComment.id}`); 216 | 217 | const { status } = response; 218 | const { data } = response.body; 219 | 220 | expect(status).toBe(200); 221 | expect(response.statusCode).toBe(200); 222 | 223 | expect(data.name).toBe(fakeComment.name); 224 | 225 | const deletedComment = await Comment.findByPk(fakeComment.id); 226 | expect(deletedComment).toBe(null); 227 | }); 228 | 229 | test('/DELETE - Comment does not exists, comment cant be deleted', async () => { 230 | const { comment: fakeComment } = buildComment(); 231 | const { id } = fakeComment; 232 | 233 | const response = await request(app).delete(`${ENDPOINT}/${id}`); 234 | 235 | const { statusCode } = response; 236 | expect(statusCode).toBe(404); 237 | }); 238 | }); 239 | -------------------------------------------------------------------------------- /src/server/tests/createPerson.test.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { buildPerson, createPerson } from './factories'; 3 | import { startDatabase } from './utils'; 4 | import { Person } from 'data/models'; 5 | import { app } from 'server/app'; 6 | 7 | const ENDPOINT = '/create_person'; 8 | 9 | describe('CreatePerson tests', () => { 10 | beforeEach(async () => { 11 | await startDatabase(); 12 | }); 13 | 14 | afterAll(async () => { 15 | await app.close(); 16 | }); 17 | 18 | test('/POST - Response with a new created person', async () => { 19 | const { person: fakePerson } = buildPerson(); 20 | 21 | const response = await request(app).post(ENDPOINT).send(fakePerson); 22 | 23 | expect(response.status).toBe(201); 24 | expect(response.statusCode).toBe(201); 25 | 26 | const person = await Person.findByPk(fakePerson.id); 27 | 28 | expect(person.id).toBe(fakePerson.id); 29 | expect(person.email).toBe(fakePerson.email); 30 | expect(person.firstname).toBe(fakePerson.firstname); 31 | expect(person.lastname).toBe(fakePerson.lastname); 32 | expect(new Date(person.lastLogin).toUTCString()).toEqual(fakePerson.lastLogin.toUTCString()); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/server/tests/factories/comment.factory.js: -------------------------------------------------------------------------------- 1 | import { random, date } from 'faker'; 2 | import { buildTodo, createTodo } from './todo.factory'; 3 | import { Comment } from 'data/models'; 4 | import { commentStatusChoices } from 'server/utils/constants/fieldChoices'; 5 | import { getRandomValueFromArray } from 'server/utils/functions'; 6 | 7 | const buildComment = () => { 8 | const todoDict = buildTodo(); 9 | 10 | return { 11 | comment: { 12 | id: random.number(), 13 | message: random.word(512), 14 | submitted: date.past(), 15 | status: getRandomValueFromArray(commentStatusChoices), 16 | todo: todoDict.todo.id, 17 | }, 18 | todoDict, 19 | }; 20 | }; 21 | 22 | const createComment = async (fakeDict) => { 23 | const { comment, todoDict } = fakeDict; 24 | await createTodo(todoDict); 25 | 26 | await Comment.create(comment); 27 | }; 28 | 29 | export { buildComment, createComment }; 30 | -------------------------------------------------------------------------------- /src/server/tests/factories/index.js: -------------------------------------------------------------------------------- 1 | import { buildComment, createComment } from './comment.factory'; 2 | import { buildPerson, createPerson } from './person.factory'; 3 | import { buildTodo, createTodo } from './todo.factory'; 4 | 5 | export { buildTodo, createTodo, buildComment, createComment, buildPerson, createPerson }; 6 | -------------------------------------------------------------------------------- /src/server/tests/factories/person.factory.js: -------------------------------------------------------------------------------- 1 | import { random, date } from 'faker'; 2 | import { Person } from 'data/models'; 3 | 4 | const buildPerson = () => { 5 | return { 6 | person: { 7 | id: random.number(), 8 | email: random.word(100), 9 | firstname: random.word(100), 10 | lastname: random.word(100), 11 | lastLogin: date.past(), 12 | }, 13 | }; 14 | }; 15 | 16 | const createPerson = async (fakeDict) => { 17 | const { person } = fakeDict; 18 | 19 | await Person.create(person); 20 | }; 21 | 22 | export { buildPerson, createPerson }; 23 | -------------------------------------------------------------------------------- /src/server/tests/factories/todo.factory.js: -------------------------------------------------------------------------------- 1 | import { random, date } from 'faker'; 2 | import { buildPerson, createPerson } from './person.factory'; 3 | import { Todo } from 'data/models'; 4 | 5 | const buildTodo = () => { 6 | const assigneeDict = buildPerson(); 7 | 8 | return { 9 | todo: { 10 | id: random.number(), 11 | title: random.word(255), 12 | description: random.word(1024), 13 | dueDate: date.past(), 14 | done: random.boolean(), 15 | assignee: assigneeDict.person.id, 16 | }, 17 | assigneeDict, 18 | }; 19 | }; 20 | 21 | const createTodo = async (fakeDict) => { 22 | const { todo, assigneeDict } = fakeDict; 23 | await createPerson(assigneeDict); 24 | 25 | await Todo.create(todo); 26 | }; 27 | 28 | export { buildTodo, createTodo }; 29 | -------------------------------------------------------------------------------- /src/server/tests/todo.test.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { buildTodo, buildPerson, createTodo, createPerson } from './factories'; 3 | import { startDatabase } from './utils'; 4 | import { Todo, Person } from 'data/models'; 5 | import { app } from 'server/app'; 6 | 7 | const ENDPOINT = '/todo'; 8 | 9 | describe('Todo tests', () => { 10 | beforeEach(async () => { 11 | await startDatabase(); 12 | }); 13 | 14 | afterAll(async () => { 15 | await app.close(); 16 | }); 17 | 18 | test('/POST - Response with a new created todo', async () => { 19 | const { todo: fakeTodo, assigneeDict } = buildTodo(); 20 | 21 | createPerson(assigneeDict); 22 | 23 | const response = await request(app).post(ENDPOINT).send(fakeTodo); 24 | 25 | expect(response.status).toBe(201); 26 | expect(response.statusCode).toBe(201); 27 | 28 | const todo = await Todo.findByPk(fakeTodo.id); 29 | 30 | expect(todo.id).toBe(fakeTodo.id); 31 | expect(todo.title).toBe(fakeTodo.title); 32 | expect(todo.description).toBe(fakeTodo.description); 33 | expect(todo.done).toBe(fakeTodo.done); 34 | expect(new Date(todo.dueDate).toUTCString()).toEqual(fakeTodo.dueDate.toUTCString()); 35 | expect(todo.assignee).toBe(fakeTodo.assignee); 36 | }); 37 | 38 | test('/POST - does not exists, todo cant be created', async () => { 39 | const { todo: fakeTodo } = buildTodo(); 40 | 41 | const response = await request(app).post(ENDPOINT).send(fakeTodo); 42 | 43 | const { statusCode } = response; 44 | expect(statusCode).toBe(404); 45 | }); 46 | 47 | test('/GET - Response with a todo', async () => { 48 | const todoDict = buildTodo(); 49 | await createTodo(todoDict); 50 | const { todo: fakeTodo } = todoDict; 51 | 52 | const response = await request(app).get(`${ENDPOINT}/${fakeTodo.id}`); 53 | 54 | const { statusCode, status } = response; 55 | const { data } = response.body; 56 | 57 | expect(status).toBe(200); 58 | expect(statusCode).toBe(200); 59 | 60 | expect(data.id).toBe(fakeTodo.id); 61 | expect(data.title).toBe(fakeTodo.title); 62 | expect(data.description).toBe(fakeTodo.description); 63 | expect(data.done).toBe(fakeTodo.done); 64 | expect(new Date(data.dueDate).toUTCString()).toEqual(fakeTodo.dueDate.toUTCString()); 65 | expect(data.comments).toEqual([]); 66 | expect(data.assignee).toBe(fakeTodo.assignee); 67 | }); 68 | 69 | test('/GET - Response with a todo not found', async () => { 70 | const { id } = await buildTodo(); 71 | const response = await request(app).get(`${ENDPOINT}/${id}`); 72 | const { statusCode } = response; 73 | expect(statusCode).toBe(404); 74 | }); 75 | 76 | test('/GET - Response with a list of todos', async () => { 77 | const todoDict = buildTodo(); 78 | await createTodo(todoDict); 79 | const { todo: fakeTodo } = todoDict; 80 | 81 | const response = await request(app).get(ENDPOINT); 82 | 83 | const { statusCode, status } = response; 84 | const { data } = response.body; 85 | 86 | expect(status).toBe(200); 87 | expect(statusCode).toBe(200); 88 | 89 | expect(data.length).toBe(1); 90 | 91 | expect(data[0].id).toBe(fakeTodo.id); 92 | expect(data[0].title).toBe(fakeTodo.title); 93 | expect(data[0].description).toBe(fakeTodo.description); 94 | expect(data[0].done).toBe(fakeTodo.done); 95 | expect(new Date(data[0].dueDate).toUTCString()).toEqual(fakeTodo.dueDate.toUTCString()); 96 | expect(data[0].comments).toEqual([]); 97 | expect(data[0].assignee).toBe(fakeTodo.assignee); 98 | }); 99 | 100 | test('/PUT - Response with an updated todo', async () => { 101 | const todoDict = buildTodo(); 102 | await createTodo(todoDict); 103 | const { todo: fakeTodo } = todoDict; 104 | 105 | const { todo: otherFakeTodo } = buildTodo(); 106 | 107 | const response = await request(app).put(`${ENDPOINT}/${fakeTodo.id}`).send({ 108 | title: otherFakeTodo.title, 109 | description: otherFakeTodo.description, 110 | dueDate: otherFakeTodo.dueDate, 111 | done: otherFakeTodo.done, 112 | assignee: fakeTodo.assignee, 113 | }); 114 | 115 | const { status } = response; 116 | const { data } = response.body; 117 | 118 | expect(status).toBe(200); 119 | expect(response.statusCode).toBe(200); 120 | 121 | expect(data.title).toBe(otherFakeTodo.title); 122 | expect(data.description).toBe(otherFakeTodo.description); 123 | expect(data.done).toBe(otherFakeTodo.done); 124 | expect(new Date(data.dueDate).toUTCString()).toEqual(otherFakeTodo.dueDate.toUTCString()); 125 | 126 | const updatedTodo = await Todo.findByPk(fakeTodo.id); 127 | 128 | expect(updatedTodo.title).toBe(otherFakeTodo.title); 129 | expect(updatedTodo.description).toBe(otherFakeTodo.description); 130 | expect(updatedTodo.done).toBe(otherFakeTodo.done); 131 | expect(new Date(updatedTodo.dueDate).toUTCString()).toEqual( 132 | otherFakeTodo.dueDate.toUTCString() 133 | ); 134 | }); 135 | 136 | test('/PUT - does not exists, todo cant be updated', async () => { 137 | const todoDict = buildTodo(); 138 | await createTodo(todoDict); 139 | const { todo: fakeTodo } = todoDict; 140 | 141 | const { person: anotherFakeAssignee } = buildPerson(); 142 | const { id: assignee } = anotherFakeAssignee; 143 | 144 | const response = await request(app).put(`${ENDPOINT}/${fakeTodo.id}`).send({ 145 | title: fakeTodo.title, 146 | description: fakeTodo.description, 147 | dueDate: fakeTodo.dueDate, 148 | done: fakeTodo.done, 149 | assignee, 150 | }); 151 | 152 | const { statusCode } = response; 153 | expect(statusCode).toBe(404); 154 | }); 155 | 156 | test('/PUT - Todo does not exists, todo cant be updated', async () => { 157 | const { todo: fakeTodo } = buildTodo(); 158 | 159 | const response = await request(app).put(`${ENDPOINT}/${fakeTodo.id}`).send({ 160 | title: fakeTodo.title, 161 | description: fakeTodo.description, 162 | dueDate: fakeTodo.dueDate, 163 | done: fakeTodo.done, 164 | assignee: fakeTodo.assignee, 165 | }); 166 | 167 | const { statusCode } = response; 168 | expect(statusCode).toBe(404); 169 | }); 170 | 171 | test('/PATCH - Response with an updated todo', async () => { 172 | const todoDict = buildTodo(); 173 | await createTodo(todoDict); 174 | const { todo: fakeTodo } = todoDict; 175 | 176 | const { todo: anotherfakeTodo } = buildTodo(); 177 | const { title } = anotherfakeTodo; 178 | 179 | const response = await request(app).patch(`${ENDPOINT}/${fakeTodo.id}`).send({ title }); 180 | 181 | const { status } = response; 182 | const { data } = response.body; 183 | 184 | expect(status).toBe(200); 185 | expect(response.statusCode).toBe(200); 186 | 187 | expect(data.title).toBe(title); 188 | 189 | const updatedTodo = await Todo.findByPk(fakeTodo.id); 190 | 191 | expect(updatedTodo.title).toBe(anotherfakeTodo.title); 192 | }); 193 | 194 | test('/PATCH - assignee does not exists, todo cant be updated', async () => { 195 | const todoDict = buildTodo(); 196 | await createTodo(todoDict); 197 | const { todo: fakeTodo } = todoDict; 198 | 199 | const { person: anotherFakeAssignee } = buildPerson(); 200 | const { id: assignee } = anotherFakeAssignee; 201 | 202 | const response = await request(app).patch(`${ENDPOINT}/${fakeTodo.id}`).send({ assignee }); 203 | const { statusCode } = response; 204 | expect(statusCode).toBe(404); 205 | }); 206 | 207 | test('/PATCH - Todo does not exists, todo cant be updated', async () => { 208 | const { todo: fakeTodo } = buildTodo(); 209 | 210 | const response = await request(app) 211 | .patch(`${ENDPOINT}/${fakeTodo.id}`) 212 | .send({ title: fakeTodo.title }); 213 | 214 | const { statusCode } = response; 215 | expect(statusCode).toBe(404); 216 | }); 217 | 218 | test('/DELETE - Response with a deleted todo', async () => { 219 | const todoDict = buildTodo(); 220 | await createTodo(todoDict); 221 | const { todo: fakeTodo } = todoDict; 222 | 223 | const response = await request(app).delete(`${ENDPOINT}/${fakeTodo.id}`); 224 | 225 | const { status } = response; 226 | const { data } = response.body; 227 | 228 | expect(status).toBe(200); 229 | expect(response.statusCode).toBe(200); 230 | 231 | expect(data.name).toBe(fakeTodo.name); 232 | 233 | const deletedTodo = await Todo.findByPk(fakeTodo.id); 234 | expect(deletedTodo).toBe(null); 235 | }); 236 | 237 | test('/DELETE - Todo does not exists, todo cant be deleted', async () => { 238 | const { todo: fakeTodo } = buildTodo(); 239 | const { id } = fakeTodo; 240 | 241 | const response = await request(app).delete(`${ENDPOINT}/${id}`); 242 | 243 | const { statusCode } = response; 244 | expect(statusCode).toBe(404); 245 | }); 246 | }); 247 | -------------------------------------------------------------------------------- /src/server/tests/utils.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { sequelize } from 'data/models'; 3 | 4 | const startDatabase = async () => { 5 | await sequelize.sync({ force: true }); 6 | }; 7 | 8 | const deleteDatabase = (db) => db.drop(); 9 | 10 | export { request, startDatabase, deleteDatabase }; 11 | -------------------------------------------------------------------------------- /src/server/utils/constants/errors.js: -------------------------------------------------------------------------------- 1 | export const errors = { 2 | validation: 'validation_error', 3 | parse: 'parse_error', 4 | not_authenticated: 'not_authenticated', 5 | permission_denied: 'permission_denied', 6 | not_found: 'not_found', 7 | method_not_allowed: 'method_not_allowed', 8 | not_acceptable: 'not_acceptable', 9 | unsupported_media_type: 'unsupported_media_type', 10 | throttled: 'throttled', 11 | server: 'internal_error', 12 | }; 13 | -------------------------------------------------------------------------------- /src/server/utils/constants/fieldChoices.js: -------------------------------------------------------------------------------- 1 | const commentStatusChoices = ['read', 'unread']; 2 | 3 | export { commentStatusChoices }; 4 | -------------------------------------------------------------------------------- /src/server/utils/errors/BadRequest.js: -------------------------------------------------------------------------------- 1 | import httpStatus, { BAD_REQUEST } from 'http-status'; 2 | import { errors } from 'utils/constants/errors'; 3 | import { BaseError } from './BaseError'; 4 | 5 | class BadRequest extends BaseError { 6 | constructor(message) { 7 | super(errors.validation, BAD_REQUEST, message || httpStatus['400_MESSAGE']); 8 | } 9 | } 10 | 11 | export { BadRequest }; 12 | -------------------------------------------------------------------------------- /src/server/utils/errors/BaseError.js: -------------------------------------------------------------------------------- 1 | class BaseError extends Error { 2 | constructor(type, statusCode, message) { 3 | super(); 4 | this.type = type; 5 | this.statusCode = statusCode; 6 | this.message = message; 7 | } 8 | } 9 | 10 | export { BaseError }; 11 | -------------------------------------------------------------------------------- /src/server/utils/errors/Forbidden.js: -------------------------------------------------------------------------------- 1 | import httpStatus, { FORBIDDEN } from 'http-status'; 2 | import { errors } from 'utils/constants/errors'; 3 | import { BaseError } from './BaseError'; 4 | 5 | class Forbidden extends BaseError { 6 | constructor(message) { 7 | super(errors.permission_denied, FORBIDDEN, message || httpStatus['403_MESSAGE']); 8 | } 9 | } 10 | 11 | export { Forbidden }; 12 | -------------------------------------------------------------------------------- /src/server/utils/errors/MethodNotAllowed.js: -------------------------------------------------------------------------------- 1 | import httpStatus, { METHOD_NOT_ALLOWED } from 'http-status'; 2 | import { errors } from 'utils/constants/errors'; 3 | import { BaseError } from './BaseError'; 4 | 5 | class MethodNotAllowed extends BaseError { 6 | constructor(message) { 7 | super(errors.method_not_allowed, METHOD_NOT_ALLOWED, message || httpStatus['405_MESSAGE']); 8 | } 9 | } 10 | 11 | export { MethodNotAllowed }; 12 | -------------------------------------------------------------------------------- /src/server/utils/errors/NotAcceptable.js: -------------------------------------------------------------------------------- 1 | import httpStatus, { NOT_ACCEPTABLE } from 'http-status'; 2 | import { errors } from 'utils/constants/errors'; 3 | import { BaseError } from './BaseError'; 4 | 5 | class NotAcceptable extends BaseError { 6 | constructor(message) { 7 | super(errors.not_acceptable, NOT_ACCEPTABLE, message || httpStatus['406_MESSAGE']); 8 | } 9 | } 10 | 11 | export { NotAcceptable }; 12 | -------------------------------------------------------------------------------- /src/server/utils/errors/NotFound.js: -------------------------------------------------------------------------------- 1 | import httpStatus, { NOT_FOUND } from 'http-status'; 2 | import { errors } from 'utils/constants/errors'; 3 | import { BaseError } from './BaseError'; 4 | 5 | class NotFound extends BaseError { 6 | constructor(message) { 7 | super(errors.not_found, NOT_FOUND, message || httpStatus['404_MESSAGE']); 8 | } 9 | } 10 | 11 | export { NotFound }; 12 | -------------------------------------------------------------------------------- /src/server/utils/errors/Throttled.js: -------------------------------------------------------------------------------- 1 | import httpStatus, { TOO_MANY_REQUESTS } from 'http-status'; 2 | import { errors } from 'utils/constants/errors'; 3 | import { BaseError } from './BaseError'; 4 | 5 | class Throttled extends BaseError { 6 | constructor(message) { 7 | super(errors.throttled, TOO_MANY_REQUESTS, message || httpStatus['429_MESSAGE']); 8 | } 9 | } 10 | 11 | export { Throttled }; 12 | -------------------------------------------------------------------------------- /src/server/utils/errors/Unauthorized.js: -------------------------------------------------------------------------------- 1 | import httpStatus, { UNAUTHORIZED } from 'http-status'; 2 | import { errors } from 'utils/constants/errors'; 3 | import { BaseError } from './BaseError'; 4 | 5 | class Unauthorized extends BaseError { 6 | constructor(message) { 7 | super(errors.not_authenticated, UNAUTHORIZED, message || httpStatus['401_MESSAGE']); 8 | } 9 | } 10 | 11 | export { Unauthorized }; 12 | -------------------------------------------------------------------------------- /src/server/utils/errors/UnsupportedMediaType.js: -------------------------------------------------------------------------------- 1 | import httpStatus, { UNSUPPORTED_MEDIA_TYPE } from 'http-status'; 2 | import { errors } from 'utils/constants/errors'; 3 | import { BaseError } from './BaseError'; 4 | 5 | class UnsupportedMediaType extends BaseError { 6 | constructor(message) { 7 | super( 8 | errors.unsupported_media_type, 9 | UNSUPPORTED_MEDIA_TYPE, 10 | message || httpStatus['415_MESSAGE'] 11 | ); 12 | } 13 | } 14 | 15 | export { UnsupportedMediaType }; 16 | -------------------------------------------------------------------------------- /src/server/utils/errors/index.js: -------------------------------------------------------------------------------- 1 | import { BadRequest } from './BadRequest'; 2 | import { NotFound } from './NotFound'; 3 | import { Unauthorized } from './Unauthorized'; 4 | import { Forbidden } from './Forbidden'; 5 | import { MethodNotAllowed } from './MethodNotAllowed'; 6 | 7 | export { BadRequest, NotFound, Unauthorized, Forbidden, MethodNotAllowed }; 8 | -------------------------------------------------------------------------------- /src/server/utils/functions.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import utc from 'dayjs/plugin/utc'; 3 | 4 | dayjs.extend(utc); 5 | 6 | const createErrorResponse = (statusCode, type, param, message) => ({ 7 | status_code: statusCode, 8 | type, 9 | param, 10 | message, 11 | }); 12 | 13 | const createSuccessResponse = (statusCode, data) => ({ status_code: statusCode, data }); 14 | 15 | const getRandomValueFromArray = (arr) => arr[Math.floor(Math.random() * arr.length)]; 16 | 17 | const dateToUTC = (date) => dayjs.utc(date); 18 | 19 | export { createErrorResponse, createSuccessResponse, getRandomValueFromArray, dateToUTC }; 20 | -------------------------------------------------------------------------------- /src/server/validations/comment.validation.js: -------------------------------------------------------------------------------- 1 | import { Joi } from 'express-validation'; 2 | import { commentStatusChoices } from 'server/utils/constants/fieldChoices'; 3 | 4 | const commentValidation = { 5 | getAll: { 6 | query: Joi.object({ 7 | id: Joi.number().integer(), 8 | message: Joi.string().max(512), 9 | submitted: Joi.date(), 10 | status: Joi.string().valid(...commentStatusChoices), 11 | }), 12 | }, 13 | create: { 14 | body: Joi.object({ 15 | todo: Joi.number().integer().required(), 16 | message: Joi.string().max(512), 17 | submitted: Joi.date(), 18 | status: Joi.string().valid(...commentStatusChoices), 19 | }), 20 | }, 21 | update: { 22 | params: Joi.object({ 23 | id: Joi.number().integer().required(), 24 | }), 25 | body: Joi.object({ 26 | message: Joi.string().max(512).required(), 27 | submitted: Joi.date().required(), 28 | status: Joi.string() 29 | .valid(...commentStatusChoices) 30 | .required(), 31 | todo: Joi.number().integer().required(), 32 | }), 33 | }, 34 | partialUpdate: { 35 | params: Joi.object({ 36 | id: Joi.number().integer().required(), 37 | }), 38 | body: Joi.object({ 39 | message: Joi.string().max(512), 40 | submitted: Joi.date(), 41 | status: Joi.string().valid(...commentStatusChoices), 42 | todo: Joi.number().integer(), 43 | }), 44 | }, 45 | destroy: { 46 | params: Joi.object({ 47 | id: Joi.number().integer().required(), 48 | }), 49 | }, 50 | }; 51 | 52 | export { commentValidation }; 53 | -------------------------------------------------------------------------------- /src/server/validations/createPerson.validation.js: -------------------------------------------------------------------------------- 1 | import { Joi } from 'express-validation'; 2 | 3 | const createPersonValidation = { 4 | create: { 5 | body: Joi.object({ 6 | email: Joi.string().max(100), 7 | firstname: Joi.string().max(100), 8 | lastname: Joi.string().max(100), 9 | lastLogin: Joi.date(), 10 | }), 11 | }, 12 | }; 13 | 14 | export { createPersonValidation }; 15 | -------------------------------------------------------------------------------- /src/server/validations/index.js: -------------------------------------------------------------------------------- 1 | import { commentValidation } from './comment.validation'; 2 | import { createPersonValidation } from './createPerson.validation'; 3 | import { personValidation } from './person.validation'; 4 | import { todoValidation } from './todo.validation'; 5 | 6 | const options = { keyByField: true }; 7 | 8 | export { todoValidation, commentValidation, personValidation, createPersonValidation, options }; 9 | -------------------------------------------------------------------------------- /src/server/validations/person.validation.js: -------------------------------------------------------------------------------- 1 | import { Joi } from 'express-validation'; 2 | 3 | const personValidation = { 4 | getAll: { 5 | query: Joi.object({ 6 | id: Joi.number().integer(), 7 | email: Joi.string().max(100), 8 | firstname: Joi.string().max(100), 9 | lastname: Joi.string().max(100), 10 | lastLogin: Joi.date(), 11 | }), 12 | }, 13 | create: { 14 | body: Joi.object({ 15 | email: Joi.string().max(100), 16 | firstname: Joi.string().max(100), 17 | lastname: Joi.string().max(100), 18 | lastLogin: Joi.date(), 19 | }), 20 | }, 21 | update: { 22 | params: Joi.object({ 23 | id: Joi.number().integer().required(), 24 | }), 25 | body: Joi.object({ 26 | email: Joi.string().max(100).required(), 27 | firstname: Joi.string().max(100).required(), 28 | lastname: Joi.string().max(100).required(), 29 | lastLogin: Joi.date().required(), 30 | }), 31 | }, 32 | partialUpdate: { 33 | params: Joi.object({ 34 | id: Joi.number().integer().required(), 35 | }), 36 | body: Joi.object({ 37 | email: Joi.string().max(100), 38 | firstname: Joi.string().max(100), 39 | lastname: Joi.string().max(100), 40 | lastLogin: Joi.date(), 41 | }), 42 | }, 43 | destroy: { 44 | params: Joi.object({ 45 | id: Joi.number().integer().required(), 46 | }), 47 | }, 48 | }; 49 | 50 | export { personValidation }; 51 | -------------------------------------------------------------------------------- /src/server/validations/todo.validation.js: -------------------------------------------------------------------------------- 1 | import { Joi } from 'express-validation'; 2 | 3 | const todoValidation = { 4 | getAll: { 5 | query: Joi.object({ 6 | id: Joi.number().integer(), 7 | title: Joi.string().max(255), 8 | description: Joi.string().max(1024), 9 | dueDate: Joi.date(), 10 | done: Joi.boolean(), 11 | }), 12 | }, 13 | create: { 14 | body: Joi.object({ 15 | assignee: Joi.number().integer().required(), 16 | title: Joi.string().max(255).required(), 17 | description: Joi.string().max(1024), 18 | dueDate: Joi.date(), 19 | done: Joi.boolean(), 20 | }), 21 | }, 22 | update: { 23 | params: Joi.object({ 24 | id: Joi.number().integer().required(), 25 | }), 26 | body: Joi.object({ 27 | title: Joi.string().max(255).required(), 28 | description: Joi.string().max(1024).required(), 29 | dueDate: Joi.date().required(), 30 | done: Joi.boolean().required(), 31 | assignee: Joi.number().integer().required(), 32 | }), 33 | }, 34 | partialUpdate: { 35 | params: Joi.object({ 36 | id: Joi.number().integer().required(), 37 | }), 38 | body: Joi.object({ 39 | title: Joi.string().max(255), 40 | description: Joi.string().max(1024), 41 | dueDate: Joi.date(), 42 | done: Joi.boolean(), 43 | assignee: Joi.number().integer(), 44 | }), 45 | }, 46 | destroy: { 47 | params: Joi.object({ 48 | id: Joi.number().integer().required(), 49 | }), 50 | }, 51 | }; 52 | 53 | export { todoValidation }; 54 | -------------------------------------------------------------------------------- /src/tests/comment.test.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { buildComment, buildTodo, createComment, createTodo } from './factories'; 3 | import { startDatabase } from './utils'; 4 | import { Comment, Todo } from 'data/models'; 5 | import { app } from 'server/app'; 6 | 7 | const ENDPOINT = '/comment'; 8 | 9 | describe('Comment tests', () => { 10 | beforeEach(async () => { 11 | await startDatabase(); 12 | }); 13 | 14 | afterAll(async () => { 15 | await app.close(); 16 | }); 17 | 18 | test('/POST - Response with a new created comment', async () => { 19 | const todoDict = await buildTodo({}); 20 | const fakeTodo = await createTodo(todoDict); 21 | 22 | const fakeComment = await buildComment({ todo: fakeTodo.id }); 23 | 24 | const response = await request(app).post(ENDPOINT).send(fakeComment); 25 | 26 | expect(response.status).toBe(201); 27 | expect(response.statusCode).toBe(201); 28 | 29 | const responseComment = response.body.data; 30 | 31 | const comment = await Comment.findByPk(responseComment.id); 32 | 33 | expect(comment.message).toBe(fakeComment.message); 34 | expect(comment.submitted.toISOString()).toEqual(fakeComment.submitted); 35 | expect(comment.status).toBe(fakeComment.status); 36 | 37 | expect(comment.todo).toBe(fakeComment.todo); 38 | }); 39 | 40 | test('/POST - todo does not exists, comment cant be created', async () => { 41 | const fakeComment = await buildComment({}); 42 | const todo = await Todo.findOne({ where: { id: fakeComment.todo } }); 43 | await todo.destroy(); 44 | 45 | const response = await request(app).post(ENDPOINT).send(fakeComment); 46 | 47 | const { statusCode } = response; 48 | expect(statusCode).toBe(404); 49 | }); 50 | 51 | test('/GET - Response with a comment', async () => { 52 | const todoDict = await buildTodo({}); 53 | const fakeTodo = await createTodo(todoDict); 54 | 55 | const commentDict = await buildComment({ todo: fakeTodo.id }); 56 | const fakeComment = await createComment(commentDict); 57 | 58 | const response = await request(app).get(`${ENDPOINT}/${fakeComment.id}`); 59 | 60 | const { statusCode, status } = response; 61 | const { data } = response.body; 62 | 63 | expect(status).toBe(200); 64 | expect(statusCode).toBe(200); 65 | 66 | expect(data.id).toBe(fakeComment.id); 67 | expect(data.message).toBe(fakeComment.message); 68 | expect(data.submitted).toBe(fakeComment.submitted.toISOString()); 69 | expect(data.status).toBe(fakeComment.status); 70 | 71 | expect(data.todo).toBe(fakeComment.todo); 72 | }); 73 | 74 | test('/GET - Response with a comment not found', async () => { 75 | const commentDict = await buildComment({}); 76 | const fakeComment = await createComment(commentDict); 77 | const id = fakeComment.id; 78 | await fakeComment.destroy(); 79 | 80 | const response = await request(app).get(`${ENDPOINT}/${id}`); 81 | const { statusCode } = response; 82 | expect(statusCode).toBe(404); 83 | }); 84 | 85 | test('/GET - Response with a list of comments', async () => { 86 | const todoDict = await buildTodo({}); 87 | const fakeTodo = await createTodo(todoDict); 88 | 89 | const commentDict = await buildComment({ todo: fakeTodo.id }); 90 | const fakeComment = await createComment(commentDict); 91 | 92 | const response = await request(app).get(ENDPOINT); 93 | 94 | const { statusCode, status } = response; 95 | const { data } = response.body; 96 | 97 | expect(status).toBe(200); 98 | expect(statusCode).toBe(200); 99 | 100 | const allComment = await Comment.findAll(); 101 | expect(data.length).toBe(allComment.length); 102 | }); 103 | 104 | test('/PUT - Response with an updated comment', async () => { 105 | const todoDict = await buildTodo({}); 106 | const fakeTodo = await createTodo(todoDict); 107 | 108 | const commentDict = await buildComment({ todo: fakeTodo.id }); 109 | const fakeComment = await createComment(commentDict); 110 | 111 | const anotherTodoDict = await buildTodo({}); 112 | const anotherFakeTodo = await createTodo(anotherTodoDict); 113 | 114 | const anotherFakeComment = await buildComment({ todo: anotherFakeTodo.id }); 115 | 116 | const response = await request(app).put(`${ENDPOINT}/${fakeComment.id}`).send({ 117 | message: anotherFakeComment.message, 118 | submitted: anotherFakeComment.submitted, 119 | status: anotherFakeComment.status, 120 | todo: anotherFakeComment.todo, 121 | }); 122 | 123 | const { status } = response; 124 | const { data } = response.body; 125 | 126 | expect(status).toBe(200); 127 | expect(response.statusCode).toBe(200); 128 | 129 | expect(data.message).toBe(anotherFakeComment.message); 130 | expect(data.submitted).toBe(anotherFakeComment.submitted); 131 | expect(data.status).toBe(anotherFakeComment.status); 132 | 133 | expect(data.todo).toBe(anotherFakeComment.todo); 134 | 135 | const updatedComment = await Comment.findByPk(fakeComment.id); 136 | 137 | expect(updatedComment.message).toBe(anotherFakeComment.message); 138 | expect(updatedComment.submitted.toISOString()).toEqual(anotherFakeComment.submitted); 139 | expect(updatedComment.status).toBe(anotherFakeComment.status); 140 | 141 | expect(updatedComment.todo).toBe(anotherFakeComment.todo); 142 | }); 143 | 144 | test('/PUT - todo does not exists, comment cant be updated', async () => { 145 | const todoDict = await buildTodo({}); 146 | const fakeTodo = await createTodo(todoDict); 147 | 148 | const commentDict = await buildComment({ todo: fakeTodo.id }); 149 | const fakeComment = await createComment(commentDict); 150 | 151 | const anotherTodoDict = await buildTodo({}); 152 | const anotherFakeTodo = await createTodo(anotherTodoDict); 153 | 154 | fakeComment.todo = anotherFakeTodo.id; 155 | 156 | await anotherFakeTodo.destroy(); 157 | 158 | const response = await request(app).put(`${ENDPOINT}/${fakeComment.id}`).send({ 159 | message: fakeComment.message, 160 | submitted: fakeComment.submitted, 161 | status: fakeComment.status, 162 | todo: fakeComment.todo, 163 | }); 164 | 165 | const { statusCode } = response; 166 | expect(statusCode).toBe(404); 167 | }); 168 | 169 | test('/PUT - Comment does not exists, comment cant be updated', async () => { 170 | const commentDict = await buildComment({}); 171 | const fakeComment = await createComment(commentDict); 172 | const id = fakeComment.id; 173 | await fakeComment.destroy(); 174 | 175 | const response = await request(app).put(`${ENDPOINT}/${id}`).send({ 176 | message: fakeComment.message, 177 | submitted: fakeComment.submitted, 178 | status: fakeComment.status, 179 | todo: fakeComment.todo, 180 | }); 181 | 182 | const { statusCode } = response; 183 | expect(statusCode).toBe(404); 184 | }); 185 | 186 | test('/PATCH - Response with an updated comment', async () => { 187 | const todoDict = await buildTodo({}); 188 | const fakeTodo = await createTodo(todoDict); 189 | 190 | const commentDict = await buildComment({ todo: fakeTodo.id }); 191 | const fakeComment = await createComment(commentDict); 192 | 193 | const anotherTodoDict = await buildTodo({}); 194 | const anotherFakeTodo = await createTodo(anotherTodoDict); 195 | 196 | const anotherFakeComment = await buildComment({ todo: anotherFakeTodo.id }); 197 | 198 | const response = await request(app) 199 | .patch(`${ENDPOINT}/${fakeComment.id}`) 200 | .send({ message: anotherFakeComment.message }); 201 | 202 | const { status } = response; 203 | const { data } = response.body; 204 | 205 | expect(status).toBe(200); 206 | expect(response.statusCode).toBe(200); 207 | 208 | expect(data.message).toBe(anotherFakeComment.message); 209 | 210 | const updatedComment = await Comment.findByPk(fakeComment.id); 211 | 212 | expect(updatedComment.message).toBe(anotherFakeComment.message); 213 | }); 214 | 215 | test('/PATCH - todo does not exists, comment cant be updated', async () => { 216 | const commentDict = await buildComment({}); 217 | const fakeComment = await createComment(commentDict); 218 | 219 | const todoDict = await buildTodo({}); 220 | const fakeTodo = await createTodo(todoDict); 221 | 222 | const fakeTodoId = fakeTodo.id; 223 | await fakeTodo.destroy(); 224 | 225 | const response = await request(app).patch(`${ENDPOINT}/${fakeComment.id}`).send({ 226 | todo: fakeTodoId, 227 | }); 228 | 229 | const { statusCode } = response; 230 | expect(statusCode).toBe(404); 231 | }); 232 | 233 | test('/PATCH - Comment does not exists, comment cant be updated', async () => { 234 | const commentDict = await buildComment({}); 235 | const fakeComment = await createComment(commentDict); 236 | const id = fakeComment.id; 237 | const message = fakeComment.message; 238 | await fakeComment.destroy(); 239 | 240 | const response = await request(app).patch(`${ENDPOINT}/${id}`).send({ message: message }); 241 | 242 | const { statusCode } = response; 243 | expect(statusCode).toBe(404); 244 | }); 245 | 246 | test('/DELETE - Response with a deleted comment', async () => { 247 | const commentDict = await buildComment({}); 248 | const fakeComment = await createComment(commentDict); 249 | 250 | const response = await request(app).delete(`${ENDPOINT}/${fakeComment.id}`); 251 | 252 | const { status } = response; 253 | const { data } = response.body; 254 | 255 | expect(status).toBe(200); 256 | expect(response.statusCode).toBe(200); 257 | 258 | expect(data.id).toBe(fakeComment.id); 259 | 260 | const deletedComment = await Comment.findByPk(fakeComment.id); 261 | expect(deletedComment).toBe(null); 262 | }); 263 | 264 | test('/DELETE - Comment does not exists, comment cant be deleted', async () => { 265 | const commentDict = await buildComment({}); 266 | const fakeComment = await createComment(commentDict); 267 | const id = fakeComment.id; 268 | await fakeComment.destroy(); 269 | 270 | const response = await request(app).delete(`${ENDPOINT}/${id}`); 271 | 272 | const { statusCode } = response; 273 | expect(statusCode).toBe(404); 274 | }); 275 | }); 276 | -------------------------------------------------------------------------------- /src/tests/createPerson.test.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { buildPerson, createPerson } from './factories'; 3 | import { startDatabase } from './utils'; 4 | import { Person } from 'data/models'; 5 | import { app } from 'server/app'; 6 | 7 | const ENDPOINT = '/create_person'; 8 | 9 | describe('CreatePerson tests', () => { 10 | beforeEach(async () => { 11 | await startDatabase(); 12 | }); 13 | 14 | afterAll(async () => { 15 | await app.close(); 16 | }); 17 | 18 | test('/POST - Response with a new created person', async () => { 19 | const fakePerson = await buildPerson({}); 20 | 21 | const response = await request(app).post(ENDPOINT).send(fakePerson); 22 | 23 | expect(response.status).toBe(201); 24 | expect(response.statusCode).toBe(201); 25 | 26 | const responsePerson = response.body.data; 27 | 28 | const person = await Person.findByPk(responsePerson.id); 29 | 30 | expect(person.email).toBe(fakePerson.email); 31 | expect(person.firstname).toBe(fakePerson.firstname); 32 | expect(person.lastname).toBe(fakePerson.lastname); 33 | expect(person.lastLogin.toISOString()).toEqual(fakePerson.lastLogin); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/tests/factories/comment.factory.js: -------------------------------------------------------------------------------- 1 | import { random, datatype, date } from 'faker'; 2 | import { buildTodo, createTodo } from './todo.factory'; 3 | import { Comment } from 'data/models'; 4 | import { commentStatusChoices } from 'server/utils/constants/fieldChoices'; 5 | import { dateToUTC, getRandomValueFromArray } from 'server/utils/functions'; 6 | 7 | const buildComment = async (commentFks) => { 8 | let resComment = {}; 9 | let todo = commentFks.todo; 10 | 11 | resComment.message = random.word().slice(0, 512); 12 | resComment.submitted = date.past().toJSON(); 13 | resComment.status = getRandomValueFromArray(commentStatusChoices); 14 | 15 | if (commentFks.todo === null || typeof commentFks.todo === 'undefined') { 16 | const fakeTodo = await buildTodo({}); 17 | const createdFakeTodo = await createTodo(fakeTodo); 18 | todo = createdFakeTodo.id; 19 | } 20 | 21 | resComment.todo = todo; 22 | 23 | return resComment; 24 | }; 25 | 26 | const createComment = async (fakeComment) => { 27 | const comment = await Comment.create(fakeComment); 28 | return comment; 29 | }; 30 | 31 | export { buildComment, createComment }; 32 | -------------------------------------------------------------------------------- /src/tests/factories/index.js: -------------------------------------------------------------------------------- 1 | import { buildComment, createComment } from './comment.factory'; 2 | import { buildPerson, createPerson } from './person.factory'; 3 | import { buildTodo, createTodo } from './todo.factory'; 4 | 5 | export { buildTodo, createTodo, buildComment, createComment, buildPerson, createPerson }; 6 | -------------------------------------------------------------------------------- /src/tests/factories/person.factory.js: -------------------------------------------------------------------------------- 1 | import { random, datatype, date } from 'faker'; 2 | import { Person } from 'data/models'; 3 | import { dateToUTC } from 'server/utils/functions'; 4 | 5 | const buildPerson = async (personFks) => { 6 | let resPerson = {}; 7 | 8 | resPerson.email = random.word().slice(0, 100); 9 | resPerson.firstname = random.word().slice(0, 100); 10 | resPerson.lastname = random.word().slice(0, 100); 11 | resPerson.lastLogin = date.past().toJSON(); 12 | 13 | return resPerson; 14 | }; 15 | 16 | const createPerson = async (fakePerson) => { 17 | const person = await Person.create(fakePerson); 18 | return person; 19 | }; 20 | 21 | export { buildPerson, createPerson }; 22 | -------------------------------------------------------------------------------- /src/tests/factories/todo.factory.js: -------------------------------------------------------------------------------- 1 | import { random, datatype, date } from 'faker'; 2 | import { buildPerson, createPerson } from './person.factory'; 3 | import { Todo } from 'data/models'; 4 | import { dateToUTC } from 'server/utils/functions'; 5 | 6 | const buildTodo = async (todoFks) => { 7 | let resTodo = {}; 8 | let assignee = todoFks.assignee; 9 | 10 | resTodo.title = random.word().slice(0, 255); 11 | resTodo.description = random.word().slice(0, 1024); 12 | resTodo.dueDate = date.past().toJSON(); 13 | resTodo.done = datatype.boolean(); 14 | 15 | if (todoFks.assignee === null || typeof todoFks.assignee === 'undefined') { 16 | const fakeAssignee = await buildPerson({}); 17 | const createdFakeAssignee = await createPerson(fakeAssignee); 18 | assignee = createdFakeAssignee.id; 19 | } 20 | 21 | resTodo.assignee = assignee; 22 | 23 | return resTodo; 24 | }; 25 | 26 | const createTodo = async (fakeTodo) => { 27 | const todo = await Todo.create(fakeTodo); 28 | return todo; 29 | }; 30 | 31 | export { buildTodo, createTodo }; 32 | -------------------------------------------------------------------------------- /src/tests/todo.test.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { buildTodo, buildPerson, createTodo, createPerson } from './factories'; 3 | import { startDatabase } from './utils'; 4 | import { Todo, Person } from 'data/models'; 5 | import { app } from 'server/app'; 6 | 7 | const ENDPOINT = '/todo'; 8 | 9 | describe('Todo tests', () => { 10 | beforeEach(async () => { 11 | await startDatabase(); 12 | }); 13 | 14 | afterAll(async () => { 15 | await app.close(); 16 | }); 17 | 18 | test('/POST - Response with a new created todo', async () => { 19 | const assigneeDict = await buildPerson({}); 20 | const fakeAssignee = await createPerson(assigneeDict); 21 | 22 | const fakeTodo = await buildTodo({ assignee: fakeAssignee.id }); 23 | 24 | const response = await request(app).post(ENDPOINT).send(fakeTodo); 25 | 26 | expect(response.status).toBe(201); 27 | expect(response.statusCode).toBe(201); 28 | 29 | const responseTodo = response.body.data; 30 | 31 | const todo = await Todo.findByPk(responseTodo.id); 32 | 33 | expect(todo.title).toBe(fakeTodo.title); 34 | expect(todo.description).toBe(fakeTodo.description); 35 | expect(todo.dueDate.toISOString()).toEqual(fakeTodo.dueDate); 36 | expect(todo.done).toBe(fakeTodo.done); 37 | 38 | expect(todo.assignee).toBe(fakeTodo.assignee); 39 | }); 40 | 41 | test('/POST - assignee does not exists, todo cant be created', async () => { 42 | const fakeTodo = await buildTodo({}); 43 | const assignee = await Person.findOne({ where: { id: fakeTodo.assignee } }); 44 | await assignee.destroy(); 45 | 46 | const response = await request(app).post(ENDPOINT).send(fakeTodo); 47 | 48 | const { statusCode } = response; 49 | expect(statusCode).toBe(404); 50 | }); 51 | 52 | test('/GET - Response with a todo', async () => { 53 | const assigneeDict = await buildPerson({}); 54 | const fakeAssignee = await createPerson(assigneeDict); 55 | 56 | const todoDict = await buildTodo({ assignee: fakeAssignee.id }); 57 | const fakeTodo = await createTodo(todoDict); 58 | 59 | const response = await request(app).get(`${ENDPOINT}/${fakeTodo.id}`); 60 | 61 | const { statusCode, status } = response; 62 | const { data } = response.body; 63 | 64 | expect(status).toBe(200); 65 | expect(statusCode).toBe(200); 66 | 67 | expect(data.id).toBe(fakeTodo.id); 68 | expect(data.title).toBe(fakeTodo.title); 69 | expect(data.description).toBe(fakeTodo.description); 70 | expect(data.dueDate).toBe(fakeTodo.dueDate.toISOString()); 71 | expect(data.done).toBe(fakeTodo.done); 72 | 73 | expect(data.comments).toEqual([]); 74 | expect(data.assignee).toBe(fakeTodo.assignee); 75 | }); 76 | 77 | test('/GET - Response with a todo not found', async () => { 78 | const todoDict = await buildTodo({}); 79 | const fakeTodo = await createTodo(todoDict); 80 | const id = fakeTodo.id; 81 | await fakeTodo.destroy(); 82 | 83 | const response = await request(app).get(`${ENDPOINT}/${id}`); 84 | const { statusCode } = response; 85 | expect(statusCode).toBe(404); 86 | }); 87 | 88 | test('/GET - Response with a list of todos', async () => { 89 | const assigneeDict = await buildPerson({}); 90 | const fakeAssignee = await createPerson(assigneeDict); 91 | 92 | const todoDict = await buildTodo({ assignee: fakeAssignee.id }); 93 | const fakeTodo = await createTodo(todoDict); 94 | 95 | const response = await request(app).get(ENDPOINT); 96 | 97 | const { statusCode, status } = response; 98 | const { data } = response.body; 99 | 100 | expect(status).toBe(200); 101 | expect(statusCode).toBe(200); 102 | 103 | const allTodo = await Todo.findAll(); 104 | expect(data.length).toBe(allTodo.length); 105 | }); 106 | 107 | test('/PUT - Response with an updated todo', async () => { 108 | const assigneeDict = await buildPerson({}); 109 | const fakeAssignee = await createPerson(assigneeDict); 110 | 111 | const todoDict = await buildTodo({ assignee: fakeAssignee.id }); 112 | const fakeTodo = await createTodo(todoDict); 113 | 114 | const anotherAssigneeDict = await buildPerson({}); 115 | const anotherFakeAssignee = await createPerson(anotherAssigneeDict); 116 | 117 | const anotherFakeTodo = await buildTodo({ assignee: anotherFakeAssignee.id }); 118 | 119 | const response = await request(app).put(`${ENDPOINT}/${fakeTodo.id}`).send({ 120 | title: anotherFakeTodo.title, 121 | description: anotherFakeTodo.description, 122 | dueDate: anotherFakeTodo.dueDate, 123 | done: anotherFakeTodo.done, 124 | assignee: anotherFakeTodo.assignee, 125 | }); 126 | 127 | const { status } = response; 128 | const { data } = response.body; 129 | 130 | expect(status).toBe(200); 131 | expect(response.statusCode).toBe(200); 132 | 133 | expect(data.title).toBe(anotherFakeTodo.title); 134 | expect(data.description).toBe(anotherFakeTodo.description); 135 | expect(data.dueDate).toBe(anotherFakeTodo.dueDate); 136 | expect(data.done).toBe(anotherFakeTodo.done); 137 | 138 | expect(data.assignee).toBe(anotherFakeTodo.assignee); 139 | 140 | const updatedTodo = await Todo.findByPk(fakeTodo.id); 141 | 142 | expect(updatedTodo.title).toBe(anotherFakeTodo.title); 143 | expect(updatedTodo.description).toBe(anotherFakeTodo.description); 144 | expect(updatedTodo.dueDate.toISOString()).toEqual(anotherFakeTodo.dueDate); 145 | expect(updatedTodo.done).toBe(anotherFakeTodo.done); 146 | 147 | expect(updatedTodo.assignee).toBe(anotherFakeTodo.assignee); 148 | }); 149 | 150 | test('/PUT - assignee does not exists, todo cant be updated', async () => { 151 | const assigneeDict = await buildPerson({}); 152 | const fakeAssignee = await createPerson(assigneeDict); 153 | 154 | const todoDict = await buildTodo({ assignee: fakeAssignee.id }); 155 | const fakeTodo = await createTodo(todoDict); 156 | 157 | const anotherAssigneeDict = await buildPerson({}); 158 | const anotherFakeAssignee = await createPerson(anotherAssigneeDict); 159 | 160 | fakeTodo.assignee = anotherFakeAssignee.id; 161 | 162 | await anotherFakeAssignee.destroy(); 163 | 164 | const response = await request(app).put(`${ENDPOINT}/${fakeTodo.id}`).send({ 165 | title: fakeTodo.title, 166 | description: fakeTodo.description, 167 | dueDate: fakeTodo.dueDate, 168 | done: fakeTodo.done, 169 | assignee: fakeTodo.assignee, 170 | }); 171 | 172 | const { statusCode } = response; 173 | expect(statusCode).toBe(404); 174 | }); 175 | 176 | test('/PUT - Todo does not exists, todo cant be updated', async () => { 177 | const todoDict = await buildTodo({}); 178 | const fakeTodo = await createTodo(todoDict); 179 | const id = fakeTodo.id; 180 | await fakeTodo.destroy(); 181 | 182 | const response = await request(app).put(`${ENDPOINT}/${id}`).send({ 183 | title: fakeTodo.title, 184 | description: fakeTodo.description, 185 | dueDate: fakeTodo.dueDate, 186 | done: fakeTodo.done, 187 | assignee: fakeTodo.assignee, 188 | }); 189 | 190 | const { statusCode } = response; 191 | expect(statusCode).toBe(404); 192 | }); 193 | 194 | test('/PATCH - Response with an updated todo', async () => { 195 | const assigneeDict = await buildPerson({}); 196 | const fakeAssignee = await createPerson(assigneeDict); 197 | 198 | const todoDict = await buildTodo({ assignee: fakeAssignee.id }); 199 | const fakeTodo = await createTodo(todoDict); 200 | 201 | const anotherAssigneeDict = await buildPerson({}); 202 | const anotherFakeAssignee = await createPerson(anotherAssigneeDict); 203 | 204 | const anotherFakeTodo = await buildTodo({ assignee: anotherFakeAssignee.id }); 205 | 206 | const response = await request(app) 207 | .patch(`${ENDPOINT}/${fakeTodo.id}`) 208 | .send({ title: anotherFakeTodo.title }); 209 | 210 | const { status } = response; 211 | const { data } = response.body; 212 | 213 | expect(status).toBe(200); 214 | expect(response.statusCode).toBe(200); 215 | 216 | expect(data.title).toBe(anotherFakeTodo.title); 217 | 218 | const updatedTodo = await Todo.findByPk(fakeTodo.id); 219 | 220 | expect(updatedTodo.title).toBe(anotherFakeTodo.title); 221 | }); 222 | 223 | test('/PATCH - assignee does not exists, todo cant be updated', async () => { 224 | const todoDict = await buildTodo({}); 225 | const fakeTodo = await createTodo(todoDict); 226 | 227 | const assigneeDict = await buildPerson({}); 228 | const fakeAssignee = await createPerson(assigneeDict); 229 | 230 | const fakeAssigneeId = fakeAssignee.id; 231 | await fakeAssignee.destroy(); 232 | 233 | const response = await request(app).patch(`${ENDPOINT}/${fakeTodo.id}`).send({ 234 | assignee: fakeAssigneeId, 235 | }); 236 | 237 | const { statusCode } = response; 238 | expect(statusCode).toBe(404); 239 | }); 240 | 241 | test('/PATCH - Todo does not exists, todo cant be updated', async () => { 242 | const todoDict = await buildTodo({}); 243 | const fakeTodo = await createTodo(todoDict); 244 | const id = fakeTodo.id; 245 | const title = fakeTodo.title; 246 | await fakeTodo.destroy(); 247 | 248 | const response = await request(app).patch(`${ENDPOINT}/${id}`).send({ title: title }); 249 | 250 | const { statusCode } = response; 251 | expect(statusCode).toBe(404); 252 | }); 253 | 254 | test('/DELETE - Response with a deleted todo', async () => { 255 | const todoDict = await buildTodo({}); 256 | const fakeTodo = await createTodo(todoDict); 257 | 258 | const response = await request(app).delete(`${ENDPOINT}/${fakeTodo.id}`); 259 | 260 | const { status } = response; 261 | const { data } = response.body; 262 | 263 | expect(status).toBe(200); 264 | expect(response.statusCode).toBe(200); 265 | 266 | expect(data.id).toBe(fakeTodo.id); 267 | 268 | const deletedTodo = await Todo.findByPk(fakeTodo.id); 269 | expect(deletedTodo).toBe(null); 270 | }); 271 | 272 | test('/DELETE - Todo does not exists, todo cant be deleted', async () => { 273 | const todoDict = await buildTodo({}); 274 | const fakeTodo = await createTodo(todoDict); 275 | const id = fakeTodo.id; 276 | await fakeTodo.destroy(); 277 | 278 | const response = await request(app).delete(`${ENDPOINT}/${id}`); 279 | 280 | const { statusCode } = response; 281 | expect(statusCode).toBe(404); 282 | }); 283 | }); 284 | -------------------------------------------------------------------------------- /src/tests/utils.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { sequelize } from 'data/models'; 3 | 4 | const startDatabase = async () => { 5 | await sequelize.sync({ force: true }); 6 | }; 7 | 8 | const deleteDatabase = (db) => db.drop(); 9 | 10 | export { request, startDatabase, deleteDatabase }; 11 | -------------------------------------------------------------------------------- /todoapp.im: -------------------------------------------------------------------------------- 1 | settings 2 | 3 | app: 4 | # your application name 5 | name: todoapp 6 | # choose one: [django, node] 7 | framework: node 8 | 9 | api: 10 | # choose one: [rest, graphql] 11 | format: rest 12 | 13 | end settings 14 | 15 | 16 | # database type: [sqlite, mysql, posgresql]> 17 | database todoapp-db sqlite3 18 | 19 | 20 | # datamodel spec section 21 | Model Todo { 22 | id integer [primarykey, default auto-increment] 23 | title string [maxlength 255, not-null] 24 | description string [maxlength 1024] 25 | due_date datetime [default now] 26 | done boolean 27 | } 28 | 29 | Model Comment { 30 | id integer [primarykey, default auto-increment] 31 | message string [maxlength 512] 32 | submitted datetime [default now] 33 | status string [choice ["read", "unread"] ] 34 | } 35 | 36 | Relation todo_comments { 37 | many comments from Comment 38 | one todo from Todo 39 | } 40 | 41 | Model Person { 42 | id integer [primarykey, default auto-increment] 43 | email string [maxlength 100] 44 | firstname string [maxlength 100] 45 | lastname string [maxlength 100] 46 | last_login datetime [default now] 47 | } 48 | 49 | Relation todo_assignee { 50 | many todos from Todo 51 | one assignee from Person 52 | } 53 | 54 | 55 | # api spec section 56 | API /todo { 57 | model Todo 58 | actions CRUD 59 | permissions [] 60 | filter [done] 61 | } 62 | 63 | API /comment { 64 | model Comment 65 | actions CRUD 66 | filter [status] 67 | } 68 | 69 | API /create_person { 70 | model Person 71 | actions [Create] 72 | } 73 | 74 | 75 | --------------------------------------------------------------------------------