diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 100% rename from .eslintrc.js rename to .eslintrc.cjs diff --git a/app.js b/app.js index 40fd9bc167f..63004ca678e 100644 --- a/app.js +++ b/app.js @@ -1,25 +1,27 @@ -const express = require('express') -const logger = require('morgan') -const cors = require('cors') +import express from "express"; +import logger from "morgan"; +import cors from "cors"; -const contactsRouter = require('./routes/api/contacts') +import { router as contactsRouter } from "./routes/api/contactsRouter.js"; -const app = express() +const app = express(); -const formatsLogger = app.get('env') === 'development' ? 'dev' : 'short' +const formatsLogger = app.get("env") === "development" ? "dev" : "short"; -app.use(logger(formatsLogger)) -app.use(cors()) -app.use(express.json()) +app.use(logger(formatsLogger)); +app.use(cors()); +app.use(express.json()); -app.use('/api/contacts', contactsRouter) +app.use("/api/contacts", contactsRouter); app.use((req, res) => { - res.status(404).json({ message: 'Not found' }) -}) + res.status(404).json({ message: "Not found" }); +}); -app.use((err, req, res, next) => { - res.status(500).json({ message: err.message }) -}) +app.use((err, _req, res, _next) => { + const { status = 500, message = "Server error" } = err; + res.status(status).json({ message }); +}); -module.exports = app +// module.exports = app; +export { app }; diff --git a/helpers/HttpError.js b/helpers/HttpError.js new file mode 100644 index 00000000000..d20f40ae3b6 --- /dev/null +++ b/helpers/HttpError.js @@ -0,0 +1,15 @@ +const messages = { + 400: "Bad request", + 401: "Unauthorized", + 403: "Forbidden", + 404: "Not found", + 409: "Conflict", +}; + +const httpError = (status, message = messages[status]) => { + const error = new Error(message); + error.status = status; + return error; +}; + +export { httpError }; diff --git a/images/DELETE.jpg b/images/DELETE.jpg new file mode 100644 index 00000000000..a5111e559d3 Binary files /dev/null and b/images/DELETE.jpg differ diff --git a/images/GET_all.jpg b/images/GET_all.jpg new file mode 100644 index 00000000000..aa99e7579ca Binary files /dev/null and b/images/GET_all.jpg differ diff --git a/images/GET_id.jpg b/images/GET_id.jpg new file mode 100644 index 00000000000..08e8a93b332 Binary files /dev/null and b/images/GET_id.jpg differ diff --git a/images/POST.jpg b/images/POST.jpg new file mode 100644 index 00000000000..da29f99b2e9 Binary files /dev/null and b/images/POST.jpg differ diff --git a/images/PUT.jpg b/images/PUT.jpg new file mode 100644 index 00000000000..44e040a03a9 Binary files /dev/null and b/images/PUT.jpg differ diff --git a/models/contacts.js b/models/contacts.js index 409d11c7c09..f22501431a2 100644 --- a/models/contacts.js +++ b/models/contacts.js @@ -1,19 +1,64 @@ -// const fs = require('fs/promises') +import fs from "fs/promises"; +import path from "path"; +import { nanoid } from "nanoid"; -const listContacts = async () => {} +const contactsPath = path.join("models", "contacts.json"); -const getContactById = async (contactId) => {} +const listContacts = async () => { + const contacts = await fs.readFile(contactsPath); + return JSON.parse(contacts); +}; -const removeContact = async (contactId) => {} +const getContactById = async (contactId) => { + const contacts = await listContacts(); + const result = contacts.find((contact) => contact.id === contactId); + return result || null; +}; -const addContact = async (body) => {} +const removeContact = async (contactId) => { + const contacts = await listContacts(); + const index = contacts.findIndex((item) => item.id === contactId); -const updateContact = async (contactId, body) => {} + if (index === -1) { + return null; + } -module.exports = { - listContacts, - getContactById, - removeContact, - addContact, - updateContact, -} + const deletedContact = contacts.splice(index, 1); + await fs.writeFile(contactsPath, JSON.stringify(contacts, null, 2)); + return deletedContact; +}; + +const addContact = async ({ name, email, phone }) => { + const contacts = await listContacts(); + const newContact = { + id: nanoid(), + name, + email, + phone, + }; + const allContacts = [...contacts, newContact]; + await fs.writeFile(contactsPath, JSON.stringify(allContacts, null, 2)); + return newContact; +}; + +const updateContact = async (id, { name, email, phone }) => { + const contacts = await listContacts(); + const index = contacts.findIndex((item) => item.id === id); + + if (index === -1) { + return null; + } + + contacts[index] = { + id, + name, + email, + phone, + }; + + await fs.writeFile(contactsPath, JSON.stringify(contacts, null, 2)); + return contacts[index]; +}; + +// prettier-ignore +export { listContacts, getContactById, removeContact, addContact, updateContact }; diff --git a/package-lock.json b/package-lock.json index e6d047044e5..7d9ef1d9b25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,12 @@ "name": "template", "version": "0.0.0", "dependencies": { - "cors": "2.8.5", - "cross-env": "7.0.3", - "express": "4.17.1", - "morgan": "1.10.0" + "cors": "^2.8.5", + "cross-env": "^7.0.3", + "express": "^4.17.1", + "joi": "^17.13.3", + "morgan": "^1.10.0", + "nanoid": "^5.0.7" }, "devDependencies": { "eslint": "7.19.0", @@ -141,6 +143,37 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "node_modules/@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -677,6 +710,7 @@ "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", "dependencies": { "object-assign": "^4", "vary": "^1" @@ -689,6 +723,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.1" }, @@ -1354,6 +1389,7 @@ "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "license": "MIT", "dependencies": { "accepts": "~1.3.7", "array-flatten": "1.1.1", @@ -2166,6 +2202,19 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2401,6 +2450,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "license": "MIT", "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", @@ -2439,6 +2489,24 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/nanoid": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", + "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3757,6 +3825,37 @@ "strip-json-comments": "^3.1.1" } }, + "@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -3803,8 +3902,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} + "dev": true }, "ajv": { "version": "6.12.6", @@ -4412,15 +4510,13 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", - "dev": true, - "requires": {} + "dev": true }, "eslint-config-standard": { "version": "16.0.3", "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz", "integrity": "sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg==", - "dev": true, - "requires": {} + "dev": true }, "eslint-import-resolver-node": { "version": "0.3.6", @@ -4554,8 +4650,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-5.2.0.tgz", "integrity": "sha512-SftLb1pUG01QYq2A/hGAWfDRXqYD82zE7j7TopDOyNdU+7SvvoXREls/+PRTY17vUXzXnZA/zfnyKgRH6x4JJw==", - "dev": true, - "requires": {} + "dev": true }, "eslint-scope": { "version": "5.1.1", @@ -5269,6 +5364,18 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, + "joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "requires": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5486,6 +5593,11 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "nanoid": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", + "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==" + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", diff --git a/package.json b/package.json index 5045e827160..b37b6bdd6c3 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "template", "version": "0.0.0", "private": true, + "type": "module", "scripts": { "start": "cross-env NODE_ENV=production node ./server.js", "start:dev": "cross-env NODE_ENV=development nodemon ./server.js", @@ -9,10 +10,12 @@ "lint:fix": "eslint --fix **/*.js" }, "dependencies": { - "cors": "2.8.5", - "cross-env": "7.0.3", - "express": "4.17.1", - "morgan": "1.10.0" + "cors": "^2.8.5", + "cross-env": "^7.0.3", + "express": "^4.17.1", + "joi": "^17.13.3", + "morgan": "^1.10.0", + "nanoid": "^5.0.7" }, "devDependencies": { "eslint": "7.19.0", diff --git a/routes/api/contacts.js b/routes/api/contacts.js deleted file mode 100644 index a60ebd69231..00000000000 --- a/routes/api/contacts.js +++ /dev/null @@ -1,25 +0,0 @@ -const express = require('express') - -const router = express.Router() - -router.get('/', async (req, res, next) => { - res.json({ message: 'template message' }) -}) - -router.get('/:contactId', async (req, res, next) => { - res.json({ message: 'template message' }) -}) - -router.post('/', async (req, res, next) => { - res.json({ message: 'template message' }) -}) - -router.delete('/:contactId', async (req, res, next) => { - res.json({ message: 'template message' }) -}) - -router.put('/:contactId', async (req, res, next) => { - res.json({ message: 'template message' }) -}) - -module.exports = router diff --git a/routes/api/contactsRouter.js b/routes/api/contactsRouter.js new file mode 100644 index 00000000000..5644a099f36 --- /dev/null +++ b/routes/api/contactsRouter.js @@ -0,0 +1,87 @@ +import express from "express"; +// prettier-ignore +import {listContacts, getContactById, removeContact, addContact, updateContact} from "../../models/contacts.js"; +import { contactValidation } from "../../validations/validation.js"; +import { httpError } from "../../helpers/httpError.js"; + +const router = express.Router(); + +router.get("/", async (_req, res, next) => { + try { + const result = await listContacts(); + res.json(result); + } catch (error) { + next(error); + } +}); + +router.get("/:contactId", async (req, res, next) => { + try { + const { contactId } = req.params; + const result = await getContactById(contactId); + + if (!result) { + throw httpError(404); + } + + res.json(result); + } catch (error) { + next(error); + } +}); + +router.post("/", async (req, res, next) => { + try { + // Preventing lack of necessary data + const { error } = contactValidation.validate(req.body); + if (error) { + throw httpError(400, "missing required name field"); + } + + const result = await addContact(req.body); + res.status(201).json(result); + } catch (error) { + next(error); + } +}); + +router.delete("/:contactId", async (req, res, next) => { + try { + const { contactId } = req.params; + const result = await removeContact(contactId); + + if (!result) { + throw httpError(404); + } + + res.json({ + message: "Contact deleted", + }); + } catch (error) { + next(error); + } +}); + +router.put("/:contactId", async (req, res, next) => { + try { + // Preventing lack of necessary data + const { error } = contactValidation.validate(req.body); + if (error) { + throw httpError(400, "missing fields"); + } + + const { contactId } = req.params; + const result = await updateContact(contactId, req.body); + + if (!result) { + throw httpError(404); + } + + res.json(result); + } catch (error) { + next(error); + } +}); + +// module.exports = router; +export { router }; diff --git a/server.js b/server.js index fc4e4c6bb3a..067dc0f614a 100644 --- a/server.js +++ b/server.js @@ -1,4 +1,4 @@ -const app = require("./app"); +import { app } from "./app.js"; app.listen(3000, () => { console.log("Server is running. Use our API on port: 3000"); diff --git a/validations/validation.js b/validations/validation.js new file mode 100644 index 00000000000..284b6e89c09 --- /dev/null +++ b/validations/validation.js @@ -0,0 +1,11 @@ +import Joi from "joi"; + +// Define validation for adding a contact +const contactValidation = Joi.object({ + name: Joi.string().required(), + email: Joi.string().required(), + phone: Joi.string().required(), + favorite: Joi.boolean(), +}); + +export { contactValidation };