├── .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 | [](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 |
--------------------------------------------------------------------------------