diff --git a/.env b/.env new file mode 100644 index 000000000..a705952b0 --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +POSTGRES_DB = 'postgres' +POSTGRES_USER = 'postgres' +POSTGRES_HOST = 'localhost' +POSTGRES_PORT = '5432' +POSTGRES_PASSWORD = '123123' diff --git a/.github/workflows/test.yml-template b/.github/workflows/test.yml-template new file mode 100644 index 000000000..03d2b7ff2 --- /dev/null +++ b/.github/workflows/test.yml-template @@ -0,0 +1,44 @@ +name: Test + +on: + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20.x] + services: + postgres: + image: postgres:latest + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: students + POSTGRES_PORT: 5432 + POSTGRES_HOST: localhost + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v2 + - name: Set up Node.js + uses: actions/setup-node@v1 + with: + node-version: '20' + - name: Install dependencies + run: npm install + - name: Run tests + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: students + POSTGRES_HOST: localhost + POSTGRES_PORT: 5432 + run: npm test diff --git a/.gitignore b/.gitignore index ed48a299d..bd6a178a8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,3 @@ node_modules # MacOS .DS_Store - -# env files -*.env -.env* diff --git a/package-lock.json b/package-lock.json index cd25728ae..5e567a038 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,14 +10,14 @@ "hasInstallScript": true, "license": "GPL-3.0", "dependencies": { - "cors": "^2.8.5", + "cors": "^2.8.6", "express": "^4.19.2", "pg": "^8.12.0", "sequelize": "^6.37.3" }, "devDependencies": { "@mate-academy/eslint-config": "latest", - "@mate-academy/scripts": "^1.8.6", + "@mate-academy/scripts": "^2.1.3", "axios": "^1.7.2", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", @@ -1474,10 +1474,11 @@ } }, "node_modules/@mate-academy/scripts": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.6.tgz", - "integrity": "sha512-b4om/whj4G9emyi84ORE3FRZzCRwRIesr8tJHXa8EvJdOaAPDpzcJ8A0sFfMsWH9NUOVmOwkBtOXDu5eZZ00Ig==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-2.1.3.tgz", + "integrity": "sha512-a07wHTj/1QUK2Aac5zHad+sGw4rIvcNl5lJmJpAD7OxeSbnCdyI6RXUHwXhjF5MaVo9YHrJ0xVahyERS2IIyBQ==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/rest": "^17.11.2", "@types/get-port": "^4.2.0", @@ -3009,15 +3010,20 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", "dependencies": { "object-assign": "^4", "vary": "^1" }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/create-jest": { diff --git a/package.json b/package.json index bdf079250..2c50223e6 100644 --- a/package.json +++ b/package.json @@ -16,14 +16,14 @@ "author": "Mate academy", "license": "GPL-3.0", "dependencies": { - "cors": "^2.8.5", + "cors": "^2.8.6", "express": "^4.19.2", "pg": "^8.12.0", "sequelize": "^6.37.3" }, "devDependencies": { "@mate-academy/eslint-config": "latest", - "@mate-academy/scripts": "^1.8.6", + "@mate-academy/scripts": "^2.1.3", "axios": "^1.7.2", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", diff --git a/src/controllers/expenses.controller.js b/src/controllers/expenses.controller.js new file mode 100644 index 000000000..b10e90a6c --- /dev/null +++ b/src/controllers/expenses.controller.js @@ -0,0 +1,149 @@ +/* eslint-disable curly */ +const expenses = require('../entitys/expenses.reposytory.js'); +const users = require('../entitys/users.reposytory.js'); + +const isDate = (str) => { + const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/; + + // eslint-disable-next-line curly + if (!isoRegex.test(str)) return false; + + const date = new Date(str); + + return !isNaN(date.getTime()); +}; + +const checkDataForUpdate = (o) => { + return Object.keys(o).every((k) => { + switch (k) { + case 'spentAt': + return isDate(o[k]); + case 'title': + case 'category': + case 'note': + return typeof o[k] === 'string'; + case 'amount': + return typeof o[k] === 'number'; + default: + return false; + } + }); +}; + +const getAll = async (req, res) => { + const filterOptions = { userId: 'userId', categories: 'category' }; + const filterParams = {}; + + for (const key in req.query) { + if (filterOptions[key]) filterParams[filterOptions[key]] = req.query[key]; + } + + res.send(await expenses.getAll(filterParams)); +}; + +const create = async (req, res) => { + const { userId, spentAt, title, amount, category, note } = req.body; + + const intUserId = Number(userId); + + const userExist = !Number.isNaN(intUserId) + ? await users.getById(intUserId) + : null; + + if ( + !userExist || + !isDate(spentAt) || + typeof title !== 'string' || + typeof amount !== 'number' + ) { + res.sendStatus(400); + + return; + } + + const newExpense = { + userId: intUserId, + spentAt, + title, + amount, + }; + + if (typeof note === 'string') newExpense.note = note; + if (typeof category === 'string') newExpense.category = category; + + res.statusCode = 201; + res.send(await expenses.create(newExpense)); +}; + +const getById = async (req, res) => { + const { id } = req.params; + const intId = Number(id); + + if (Number.isNaN(intId)) { + res.sendStatus(400); + + return; + } + + const expense = await expenses.getById(intId); + + if (!expense) { + res.sendStatus(404); + + return; + } + + res.send(expense); +}; + +const remove = async (req, res) => { + const { id } = req.params; + + const intId = Number(id); + + if (Number.isNaN(intId)) { + res.sendStatus(400); + + return; + } + + const result = await expenses.remove(intId); + + if (result <= 0) { + res.sendStatus(404); + + return; + } + + res.send(204); +}; + +const update = async (req, res) => { + const { id } = req.params; + + const intId = Number(id); + + if (Number.isNaN(intId) || !checkDataForUpdate(req.body)) { + res.sendStatus(400); + + return; + } + + const [, expense] = await expenses.update(intId, req.body); + + if (!expense[0]) { + res.sendStatus(404); + + return; + } + + res.send(expense[0]); +}; + +module.exports = { + getAll, + create, + getById, + remove, + update, +}; diff --git a/src/controllers/users.controller.js b/src/controllers/users.controller.js new file mode 100644 index 000000000..11e77424b --- /dev/null +++ b/src/controllers/users.controller.js @@ -0,0 +1,101 @@ +const users = require('../entitys/users.reposytory'); + +// const isUUID = (d) => { +// // eslint-disable-next-line max-len, prettier/prettier +// const pattern = +// /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +// return pattern.test(d); +// }; + +const getAll = async (req, res) => { + res.send(await users.getAll()); +}; + +const create = async (req, res) => { + const { name } = req.body; + + if (typeof name !== 'string') { + res.sendStatus(400); + + return; + } + + res.statusCode = 201; + res.send(await users.create({ name })); +}; + +const getById = async (req, res) => { + const { id } = req.params; + + const intId = Number(id); + + if (Number.isNaN(intId)) { + res.sendStatus(400); + + return; + } + + const user = await users.getById(intId); + + if (!user) { + res.sendStatus(404); + + return; + } + + res.send(user); +}; + +const removeById = async (req, res) => { + const { id } = req.params; + + const intId = Number(id); + + if (Number.isNaN(intId)) { + res.sendStatus(400); + + return; + } + + const result = await users.remove(intId); + + if (result <= 0) { + res.sendStatus(404); + + return; + } + + res.sendStatus(204); +}; + +const update = async (req, res) => { + const { name } = req.body; + const { id } = req.params; + + const intId = Number(id); + + if (typeof name !== 'string' || Number.isNaN(intId)) { + res.sendStatus(400); + + return; + } + + const [, updatedUsers] = await users.update(intId, { name }); + + if (!updatedUsers[0]) { + res.sendStatus(404); + + return; + } + + res.send(updatedUsers[0]); +}; + +module.exports = { + getAll, + create, + getById, + removeById, + update, +}; diff --git a/src/createServer.js b/src/createServer.js index 1ea5542d6..5ccd11e7f 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,7 +1,19 @@ 'use strict'; +const cors = require('cors'); +const express = require('express'); +const { router: usersRouter } = require('./routers/users.router.js'); +const { router: expensesRouter } = require('./routers/expenses.router.js'); + const createServer = () => { - // your code goes here + const app = express(); + + app.use(cors(), express.json()); + + app.use('/users', usersRouter); + app.use('/expenses', expensesRouter); + + return app; }; module.exports = { diff --git a/src/entitys/expenses.reposytory.js b/src/entitys/expenses.reposytory.js new file mode 100644 index 000000000..b68a540bd --- /dev/null +++ b/src/entitys/expenses.reposytory.js @@ -0,0 +1,30 @@ +const { models } = require('../models/models.js'); +const Expense = models.Expense; + +const getAll = (filterParams = {}) => { + return Expense.findAll({ where: filterParams }); +}; + +const create = (data) => { + return Expense.create(data); +}; + +const getById = (id) => { + return Expense.findByPk(id); +}; + +const remove = (id) => { + return Expense.destroy({ where: { id } }); +}; + +const update = (id, data) => { + return Expense.update(data, { where: { id }, returning: true }); +}; + +module.exports = { + getAll, + create, + getById, + remove, + update, +}; diff --git a/src/entitys/users.reposytory.js b/src/entitys/users.reposytory.js new file mode 100644 index 000000000..0d4ada8d8 --- /dev/null +++ b/src/entitys/users.reposytory.js @@ -0,0 +1,34 @@ +const { models } = require('../models/models.js'); +const User = models.User; + +const getAll = () => { + return User.findAll(); +}; + +const create = ({ name }) => { + return User.create({ name }); +}; + +const getById = (id) => { + return User.findByPk(id); +}; + +const remove = (id) => { + return User.destroy({ + where: { + id, + }, + }); +}; + +const update = (id, { name }) => { + return User.update({ name }, { where: { id }, returning: true }); +}; + +module.exports = { + getAll, + create, + getById, + remove, + update, +}; diff --git a/src/models/Expense.model.js b/src/models/Expense.model.js index 567e1c3e7..792b02288 100644 --- a/src/models/Expense.model.js +++ b/src/models/Expense.model.js @@ -1,9 +1,44 @@ 'use strict'; +const { DataTypes } = require('sequelize'); const { sequelize } = require('../db.js'); const Expense = sequelize.define( - // your code goes here + 'Expense', + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + spentAt: { + type: DataTypes.DATE, + allowNull: false, + }, + title: { + type: DataTypes.STRING, + allowNull: false, + }, + amount: { + type: DataTypes.INTEGER, + allowNull: false, + }, + category: { + type: DataTypes.STRING, + }, + note: { + type: DataTypes.STRING, + }, + }, + { + createdAt: false, + updatedAt: false, + tableName: 'expenses', + }, ); module.exports = { diff --git a/src/models/User.model.js b/src/models/User.model.js index 61861c9e4..792a5a1dc 100644 --- a/src/models/User.model.js +++ b/src/models/User.model.js @@ -1,9 +1,26 @@ 'use strict'; +const { DataTypes } = require('sequelize'); const { sequelize } = require('../db.js'); const User = sequelize.define( - // your code goes here + 'User', + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + createdAt: false, + updatedAt: false, + tableName: 'users', + }, ); module.exports = { diff --git a/src/routers/expenses.router.js b/src/routers/expenses.router.js new file mode 100644 index 000000000..9807541c2 --- /dev/null +++ b/src/routers/expenses.router.js @@ -0,0 +1,14 @@ +const express = require('express'); +const expensesController = require('../controllers/expenses.controller.js'); + +const router = express.Router(); + +router.get('/', expensesController.getAll); +router.post('/', expensesController.create); +router.get('/:id', expensesController.getById); +router.delete('/:id', expensesController.remove); +router.patch('/:id', expensesController.update); + +module.exports = { + router, +}; diff --git a/src/routers/users.router.js b/src/routers/users.router.js new file mode 100644 index 000000000..5fac491dc --- /dev/null +++ b/src/routers/users.router.js @@ -0,0 +1,12 @@ +const express = require('express'); +const usersControllers = require('../controllers/users.controller.js'); + +const router = express.Router(); + +router.get('/', usersControllers.getAll); +router.post('/', usersControllers.create); +router.get('/:id', usersControllers.getById); +router.delete('/:id', usersControllers.removeById); +router.patch('/:id', usersControllers.update); + +module.exports = { router };