From 2e5dcb536eae560ef388ab0ea6c72ea01c009c7d Mon Sep 17 00:00:00 2001 From: caidam Date: Tue, 16 Jan 2024 23:15:05 +0100 Subject: [PATCH 01/11] 1.2 read from database --- .env.sample | 12 +- .gitignore | 2 + README.md | 155 ++++++++++++++++++++++ database.js | 23 ++++ docker-compose.yml | 22 ++++ index.js | 3 +- package-lock.json | 193 +++++++++++++++++++++++++++- package.json | 4 +- src/app.js | 7 + src/controllers/movieControllers.js | 57 ++++---- src/controllers/userControllers.js | 36 ++++++ tests/movies.test.js | 4 +- tests/users.test.js | 4 +- 13 files changed, 482 insertions(+), 40 deletions(-) create mode 100644 README.md create mode 100644 database.js create mode 100644 docker-compose.yml create mode 100644 src/controllers/userControllers.js diff --git a/.env.sample b/.env.sample index d5349ec8a..56115f89b 100644 --- a/.env.sample +++ b/.env.sample @@ -1 +1,11 @@ -APP_PORT=5000 +# MySQL variables +MYSQL_ROOT_PASSWORD=replace-root-pwd +MYSQL_DATABASE=replace-db-name +MYSQL_USER=replace-db-user +MYSQL_PASSWORD=replace-db-pwd +MYSQL_PORT=3306 +# make sure the MYSQL_SERVICE_IP matches with the one corresponding to the mysql container in the docker-compose.yml file +MYSQL_SERVICE_IP=172.19.0.2 + +# app variables +APP_PORT=5000 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3c3629e64..6149f9bac 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ node_modules +.env +/mysql-data/* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..804794ff4 --- /dev/null +++ b/README.md @@ -0,0 +1,155 @@ +# Express API + +[ref](https://odyssey.wildcodeschool.com/quests/395) + +### Create `.env` file + +1. install `dotenv` + +```bash +npm install dotenv +``` + +2. create `.env` file + +This file must not be pushed to GitHub but you can include a .env.sample code to share indications on what to declare. + +If you cloned a repo with such a file you can use : + +```bash +cp .env.sample .env +``` + +modify the existing variables accordingly and add the `.env`in your `.gitignore`. + +3. add below line at the top of `index.js` + +```js +require("dotenv").config(); +``` + +4. consume variables as follows + +```js +const port = process.env.APP_PORT; +``` + +5. your environment is set up, run with `npm run dev` + +### Create the database (via docker compose) + +1. prepare the files + +Make sure you have docker installed and update the MYSQL related variables in the `.env` file. + +Data from `express_quests.sql` database file will be mounted into the container and the updated data will be persisted in the mysql-data file. + +Refer to the `docker-compose.yml` file if you want to update these settings. + + +2. create mysql container + +Create the container by running the below in the terminal : + +```bash +docker compose up -d +``` + +You can access the CLI and check everything is in order with this command : + +```bash +docker exec -it mysql-express-container mysql -u root -p +``` + +### Install MySQL 2 module + +https://www.npmjs.com/package/mysql2 + +```bash +npm install mysql2 +``` + +### Configure database access + +1. Create a `database.js` file next to `index.js` + +2. Require the `dotenv` package + +Make sure you require the dotenv package at the very top. + +```js +require("dotenv").config(); +``` + +3. import `mysql2` package + +4. Use `mysql.createPool` to prepare a connection + +see `database.js` file + + +5. test connection + +in `database.js` + +```js +database + .getConnection() + .then(() => { + console.log("Can reach database"); + }) + .catch((err) => { + console.error(err); + }); +``` + +and run the following command : + +```bash +npx nodemon database.js +``` + +6. don't forget do include `module.exports = database;` at the end of `database.js` + +### Postman + +Postman is a great and powerful tool that we can use to test our routes. + +1. Install Postman + +on ubuntu use the command : +```bash +snap install postman +``` +for other os check [here](https://learning.postman.com/docs/getting-started/installation/installation-and-updates/#install-postman-on-linux) + +to launch postman on ubuntu : + +```bash +# run in the background +postman & + +# bring to the foreground +fg + +# get back to the backgroung +# first suspend with `ctrl + Z` then +bg + +# prevent background process from being terminated +# when you close the terminal +nohup postman & + +``` + +### Test lifecycle management + +The connection to the database prevents our test script from completing its execution, so it will have to be cut after all the tests have been executed. + +Add the following to the test files : + +```bash +const database = require("../database") + +afterAll(() => database.end()); +``` \ No newline at end of file diff --git a/database.js b/database.js new file mode 100644 index 000000000..c168e6e41 --- /dev/null +++ b/database.js @@ -0,0 +1,23 @@ +require("dotenv").config(); + +const mysql = require("mysql2/promise"); + +const database = mysql.createPool({ + host: process.env.MYSQL_SERVICE_IP, // address of the server + port: process.env.MYSQL_PORT, // port of the DB server (mysql), not to be confused with the APP_PORT ! + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE, + }); + +// test connection +database +.getConnection() +.then(() => { +console.log("Can reach database"); +}) +.catch((err) => { +console.error(err); +}); + +module.exports = database; \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..02bf9370a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mysql-express-container: + image: mysql:latest + container_name: mysql-express-container + env_file: + - .env + ports: + - "${MYSQL_PORT:-3306}:3306" + volumes: + - ./express_quests.sql:/docker-entrypoint-initdb.d/express_quests.sql + - ./mysql-data:/var/lib/mysql + networks: + your_network_name: + ipv4_address: 172.19.0.2 + +networks: + your_network_name: + ipam: + config: + - subnet: 172.19.0.0/16 \ No newline at end of file diff --git a/index.js b/index.js index d6626f115..6a5c56b9e 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ +require("dotenv").config(); const app = require("./src/app"); -const port = 5000; +const port = process.env.APP_PORT; app .listen(port, () => { diff --git a/package-lock.json b/package-lock.json index d6aeb7259..c8f0176ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,9 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "express": "^4.18.2" + "dotenv": "^16.3.1", + "express": "^4.18.2", + "mysql2": "^3.7.0" }, "devDependencies": { "jest": "^29.7.0", @@ -1809,6 +1811,14 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1854,6 +1864,17 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2169,6 +2190,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2493,6 +2522,11 @@ "node": ">=0.12.0" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -3306,6 +3340,11 @@ "node": ">=8" } }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3468,6 +3507,62 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/mysql2": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.7.0.tgz", + "integrity": "sha512-c45jA3Jc1X8yJKzrWu1GpplBKGwv/wIV6ITZTlCSY7npF2YfJR+6nMP5e+NTQhUeJPSyOQAbGDCGEHbAl8HN9w==", + "dependencies": { + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru-cache": "^8.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mysql2/node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "engines": { + "node": ">=16.14" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4044,6 +4139,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -4164,6 +4264,14 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -6048,6 +6156,11 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true }, + "denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==" + }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -6080,6 +6193,11 @@ "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true }, + "dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==" + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6324,6 +6442,14 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, + "generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "requires": { + "is-property": "^1.0.2" + } + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -6552,6 +6678,11 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -7171,6 +7302,11 @@ "p-locate": "^4.1.0" } }, + "long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7293,6 +7429,51 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "mysql2": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.7.0.tgz", + "integrity": "sha512-c45jA3Jc1X8yJKzrWu1GpplBKGwv/wIV6ITZTlCSY7npF2YfJR+6nMP5e+NTQhUeJPSyOQAbGDCGEHbAl8HN9w==", + "requires": { + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru-cache": "^8.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==" + } + } + }, + "named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "requires": { + "lru-cache": "^7.14.1" + }, + "dependencies": { + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==" + } + } + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -7705,6 +7886,11 @@ } } }, + "seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -7803,6 +7989,11 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==" + }, "stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", diff --git a/package.json b/package.json index 64b8ae6d8..e39cc52e5 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ }, "homepage": "https://github.com/WildCodeSchool/Express-Quests#readme", "dependencies": { - "express": "^4.18.2" + "dotenv": "^16.3.1", + "express": "^4.18.2", + "mysql2": "^3.7.0" }, "devDependencies": { "jest": "^29.7.0", diff --git a/src/app.js b/src/app.js index 19f5d86d4..f2fa146f0 100644 --- a/src/app.js +++ b/src/app.js @@ -3,8 +3,15 @@ const express = require("express"); const app = express(); const movieControllers = require("./controllers/movieControllers"); +const userControllers = require("./controllers/userControllers"); +// movieControllers routes app.get("/api/movies", movieControllers.getMovies); app.get("/api/movies/:id", movieControllers.getMovieById); +// userControllers routes +app.get("/api/users", userControllers.getUsers); +app.get("/api/users/:id", userControllers.getUserById); + + module.exports = app; diff --git a/src/controllers/movieControllers.js b/src/controllers/movieControllers.js index e3bb0053f..8cb20e00e 100644 --- a/src/controllers/movieControllers.js +++ b/src/controllers/movieControllers.js @@ -1,44 +1,33 @@ -const movies = [ - { - id: 1, - title: "Citizen Kane", - director: "Orson Wells", - year: "1941", - color: false, - duration: 120, - }, - { - id: 2, - title: "The Godfather", - director: "Francis Ford Coppola", - year: "1972", - color: true, - duration: 180, - }, - { - id: 3, - title: "Pulp Fiction", - director: "Quentin Tarantino", - year: "1994", - color: true, - duration: 180, - }, -]; +const database = require("../../database"); const getMovies = (req, res) => { - res.json(movies); + database + .query("select * from movies") + .then(([movies]) => { + res.json(movies); + }) + .catch((err) => { + console.error(err); + res.sendStatus(500); + }); }; const getMovieById = (req, res) => { const id = parseInt(req.params.id); - const movie = movies.find((movie) => movie.id === id); - - if (movie != null) { - res.json(movie); - } else { - res.status(404).send("Not Found"); - } + database + .query("select * from movies where id = ?", [id]) + .then(([movies]) => { + if (movies[0] != null) { + res.json(movies[0]); + } else { + res.sendStatus(404); + } + }) + .catch((err) => { + console.error(err); + res.sendStatus(500); + }); }; module.exports = { diff --git a/src/controllers/userControllers.js b/src/controllers/userControllers.js new file mode 100644 index 000000000..999b3cec5 --- /dev/null +++ b/src/controllers/userControllers.js @@ -0,0 +1,36 @@ +const database = require("../../database"); + +const getUsers = (req, res) => { + database + .query("select * from users") + .then(([users]) => { + res.json(users); + }) + .catch((err) => { + console.error(err); + res.sendStatus(500); + }); +}; + +const getUserById = (req, res) => { + const id = parseInt(req.params.id); + + database + .query("select * from users where id = ?", [id]) + .then(([users]) => { + if (users[0] != null) { + res.json(users[0]); + } else { + res.sendStatus(404); + } + }) + .catch((err) => { + console.error(err); + res.sendStatus(500); + }); +}; + +module.exports = { + getUsers, + getUserById, +}; diff --git a/tests/movies.test.js b/tests/movies.test.js index 09621f56f..b792c60c9 100644 --- a/tests/movies.test.js +++ b/tests/movies.test.js @@ -1,6 +1,8 @@ const request = require("supertest"); - const app = require("../src/app"); +const database = require("../database") + +afterAll(() => database.end()); describe("GET /api/movies", () => { it("should return all movies", async () => { diff --git a/tests/users.test.js b/tests/users.test.js index f69d5e9e0..666c9e6bb 100644 --- a/tests/users.test.js +++ b/tests/users.test.js @@ -1,6 +1,8 @@ const request = require("supertest"); - const app = require("../src/app"); +const database = require("../database") + +afterAll(() => database.end()); describe("GET /api/users", () => { it("should return all users", async () => { From 14bcc92d96537bab08b40bc4179e6455f241f0ff Mon Sep 17 00:00:00 2001 From: caidam Date: Tue, 16 Jan 2024 23:28:26 +0100 Subject: [PATCH 02/11] uningnored mysql-data folder --- .gitignore | 3 ++- mysql-data/dont-delete-folder__.md | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 mysql-data/dont-delete-folder__.md diff --git a/.gitignore b/.gitignore index 6149f9bac..cbb0c5cb5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules .env -/mysql-data/* \ No newline at end of file +/mysql-data/* +!/mysql-data/dont-delete-folder__.md \ No newline at end of file diff --git a/mysql-data/dont-delete-folder__.md b/mysql-data/dont-delete-folder__.md new file mode 100644 index 000000000..7723058dc --- /dev/null +++ b/mysql-data/dont-delete-folder__.md @@ -0,0 +1 @@ +This folder is where the data of the mysql container is persisted, don't delete it. \ No newline at end of file From 2512bb16ecd9b6b652e2177e8ae8c36d80325897 Mon Sep 17 00:00:00 2001 From: caidam Date: Wed, 17 Jan 2024 11:25:49 +0100 Subject: [PATCH 03/11] addded post routes to add movies and users --- src/app.js | 6 ++++++ src/controllers/movieControllers.js | 25 ++++++++++++++++++++++--- src/controllers/userControllers.js | 18 ++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/app.js b/src/app.js index f2fa146f0..cc71af9bd 100644 --- a/src/app.js +++ b/src/app.js @@ -2,6 +2,8 @@ const express = require("express"); const app = express(); +app.use(express.json()); + const movieControllers = require("./controllers/movieControllers"); const userControllers = require("./controllers/userControllers"); @@ -9,9 +11,13 @@ const userControllers = require("./controllers/userControllers"); app.get("/api/movies", movieControllers.getMovies); app.get("/api/movies/:id", movieControllers.getMovieById); +app.post("/api/movies", movieControllers.postMovie); + // userControllers routes app.get("/api/users", userControllers.getUsers); app.get("/api/users/:id", userControllers.getUserById); +app.post("/api/users", userControllers.postUser); + module.exports = app; diff --git a/src/controllers/movieControllers.js b/src/controllers/movieControllers.js index 8cb20e00e..5e346fe1c 100644 --- a/src/controllers/movieControllers.js +++ b/src/controllers/movieControllers.js @@ -16,9 +16,9 @@ const getMovieById = (req, res) => { const id = parseInt(req.params.id); database - .query("select * from movies where id = ?", [id]) - .then(([movies]) => { - if (movies[0] != null) { + .query("select * from movies where id = ?", [id]) + .then(([movies]) => { + if (movies[0] != null) { res.json(movies[0]); } else { res.sendStatus(404); @@ -28,9 +28,28 @@ const getMovieById = (req, res) => { console.error(err); res.sendStatus(500); }); + }; + +const postMovie = (req, res) => { + const { title, director, year, color, duration } = req.body; + + database + .query( + "INSERT INTO movies(title, director, year, color, duration) VALUES (?, ?, ?, ?, ?)", + [title, director, year, color, duration] + ) + .then(([result]) => { + res.status(201).send({ id: result.insertId}) + }) + .catch((err) => { + console.error(err); + res.sendStatus(500); + }); }; + module.exports = { getMovies, getMovieById, + postMovie, }; diff --git a/src/controllers/userControllers.js b/src/controllers/userControllers.js index 999b3cec5..763ad583c 100644 --- a/src/controllers/userControllers.js +++ b/src/controllers/userControllers.js @@ -30,7 +30,25 @@ const getUserById = (req, res) => { }); }; +const postUser = (req, res) => { + const { firstname, lastname, email, city, language } = req.body; + + database + .query( + "INSERT INTO users(firstname, lastname, email, city, language) VALUES (?, ?, ?, ?, ?)", + [firstname, lastname, email, city, language] + ) + .then(([result]) => { + res.status(201).send({ id: result.insertId}) + }) + .catch((err) => { + console.error(err); + res.sendStatus(500); + }); +}; + module.exports = { getUsers, getUserById, + postUser, }; From c1b6d1ba4fd4eb306b6669d6d8c40c7b5121db21 Mon Sep 17 00:00:00 2001 From: caidam Date: Wed, 17 Jan 2024 12:03:16 +0100 Subject: [PATCH 04/11] added tests for post routes --- tests/movies.test.js | 52 +++++++++++++++++++++++++++++++++++++++++++ tests/users.test.js | 53 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/tests/movies.test.js b/tests/movies.test.js index b792c60c9..cb15e5e96 100644 --- a/tests/movies.test.js +++ b/tests/movies.test.js @@ -29,3 +29,55 @@ describe("GET /api/movies/:id", () => { expect(response.status).toEqual(404); }); }); + +describe("POST /api/movies", () => { + it("should return created movie", async () => { + const newMovie = { + title: "Star Wars", + director: "George Lucas", + year: "1977", + color: "1", + duration: 120, + }; + + const response = await request(app).post("/api/movies").send(newMovie); + + expect(response.status).toEqual(201); + expect(response.body).toHaveProperty("id"); + expect(typeof response.body.id).toBe("number"); + + const [result] = await database.query( + "SELECT * FROM movies WHERE id=?", + response.body.id + ); + + const [movieInDatabase] = result; + + expect(movieInDatabase).toHaveProperty("id"); + + expect(movieInDatabase).toHaveProperty("title"); + expect(movieInDatabase.title).toStrictEqual(newMovie.title); + + expect(movieInDatabase).toHaveProperty("director"); + expect(movieInDatabase.director).toStrictEqual(newMovie.director); + + expect(movieInDatabase).toHaveProperty("year"); + expect(movieInDatabase.year).toStrictEqual(newMovie.year); + + expect(movieInDatabase).toHaveProperty("color"); + expect(movieInDatabase.color).toStrictEqual(newMovie.color); + + expect(movieInDatabase).toHaveProperty("duration"); + expect(movieInDatabase.duration).toStrictEqual(newMovie.duration); + }); + + it("should return an error", async () => { + const movieWithMissingProps = { title: "Harry Potter" }; + + const response = await request(app) + .post("/api/movies") + .send(movieWithMissingProps); + + expect(response.status).toEqual(500); + }); +}); diff --git a/tests/users.test.js b/tests/users.test.js index 666c9e6bb..b973d2693 100644 --- a/tests/users.test.js +++ b/tests/users.test.js @@ -1,6 +1,7 @@ const request = require("supertest"); const app = require("../src/app"); const database = require("../database") +const crypto = require("node:crypto") afterAll(() => database.end()); @@ -29,3 +30,55 @@ describe("GET /api/users/:id", () => { expect(response.status).toEqual(404); }); }); + +describe("POST /api/users", () => { + it("should return created user", async () => { + const newUser = { + firstname: "Marie", + lastname: "Martin", + email: `${crypto.randomUUID()}@wild.co`, + city: "Paris", + language: "French", + }; + + const response = await request(app).post("/api/users").send(newUser); + + expect(response.status).toEqual(201); + expect(response.body).toHaveProperty("id"); + expect(typeof response.body.id).toBe("number"); + + const [result] = await database.query( + "SELECT * FROM users WHERE id=?", + response.body.id + ); + + const [userInDatabase] = result; + + expect(userInDatabase).toHaveProperty("id"); + + expect(userInDatabase).toHaveProperty("firstname"); + expect(userInDatabase.firstname).toStrictEqual(newUser.firstname); + + expect(userInDatabase).toHaveProperty("lastname"); + expect(userInDatabase.lastname).toStrictEqual(newUser.lastname); + + expect(userInDatabase).toHaveProperty("email"); + expect(userInDatabase.email).toStrictEqual(newUser.email); + + expect(userInDatabase).toHaveProperty("city"); + expect(userInDatabase.city).toStrictEqual(newUser.city); + + expect(userInDatabase).toHaveProperty("language"); + expect(userInDatabase.language).toStrictEqual(newUser.language); + }); + + it("should return an error", async () => { + const userWithMissingProps = { firstname: "Harry" }; + + const response = await request(app) + .post("/api/users") + .send(userWithMissingProps); + + expect(response.status).toEqual(500); + }); +}); From c451eb22792da7e419cc697e2bc06466a37a6334 Mon Sep 17 00:00:00 2001 From: caidam Date: Wed, 17 Jan 2024 12:40:38 +0100 Subject: [PATCH 05/11] added put routes to modify movie and user --- src/app.js | 5 +++++ src/controllers/movieControllers.js | 28 +++++++++++++++++++++++++++- src/controllers/userControllers.js | 24 ++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/app.js b/src/app.js index cc71af9bd..078c5874d 100644 --- a/src/app.js +++ b/src/app.js @@ -13,11 +13,16 @@ app.get("/api/movies/:id", movieControllers.getMovieById); app.post("/api/movies", movieControllers.postMovie); +app.put("/api/movies/:id", movieControllers.updateMovie); + // userControllers routes app.get("/api/users", userControllers.getUsers); app.get("/api/users/:id", userControllers.getUserById); app.post("/api/users", userControllers.postUser); +app.put("/api/users/:id", userControllers.updateUser); + + module.exports = app; diff --git a/src/controllers/movieControllers.js b/src/controllers/movieControllers.js index 5e346fe1c..9e0641ede 100644 --- a/src/controllers/movieControllers.js +++ b/src/controllers/movieControllers.js @@ -1,5 +1,6 @@ const database = require("../../database"); +// GET const getMovies = (req, res) => { database .query("select * from movies") @@ -29,7 +30,8 @@ const getMovieById = (req, res) => { res.sendStatus(500); }); }; - + +// POST const postMovie = (req, res) => { const { title, director, year, color, duration } = req.body; @@ -48,8 +50,32 @@ const postMovie = (req, res) => { }; +// PUT +const updateMovie = (req, res) => { + const id = parseInt(req.params.id); + const { title, director, year, color, duration } = req.body; + + database + .query( + "update movies set title = ?, director = ?, year = ?, color = ?, duration = ? where id = ?", + [title, director, year, color, duration, id] + ) + .then(([result]) => { + if (result.affectedRows === 0) { + res.sendStatus(404); + } else { + res.sendStatus(204); + } + }) + .catch((err) => { + console.error(err); + res.sendStatus(500); + }); +}; + module.exports = { getMovies, getMovieById, postMovie, + updateMovie, }; diff --git a/src/controllers/userControllers.js b/src/controllers/userControllers.js index 763ad583c..2183a34e4 100644 --- a/src/controllers/userControllers.js +++ b/src/controllers/userControllers.js @@ -47,8 +47,32 @@ const postUser = (req, res) => { }); }; +// PUT +const updateUser = (req, res) => { + const id = parseInt(req.params.id); + const { firstname, lastname, email, city, language } = req.body; + + database + .query( + "update users set firstname = ?, lastname = ?, email = ?, city = ?, language = ? where id = ?", + [firstname, lastname, email, city, language, id] + ) + .then(([result]) => { + if (result.affectedRows === 0) { + res.sendStatus(404); + } else { + res.sendStatus(204); + } + }) + .catch((err) => { + console.error(err); + res.sendStatus(500); + }); +}; + module.exports = { getUsers, getUserById, postUser, + updateUser, }; From b6b246252020d121e914be491ae1df81f5d06701 Mon Sep 17 00:00:00 2001 From: caidam Date: Wed, 17 Jan 2024 13:16:05 +0100 Subject: [PATCH 06/11] created tests for put routes --- tests/movies.test.js | 84 ++++++++++++++++++++++++++++++++++++++++++++ tests/users.test.js | 79 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) diff --git a/tests/movies.test.js b/tests/movies.test.js index cb15e5e96..2dd74a10a 100644 --- a/tests/movies.test.js +++ b/tests/movies.test.js @@ -4,6 +4,8 @@ const database = require("../database") afterAll(() => database.end()); + +// GET describe("GET /api/movies", () => { it("should return all movies", async () => { const response = await request(app).get("/api/movies"); @@ -30,6 +32,8 @@ describe("GET /api/movies/:id", () => { }); }); + +// POST describe("POST /api/movies", () => { it("should return created movie", async () => { const newMovie = { @@ -81,3 +85,83 @@ describe("POST /api/movies", () => { expect(response.status).toEqual(500); }); }); + + +// PUT +describe("PUT /api/movies/:id", () => { + it("should edit movie", async () => { + const newMovie = { + title: "Avatar", + director: "James Cameron", + year: "2010", + color: "1", + duration: 162, + }; + + const [result] = await database.query( + "INSERT INTO movies(title, director, year, color, duration) VALUES (?, ?, ?, ?, ?)", + [newMovie.title, newMovie.director, newMovie.year, newMovie.color, newMovie.duration] + ); + + const id = result.insertId; + + const updatedMovie = { + title: "Wild is life", + director: "Alan Smithee", + year: "2023", + color: "0", + duration: 120, + }; + + const response = await request(app) + .put(`/api/movies/${id}`) + .send(updatedMovie); + + expect(response.status).toEqual(204); + + const [movies] = await database.query("SELECT * FROM movies WHERE id=?", id); + + const [movieInDatabase] = movies; + + expect(movieInDatabase).toHaveProperty("id"); + + expect(movieInDatabase).toHaveProperty("title"); + expect(movieInDatabase.title).toStrictEqual(updatedMovie.title); + + expect(movieInDatabase).toHaveProperty("director"); + expect(movieInDatabase.director).toStrictEqual(updatedMovie.director); + + expect(movieInDatabase).toHaveProperty("year"); + expect(movieInDatabase.year).toStrictEqual(updatedMovie.year); + + expect(movieInDatabase).toHaveProperty("color"); + expect(movieInDatabase.color).toStrictEqual(updatedMovie.color); + + expect(movieInDatabase).toHaveProperty("duration"); + expect(movieInDatabase.duration).toStrictEqual(updatedMovie.duration); + }); + + it("should return an error", async () => { + const movieWithMissingProps = { title: "Harry Potter" }; + + const response = await request(app) + .put(`/api/movies/1`) + .send(movieWithMissingProps); + + expect(response.status).toEqual(500); + }); + + it("should return no movie", async () => { + const newMovie = { + title: "Avatar", + director: "James Cameron", + year: "2009", + color: "1", + duration: 162, + }; + + const response = await request(app).put("/api/movies/0").send(newMovie); + + expect(response.status).toEqual(404); + }); +}); \ No newline at end of file diff --git a/tests/users.test.js b/tests/users.test.js index b973d2693..3dfdf1174 100644 --- a/tests/users.test.js +++ b/tests/users.test.js @@ -82,3 +82,82 @@ describe("POST /api/users", () => { expect(response.status).toEqual(500); }); }); + +// PUT +describe("PUT /api/users/:id", () => { + it("should edit user", async () => { + const newUser = { + firstname: "Test_User", + lastname: "Test", + email: `${crypto.randomUUID()}@wild.co`, + city: "Paris", + language: "French", + }; + + const [result] = await database.query( + "INSERT INTO users(firstname, lastname, email, city, language) VALUES (?, ?, ?, ?, ?)", + [newUser.firstname, newUser.lastname, newUser.email, newUser.city, newUser.language] + ); + + const id = result.insertId; + + const updatedUser = { + firstname: "Marie", + lastname: "Test", + email: `${crypto.randomUUID()}@wild.co`, + city: "Lyon", + language: "French", + }; + + const response = await request(app) + .put(`/api/users/${id}`) + .send(updatedUser); + + expect(response.status).toEqual(204); + + const [users] = await database.query("SELECT * FROM users WHERE id=?", id); + + const [userInDatabase] = users; + + expect(userInDatabase).toHaveProperty("id"); + + expect(userInDatabase).toHaveProperty("firstname"); + expect(userInDatabase.firstname).toStrictEqual(updatedUser.firstname); + + expect(userInDatabase).toHaveProperty("lastname"); + expect(userInDatabase.lastname).toStrictEqual(updatedUser.lastname); + + expect(userInDatabase).toHaveProperty("email"); + expect(userInDatabase.email).toStrictEqual(updatedUser.email); + + expect(userInDatabase).toHaveProperty("city"); + expect(userInDatabase.city).toStrictEqual(updatedUser.city); + + expect(userInDatabase).toHaveProperty("language"); + expect(userInDatabase.language).toStrictEqual(updatedUser.language); + }); + + it("should return an error", async () => { + const userWithMissingProps = { firstname: "Harry Potter" }; + + const response = await request(app) + .put(`/api/users/1`) + .send(userWithMissingProps); + + expect(response.status).toEqual(500); + }); + + it("should return no movie", async () => { + const newUser = { + firstname: "Avatar", + lastname: "James Cameron", + email: "2009", + city: "1", + language: 162, + }; + + const response = await request(app).put("/api/users/0").send(newUser); + + expect(response.status).toEqual(404); + }); +}); \ No newline at end of file From 3475948443186b2e2a9d987cce0b6d371b4b027e Mon Sep 17 00:00:00 2001 From: caidam Date: Wed, 17 Jan 2024 14:48:15 +0100 Subject: [PATCH 07/11] added input validation with joi --- package-lock.json | 87 ++++++++++++++++++++++++++++++++ package.json | 1 + src/app.js | 16 ++++-- src/middlewares/validateMovie.js | 56 ++++++++++++++++++++ src/middlewares/validateUser.js | 26 ++++++++++ tests/movies.test.js | 6 +-- tests/users.test.js | 12 ++--- 7 files changed, 191 insertions(+), 13 deletions(-) create mode 100644 src/middlewares/validateMovie.js create mode 100644 src/middlewares/validateUser.js diff --git a/package-lock.json b/package-lock.json index c8f0176ba..9452a01eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "dotenv": "^16.3.1", "express": "^4.18.2", + "joi": "^17.11.1", "mysql2": "^3.7.0" }, "devDependencies": { @@ -700,6 +701,19 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1051,6 +1065,24 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@sideway/address": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", + "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -3255,6 +3287,18 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/joi": { + "version": "17.11.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.1.tgz", + "integrity": "sha512-671acnrx+w96PCcQOzvm0VYQVwNL2PVgZmDRaFuSsx8sIUmGzYElPw5lU8F3Cr0jOuPs1oM56p7W2a1cdDOwcw==", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.4", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5290,6 +5334,19 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -5568,6 +5625,24 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@sideway/address": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", + "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -7241,6 +7316,18 @@ } } }, + "joi": { + "version": "17.11.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.1.tgz", + "integrity": "sha512-671acnrx+w96PCcQOzvm0VYQVwNL2PVgZmDRaFuSsx8sIUmGzYElPw5lU8F3Cr0jOuPs1oM56p7W2a1cdDOwcw==", + "requires": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.4", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index e39cc52e5..14284e5d4 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "dotenv": "^16.3.1", "express": "^4.18.2", + "joi": "^17.11.1", "mysql2": "^3.7.0" }, "devDependencies": { diff --git a/src/app.js b/src/app.js index 078c5874d..f88d296af 100644 --- a/src/app.js +++ b/src/app.js @@ -2,6 +2,9 @@ const express = require("express"); const app = express(); +const validateMovie = require("./middlewares/validateMovie"); +const validateUser = require("./middlewares/validateUser") + app.use(express.json()); const movieControllers = require("./controllers/movieControllers"); @@ -11,17 +14,22 @@ const userControllers = require("./controllers/userControllers"); app.get("/api/movies", movieControllers.getMovies); app.get("/api/movies/:id", movieControllers.getMovieById); -app.post("/api/movies", movieControllers.postMovie); +// app.post("/api/movies", movieControllers.postMovie); +app.post("/api/movies", validateMovie, movieControllers.postMovie); + +// app.put("/api/movies/:id", movieControllers.updateMovie); +app.put("/api/movies/:id", validateMovie, movieControllers.updateMovie); -app.put("/api/movies/:id", movieControllers.updateMovie); // userControllers routes app.get("/api/users", userControllers.getUsers); app.get("/api/users/:id", userControllers.getUserById); -app.post("/api/users", userControllers.postUser); +// app.post("/api/users", userControllers.postUser); +app.post("/api/users", validateUser, userControllers.postUser); -app.put("/api/users/:id", userControllers.updateUser); +// app.put("/api/users/:id", userControllers.updateUser); +app.put("/api/users/:id", validateUser, userControllers.updateUser); diff --git a/src/middlewares/validateMovie.js b/src/middlewares/validateMovie.js new file mode 100644 index 000000000..ed905ecab --- /dev/null +++ b/src/middlewares/validateMovie.js @@ -0,0 +1,56 @@ +const Joi = require("joi"); + +const movieSchema = Joi.object({ + title: Joi.string().max(255).required(), + director: Joi.string().max(255).required(), + year: Joi.string().max(255).required(), + color: Joi.string().max(255).required(), + duration: Joi.number().integer().required(), +}); + +const validateMovie = (req, res, next) => { + const { title, director, year, color, duration } = req.body; + + const { error } = movieSchema.validate( + { title, director, year, color, duration }, + { abortEarly: false } + ); + + if (error) { + res.status(422).json({ validationErros: error.details }); + } else { + next(); + } +}; + + +// const validateMovie = (req, res, next) => { +// const { title, director, year, color, duration } = req.body; +// const errors = []; + +// if (title == null) { +// errors.push({ field: "title", message: "This field is required" }); +// } else if (title.length >= 255) { +// errors.push({field: "title", message: "Should contain less than 255 characters"}); +// } +// if (director == null) { +// errors.push({ field: "director", message: "This field is required" }); +// } +// if (year == null) { +// errors.push({ field: "year", message: "This field is required" }); +// } +// if (color == null) { +// errors.push({ field: "color", message: "This field is required" }); +// } +// if (duration == null) { +// errors.push({ field: "duration", message: "This field is required" }); +// } + +// if (errors.length) { +// res.status(422).json({ validationErrors: errors }); +// } else { +// next(); +// } +// }; + +module.exports = validateMovie \ No newline at end of file diff --git a/src/middlewares/validateUser.js b/src/middlewares/validateUser.js new file mode 100644 index 000000000..a2b95c56c --- /dev/null +++ b/src/middlewares/validateUser.js @@ -0,0 +1,26 @@ +const Joi = require("joi"); + +const userSchema = Joi.object({ + email: Joi.string().email().max(255).required(), + firstname: Joi.string().max(255).required(), + lastname: Joi.string().max(255).required(), + city: Joi.string().max(255).required(), + language: Joi.string().max(255).required(), +}); + +const validateUser = (req, res, next) => { + const { firstname, lastname, email, city, language } = req.body; + + const { error } = userSchema.validate( + { firstname, lastname, email, city, language }, + { abortEarly: false } + ); + + if (error) { + res.status(422).json({ validationErrors: error.details }); + } else { + next(); + } +}; + +module.exports = validateUser \ No newline at end of file diff --git a/tests/movies.test.js b/tests/movies.test.js index 2dd74a10a..70611240e 100644 --- a/tests/movies.test.js +++ b/tests/movies.test.js @@ -82,7 +82,7 @@ describe("POST /api/movies", () => { .post("/api/movies") .send(movieWithMissingProps); - expect(response.status).toEqual(500); + expect(response.status).toEqual(422); }); }); @@ -145,10 +145,10 @@ describe("PUT /api/movies/:id", () => { const movieWithMissingProps = { title: "Harry Potter" }; const response = await request(app) - .put(`/api/movies/1`) + .put('/api/movies/1') .send(movieWithMissingProps); - expect(response.status).toEqual(500); + expect(response.status).toEqual(422); }); it("should return no movie", async () => { diff --git a/tests/users.test.js b/tests/users.test.js index 3dfdf1174..ed36fd252 100644 --- a/tests/users.test.js +++ b/tests/users.test.js @@ -79,7 +79,7 @@ describe("POST /api/users", () => { .post("/api/users") .send(userWithMissingProps); - expect(response.status).toEqual(500); + expect(response.status).toEqual(422); }); }); @@ -144,16 +144,16 @@ describe("PUT /api/users/:id", () => { .put(`/api/users/1`) .send(userWithMissingProps); - expect(response.status).toEqual(500); + expect(response.status).toEqual(422); }); - it("should return no movie", async () => { + it("should return no user", async () => { const newUser = { firstname: "Avatar", lastname: "James Cameron", - email: "2009", - city: "1", - language: 162, + email: `${crypto.randomUUID()}@wild.co`, + city: "Paris", + language: "French", }; const response = await request(app).put("/api/users/0").send(newUser); From 534d62470590b66e129d642aac909a7e634afb8d Mon Sep 17 00:00:00 2001 From: caidam Date: Wed, 17 Jan 2024 14:50:23 +0100 Subject: [PATCH 08/11] cleaned code --- src/app.js | 4 ---- src/middlewares/validateMovie.js | 30 ------------------------------ 2 files changed, 34 deletions(-) diff --git a/src/app.js b/src/app.js index f88d296af..c0ffb777b 100644 --- a/src/app.js +++ b/src/app.js @@ -14,10 +14,8 @@ const userControllers = require("./controllers/userControllers"); app.get("/api/movies", movieControllers.getMovies); app.get("/api/movies/:id", movieControllers.getMovieById); -// app.post("/api/movies", movieControllers.postMovie); app.post("/api/movies", validateMovie, movieControllers.postMovie); -// app.put("/api/movies/:id", movieControllers.updateMovie); app.put("/api/movies/:id", validateMovie, movieControllers.updateMovie); @@ -25,10 +23,8 @@ app.put("/api/movies/:id", validateMovie, movieControllers.updateMovie); app.get("/api/users", userControllers.getUsers); app.get("/api/users/:id", userControllers.getUserById); -// app.post("/api/users", userControllers.postUser); app.post("/api/users", validateUser, userControllers.postUser); -// app.put("/api/users/:id", userControllers.updateUser); app.put("/api/users/:id", validateUser, userControllers.updateUser); diff --git a/src/middlewares/validateMovie.js b/src/middlewares/validateMovie.js index ed905ecab..9e7c38cfb 100644 --- a/src/middlewares/validateMovie.js +++ b/src/middlewares/validateMovie.js @@ -23,34 +23,4 @@ const validateMovie = (req, res, next) => { } }; - -// const validateMovie = (req, res, next) => { -// const { title, director, year, color, duration } = req.body; -// const errors = []; - -// if (title == null) { -// errors.push({ field: "title", message: "This field is required" }); -// } else if (title.length >= 255) { -// errors.push({field: "title", message: "Should contain less than 255 characters"}); -// } -// if (director == null) { -// errors.push({ field: "director", message: "This field is required" }); -// } -// if (year == null) { -// errors.push({ field: "year", message: "This field is required" }); -// } -// if (color == null) { -// errors.push({ field: "color", message: "This field is required" }); -// } -// if (duration == null) { -// errors.push({ field: "duration", message: "This field is required" }); -// } - -// if (errors.length) { -// res.status(422).json({ validationErrors: errors }); -// } else { -// next(); -// } -// }; - module.exports = validateMovie \ No newline at end of file From f0c65393d55ef924f0c94be5ec26e0e3708d2a73 Mon Sep 17 00:00:00 2001 From: caidam Date: Wed, 17 Jan 2024 15:13:09 +0100 Subject: [PATCH 09/11] added delete method for users and movies --- src/app.js | 2 ++ src/controllers/movieControllers.js | 22 ++++++++++++++++++++++ src/controllers/userControllers.js | 20 ++++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/src/app.js b/src/app.js index c0ffb777b..f8f02b750 100644 --- a/src/app.js +++ b/src/app.js @@ -18,6 +18,7 @@ app.post("/api/movies", validateMovie, movieControllers.postMovie); app.put("/api/movies/:id", validateMovie, movieControllers.updateMovie); +app.delete("/api/movies/:id", movieControllers.deleteMovie); // userControllers routes app.get("/api/users", userControllers.getUsers); @@ -27,6 +28,7 @@ app.post("/api/users", validateUser, userControllers.postUser); app.put("/api/users/:id", validateUser, userControllers.updateUser); +app.delete("/api/users/:id", userControllers.deleteUser); module.exports = app; diff --git a/src/controllers/movieControllers.js b/src/controllers/movieControllers.js index 9e0641ede..8c2650f54 100644 --- a/src/controllers/movieControllers.js +++ b/src/controllers/movieControllers.js @@ -73,9 +73,31 @@ const updateMovie = (req, res) => { }); }; +// DELETE +const deleteMovie = (req, res) => { + const id = parseInt(req.params.id); + + database + .query("delete from movies where id = ?", [id]) + .then(([result]) => { + if (result.affectedRows === 0) { + res.sendStatus(404); + } else { + res.sendStatus(204); + } + }) + .catch((err) => { + console.error(err); + res.sendStatus(500); + }); +}; + + +// Exports module.exports = { getMovies, getMovieById, postMovie, updateMovie, + deleteMovie, }; diff --git a/src/controllers/userControllers.js b/src/controllers/userControllers.js index 2183a34e4..2f31c9d83 100644 --- a/src/controllers/userControllers.js +++ b/src/controllers/userControllers.js @@ -70,9 +70,29 @@ const updateUser = (req, res) => { }); }; +// DELETE +const deleteUser = (req, res) => { + const id = parseInt(req.params.id); + + database + .query("delete from users where id = ?", [id]) + .then(([result]) => { + if (result.affectedRows === 0) { + res.sendStatus(404); + } else { + res.sendStatus(204); + } + }) + .catch((err) => { + console.error(err); + res.sendStatus(500); + }); +}; + module.exports = { getUsers, getUserById, postUser, updateUser, + deleteUser, }; From d8cb5f53ca7e690da2d94074a0cd940c1c251f24 Mon Sep 17 00:00:00 2001 From: caidam Date: Wed, 17 Jan 2024 15:39:59 +0100 Subject: [PATCH 10/11] test delete routes + auto delete test data --- tests/movies.test.js | 58 +++++++++++++++++++++++++++++++++++++++++ tests/users.test.js | 61 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/tests/movies.test.js b/tests/movies.test.js index 70611240e..3921f04bb 100644 --- a/tests/movies.test.js +++ b/tests/movies.test.js @@ -35,6 +35,15 @@ describe("GET /api/movies/:id", () => { // POST describe("POST /api/movies", () => { + let createdMovieId; + + afterEach(async () => { + if (createdMovieId) { + await database.query("DELETE FROM movies WHERE id=?", createdMovieId); + createdMovieId = null; + } + }); + it("should return created movie", async () => { const newMovie = { title: "Star Wars", @@ -46,6 +55,8 @@ describe("POST /api/movies", () => { const response = await request(app).post("/api/movies").send(newMovie); + createdMovieId = response.body.id; + expect(response.status).toEqual(201); expect(response.body).toHaveProperty("id"); expect(typeof response.body.id).toBe("number"); @@ -89,6 +100,15 @@ describe("POST /api/movies", () => { // PUT describe("PUT /api/movies/:id", () => { + let createdMovieId; + + afterEach(async () => { + if (createdMovieId) { + await database.query("DELETE FROM movies WHERE id=?", createdMovieId); + createdMovieId = null; + } + }); + it("should edit movie", async () => { const newMovie = { title: "Avatar", @@ -104,6 +124,7 @@ describe("PUT /api/movies/:id", () => { ); const id = result.insertId; + createdMovieId = id const updatedMovie = { title: "Wild is life", @@ -162,6 +183,43 @@ describe("PUT /api/movies/:id", () => { const response = await request(app).put("/api/movies/0").send(newMovie); + expect(response.status).toEqual(404); + }); +}); + +// DELETE +describe("DELETE /api/movies/:id", () => { + it("should delete a movie", async () => { + // First, insert a movie into the database + const newMovie = { + title: "Test Movie", + director: "Test Director", + year: "2022", + color: "1", + duration: 100, + }; + + const [result] = await database.query( + "INSERT INTO movies(title, director, year, color, duration) VALUES (?, ?, ?, ?, ?)", + [newMovie.title, newMovie.director, newMovie.year, newMovie.color, newMovie.duration] + ); + + const id = result.insertId; + + // Then, try to delete it + const response = await request(app).delete(`/api/movies/${id}`); + + expect(response.status).toEqual(204); + + // Finally, check that it was deleted + const [movies] = await database.query("SELECT * FROM movies WHERE id=?", id); + + expect(movies.length).toEqual(0); + }); + + it("should return an error if the movie does not exist", async () => { + const response = await request(app).delete("/api/movies/0"); + expect(response.status).toEqual(404); }); }); \ No newline at end of file diff --git a/tests/users.test.js b/tests/users.test.js index ed36fd252..28e19f422 100644 --- a/tests/users.test.js +++ b/tests/users.test.js @@ -5,6 +5,7 @@ const crypto = require("node:crypto") afterAll(() => database.end()); +// GET describe("GET /api/users", () => { it("should return all users", async () => { const response = await request(app).get("/api/users"); @@ -31,7 +32,17 @@ describe("GET /api/users/:id", () => { }); }); +// POST describe("POST /api/users", () => { + let createdUserId; + + afterEach(async () => { + if (createdUserId) { + await database.query("DELETE FROM users WHERE id=?", createdUserId); + createdUserId = null; + } + }); + it("should return created user", async () => { const newUser = { firstname: "Marie", @@ -43,6 +54,8 @@ describe("POST /api/users", () => { const response = await request(app).post("/api/users").send(newUser); + createdUserId = response.body.id; + expect(response.status).toEqual(201); expect(response.body).toHaveProperty("id"); expect(typeof response.body.id).toBe("number"); @@ -85,6 +98,15 @@ describe("POST /api/users", () => { // PUT describe("PUT /api/users/:id", () => { + let createdUserId; + + afterEach(async () => { + if (createdUserId) { + await database.query("DELETE FROM users WHERE id=?", createdUserId); + createdUserId = null; + } + }); + it("should edit user", async () => { const newUser = { firstname: "Test_User", @@ -100,6 +122,7 @@ describe("PUT /api/users/:id", () => { ); const id = result.insertId; + createdUserId = id const updatedUser = { firstname: "Marie", @@ -158,6 +181,44 @@ describe("PUT /api/users/:id", () => { const response = await request(app).put("/api/users/0").send(newUser); + expect(response.status).toEqual(404); + }); +}); + + +// DELETE +describe("DELETE /api/users/:id", () => { + it("should delete a user", async () => { + // First, insert a user into the database + const newUser = { + firstname: "Test User", + lastname: "Test lastname", + email: `${crypto.randomUUID()}@wild.co`, + city: "Paris", + language: "Spanish", + }; + + const [result] = await database.query( + "INSERT INTO users(firstname, lastname, email, city, language) VALUES (?, ?, ?, ?, ?)", + [newUser.firstname, newUser.lastname, newUser.email, newUser.city, newUser.language] + ); + + const id = result.insertId; + + // Then, try to delete it + const response = await request(app).delete(`/api/users/${id}`); + + expect(response.status).toEqual(204); + + // Finally, check that it was deleted + const [users] = await database.query("SELECT * FROM users WHERE id=?", id); + + expect(users.length).toEqual(0); + }); + + it("should return an error if the user does not exist", async () => { + const response = await request(app).delete("/api/users/0"); + expect(response.status).toEqual(404); }); }); \ No newline at end of file From 39d2beaf6c813db4646841fb9cafa036ce11c67d Mon Sep 17 00:00:00 2001 From: caidam Date: Wed, 17 Jan 2024 18:53:20 +0100 Subject: [PATCH 11/11] added query parameters handling --- src/controllers/movieControllers.js | 20 +++++++++++++++++++- src/controllers/userControllers.js | 23 ++++++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/controllers/movieControllers.js b/src/controllers/movieControllers.js index 8c2650f54..2375d4c29 100644 --- a/src/controllers/movieControllers.js +++ b/src/controllers/movieControllers.js @@ -2,8 +2,26 @@ const database = require("../../database"); // GET const getMovies = (req, res) => { + let sql = "select * from movies"; + const conditions = []; + const sqlValues = []; + + if (req.query.color != null) { + conditions.push("color = ?"); + sqlValues.push(req.query.color); + } + + if (req.query.max_duration != null) { + conditions.push("duration <= ?"); + sqlValues.push(req.query.max_duration); + } + + if (conditions.length > 0) { + sql += " WHERE " + conditions.join(" AND "); + } + database - .query("select * from movies") + .query(sql, sqlValues) .then(([movies]) => { res.json(movies); }) diff --git a/src/controllers/userControllers.js b/src/controllers/userControllers.js index 2f31c9d83..848194922 100644 --- a/src/controllers/userControllers.js +++ b/src/controllers/userControllers.js @@ -1,10 +1,30 @@ const database = require("../../database"); +// GET const getUsers = (req, res) => { + let sql = "select * from users"; + const conditions = []; + const sqlValues = []; + + if (req.query.language != null) { + conditions.push("language = ?"); + sqlValues.push(req.query.language); + } + + if (req.query.city != null) { + conditions.push("city = ?"); + sqlValues.push(req.query.city); + } + + if (conditions.length > 0) { + sql += " WHERE " + conditions.join(" AND "); + } + database - .query("select * from users") + .query(sql, sqlValues) .then(([users]) => { res.json(users); + // console.log(sql, sqlValues) }) .catch((err) => { console.error(err); @@ -30,6 +50,7 @@ const getUserById = (req, res) => { }); }; +// POST const postUser = (req, res) => { const { firstname, lastname, email, city, language } = req.body;