diff --git a/package-lock.json b/package-lock.json index 37ccdff8..717f0215 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1441,12 +1441,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1740,9 +1741,10 @@ "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==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2024,16 +2026,17 @@ } }, "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==", + "license": "MIT", "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,43 +2067,6 @@ "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/faker": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/faker/-/faker-5.5.3.tgz", @@ -2128,10 +2094,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2587,6 +2554,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -4805,6 +4773,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, diff --git a/src/routers/base.controller.js b/src/routers/base.controller.js new file mode 100644 index 00000000..205b5719 --- /dev/null +++ b/src/routers/base.controller.js @@ -0,0 +1,71 @@ +const db = require('../../db') + +class BaseController { + constructor(tableName) { + this.tableName = tableName + } + + async getAll(req, res, next) { + try { + const response = await db.query( + ` + SELECT * FROM ${this.tableName} + ` + ) + const items = response.rows + + res.json({ [this.tableName]: items }) + } catch (err) { + next(new Error(`Could not get ${this.tableName}: `, err)) + } + } + + async getById(req, res, next) { + const id = parseInt(req.params.id) + + try { + const response = await db.query( + ` + SELECT * FROM ${this.tableName} WHERE id = $1 + `, + [id] + ) + const [item] = response.rows + + res.json({ [this.tableName.slice(0, -1)]: item }) + } catch (err) { + next( + new Error( + `Could not get item from ${this.tableName} with id ${id}:`, + err + ) + ) + } + } + + async delete(req, res, next) { + const id = parseInt(req.params.id) + + try { + const response = await db.query( + ` + DELETE FROM ${this.tableName} WHERE id = $1 + returning * + `, + [id] + ) + const [item] = response.rows + + res.status(201).json({ [this.tableName.slice(0, -1)]: item }) + } catch (err) { + next( + new Error( + `Could not delete ${this.tableName} with id ${id}:`, + err + ) + ) + } + } +} + +module.exports = BaseController diff --git a/src/routers/books.controller.js b/src/routers/books.controller.js new file mode 100644 index 00000000..841d67b1 --- /dev/null +++ b/src/routers/books.controller.js @@ -0,0 +1,50 @@ +const db = require('../../db') +const BaseController = require('./base.controller') + +class BooksController extends BaseController { + constructor() { + super('books') + } + + async addBook(req, res, next) { + const { title, type, author, topic, publication_date, pages } = req.body + + try { + const response = await db.query( + ` + INSERT INTO books (title, type, author, topic, publication_date, pages) + VALUES ($1, $2, $3, $4, $5, $6) + returning * + `, + [title, type, author, topic, publication_date, pages] + ) + const [book] = response.rows + + res.status(201).json({ book: book }) + } catch (err) { + next(new Error('Could not add book: ', err)) + } + } + + async updateBook(req, res, next) { + const { title, type, author, topic, publication_date, pages } = req.body + + try { + const response = await db.query( + ` + INSERT INTO books (title, type, author, topic, publication_date, pages) + VALUES ($1, $2, $3, $4, $5, $6) + returning * + `, + [title, type, author, topic, publication_date, pages] + ) + const [book] = response.rows + + res.status(201).json({ book: book }) + } catch (err) { + next(new Error('Could not add book: ', err)) + } + } +} + +module.exports = BooksController diff --git a/src/routers/books.js b/src/routers/books.js index 1551dd87..2a30757a 100644 --- a/src/routers/books.js +++ b/src/routers/books.js @@ -1,9 +1,13 @@ const express = require('express') const router = express.Router() -const db = require("../../db"); -router.get('/', async (req, res) => { +const BooksController = require('./books.controller') +const booksController = new BooksController() -}) +router.post('/', booksController.addBook.bind(booksController)) +router.get('/', booksController.getAll.bind(booksController)) +router.get('/:id', booksController.getById.bind(booksController)) +router.put('/:id', booksController.updateBook.bind(booksController)) +router.delete('/:id', booksController.delete.bind(booksController)) module.exports = router diff --git a/src/routers/pets.controller.js b/src/routers/pets.controller.js new file mode 100644 index 00000000..ac571c53 --- /dev/null +++ b/src/routers/pets.controller.js @@ -0,0 +1,52 @@ +const db = require('../../db') +const BaseController = require('./base.controller') + +class PetsController extends BaseController { + constructor() { + super('pets') + } + + async addPet(req, res, next) { + const { name, age, type, breed, has_microchip } = req.body + + try { + const response = await db.query( + ` + INSERT INTO pets (name, age, type, breed, has_microchip) + VALUES ($1, $2, $3, $4, $5) + returning * + `, + [name, age, type, breed, has_microchip] + ) + const [pet] = response.rows + + res.status(201).json({ pet: pet }) + } catch (err) { + next(new Error('Could not add pet: ', err)) + } + } + + async updatePet(req, res, next) { + const id = parseInt(req.params.id) + const { name, age, type, breed, has_microchip } = req.body + + try { + const response = await db.query( + ` + UPDATE pets + SET name = $1, age = $2, type = $3, breed = $4, has_microchip = $5 + WHERE id = $6 + returning * + `, + [name, age, type, breed, has_microchip, id] + ) + const [pet] = response.rows + + res.status(201).json({ pet }) + } catch (err) { + next(new Error(`Could not update pet with id ${id}: `, err)) + } + } +} + +module.exports = PetsController diff --git a/src/routers/pets.js b/src/routers/pets.js new file mode 100644 index 00000000..a637ca55 --- /dev/null +++ b/src/routers/pets.js @@ -0,0 +1,13 @@ +const express = require('express') +const router = express.Router() + +const PetsController = require('./pets.controller') +const petsController = new PetsController() + +router.post('/', petsController.addPet.bind(petsController)) +router.get('/', petsController.getAll.bind(petsController)) +router.get('/:id', petsController.getById.bind(petsController)) +router.put('/:id', petsController.updatePet.bind(petsController)) +router.delete('/:id', petsController.delete.bind(petsController)) + +module.exports = router diff --git a/src/server.js b/src/server.js index dac55e5d..002ebced 100644 --- a/src/server.js +++ b/src/server.js @@ -1,16 +1,18 @@ -const express = require("express"); -const morgan = require("morgan"); -const cors = require("cors"); +const express = require('express') +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 petsRouter = require('./routers/pets.js') app.use('/books', booksRouter) +app.use('/pets', petsRouter) module.exports = app diff --git a/test/api/routes/books.spec.js b/test/api/routes/books.spec.js index 833f599d..21addaf3 100644 --- a/test/api/routes/books.spec.js +++ b/test/api/routes/books.spec.js @@ -1,88 +1,87 @@ -const supertest = require("supertest") -const app = require("../../../src/server.js") -const { book1, book2 } = require("../../fixtures/bookData.js") -const createBook = require("../../helpers/createBook.js") - -describe("Books Endpoint", () => { - describe("POST /books", () => { - - it("will create a new book", async () => { - const response = await supertest(app).post("/books").send(book1) - - expect(response.status).toEqual(201) - expect(response.body.book).not.toEqual(undefined) - expect(response.body.book.id).not.toEqual(undefined) - - for (prop in book1) { - expect(response.body.book[prop]).toEqual(book1[prop]) - } +const supertest = require('supertest') +const app = require('../../../src/server.js') +const { book1, book2 } = require('../../fixtures/bookData.js') +const createBook = require('../../helpers/createBook.js') + +describe('Books Endpoint', () => { + describe('POST /books', () => { + it('will create a new book', async () => { + const response = await supertest(app).post('/books').send(book1) + + expect(response.status).toEqual(201) + expect(response.body.book).not.toEqual(undefined) + expect(response.body.book.id).not.toEqual(undefined) + + for (prop in book1) { + expect(response.body.book[prop]).toEqual(book1[prop]) + } + }) }) - }) - describe("GET /books", () => { - beforeEach(async () => { - await createBook(Object.values(book1)) - await createBook(Object.values(book2)) + describe('GET /books', () => { + beforeEach(async () => { + await createBook(Object.values(book1)) + await createBook(Object.values(book2)) + }) + + it('will list all books', async () => { + const response = await supertest(app).get('/books') + + expect(response.status).toEqual(200) + expect(response.body.books).not.toEqual(undefined) + expect(response.body.books.length).toEqual(2) + const expectedBooks = [book1, book2] + response.body.books.forEach((retrievedBook, index) => { + expect(retrievedBook.title).toEqual(expectedBooks[index].title) + }) + }) + + it('will list a book', async () => { + const response = await supertest(app).get('/books/1') + + expect(response.status).toEqual(200) + expect(response.body.book).not.toEqual(undefined) + expect(response.body.book.id).not.toEqual(undefined) + + for (prop in book1) { + expect(response.body.book[prop]).toEqual(book1[prop]) + } + }) }) - it("will list all books", async () => { - const response = await supertest(app).get("/books") + describe('PUT /books', () => { + beforeEach(async () => { + await createBook(Object.values(book1)) + }) - expect(response.status).toEqual(200) - expect(response.body.books).not.toEqual(undefined) - expect(response.body.books.length).toEqual(2) - const expectedBooks = [book1, book2] - response.body.books.forEach((retrievedBook, index) => { - expect(retrievedBook.title).toEqual(expectedBooks[index].title) - }) - }) - - it("will list a book", async () => { - const response = await supertest(app).get("/books/1") + it('will update a books', async () => { + const response = await supertest(app).put('/books/1').send(book2) - expect(response.status).toEqual(200) - expect(response.body.book).not.toEqual(undefined) - expect(response.body.book.id).not.toEqual(undefined) + expect(response.status).toEqual(201) + expect(response.body.book).not.toEqual(undefined) + expect(response.body.book.id).not.toEqual(undefined) - for (prop in book1) { - expect(response.body.book[prop]).toEqual(book1[prop]) - } + for (prop in book2) { + expect(response.body.book[prop]).toEqual(book2[prop]) + } + }) }) - }) - describe("PUT /books", () => { - beforeEach(async () => { - await createBook(Object.values(book1)) - }) - - it("will update a books", async () => { - const response = await supertest(app).put("/books/1").send(book2) - - expect(response.status).toEqual(201) - expect(response.body.book).not.toEqual(undefined) - expect(response.body.book.id).not.toEqual(undefined) - - for (prop in book2) { - expect(response.body.book[prop]).toEqual(book2[prop]) - } - }) - }) - - describe("DELETE /books", () => { - beforeEach(async () => { - await createBook(Object.values(book1)) - }) + describe('DELETE /books', () => { + beforeEach(async () => { + await createBook(Object.values(book1)) + }) - it("will return the deleted the book", async () => { - const response = await supertest(app).delete("/books/1") + it('will return the deleted the book', async () => { + const response = await supertest(app).delete('/books/1') - expect(response.status).toEqual(201) - expect(response.body.book).not.toEqual(undefined) - expect(response.body.book.id).not.toEqual(undefined) + expect(response.status).toEqual(201) + expect(response.body.book).not.toEqual(undefined) + expect(response.body.book.id).not.toEqual(undefined) - for (prop in book1) { - expect(response.body.book[prop]).toEqual(book1[prop]) - } + for (prop in book1) { + expect(response.body.book[prop]).toEqual(book1[prop]) + } + }) }) - }) })