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..b886cb18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-async-errors": "^3.1.1", "faker": "^5.5.3", "morgan": "1.10.0", "pg": "8.6.0", @@ -2064,6 +2065,14 @@ "node": ">= 0.10.0" } }, + "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/express/node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", diff --git a/package.json b/package.json index 57d8b4fb..21dc9ad5 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-async-errors": "^3.1.1", "faker": "^5.5.3", "morgan": "1.10.0", "pg": "8.6.0", diff --git a/src/controllers/booksControllers.js b/src/controllers/booksControllers.js new file mode 100644 index 00000000..aa284117 --- /dev/null +++ b/src/controllers/booksControllers.js @@ -0,0 +1,59 @@ +const { + getAllBooks, + postNewBook, + getBookById, + updateBookById, + deleteBookById, +} = require("../dal/booksRepo.js") +const getPaginationParams = require("../utils/pagination.js") + +const getBooks = async (req, res) => { + const { page, per_page } = getPaginationParams(req) + const books = await getAllBooks(req) + + res.json({ + books, + per_page, + page + }) +} + +const postBook = async (req, res) => { + const book = await postNewBook(req) + + res.status(201).json({ + book, + }) +} + +const getBook = async (req, res) => { + const book = await getBookById(req) + + res.json({ + book, + }) +} + +const updateBook = async (req, res) => { + const book = await updateBookById(req) + + res.status(201).json({ + book, + }) +} + +const deleteBook = async (req, res) => { + const book = await deleteBookById(req) + + res.status(201).json({ + book, + }) +} + +module.exports = { + getBooks, + postBook, + getBook, + updateBook, + deleteBook, +} diff --git a/src/controllers/breedsControllers.js b/src/controllers/breedsControllers.js new file mode 100644 index 00000000..3144d940 --- /dev/null +++ b/src/controllers/breedsControllers.js @@ -0,0 +1,13 @@ +const dbConnection = require("../utils/dbConnection.js") + +const getAllBreeds = async (req, res) => { + const type = req.query.type + const sqlQuery = "select distinct breed from pets where type = $1" + const result = await dbConnection.query(sqlQuery, [type]) + + res.json({ + breeds: result.rows + }) +} + +module.exports = getAllBreeds diff --git a/src/controllers/petsControllers.js b/src/controllers/petsControllers.js new file mode 100644 index 00000000..ec7b6112 --- /dev/null +++ b/src/controllers/petsControllers.js @@ -0,0 +1,59 @@ +const { + getAllPets, + postNewPet, + getPetById, + updatePetById, + deletePetById, +} = require("../dal/petsRepo.js") +const getPaginationParams = require("../utils/pagination.js") + +const getPets = async (req, res) => { + const { page, per_page } = getPaginationParams(req) + const pets = await getAllPets(req) + + res.json({ + pets, + per_page, + page + }) +} + +const postPet = async (req, res) => { + const pet = await postNewPet(req) + + res.status(201).json({ + pet, + }) +} + +const getPet = async (req, res) => { + const pet = await getPetById(req) + + res.json({ + pet, + }) +} + +const updatePet = async (req, res) => { + const pet = await updatePetById(req) + + res.status(201).json({ + pet, + }) +} + +const deletePet = async (req, res) => { + const pet = await deletePetById(req) + + res.status(201).json({ + pet, + }) +} + +module.exports = { + getPets, + postPet, + getPet, + updatePet, + deletePet, +} diff --git a/src/dal/booksRepo.js b/src/dal/booksRepo.js new file mode 100644 index 00000000..9d412e45 --- /dev/null +++ b/src/dal/booksRepo.js @@ -0,0 +1,115 @@ +const ConflictError = require("../errors/ConflictError.js") +const MissingFieldError = require("../errors/MissingFieldError.js") +const NotFoundError = require("../errors/NotFoundError.js") +const dbConnection = require("../utils/dbConnection.js") +const getPaginationParams = require("../utils/pagination.js") + +const getAllBooks = async (req) => { + const { page, per_page } = getPaginationParams(req) + const author = req.query.author + + let sqlQuery = "select * from books" + let result = await dbConnection.query(sqlQuery) + + const calculateOffset = () => (page - 1) * per_page + + if (author) { + sqlQuery = "select * from books where author = $1 limit $2 offset $3" + result = await dbConnection.query(sqlQuery, [ + author, + per_page, + calculateOffset(), + ]) + } else { + sqlQuery = "select * from books limit $1 offset $2" + result = await dbConnection.query(sqlQuery, [per_page, calculateOffset()]) + } + + return result.rows +} + +const postNewBook = async (req) => { + const { title, type, author, topic, publication_date, pages } = req.body + + if ( + [title, type, author, topic, publication_date, pages].some( + (prop) => prop === undefined + ) + ) { + throw new MissingFieldError("Missing fields in the 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, [ + title, + type, + author, + topic, + publication_date, + pages, + ]) + + return result.rows[0] +} + +const getBookById = async (req) => { + const id = Number(req.params.id) + const sqlQuery = "select * from books where id = $1" + const result = await dbConnection.query(sqlQuery, [id]) + + if (result.rows.length === 0) { + throw new NotFoundError(`no book with id: ${id}`) + } + + return result.rows[0] +} + +const updateBookById = async (req) => { + const { title, type, author, topic, publication_date, pages } = req.body + const id = Number(req.params.id) + const conflictQuery = "select * from books where title = $1 and id != $2" + const conflictResult = await dbConnection.query(conflictQuery, [title, id]) + + if (conflictResult.rows.length > 0) { + throw new ConflictError(`A book with the title: ${title} already exists`) + } + + 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, [ + title, + type, + author, + topic, + publication_date, + pages, + id, + ]) + + if (result.rows.length === 0) { + throw new NotFoundError(`no book with id: ${id}`) + } + + return result.rows[0] +} + +const deleteBookById = async (req) => { + const id = Number(req.params.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(`no book with id: ${id}`) + } + + return result.rows[0] +} + +module.exports = { + getAllBooks, + postNewBook, + getBookById, + updateBookById, + deleteBookById, +} diff --git a/src/dal/petsRepo.js b/src/dal/petsRepo.js new file mode 100644 index 00000000..b4f8e6d3 --- /dev/null +++ b/src/dal/petsRepo.js @@ -0,0 +1,97 @@ +const MissingFieldError = require("../errors/MissingFieldError.js") +const NotFoundError = require("../errors/NotFoundError.js") +const dbConnection = require("../utils/dbConnection.js") +const getPaginationParams = require("../utils/pagination.js") + +const getAllPets = async (req) => { + const { page, per_page } = getPaginationParams(req) + + let sqlQuery = "select * from pets" + let result = await dbConnection.query(sqlQuery) + + const calculateOffset = () => (page - 1) * per_page + + sqlQuery = "select * from pets limit $1 offset $2" + result = await dbConnection.query(sqlQuery, [per_page, calculateOffset()]) + + return result.rows +} + +const postNewPet = async (req) => { + const { name, age, type, breed, has_microchip } = req.body + const undefinedProps = ['name', 'age', 'type', 'breed', 'has_microchip'].filter( + (prop) => req.body[prop] === undefined + ) + + if (undefinedProps.length > 0) { + throw new MissingFieldError(`missing fields: ${undefinedProps.join(", ")}`) + } + + const sqlQuery = + "insert into pets (name, age, type, breed, has_microchip) values ($1, $2, $3, $4, $5) returning *" + const result = await dbConnection.query(sqlQuery, [ + name, + age, + type, + breed, + has_microchip, + ]) + + return result.rows[0] +} + +const getPetById = async (req) => { + const id = Number(req.params.id) + + const sqlQuery = "select * from pets where id = $1" + const result = await dbConnection.query(sqlQuery, [id]) + + if (result.rows.length === 0) { + throw new NotFoundError(`no pet with id: ${id}`) + } + + return result.rows[0] +} + +const updatePetById = async (req) => { + const { name, age, type, breed, has_microchip } = req.body + const id = Number(req.params.id) + + 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, [ + name, + age, + type, + breed, + has_microchip, + id, + ]) + + if (result.rows.length === 0) { + throw new NotFoundError(`no pet with id: ${id}`) + } + + return result.rows[0] +} + +const deletePetById = async (req) => { + const id = Number(req.params.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(`no pet with id: ${id}`) + } + + return result.rows[0] +} + +module.exports = { + getAllPets, + postNewPet, + getPetById, + updatePetById, + deletePetById, +} diff --git a/src/errors/ConflictError.js b/src/errors/ConflictError.js new file mode 100644 index 00000000..5bae3340 --- /dev/null +++ b/src/errors/ConflictError.js @@ -0,0 +1,3 @@ +class ConflictError extends Error {} + +module.exports = ConflictError diff --git a/src/errors/MissingFieldError.js b/src/errors/MissingFieldError.js new file mode 100644 index 00000000..3e3c7e24 --- /dev/null +++ b/src/errors/MissingFieldError.js @@ -0,0 +1,3 @@ +class MissingFieldError extends Error {} + +module.exports = MissingFieldError diff --git a/src/errors/NotFoundError.js b/src/errors/NotFoundError.js new file mode 100644 index 00000000..db590d5f --- /dev/null +++ b/src/errors/NotFoundError.js @@ -0,0 +1,3 @@ +class NotFoundError extends Error {} + +module.exports = NotFoundError diff --git a/src/routers/books.js b/src/routers/books.js index 1551dd87..7b525aed 100644 --- a/src/routers/books.js +++ b/src/routers/books.js @@ -1,9 +1,17 @@ -const express = require('express') +const express = require("express") const router = express.Router() -const db = require("../../db"); +const { + getBooks, + postBook, + getBook, + updateBook, + deleteBook, +} = require("../controllers/booksControllers.js") -router.get('/', async (req, res) => { - -}) +router.get("/", getBooks) +router.post("/", postBook) +router.get("/:id", getBook) +router.put("/:id", updateBook) +router.delete("/:id", deleteBook) module.exports = router diff --git a/src/routers/breeds.js b/src/routers/breeds.js new file mode 100644 index 00000000..95990643 --- /dev/null +++ b/src/routers/breeds.js @@ -0,0 +1,7 @@ +const express = require("express") +const router = express.Router() +const getAllBreeds = require("../controllers/breedsControllers.js") + +router.get("/", getAllBreeds) + +module.exports = router diff --git a/src/routers/pets.js b/src/routers/pets.js new file mode 100644 index 00000000..f078ab7c --- /dev/null +++ b/src/routers/pets.js @@ -0,0 +1,17 @@ +const express = require("express") +const router = express.Router() +const { + getPets, + postPet, + getPet, + updatePet, + deletePet, +} = require("../controllers/petsControllers") + +router.get("/", getPets) +router.post("/", postPet) +router.get("/:id", getPet) +router.put("/:id", updatePet) +router.delete("/:id", deletePet) + +module.exports = router diff --git a/src/server.js b/src/server.js index dac55e5d..49bb9b60 100644 --- a/src/server.js +++ b/src/server.js @@ -1,16 +1,44 @@ -const express = require("express"); -const morgan = require("morgan"); -const cors = require("cors"); +require("dotenv").config() +const express = require("express") +require("express-async-errors") +const morgan = require("morgan") +const cors = require("cors") -const app = express(); +const app = express() -app.use(morgan("dev")); -app.use(cors()); -app.use(express.json()); +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 booksRouter = require("./routers/books.js") +const petsRouter = require("./routers/pets.js") +const breedsRouter = require("./routers/breeds.js") +const MissingFieldError = require("./errors/MissingFieldError.js") +const NotFoundError = require("./errors/NotFoundError.js") +const ConflictError = require("./errors/ConflictError.js") -app.use('/books', booksRouter) +app.use("/books", booksRouter) +app.use("/pets", petsRouter) +app.use("/breeds", breedsRouter) + +app.use((error, req, res, next) => { + if (error instanceof MissingFieldError) { + res.status(400).json({ + error: error.message, + }) + } + + if (error instanceof NotFoundError) { + res.status(404).json({ + error: error.message, + }) + } + + if (error instanceof ConflictError) { + res.status(409).json({ + error: error.message, + }) + } +}) module.exports = app diff --git a/src/utils/dbConnection.js b/src/utils/dbConnection.js new file mode 100644 index 00000000..ad5b5c94 --- /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 diff --git a/src/utils/pagination.js b/src/utils/pagination.js new file mode 100644 index 00000000..085d6436 --- /dev/null +++ b/src/utils/pagination.js @@ -0,0 +1,16 @@ +const MissingFieldError = require("../errors/MissingFieldError") + +const getPaginationParams = (req) => { + const page = req.query.page ? Number(req.query.page) : 1 + const per_page = req.query.perPage ? Number(req.query.perPage) : 20 + + if (per_page > 50 || per_page < 10) { + throw new MissingFieldError( + `parameter invalid perPage: ${per_page} not valid. Accepted range is 10 - 50` + ) + } + + return { page, per_page } +} + +module.exports = getPaginationParams diff --git a/test/database-cleaner/index.js b/test/database-cleaner/index.js index 13a4e464..7d3291ac 100644 --- a/test/database-cleaner/index.js +++ b/test/database-cleaner/index.js @@ -1,5 +1,5 @@ const fs = require('fs/promises') -const client = require("../../db"); +const client = require("../../src/utils/dbConnection.js"); global.beforeEach(async() => { const sqlDataForBooks = await fs.readFile('./sql/create-books.sql') diff --git a/test/helpers/createBook.js b/test/helpers/createBook.js index 5f7e9c76..f8cae53d 100644 --- a/test/helpers/createBook.js +++ b/test/helpers/createBook.js @@ -1,4 +1,4 @@ -const client = require("../../db"); +const client = require("../../src/utils/dbConnection.js"); const createBook = async (values) => { const sqlString = `INSERT INTO "books" (title, type, author, topic, publication_date, pages) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *;` diff --git a/test/helpers/createPet.js b/test/helpers/createPet.js index 8dfca845..54bb5b32 100644 --- a/test/helpers/createPet.js +++ b/test/helpers/createPet.js @@ -1,4 +1,4 @@ -const client = require("../../db"); +const client = require("../../src/utils/dbConnection.js"); const createPet = async (values) => { const sqlString = `INSERT INTO "pets" (name, age, type, breed, has_microchip) VALUES ($1, $2, $3, $4, $5) RETURNING *;` diff --git a/test/helpers/insertBooks.js b/test/helpers/insertBooks.js index 54e720ca..3bab2201 100644 --- a/test/helpers/insertBooks.js +++ b/test/helpers/insertBooks.js @@ -1,5 +1,5 @@ const fs = require('fs/promises') -const client = require("../../db"); +const client = require("../../src/utils/dbConnection.js"); const insertBooks = async () => { const sqlDataForBooks = await fs.readFile('./sql/insert-books.sql') diff --git a/test/helpers/insertPets.js b/test/helpers/insertPets.js index eadddec2..2eed6b86 100644 --- a/test/helpers/insertPets.js +++ b/test/helpers/insertPets.js @@ -1,5 +1,5 @@ const fs = require('fs/promises') -const client = require("../../db"); +const client = require("../../src/utils/dbConnection.js"); const insertPets = async () => { const sqlDataForPets = await fs.readFile('./sql/insert-pets.sql')