diff --git a/.gitignore b/.gitignore index 5540c35..97ca039 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules node_modules/* build build/* +.env diff --git a/data/deletedData.js b/data/deletedData.js new file mode 100644 index 0000000..31706b5 --- /dev/null +++ b/data/deletedData.js @@ -0,0 +1,9 @@ +const deletedUsers = [] +const deletedFilms = [] +const deletedBooks = [] + +module.exports = { + deletedUsers, + deletedFilms, + deletedBooks +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fc9d54a..ab44819 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,14 @@ "version": "1.0.0", "dependencies": { "cors": "^2.8.5", + "dotenv": "^16.4.5", "express": "^4.18.2", "morgan": "^1.10.0" }, "devDependencies": { "jest": "^28.1.3", "nodemon": "^2.0.22", + "pg": "^8.12.0", "supertest": "^6.3.3" } }, @@ -1853,6 +1855,17 @@ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3794,6 +3807,95 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/pg": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", + "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", + "dev": true, + "dependencies": { + "pg-connection-string": "^2.6.4", + "pg-pool": "^3.6.2", + "pg-protocol": "^1.6.1", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "dev": true, + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", + "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==", + "dev": true + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", + "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", + "dev": true, + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", + "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==", + "dev": true + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dev": true, + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -3833,6 +3935,45 @@ "node": ">=8" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dev": true, + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pretty-format": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", @@ -4205,6 +4346,15 @@ "source-map": "^0.6.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -4695,6 +4845,15 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index c63bc01..be4f7c3 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,14 @@ }, "dependencies": { "cors": "^2.8.5", + "dotenv": "^16.4.5", "express": "^4.18.2", "morgan": "^1.10.0" }, "devDependencies": { "jest": "^28.1.3", "nodemon": "^2.0.22", + "pg": "^8.12.0", "supertest": "^6.3.3" }, "keywords": [] diff --git a/src/controllers/books/books.js b/src/controllers/books/books.js new file mode 100644 index 0000000..9abcc60 --- /dev/null +++ b/src/controllers/books/books.js @@ -0,0 +1,115 @@ +const { getAllBooks, getBookByID, filterByTitle } = require("../../domain/books/books") +const newID = require("../../functions/createID") +const { deletedBooks } = require('../../../data/deletedData.js') + +let newBook = { + id: 0, + title: 'string', + type: 'string', + author: 'string' +} + +const getBooks = (req, res) => { + res.status(200).json({ + books: getAllBooks() + }) +} + +const addBook = (req, res) => { + newBook.id = newID(getAllBooks()) + newBook.title = req.body.title + newBook.type = req.body.type + newBook.author = req.body.author + + if ( + req.body.title === "" || + req.body.type === "" || + req.body.author === "" + ) { + throw new FieldsMissing("Missing fields") + } + + const checkTitle = filterByTitle(newBook.title) + + if(checkTitle) { + throw new AlreadyExistsError("Book already exists") + } + + getAllBooks().push(newBook) + res.status(201).json({ + book: newBook + }) +} + +const getByID = (req, res) => { + const id = Number(req.params.id) + const found = getBookByID(id) + + if (typeof id !== "number") { + throw new InvalidDataError("ID must be a number") + } + + if (!found) { + throw new NotFoundError("Book not found") + } + + res.status(200).json({ + book: found + }) +} + +const removeBook = (req, res) => { + const id = Number(req.params.id) + const found = getBookByID(id) + + if (typeof id !== "number") { + throw new InvalidDataError("ID must be a number") + } + + if (!found) { + throw new NotFoundError("Book not found") + } + + deletedBooks.push(found) + const index = getAllBooks().indexOf(found) + getAllBooks().splice(index, 1) + res.status(200).json({ + book: found + }) +} + +const updateBook = (req, res) => { + const id = Number(req.params.id) + const found = getBookByID(id) + + if (typeof id !== "number") { + throw new InvalidDataError("ID must be a number") + } + + + if (!found) { + throw new NotFoundError("Book not found") + } + + found.title = req.body.title + found.type = req.body.type + found.author = req.body.author + + const checkTitle = filterByTitle(found.title) + + if(!checkTitle) { + throw new AlreadyExistsError("Book already exists") + } + + res.status(200).json({ + book: found + }) +} + +module.exports = { + getBooks, + addBook, + getByID, + removeBook, + updateBook +} \ No newline at end of file diff --git a/src/controllers/films/films.js b/src/controllers/films/films.js new file mode 100644 index 0000000..57bb998 --- /dev/null +++ b/src/controllers/films/films.js @@ -0,0 +1,114 @@ +const { deletedFilms } = require('../../../data/deletedData.js') +const {getAllFilms, getFilmByID, getFilmByDirector, getFilmByTitle} = require('../../domain/films/films.js') +const newID = require('../../functions/createID.js') + +let newFilm = { + id: 0, + title: 'string', + director: 'string' +} + +const getAll = async (req, res) => { + res.status(200).json({ + films: getAllFilms() + }) +} + +const addFilm = (req, res) => { + newFilm.id = newID(getAllFilms()) + newFilm.title = req.body.title + newFilm.director = req.body.director + + if( + req.body.title === "" || + req.body.director === "" + ) { + throw new FieldsMissing('Missing Fields') + } + + getAllFilms().push(newFilm) + res.status(201).json({ + film: newFilm + }) +} + +const getByID = (req, res) => { + const id = Number(req.params.id) + const found = getFilmByID(id) + + if (typeof id !== "number") { + throw new InvalidDataError("ID must be a number") + } + + if (!found) { + throw new NotFoundError("Film not found") + } + + res.status(200).json({ + film: found + }) +} + +const removeFIlm = (req, res) => { + const id = Number(req.params.id) + const found = getFilmByID(id) + + if (typeof id !== "number") { + throw new InvalidDataError("ID must be a number") + } + + if (!found) { + throw new NotFoundError("Film not found") + } + + deletedFilms.push(found) + const index = getAllFilms().indexOf(found) + getAllFilms().splice(index, 1) + + res.status(200).json({ + film: found + }) +} + +const updateFilm = (req, res) => { + const id = Number(req.params.id) + const found = getFilmByID(id) + + if (typeof id !== "number") { + throw new InvalidDataError("ID must be a number") + } + + if (!found) { + throw new NotFoundError("Film not found") + } + + found.title = req.body.title + found.director = req.body.director + + if (!getFilmByTitle(found.title)) { + throw new AlreadyExistsError("Film already exists") + } + + res.status(200).json({ + film: found + }) +} + +const filterByDirector = (req, res) => { + const director = req.query.director + + const found = getFilmByDirector(director) + + res.status(200).json({ + films: found + }) +} + +module.exports = { + getAll, + addFilm, + getByID, + removeFIlm, + updateFilm, + filterByDirector +} \ No newline at end of file diff --git a/src/controllers/users/users.js b/src/controllers/users/users.js new file mode 100644 index 0000000..429bc02 --- /dev/null +++ b/src/controllers/users/users.js @@ -0,0 +1,102 @@ +const {getAllUsers, getUserById, filterUserEmails} = require('../../domain/users/users.js') +const newID = require('../../functions/createID.js') +const {deletedUsers} = require('../../../data/deletedData.js') + +let newUser = { + id: 0, + email: 'string' +} + + +const getAll = (req, res) => { + res.status(200).json({ + users: getAllUsers() + }) +} + +const createUser = (req, res) => { + newUser.id = newID(getAllUsers()) + newUser.email = req.body.email + + if (filterUserEmails(newUser.email)) { + throw new AlreadyExistsError("A user already exists with this email") + } + + if (req.body.email === "") { + throw new FieldsMissing("Email field missing") + } + + + getAllUsers().push(newUser) + res.status(201).json({ + user: newUser + }) +} + +const getByID = (req, res) => { + const id = Number(req.params.id) + const found = getUserById(id) + + if (typeof id !== "number") { + throw new InvalidDataError("ID must be a number") + } + + if (!found) { + throw new NotFoundError("Book not found") + } + + res.status(200).json({ + user: found + }) +} + +const removeUser = (req, res) => { + const id = Number(req.params.id) + const found = getUserById(id) + + if (typeof id !== "number") { + throw new InvalidDataError("ID must be a number") + } + + if (!found) { + throw new NotFoundError("Book not found") + } + + deletedUsers.push(found) + const index = getAllUsers().indexOf(found) + getAllUsers().splice(index, 1) + res.status(200).json({ + user: found + }) +} + +const updateUser = (req, res) => { + const id = Number(req.params.id) + const found = getUserById(id) + + if (typeof id !== "number") { + throw new InvalidDataError("ID must be a number") + } + + if (!found) { + throw new NotFoundError("Book not found") + } + + found.email = req.body.email + + if (!filterUserEmails(found.email)) { + throw new AlreadyExistsError("A user already exists with this email") + } + + res.status(200).json({ + user: found + }) +} + +module.exports = { + getAll, + createUser, + getByID, + removeUser, + updateUser +} diff --git a/src/domain/books/books.js b/src/domain/books/books.js new file mode 100644 index 0000000..a0ffd85 --- /dev/null +++ b/src/domain/books/books.js @@ -0,0 +1,21 @@ +const data = require('../../../data/index.js') +const books = data.books + + +const getAllBooks = () => { + return books +} + +const getBookByID = (id) => { + return getAllBooks().find((b) => b.id === id) +} + +const filterByTitle = (title) => { + return getAllBooks().find((b) => b.title === title) +} + +module.exports = { + getAllBooks, + getBookByID, + filterByTitle +} \ No newline at end of file diff --git a/src/domain/films/films.js b/src/domain/films/films.js new file mode 100644 index 0000000..b5f1510 --- /dev/null +++ b/src/domain/films/films.js @@ -0,0 +1,26 @@ +const data = require('../../../data/index.js') +const films = data.films + + +const getAllFilms = () => { + return films +} + +const getFilmByID = (id) => { + return getAllFilms().find((f) => f.id === id) +} + +const getFilmByDirector = (d) => { + return getAllFilms().filter((f) => f.director === d) +} + +const getFilmByTitle = (t) => { + return getAllFilms().find((f) => f.title === t) +} + +module.exports = { + getAllFilms, + getFilmByID, + getFilmByDirector, + getFilmByTitle +} \ No newline at end of file diff --git a/src/domain/users/users.js b/src/domain/users/users.js new file mode 100644 index 0000000..1be883a --- /dev/null +++ b/src/domain/users/users.js @@ -0,0 +1,20 @@ +const data = require('../../../data/index.js') +const users = data.users + +const getAllUsers = () => { + return users +} + +const getUserById = (id) => { + return getAllUsers().find((u) => u.id === id) +} + +const filterUserEmails = (email) => { + return getAllUsers().find((u) => u.email === email) +} + +module.exports = { + getAllUsers, + getUserById, + filterUserEmails +} \ No newline at end of file diff --git a/src/errorClasses/index.js b/src/errorClasses/index.js new file mode 100644 index 0000000..c915182 --- /dev/null +++ b/src/errorClasses/index.js @@ -0,0 +1,15 @@ +class NotFoundError extends Error { + +} + +class InvalidDataError extends Error { + +} + +class AlreadyExistsError extends Error { + +} + +class FieldsMissing extends Error { + +} \ No newline at end of file diff --git a/src/functions/createID.js b/src/functions/createID.js new file mode 100644 index 0000000..720c9ea --- /dev/null +++ b/src/functions/createID.js @@ -0,0 +1,6 @@ +function newID(data) { + const newID = data.reverse().find((d) => d.id) + return newID.id +1 +} + +module.exports = newID \ No newline at end of file diff --git a/src/functions/findId.js b/src/functions/findId.js new file mode 100644 index 0000000..834b1d4 --- /dev/null +++ b/src/functions/findId.js @@ -0,0 +1,6 @@ +function findById(data, id) { + const found = data.find((d) => d.id === id) + return found +} + +module.exports = {findById} \ No newline at end of file diff --git a/src/routers/books.js b/src/routers/books.js index 18b9a7c..1d104a2 100644 --- a/src/routers/books.js +++ b/src/routers/books.js @@ -1,4 +1,12 @@ -// Import data here... +const { Router } = require("express"); +const { getBooks, addBook, getByID, removeBook, updateBook } = require("../controllers/books/books"); +const router = Router() -// Write routes here... +router.get('/', getBooks) +router.post('/', addBook) +router.get('/:id', getByID) +router.delete('/:id', removeBook) +router.put('/:id', updateBook) + +module.exports = router diff --git a/src/routers/films.js b/src/routers/films.js index e69de29..55876b4 100644 --- a/src/routers/films.js +++ b/src/routers/films.js @@ -0,0 +1,13 @@ +const { Router } = require("express"); +const { getAll, addFilm, getByID, removeFIlm, updateFilm, filterByDirector } = require("../controllers/films/films"); + +const router = Router() + +router.get('/', getAll) +router.post('/', addFilm) +router.get('/:id', getByID) +router.delete('/:id', removeFIlm) +router.put('/:id', updateFilm) +router.get('/?director=:name', filterByDirector) + +module.exports = router \ No newline at end of file diff --git a/src/routers/users.js b/src/routers/users.js index e69de29..69fb06c 100644 --- a/src/routers/users.js +++ b/src/routers/users.js @@ -0,0 +1,16 @@ +const { Router } = require("express"); +const {getAll, createUser, getByID, removeUser, updateUser} = require("../controllers/users/users.js"); + +const router = Router() + +router.get('/', getAll) + +router.post('/', createUser) + +router.get('/:id', getByID) + +router.delete('/:id', removeUser) + +router.put('/:id', updateUser) + +module.exports = router \ No newline at end of file diff --git a/src/server.js b/src/server.js index 715321f..0ba1edc 100644 --- a/src/server.js +++ b/src/server.js @@ -1,5 +1,6 @@ const express = require("express"); const app = express(); +require('dotenv').config() const cors = require("cors"); const morgan = require("morgan"); @@ -10,9 +11,44 @@ app.use(express.json()); app.use(morgan("dev")); // REQUIRE ROUTERS -const usersRouter = require("./routers/users"); +const usersRouter = require("./routers/users.js"); +const filmsRouter = require('./routers/films.js') +const booksRouter = require('./routers/books.js') // ADD ROUTERS TO APP +app.use('/users', usersRouter) +app.use('/films', filmsRouter) +app.use('/books', booksRouter) +app.use((error, req, res, next) => { + if(error instanceof NotFoundError) { + return res.status(404).json({ + message: error.message + }) + } + + if(error instanceof InvalidDataError) { + return res.status(400).json({ + message: error.message + }) + } + + if(error instanceof AlreadyExistsError) { + return res.status(409).json({ + message: error.message + }) + } + + if(error instanceof FieldsMissing) { + return res.status(409).json({ + message: error.message + }) + } + + res.status(500).json({ + message: "Something went wrong" + }) +}) + module.exports = app