diff --git a/db/index.js b/db/index.js deleted file mode 100644 index af723442..00000000 --- a/db/index.js +++ /dev/null @@ -1,25 +0,0 @@ -// Load our .env file -require('dotenv').config() - -// Require Client obj from the postgres node module -const { Client } = require("pg"); - -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 - } -} - -module.exports = client; diff --git a/package-lock.json b/package-lock.json index 37ccdff8..e75c4691 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "body-parser": "^1.20.2", "cors": "^2.8.5", "dotenv": "^16.3.1", - "express": "^4.18.2", + "express": "^4.19.2", + "express-async-errors": "^3.1.1", "faker": "^5.5.3", "morgan": "1.10.0", "pg": "8.6.0", @@ -1740,9 +1741,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -2024,16 +2025,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -2064,41 +2065,12 @@ "node": ">= 0.10.0" } }, - "node_modules/express/node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/express/node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" + "node_modules/express-async-errors": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/express-async-errors/-/express-async-errors-3.1.1.tgz", + "integrity": "sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==", + "peerDependencies": { + "express": "^4.16.2" } }, "node_modules/faker": { diff --git a/package.json b/package.json index 57d8b4fb..6e5d33de 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "body-parser": "^1.20.2", "cors": "^2.8.5", "dotenv": "^16.3.1", - "express": "^4.18.2", + "express": "^4.19.2", + "express-async-errors": "^3.1.1", "faker": "^5.5.3", "morgan": "1.10.0", "pg": "8.6.0", diff --git a/src/dal/booksRepo.js b/src/dal/booksRepo.js new file mode 100644 index 00000000..077320b2 --- /dev/null +++ b/src/dal/booksRepo.js @@ -0,0 +1,114 @@ +const dbConnection = require('../utils/dbConnection.js') +const MissingFieldsError = require('../errors/missingFieldsError.js') +const NotFoundError = require('../errors/notFoundError.js') +const NotUniqueError = require('../errors/notUniqueError.js') + +const getAllBooks = async (type, topic, author, pages, perpages) => { + let page = 1 + let perpage = 20 + let offset = 0 + + if (pages) { + page = pages + offset = (page - 1) * perpage + } + + if (perpages > 9 && perpages < 51) { + perpage = perpages + offset = (page - 1) * perpage + } + + if (type) { + const sqlQuery = `select * from books where type = $1 limit $2 offset $3` + const result = await dbConnection.query(sqlQuery, [type, perpage, offset]) + + return result.rows + } + + if (topic) { + const sqlQuery = `select * from books where topic = $1 limit $2 offset $3` + const result = await dbConnection.query(sqlQuery, [topic, perpage, offset]) + + return result.rows + } + + if (author) { + const sqlQuery = `select * from books where author = $1 limit $2 offset $3` + const result = await dbConnection.query(sqlQuery, [author, perpage, offset]) + + return result.rows + } + + const sqlQuery = `select * from books limit $1 offset $2` + const result = await dbConnection.query(sqlQuery, [perpage, offset]) + + return result.rows +} + +const createBook = async (book) => { + if (!verifyFields(book)) { + throw new MissingFieldsError('Missing fields in request body') + } + + const sqlQuery = `insert into books (title, type, author, topic, publication_date, pages) values ($1, $2, $3, $4, $5, $6) returning *` + const result = await dbConnection.query(sqlQuery, [book.title, book.type, book.author, book.topic, book.publication_date, book.pages]) + + return result.rows[0] +} + +const getBookById = async (id) => { + const sqlQuery = `select * from books where id = $1` + const result = await dbConnection.query(sqlQuery, [id]) + + if(result.rows.length === 0) { + throw new NotFoundError('A book with the provided ID does not exist') + } + + return result.rows[0] +} + +const updateBook = async (id, bookInfo) => { + if (!verifyFields(bookInfo)) { + throw new MissingFieldsError('Missing fields in request body') + } + + const sqlQuery = `update books set title = $1, type = $2, author = $3, topic = $4, publication_date = $5, pages = $6 where id = $7 returning *` + const result = await dbConnection.query(sqlQuery, [bookInfo.title, bookInfo.type, bookInfo.author, bookInfo.topic, bookInfo.publication_date, bookInfo.pages, id]) + + if(result.rows.length === 0) { + throw new NotFoundError('A book with the provided ID does not exist') + } + + return result.rows[0] +} + +const deleteBookById = async (id) => { + const sqlQuery = `delete from books where id = $1 returning *` + const result = await dbConnection.query(sqlQuery, [id]) + + if(result.rows.length === 0) { + throw new NotFoundError('A book with the provided ID does not exist') + } + + return result.rows[0] +} + +function verifyFields(object) { + const neededProperties = ['title', 'type', 'author', 'topic', 'publication_date', 'pages'] + + for (const item of neededProperties) { + if (object[item] === undefined) { + return false + } + } + + return true +} + +module.exports = { + getAllBooks, + createBook, + getBookById, + updateBook, + deleteBookById +} \ No newline at end of file diff --git a/src/dal/petsRepo.js b/src/dal/petsRepo.js new file mode 100644 index 00000000..45a3c5f7 --- /dev/null +++ b/src/dal/petsRepo.js @@ -0,0 +1,111 @@ +const MissingFieldsError = require('../errors/missingFieldsError.js') +const NotFoundError = require('../errors/notFoundError.js') +const dbConnection = require('../utils/dbConnection.js') + +const getAllPets = async (type, pages, perpages) => { + let page = 1 + let perpage = 20 + let offset = 0 + + if (pages) { + page = pages + offset = (page - 1) * perpage + } + + if (perpages > 9 && perpages < 51) { + perpage = perpages + offset = (page - 1) * perpage + } + + if (type) { + const sqlQuery = `select * from pets where type = $1 limit $2 offset $3` + const result = await dbConnection.query(sqlQuery, [type, perpage, offset]) + + return result.rows + } + + const sqlQuery = 'select * from pets limit $1 offset $2' + const result = await dbConnection.query(sqlQuery, [perpage, offset]) + + return result.rows +} + +const createPet = async (pet) => { + if (!verifyFields(pet)) { + throw new MissingFieldsError('Missing fields in request body') + } + + const sqlQuery = `insert into pets (name, age, type, breed, has_microchip) values ($1, $2, $3, $4, $5) returning *` + const result = await dbConnection.query(sqlQuery, [pet.name, pet.age, pet.type, pet.breed, pet.has_microchip]) + + return result.rows[0] +} + +const getPetById = async (id) => { + const sqlQuery = `select * from pets where id = $1` + const result = await dbConnection.query(sqlQuery, [id]) + + if(result.rows.length === 0) { + throw new NotFoundError('A pet with the provided ID does not exist') + } + + return result.rows[0] +} + +const updatePet = async (id, petInfo) => { + if (!verifyFields(petInfo)) { + throw new MissingFieldsError('Missing fields in request body') + } + + const sqlQuery = `update pets set name = $1, age = $2, type = $3, breed = $4, has_microchip = $5 where id = $6 returning *` + const result = await dbConnection.query(sqlQuery, [petInfo.name, petInfo.age, petInfo.type, petInfo.breed, petInfo.has_microchip, id]) + + if(result.rows.length === 0) { + throw new NotFoundError('A pet with the provided ID does not exist') + } + + return result.rows[0] +} + +const deletePetById = async (id) => { + const sqlQuery = `delete from pets where id = $1 returning *` + const result = await dbConnection.query(sqlQuery, [id]) + + if(result.rows.length === 0) { + throw new NotFoundError('A pet with the provided ID does not exist') + } + + return result.rows[0] +} + +const getAllBreeds = async (type) => { + if (!type) { + throw new MissingFieldsError('Animal type is required') + } + + const sqlQuery = `select distinct breed from pets where type = $1` + const result = await dbConnection.query(sqlQuery, [type]) + + return result.rows +} + +function verifyFields(object) { + const neededProperties = ['name', 'age', 'type', 'breed', 'has_microchip'] + + for (const item of neededProperties) { + if (object[item] === undefined) { + return false + } + } + + return true +} + +module.exports = { + getAllPets, + createPet, + getPetById, + updatePet, + deletePetById, + getAllBreeds +} \ No newline at end of file diff --git a/src/errors/missingFieldsError.js b/src/errors/missingFieldsError.js new file mode 100644 index 00000000..7359cc1a --- /dev/null +++ b/src/errors/missingFieldsError.js @@ -0,0 +1,5 @@ +class MissingFieldsError extends Error { + +} + +module.exports = MissingFieldsError \ No newline at end of file diff --git a/src/errors/notFoundError.js b/src/errors/notFoundError.js new file mode 100644 index 00000000..0a1f7abc --- /dev/null +++ b/src/errors/notFoundError.js @@ -0,0 +1,5 @@ +class NotFoundError extends Error { + +} + +module.exports = NotFoundError \ No newline at end of file diff --git a/src/errors/notUniqueError.js b/src/errors/notUniqueError.js new file mode 100644 index 00000000..a96e8f02 --- /dev/null +++ b/src/errors/notUniqueError.js @@ -0,0 +1,5 @@ +class NotUniqueError extends Error { + +} + +module.exports = NotUniqueError \ No newline at end of file diff --git a/src/routers/books.js b/src/routers/books.js index 1551dd87..a37c6f39 100644 --- a/src/routers/books.js +++ b/src/routers/books.js @@ -1,9 +1,60 @@ const express = require('express') +const { getAllBooks, createBook, getBookById, updateBook, deleteBookById } = require('../dal/booksRepo.js') const router = express.Router() -const db = require("../../db"); router.get('/', async (req, res) => { + const type = req.query.type + const topic = req.query.topic + const author = req.query.author + const page = req.query.page + const perPage = req.query.per_page + const books = await getAllBooks(type, topic, author, page, perPage) + + res.json({ + books + }) +}) + +router.post('/', async (req, res) => { + const book = req.body + + const newBook = await createBook(book) + + res.status(201).json({ + book: newBook + }) +}) + +router.get('/:id', async (req, res) => { + const bookId = Number(req.params.id) + + const foundBook = await getBookById(bookId) + + res.json({ + book: foundBook + }) +}) + +router.put('/:id', async (req, res) => { + const bookId = Number(req.params.id) + const bookInfo = req.body + + const updatedBook = await updateBook(bookId, bookInfo) + + res.json({ + book: updatedBook + }) +}) + +router.delete('/:id', async (req, res) => { + const bookId = Number(req.params.id) + + const deletedBook = await deleteBookById(bookId) + + res.json({ + book: deletedBook + }) }) module.exports = router diff --git a/src/routers/breeds.js b/src/routers/breeds.js new file mode 100644 index 00000000..7a15dbab --- /dev/null +++ b/src/routers/breeds.js @@ -0,0 +1,15 @@ +const express = require('express') +const { getAllBreeds } = require('../dal/petsRepo.js') +const router = express.Router() + +router.get('/', async (req, res) => { + const type = req.query.type + + const breeds = await getAllBreeds(type) + + res.json({ + breeds + }) +}) + +module.exports = router \ No newline at end of file diff --git a/src/routers/pets.js b/src/routers/pets.js new file mode 100644 index 00000000..afe309ec --- /dev/null +++ b/src/routers/pets.js @@ -0,0 +1,58 @@ +const express = require('express') +const { getAllPets, createPet, getPetById, updatePet, deletePetById } = require('../dal/petsRepo.js') +const router = express.Router() + +router.get('/', async (req, res) => { + const type = req.query.type + const page = req.query.page + const perPage = req.query.per_page + + const pets = await getAllPets(type, page, perPage) + + res.json({ + pets + }) +}) + +router.post('/', async (req, res) => { + const pet = req.body + + const newPet = await createPet(pet) + + res.status(201).json({ + pet: newPet + }) +}) + +router.get('/:id', async (req, res) => { + const petId = Number(req.params.id) + + const foundPet = await getPetById(petId) + + res.json({ + pet: foundPet + }) +}) + +router.put('/:id', async (req, res) => { + const petId = Number(req.params.id) + const petInfo = req.body + + const updatedPet = await updatePet(petId, petInfo) + + res.json({ + pet: updatedPet + }) +}) + +router.delete('/:id', async (req, res) => { + const petId = Number(req.params.id) + + const deletedPet = await deletePetById(petId) + + res.json({ + pet: deletedPet + }) +}) + +module.exports = router \ No newline at end of file diff --git a/src/server.js b/src/server.js index dac55e5d..fc6c2639 100644 --- a/src/server.js +++ b/src/server.js @@ -1,4 +1,6 @@ +require('dotenv').config() const express = require("express"); +require('express-async-errors'); const morgan = require("morgan"); const cors = require("cors"); @@ -8,9 +10,39 @@ app.use(morgan("dev")); app.use(cors()); app.use(express.json()); -//TODO: Implement books and pets APIs using Express Modular Routers const booksRouter = require('./routers/books.js') +const petsRouter = require('./routers/pets.js') +const breedsRouter = require('./routers/breeds.js') +const MissingFieldsError = require('./errors/missingFieldsError.js') +const NotFoundError = require('./errors/notFoundError.js') +const NotUniqueError = require('./errors/notUniqueError.js') app.use('/books', booksRouter) +app.use('/pets', petsRouter) +app.use('/breeds', breedsRouter) + +app.use((error, req, res, next) => { + if (error instanceof MissingFieldsError) { + return res.status(400).json({ + error: error.message + }) + } + + if (error instanceof NotFoundError) { + return res.status(404).json({ + error: error.message + }) + } + + if (error instanceof NotUniqueError) { + return res.status(409).json({ + error: error.message + }) + } + + res.status(500).json({ + message: 'Something went wrong' + }) +}) module.exports = app diff --git a/src/utils/dbConnection.js b/src/utils/dbConnection.js new file mode 100644 index 00000000..0f9f11ed --- /dev/null +++ b/src/utils/dbConnection.js @@ -0,0 +1,16 @@ +const { Pool } = require('pg'); + +const { PGHOST, PGDATABASE, PGUSER, PGPASSWORD } = process.env; + +const dbConnection = new Pool({ + host: PGHOST, + database: PGDATABASE, + username: PGUSER, + password: PGPASSWORD, + port: 5432, + ssl: { + require: true, + }, + }) + +module.exports = dbConnection \ No newline at end of file