diff --git a/package-lock.json b/package-lock.json index cd25728ae..03a0a7224 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ }, "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", diff --git a/package.json b/package.json index bdf079250..e7103013c 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ }, "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/controlers/categories.controler.js b/src/controlers/categories.controler.js new file mode 100644 index 000000000..861058d07 --- /dev/null +++ b/src/controlers/categories.controler.js @@ -0,0 +1,84 @@ +const categoryServices = require('../services/categories.services'); + +const getAll = async (req, res) => { + const categories = await categoryServices.get(); + + res.status(200).send(categories); +}; + +const getOne = async (req, res) => { + const { id } = req.params; + const normalaizedId = Number(id); + + if (isNaN(normalaizedId)) { + return res.status(400).send('Bad request'); + } + + const category = await categoryServices.getOne(normalaizedId); + + if (!category) { + return res.status(404).send('Not found'); + } + + res.status(200).send(category); +}; + +const add = async (req, res) => { + const { title } = req.body; + + if (!title || typeof title !== 'string') { + return res.status(400).send('Bad request'); + } + + const newCategory = await categoryServices.add(title.trim()); + + res.status(201).send(newCategory); +}; + +const remove = async (req, res) => { + const { id } = req.params; + const normalaizedId = Number(id); + + if (isNaN(normalaizedId)) { + return res.status(400).send('Bad request'); + } + + const category = await categoryServices.getOne(normalaizedId); + + if (!category) { + return res.status(404).send('Not found'); + } + + await categoryServices.remove(normalaizedId); + res.sendStatus(204); +}; + +const update = async (req, res) => { + const { id } = req.params; + const { title } = req.body; + const normalaizedId = Number(id); + + if (isNaN(normalaizedId) || !title || typeof title !== 'string') { + return res.status(400).send('Bad request'); + } + + const category = await categoryServices.getOne(normalaizedId); + + if (!category) { + return res.status(404).send('Not found'); + } + + await categoryServices.update({ id: normalaizedId, title: title.trim() }); + + const updatedCategory = await categoryServices.getOne(normalaizedId); + + res.status(200).send(updatedCategory); +}; + +module.exports = { + getAll, + getOne, + add, + remove, + update, +}; diff --git a/src/controlers/expenses.controler.js b/src/controlers/expenses.controler.js new file mode 100644 index 000000000..e72064072 --- /dev/null +++ b/src/controlers/expenses.controler.js @@ -0,0 +1,200 @@ +const expenseServices = require('../services/expenses.services'); +const userServices = require('../services/user.services'); + +const getAll = async (req, res) => { + let { userId, categories, from, to } = req.query; + + if (userId) { + userId = Number(userId); + + if (isNaN(userId)) { + return res.status(400).send('Bad request'); + } + } else { + userId = null; + } + + if (categories && categories.length > 0) { + categories = categories + .split(',') + .map((c) => c.trim()) + .filter(Boolean); + } else { + categories = null; + } + + if (from) { + const parsed = Date.parse(from); + + if (isNaN(parsed)) { + return res.status(400).send('Bad request'); + } + + from = new Date(parsed); + } else { + from = null; + } + + if (to) { + const parsed = Date.parse(to); + + if (isNaN(parsed)) { + return res.status(400).send('Bad request'); + } + + to = new Date(parsed); + } else { + to = null; + } + + const data = await expenseServices.get({ + userId, + categories, + from, + to, + }); + + res.status(200).send(data); +}; + +const getOne = async (req, res) => { + const { id } = req.params; + const normalaizedId = Number(id); + + if (isNaN(normalaizedId)) { + return res.status(400).send('Bad request'); + } + + const expense = await expenseServices.getOne(normalaizedId); + + if (!expense) { + return res.status(404).send('Not found'); + } + + res.status(200).send(expense); +}; + +const add = async (req, res) => { + const { userId, spentAt, title, amount, category, note } = req.body; + + const normalaizedId = Number(userId); + const normalaizedAmount = Number(amount); + const normalaizedTitle = title ? title.trim() : null; + const normalaizedSpentAt = spentAt ? Date.parse(spentAt) : Date.now(); + + const checker = + isNaN(normalaizedId) || + isNaN(normalaizedAmount) || + !normalaizedTitle || + isNaN(normalaizedSpentAt); + + if (checker) { + return res.status(400).send('Bad request'); + } + + const user = await userServices.getOne(normalaizedId); + + if (!user) { + return res.status(400).send('Not found'); + } + + const newExpense = await expenseServices.add({ + userId: normalaizedId, + title: normalaizedTitle, + amount: normalaizedAmount, + spentAt: normalaizedSpentAt, + category, + note: note ? note.trim() : null, + }); + + res.status(201).send(newExpense); +}; + +const remove = async (req, res) => { + const { id } = req.params; + const narmalaizedId = Number(id); + + if (isNaN(narmalaizedId)) { + return res.status(400).send('Bad request'); + } + + const expense = await expenseServices.getOne(narmalaizedId); + + if (!expense) { + return res.status(404).send('Not found'); + } + + await expenseServices.remove(narmalaizedId); + res.sendStatus(204); +}; + +const update = async (req, res) => { + const { id } = req.params; + const { spentAt, title, amount, category, note } = req.body; + + const normalaizedId = Number(id); + + if (isNaN(normalaizedId)) { + return res.status(400).send('Bad request'); + } + + const expense = await expenseServices.getOne(normalaizedId); + + if (!expense) { + return res.status(404).send('Not found'); + } + + const data = {}; + + if (title !== undefined) { + const normalaizedTitle = title.trim(); + + if (!normalaizedTitle) { + return res.status(400).send('Bad request'); + } + + data.title = normalaizedTitle; + } + + if (amount !== undefined) { + const normalaizedAmount = Number(amount); + + if (isNaN(normalaizedAmount)) { + return res.status(400).send('Bad request'); + } + + data.amount = normalaizedAmount; + } + + if (spentAt !== undefined) { + const normalaizedSpentAt = Date.parse(spentAt); + + if (isNaN(normalaizedSpentAt)) { + return res.status(400).send('Bad request'); + } + + data.spentAt = new Date(normalaizedSpentAt); + } + + if (category !== undefined) { + data.category = category || null; + } + + if (note !== undefined) { + data.note = note.trim() || null; + } + + await expenseServices.update({ id: normalaizedId, ...data }); + + const updatedExpense = await expenseServices.getOne(normalaizedId); + + res.status(200).send(updatedExpense); +}; + +module.exports = { + getAll, + getOne, + add, + remove, + update, +}; diff --git a/src/controlers/user.controler.js b/src/controlers/user.controler.js new file mode 100644 index 000000000..ae4f01bec --- /dev/null +++ b/src/controlers/user.controler.js @@ -0,0 +1,88 @@ +const userServices = require('../services/user.services'); + +const getAll = async (req, res) => { + const users = await userServices.getAll(); + + res.status(200).send(users); +}; + +const getOne = async (req, res) => { + const { id } = req.params; + const normalaizedId = Number(id); + + if (isNaN(normalaizedId)) { + return res.status(400).send('Bad request'); + } + + const user = await userServices.getOne(normalaizedId); + + if (!user) { + return res.status(404).send('Not found'); + } + + res.status(200).send(user); +}; + +const add = async (req, res) => { + const { name } = req.body; + + if (!name || !name.trim()) { + return res.status(400).send('Bad request'); + } + + const normalaizedName = name.trim(); + + const newUser = await userServices.add(normalaizedName); + + res.status(201).send(newUser); +}; + +const remove = async (req, res) => { + const { id } = req.params; + const normalaizedId = Number(id); + + if (isNaN(normalaizedId)) { + return res.status(400).send('Bad request'); + } + + const userForRemove = await userServices.getOne(normalaizedId); + + if (!userForRemove) { + return res.status(404).send('Not found'); + } + + await userServices.remove(normalaizedId); + res.sendStatus(204); +}; + +const update = async (req, res) => { + const { id } = req.params; + const { name } = req.body; + const normalaizedId = Number(id); + + if (isNaN(normalaizedId) || !name || !name.trim()) { + return res.status(400).send('Bad request'); + } + + const normalaizedName = name.trim(); + + const userForUpdate = await userServices.getOne(normalaizedId); + + if (!userForUpdate) { + return res.status(404).send('Not Found'); + } + + await userServices.update({ id: normalaizedId, name: normalaizedName }); + + const updatedUser = await userServices.getOne(normalaizedId); + + res.status(200).send(updatedUser); +}; + +module.exports = { + getAll, + getOne, + update, + remove, + add, +}; diff --git a/src/createServer.js b/src/createServer.js index 1ea5542d6..2c132af61 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,7 +1,25 @@ 'use strict'; +const express = require('express'); +const cors = require('cors'); +const userRouter = require('./routes/user.routes'); +const expensesRouter = require('./routes/expenses.routes'); +const categoriesRouter = require('./routes/categories.routes'); + const createServer = () => { - // your code goes here + const app = express(); + + app.use(cors()); + + app.use(express.json({ limit: '1mb' })); + + app.use('/users', userRouter); + + app.use('/expenses', expensesRouter); + + app.use('/categories', categoriesRouter); + + return app; }; module.exports = { diff --git a/src/db.js b/src/db.js index 1ba3046cc..b9ad3928e 100644 --- a/src/db.js +++ b/src/db.js @@ -3,7 +3,6 @@ const { Sequelize } = require('sequelize'); const utils = require('util'); -// Needed for testing purposes, do not remove require('dotenv').config(); global.TextEncoder = utils.TextEncoder; @@ -15,18 +14,13 @@ const { POSTGRES_DB, } = process.env; -/* - All credentials setted to default values (exsept password - it is exapmle) - replace if needed with your own -*/ - const sequelize = new Sequelize({ database: POSTGRES_DB || 'postgres', username: POSTGRES_USER || 'postgres', host: POSTGRES_HOST || 'localhost', dialect: 'postgres', port: POSTGRES_PORT || 5432, - password: POSTGRES_PASSWORD || '123', + password: POSTGRES_PASSWORD || 'Mocliamg1', }); module.exports = { diff --git a/src/models/Category.model.js b/src/models/Category.model.js new file mode 100644 index 000000000..c9504ec92 --- /dev/null +++ b/src/models/Category.model.js @@ -0,0 +1,25 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../db.js'); + +const Category = sequelize.define( + 'Category', + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + title: { + type: DataTypes.TEXT, + allowNull: false, + unique: true, + }, + }, + { + tableName: 'categories', + updatedAt: false, + createdAt: false, + }, +); + +module.exports = { Category }; diff --git a/src/models/Expense.model.js b/src/models/Expense.model.js index 567e1c3e7..47c4df67e 100644 --- a/src/models/Expense.model.js +++ b/src/models/Expense.model.js @@ -1,9 +1,42 @@ 'use strict'; +const { DataTypes } = require('sequelize'); const { sequelize } = require('../db.js'); const Expense = sequelize.define( - // your code goes here + 'Expense', + { + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + spentAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + title: { + type: DataTypes.TEXT, + allowNull: false, + }, + amount: { + type: DataTypes.FLOAT, + allowNull: false, + }, + category: { + type: DataTypes.TEXT, + allowNull: true, + }, + note: { + type: DataTypes.TEXT, + allowNull: true, + }, + }, + { + tableName: 'expenses', + createdAt: false, + updatedAt: false, + }, ); module.exports = { diff --git a/src/models/User.model.js b/src/models/User.model.js index 61861c9e4..b1edee536 100644 --- a/src/models/User.model.js +++ b/src/models/User.model.js @@ -1,9 +1,21 @@ 'use strict'; +const { DataTypes } = require('sequelize'); const { sequelize } = require('../db.js'); const User = sequelize.define( - // your code goes here + 'User', + { + name: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + tableName: 'users', + createdAt: false, + updatedAt: false, + }, ); module.exports = { diff --git a/src/models/models.js b/src/models/models.js index b43b55752..544e0a26a 100644 --- a/src/models/models.js +++ b/src/models/models.js @@ -2,10 +2,12 @@ const { User } = require('./User.model'); const { Expense } = require('./Expense.model'); +const { Category } = require('./Category.model'); module.exports = { models: { User, Expense, + Category, }, }; diff --git a/src/routes/categories.routes.js b/src/routes/categories.routes.js new file mode 100644 index 000000000..6b8881464 --- /dev/null +++ b/src/routes/categories.routes.js @@ -0,0 +1,12 @@ +const express = require('express'); +const categoriesControler = require('../controlers/categories.controler'); + +const router = express.Router(); + +router.get('/', categoriesControler.getAll); +router.get('/:id', categoriesControler.getOne); +router.post('/', categoriesControler.add); +router.delete('/:id', categoriesControler.remove); +router.patch('/:id', categoriesControler.update); + +module.exports = router; diff --git a/src/routes/expenses.routes.js b/src/routes/expenses.routes.js new file mode 100644 index 000000000..748cbceb4 --- /dev/null +++ b/src/routes/expenses.routes.js @@ -0,0 +1,16 @@ +const express = require('express'); +const expenseControler = require('../controlers/expenses.controler'); + +const router = express.Router(); + +router.get('/', expenseControler.getAll); + +router.get('/:id', expenseControler.getOne); + +router.post('/', expenseControler.add); + +router.delete('/:id', expenseControler.remove); + +router.patch('/:id', expenseControler.update); + +module.exports = router; diff --git a/src/routes/user.routes.js b/src/routes/user.routes.js new file mode 100644 index 000000000..d1cf0acb1 --- /dev/null +++ b/src/routes/user.routes.js @@ -0,0 +1,16 @@ +const express = require('express'); +const usersControler = require('../controlers/user.controler'); + +const router = express.Router(); + +router.get('/', usersControler.getAll); + +router.get('/:id', usersControler.getOne); + +router.post('/', usersControler.add); + +router.delete('/:id', usersControler.remove); + +router.patch('/:id', usersControler.update); + +module.exports = router; diff --git a/src/services/categories.services.js b/src/services/categories.services.js new file mode 100644 index 000000000..7220bd718 --- /dev/null +++ b/src/services/categories.services.js @@ -0,0 +1,29 @@ +const { + models: { Category }, +} = require('../models/models'); + +const get = async () => Category.findAll(); + +const getOne = async (id) => Category.findByPk(id); + +const add = async (title) => { + const newCategory = await Category.create({ title }); + + return newCategory; +}; + +const remove = async (id) => { + await Category.destroy({ where: { id } }); +}; + +const update = async ({ id, title }) => { + await Category.update({ title }, { where: { id } }); +}; + +module.exports = { + get, + getOne, + add, + remove, + update, +}; diff --git a/src/services/expenses.services.js b/src/services/expenses.services.js new file mode 100644 index 000000000..8d0eb3dae --- /dev/null +++ b/src/services/expenses.services.js @@ -0,0 +1,53 @@ +const { Op } = require('sequelize'); +const { models } = require('../models/models'); +const Expenses = models.Expense; + +const get = async ({ userId, categories, from, to }) => { + const where = {}; + + if (userId) { + where.userId = userId; + } + + if (categories) { + where.category = { [Op.in]: categories }; + } + + if (from || to) { + where.spentAt = {}; + + if (from) { + where.spentAt[Op.gte] = from; + } + + if (to) { + where.spentAt[Op.lte] = to; + } + } + + return Expenses.findAll({ where: where }); +}; + +const getOne = async (id) => Expenses.findByPk(id); + +const add = async (data) => { + const newExpense = await Expenses.create(data); + + return newExpense; +}; + +const remove = async (id) => { + await Expenses.destroy({ where: { id } }); +}; + +const update = async ({ id, ...data }) => { + await Expenses.update({ ...data }, { where: { id } }); +}; + +module.exports = { + get, + getOne, + add, + remove, + update, +}; diff --git a/src/services/user.services.js b/src/services/user.services.js new file mode 100644 index 000000000..f40c0ecb6 --- /dev/null +++ b/src/services/user.services.js @@ -0,0 +1,38 @@ +const { models } = require('../models/models'); +const User = models.User; + +const getAll = async () => { + const data = await User.findAll(); + + return data; +}; + +const getOne = async (id) => { + return User.findByPk(id); +}; + +const add = async (name) => { + const newTodo = await User.create({ name }); + + return newTodo; +}; + +const remove = async (id) => { + await User.destroy({ + where: { + id, + }, + }); +}; + +const update = async ({ id, name }) => { + await User.update({ name }, { where: { id } }); +}; + +module.exports = { + getAll, + getOne, + add, + remove, + update, +};