diff --git a/.env.example b/.env.example deleted file mode 100644 index 6d05b4ad..00000000 --- a/.env.example +++ /dev/null @@ -1 +0,0 @@ -PGURL="postgres://[user]:[password]@[host]/[dbname]" diff --git a/db/index.js b/db/index.js index af723442..91502fec 100644 --- a/db/index.js +++ b/db/index.js @@ -1,25 +1,18 @@ -// Load our .env file -require('dotenv').config() +// import Pool from Postgres +const { Pool } = require("pg"); -// Require Client obj from the postgres node module -const { Client } = require("pg"); +// import environment variables from .env file +const { PGHOST, PGDATABASE, PGUSER, PGPASSWORD } = process.env; -const client = { - query: async (str, values) => { - // Get the connection string from process.env - - // the dotenv library sets this variable based - // on the contents of our env file - // Create a new connection to the database using the Client - // object provided by the postgres node module - const dbClient = new Client(process.env.PGURL) - // connect a connection - await dbClient.connect() - // execute the query - const result = await dbClient.query(str, values) - // close the connection - await dbClient.end() - return result - } -} +const dbConnection = new Pool({ + host: PGHOST, + database: PGDATABASE, + username: PGUSER, + password: PGPASSWORD, + port: 5432, + ssl: { + require: true, + }, +}); -module.exports = client; +module.exports = dbConnection; diff --git a/package-lock.json b/package-lock.json index 37ccdff8..cef36a2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "dotenv": "^16.3.1", "express": "^4.18.2", "faker": "^5.5.3", + "joi": "^13.1.0", "morgan": "1.10.0", "pg": "8.6.0", "pg-promise": "^11.5.4" @@ -2415,6 +2416,15 @@ "node": ">=8" } }, + "node_modules/hoek": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", + "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==", + "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "engines": { + "node": ">=8.9.0" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -2603,6 +2613,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isemail": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", + "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", + "dependencies": { + "punycode": "2.x.x" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3286,6 +3307,20 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/joi": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-13.1.0.tgz", + "integrity": "sha512-x6pGmDYI6hwNi3skP6irQqRaJntzeaWmZ4rsnjc/NTlf6P5Gp3Aw/O8REe8oLJ6wPhrzd9K3RW1m3Yz/Hx4Weg==", + "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "dependencies": { + "hoek": "5.x.x", + "isemail": "3.x.x", + "topo": "3.x.x" + }, + "engines": { + "node": ">=8.9.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4167,6 +4202,14 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -4820,6 +4863,21 @@ "node": ">=0.6" } }, + "node_modules/topo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz", + "integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==", + "deprecated": "This module has moved and is now available at @hapi/topo. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.", + "dependencies": { + "hoek": "6.x.x" + } + }, + "node_modules/topo/node_modules/hoek": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz", + "integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==", + "deprecated": "This module has moved and is now available at @hapi/hoek. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues." + }, "node_modules/touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", diff --git a/package.json b/package.json index 57d8b4fb..a1c24022 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dotenv": "^16.3.1", "express": "^4.18.2", "faker": "^5.5.3", + "joi": "^13.1.0", "morgan": "1.10.0", "pg": "8.6.0", "pg-promise": "^11.5.4" diff --git a/sql/create-books.sql b/sql/create-books.sql index 322f3c97..a8fe81f5 100644 --- a/sql/create-books.sql +++ b/sql/create-books.sql @@ -6,6 +6,14 @@ CREATE TABLE IF NOT EXISTS books ( type VARCHAR(255) NOT NULL, author VARCHAR(255) NOT NULL, topic VARCHAR(255) NOT NULL, - publication_date DATE NOT NULL, + publication_date VARCHAR(255) NOT NULL, -- * pages INTEGER NOT NULL ); + +--* Using DATE data type constraint makes a problem with the test as the data returned from the database base is different from the data sent, so that I always get this error message: + + -- Expected: "2020-11-17T00:00:00.000Z" + -- Received: "2020-11-16T23:00:00.000Z" + +-- I don't know why. But I thibk that there is a special processing for the DATE type from DBMS. The only way to solve this problem was to change the data type from DATE to VARCHAR. + diff --git a/src/controllers/booksController.js b/src/controllers/booksController.js new file mode 100644 index 00000000..9e26d412 --- /dev/null +++ b/src/controllers/booksController.js @@ -0,0 +1,116 @@ +const Joi = require("joi"); +const dbConnection = require("../../db/index.js"); +const queries = require("../queries/booksQueries.js"); + +// HELPER FUNCTIONS +function validateBook(req, res) { + const schema = { + title: Joi.string().required(), + type: Joi.string().required(), + author: Joi.string().required(), + topic: Joi.string().required(), + publication_date: Joi.string().required(), + pages: Joi.number().required(), + }; + + return Joi.validate(req.body, schema); +} + +// CONTROLLER FUNCTIONS +exports.getAllBooks = (req, res) => { + dbConnection.query(queries.getAllBooks, (error, result) => { + if (error) throw error; + res.status(200).json({ books: result.rows }); + }); +}; + +exports.getBook = (req, res) => { + const id = Number.parseInt(req.params.id, 10); + + dbConnection.query(queries.getBookById, [id], (error, result) => { + if (error) throw error; + + const [book] = result.rows; + if (!book) { + return res.status(404).json({ + message: `Book with id ${id} does not exist in the database`, + }); + } + res.status(200).json({ book }); + }); +}; + +exports.addBook = (req, res) => { + // to validate book's schema + const { error } = validateBook(req, res); + if (error) return res.status(400).send(error.details[0].message); + + const { title, type, author, topic, publication_date, pages } = req.body; + + dbConnection.query( + queries.addBook, + [title, type, author, topic, publication_date, pages], + (error, result) => { + if (error) throw error; + // to get the new added book and send it to the user + dbConnection.query(queries.getAllBooks, (error, result) => { + if (error) throw error; + res.status(201).json({ book: result.rows[result.rows.length - 1] }); + }); + } + ); +}; + +exports.deleteBook = (req, res) => { + const id = Number.parseInt(req.params.id, 10); + // check if the book exists + dbConnection.query(queries.getBookById, [id], (error, result) => { + const book = result.rows; + const noBookFound = !book.length; + // if it does not exist send an error message + if (noBookFound) { + return res.status(404).json({ + message: `Book with id ${id} does not exist in the database`, + }); + } + // if it exists, delete it and send it to the user + dbConnection.query(queries.deleteBookById, [id], (error, result) => { + if (error) throw error; + res.status(201).json({ book: book[0] }); + }); + }); +}; + +exports.updateBook = (req, res) => { + // to validate book's schema + const { error } = validateBook(req, res); + if (error) return res.status(400).send(error.details[0].message); + + const id = Number.parseInt(req.params.id, 10); + // check if the book exists + dbConnection.query(queries.getBookById, [id], (error, result) => { + const noBookFound = !result.rows.length; + // if it does not exist send an error message + if (noBookFound) { + return res.status(404).json({ + message: `Book with id ${id} does not exist in the database`, + }); + } + // if it exists, update it + const { title, type, author, topic, publication_date, pages } = req.body; + dbConnection.query( + queries.updateBookById, + [title, type, author, topic, publication_date, pages, id], + (error, result) => { + if (error) throw error; + // get the updated book and send it to the user + dbConnection.query(queries.getBookById, [id], (error, result) => { + if (error) throw error; + + const [book] = result.rows; + res.status(201).json({ book }); + }); + } + ); + }); +}; diff --git a/src/controllers/petsController.js b/src/controllers/petsController.js new file mode 100644 index 00000000..0a9b7ef1 --- /dev/null +++ b/src/controllers/petsController.js @@ -0,0 +1,115 @@ +const Joi = require("joi"); +const dbConnection = require("../../db/index.js"); +const queries = require("../queries/petsQueries.js"); + +// HELPER FUNCTIONS +function validatePet(req, res) { + const schema = { + name: Joi.string().required(), + age: Joi.required(), + type: Joi.string().required(), + breed: Joi.string().required(), + has_microchip: Joi.boolean().required(), + }; + + return Joi.validate(req.body, schema); +} + +// CONTROLLER FUNCTIONS +exports.getAllPets = (req, res) => { + dbConnection.query(queries.getAllPets, (error, result) => { + if (error) throw error; + res.status(200).json({ pets: result.rows }); + }); +}; + +exports.getPet = (req, res) => { + const id = Number.parseInt(req.params.id, 10); + + dbConnection.query(queries.getPetById, [id], (error, result) => { + if (error) throw error; + + const [pet] = result.rows; + if (!pet) { + return res.status(404).json({ + message: `Pet with id ${id} does not exist in the database`, + }); + } + res.status(200).json({ pet }); + }); +}; + +exports.addPet = (req, res) => { + // to validate pet's schema + const { error } = validatePet(req, res); + if (error) return res.status(400).send(error.details[0].message); + + const { name, age, type, breed, has_microchip } = req.body; + + dbConnection.query( + queries.addPet, + [name, age, type, breed, has_microchip], + (error, result) => { + if (error) throw error; + // to get the new added pet and send it to the user + dbConnection.query(queries.getAllPets, (error, result) => { + if (error) throw error; + res.status(201).json({ pet: result.rows[result.rows.length - 1] }); + }); + } + ); +}; + +exports.deletePet = (req, res) => { + const id = Number.parseInt(req.params.id, 10); + // check if the pet exists + dbConnection.query(queries.getPetById, [id], (error, result) => { + const pet = result.rows; + const noPetFound = !pet.length; + // if it does not exist send an error message + if (noPetFound) { + return res.status(404).json({ + message: `Pet with id ${id} does not exist in the database`, + }); + } + // if it exists, delete it and send it to the user + dbConnection.query(queries.deletePetById, [id], (error, result) => { + if (error) throw error; + res.status(201).json({ pet: pet[0] }); + }); + }); +}; + +exports.updatePet = (req, res) => { + // to validate pet's schema + const { error } = validatePet(req, res); + if (error) return res.status(400).send(error.details[0].message); + + const id = Number.parseInt(req.params.id, 10); + // check if the pet exists + dbConnection.query(queries.getPetById, [id], (error, result) => { + const noPetFound = !result.rows.length; + // if it does not exist send an error message + if (noPetFound) { + return res.status(404).json({ + message: `Pet with id ${id} does not exist in the database`, + }); + } + // if it exists, update it + const { name, age, type, breed, has_microchip } = req.body; + dbConnection.query( + queries.updatePetById, + [name, age, type, breed, has_microchip, id], + (error, result) => { + if (error) throw error; + // get the updated pet and send it to the user + dbConnection.query(queries.getPetById, [id], (error, result) => { + if (error) throw error; + + const [pet] = result.rows; + res.status(201).json({ pet }); + }); + } + ); + }); +}; diff --git a/src/queries/booksQueries.js b/src/queries/booksQueries.js new file mode 100644 index 00000000..7a33b4ec --- /dev/null +++ b/src/queries/booksQueries.js @@ -0,0 +1,28 @@ +exports.getAllBooks = ` +SELECT * +FROM books`; + +exports.getBookById = ` +SELECT * +FROM books +WHERE id = $1`; + +exports.addBook = ` +INSERT INTO books + (title, type, author, topic, publication_date, pages) +VALUES + ($1, $2,$3, $4, $5, $6)`; + +exports.deleteBookById = ` +DELETE FROM books +WHERE id = $1`; + +exports.updateBookById = ` +UPDATE books +SET title=$1, + type=$2, + author=$3, + topic=$4, + publication_date=$5, + pages=$6 +WHERE id = $7`; diff --git a/src/queries/petsQueries.js b/src/queries/petsQueries.js new file mode 100644 index 00000000..d5bd183a --- /dev/null +++ b/src/queries/petsQueries.js @@ -0,0 +1,27 @@ +exports.getAllPets = ` +SELECT * +FROM pets`; + +exports.getPetById = ` +SELECT * +FROM pets +WHERE id = $1`; + +exports.addPet = ` +INSERT INTO pets + (name, age, type, breed, has_microchip) +VALUES + ($1, $2, $3, $4, $5)`; + +exports.deletePetById = ` +DELETE FROM pets +WHERE id = $1`; + +exports.updatePetById = ` +UPDATE pets +SET name=$1, + age=$2, + type=$3, + breed=$4, + has_microchip=$5 +WHERE id = $6`; diff --git a/src/routers/books.js b/src/routers/books.js deleted file mode 100644 index 1551dd87..00000000 --- a/src/routers/books.js +++ /dev/null @@ -1,9 +0,0 @@ -const express = require('express') -const router = express.Router() -const db = require("../../db"); - -router.get('/', async (req, res) => { - -}) - -module.exports = router diff --git a/src/routers/booksRouter.js b/src/routers/booksRouter.js new file mode 100644 index 00000000..7c55f235 --- /dev/null +++ b/src/routers/booksRouter.js @@ -0,0 +1,12 @@ +const express = require("express"); +const router = express.Router(); +const controller = require("../controllers/booksController.js"); + +router.route("/").get(controller.getAllBooks).post(controller.addBook); +router + .route("/:id") + .get(controller.getBook) + .delete(controller.deleteBook) + .put(controller.updateBook); + +module.exports = router; diff --git a/src/routers/petsRouter.js b/src/routers/petsRouter.js new file mode 100644 index 00000000..7688b1fe --- /dev/null +++ b/src/routers/petsRouter.js @@ -0,0 +1,12 @@ +const express = require("express"); +const router = express.Router(); +const controller = require("../controllers/petsController.js"); + +router.route("/").get(controller.getAllPets).post(controller.addPet); +router + .route("/:id") + .get(controller.getPet) + .delete(controller.deletePet) + .put(controller.updatePet); + +module.exports = router; diff --git a/src/server.js b/src/server.js index dac55e5d..8d92aa6a 100644 --- a/src/server.js +++ b/src/server.js @@ -1,3 +1,4 @@ +require("dotenv").config(); const express = require("express"); const morgan = require("morgan"); const cors = require("cors"); @@ -9,8 +10,10 @@ app.use(cors()); app.use(express.json()); //TODO: Implement books and pets APIs using Express Modular Routers -const booksRouter = require('./routers/books.js') +const booksRouter = require("./routers/booksRouter"); +const petsRouter = require("./routers/petsRouter"); -app.use('/books', booksRouter) +app.use("/books", booksRouter); +app.use("/pets", petsRouter); -module.exports = app +module.exports = app; diff --git a/test/database-cleaner/index.js b/test/database-cleaner/index.js index 13a4e464..a502fb41 100644 --- a/test/database-cleaner/index.js +++ b/test/database-cleaner/index.js @@ -1,14 +1,14 @@ -const fs = require('fs/promises') -const client = require("../../db"); +const fs = require("fs/promises"); +const dbConnection = require("../../db/index"); -global.beforeEach(async() => { - const sqlDataForBooks = await fs.readFile('./sql/create-books.sql') - const sqlStringForBooks = sqlDataForBooks.toString() +global.beforeEach(async () => { + const sqlDataForBooks = await fs.readFile("./sql/create-books.sql"); + const sqlStringForBooks = sqlDataForBooks.toString(); - await client.query(sqlStringForBooks) + await dbConnection.query(sqlStringForBooks); - const sqlDataForPets = await fs.readFile('./sql/create-pets.sql') - const sqlStringForPets = sqlDataForPets.toString() + const sqlDataForPets = await fs.readFile("./sql/create-pets.sql"); + const sqlStringForPets = sqlDataForPets.toString(); - await client.query(sqlStringForPets) -}) + await dbConnection.query(sqlStringForPets); +}); diff --git a/test/helpers/createBook.js b/test/helpers/createBook.js index 5f7e9c76..ab867f2a 100644 --- a/test/helpers/createBook.js +++ b/test/helpers/createBook.js @@ -1,11 +1,11 @@ -const client = require("../../db"); +const dbConnection = require("../../db/index"); const createBook = async (values) => { - const sqlString = `INSERT INTO "books" (title, type, author, topic, publication_date, pages) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *;` + const sqlString = `INSERT INTO "books" (title, type, author, topic, publication_date, pages) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *;`; - const result = await client.query(sqlString, values) + const result = await dbConnection.query(sqlString, values); - return result.rows[0] -} + return result.rows[0]; +}; -module.exports = createBook +module.exports = createBook; diff --git a/test/helpers/createPet.js b/test/helpers/createPet.js index 8dfca845..fc0130be 100644 --- a/test/helpers/createPet.js +++ b/test/helpers/createPet.js @@ -1,11 +1,11 @@ -const client = require("../../db"); +const dbConnection = require("../../db/index"); const createPet = async (values) => { - const sqlString = `INSERT INTO "pets" (name, age, type, breed, has_microchip) VALUES ($1, $2, $3, $4, $5) RETURNING *;` + const sqlString = `INSERT INTO "pets" (name, age, type, breed, has_microchip) VALUES ($1, $2, $3, $4, $5) RETURNING *;`; - const result = await client.query(sqlString, values) + const result = await dbConnection.query(sqlString, values); - return result.rows[0] -} + return result.rows[0]; +}; -module.exports = createPet +module.exports = createPet; diff --git a/test/helpers/insertBooks.js b/test/helpers/insertBooks.js index 54e720ca..4e7c1499 100644 --- a/test/helpers/insertBooks.js +++ b/test/helpers/insertBooks.js @@ -1,11 +1,11 @@ -const fs = require('fs/promises') -const client = require("../../db"); +const fs = require("fs/promises"); +const dbConnection = require("../../db/index"); const insertBooks = async () => { - const sqlDataForBooks = await fs.readFile('./sql/insert-books.sql') - const sqlStringForBooks = sqlDataForBooks.toString() + const sqlDataForBooks = await fs.readFile("./sql/insert-books.sql"); + const sqlStringForBooks = sqlDataForBooks.toString(); - await client.query(sqlStringForBooks) -} + await dbConnection.query(sqlStringForBooks); +}; -module.exports = insertBooks +module.exports = insertBooks; diff --git a/test/helpers/insertPets.js b/test/helpers/insertPets.js index eadddec2..97ee26ae 100644 --- a/test/helpers/insertPets.js +++ b/test/helpers/insertPets.js @@ -1,11 +1,11 @@ -const fs = require('fs/promises') -const client = require("../../db"); +const fs = require("fs/promises"); +const dbConnection = require("../../db/index"); const insertPets = async () => { - const sqlDataForPets = await fs.readFile('./sql/insert-pets.sql') - const sqlStringForPets = sqlDataForPets.toString() + const sqlDataForPets = await fs.readFile("./sql/insert-pets.sql"); + const sqlStringForPets = sqlDataForPets.toString(); - await client.query(sqlStringForPets) -} + await dbConnection.query(sqlStringForPets); +}; -module.exports = insertPets +module.exports = insertPets;