diff --git a/src/controllers/expensesController.js b/src/controllers/expensesController.js new file mode 100644 index 00000000..388ffb1a --- /dev/null +++ b/src/controllers/expensesController.js @@ -0,0 +1,86 @@ +const expensesService = require('../services/expensesService'); +const expensesHelpers = require('../helpers/expense.helper'); +const userHelpers = require('../helpers/user.helper'); +const STATUS_CODE = require('../utils/statusCodes'); + +const getAll = async (req, res) => { + const expenses = await expensesService.getExpenses(req.query); + + res + .status(STATUS_CODE.SUCCESS) + .send(expenses.map((expense) => expensesHelpers.normalize(expense))); +}; + +const getOne = async (req, res) => { + const { id } = req.params; + + if (await expensesHelpers.isExpenseExist(id)) { + return res + .status(STATUS_CODE.NOT_FOUND) + .send('Expense with such id not found'); + } + + const expense = await expensesService.getExpenseById(id); + + res.status(STATUS_CODE.SUCCESS).send(expensesHelpers.normalize(expense)); +}; + +const create = async (req, res) => { + if (await userHelpers.isUserExist(req.body.userId)) { + return res.status(STATUS_CODE.BAD_REQUEST).send('User not found'); + } + + try { + expensesHelpers.validateRequestBodyFields(req.body); + + const expense = await expensesService.create(req.body); + + res.statusCode = STATUS_CODE.CREATED; + + res.send(expense); + } catch (error) { + res.status(STATUS_CODE.BAD_REQUEST).send(error.message); + } +}; + +const remove = async (req, res) => { + const { id } = req.params; + + if (await expensesHelpers.isExpenseExist(id)) { + return res + .status(STATUS_CODE.NOT_FOUND) + .send('Expense with such id not found'); + } + + await expensesService.remove(id); + res.sendStatus(STATUS_CODE.NO_CONTENT); +}; + +const update = async (req, res) => { + const { id } = req.params; + const { title } = req.body; + + if (await expensesHelpers.isExpenseExist(id)) { + return res + .status(STATUS_CODE.NOT_FOUND) + .send('Expense with such id not found'); + } + + if (typeof title !== 'string') { + return res.sendStatus(STATUS_CODE.BAD_REQUEST); + } + + const updatedExpense = await expensesService.update({ id, title }); + + res + .status(STATUS_CODE.SUCCESS) + .send(expensesHelpers.normalize(updatedExpense)); +}; + +module.exports = { + getAll, + getOne, + create, + remove, + update, +}; diff --git a/src/controllers/usersController.js b/src/controllers/usersController.js new file mode 100644 index 00000000..6e6cfd23 --- /dev/null +++ b/src/controllers/usersController.js @@ -0,0 +1,82 @@ +const userService = require('../services/usersService'); +const userHelpers = require('../helpers/user.helper'); +const STATUS_CODE = require('../utils/statusCodes'); + +const getAll = async (_, res) => { + const users = await userService.getAll(); + + res + .status(STATUS_CODE.SUCCESS) + .send(users.map((user) => userHelpers.normalize(user))); +}; + +const getOne = async (req, res) => { + const { id } = req.params; + + if (await userHelpers.isUserExist(id, res)) { + return res + .status(STATUS_CODE.NOT_FOUND) + .send('User with such id not found'); + } + + const user = await userService.getById(id); + + res.status(STATUS_CODE.SUCCESS).send(userHelpers.normalize(user)); +}; + +const create = async (req, res) => { + const { name } = req.body; + + if (userHelpers.isNameValid(name)) { + return res + .status(STATUS_CODE.BAD_REQUEST) + .send('Error: "name" is required and must be a string.'); + } + + const user = await userService.create(name); + + res.status(STATUS_CODE.CREATED).send(userHelpers.normalize(user)); +}; + +const remove = async (req, res) => { + const { id } = req.params; + + if (await userHelpers.isUserExist(id, res)) { + return res + .status(STATUS_CODE.NOT_FOUND) + .send('User with such id not found'); + } + + await userService.remove(id); + + res.sendStatus(STATUS_CODE.NO_CONTENT); +}; + +const update = async (req, res) => { + const { id } = req.params; + const { name } = req.body; + + if (await userHelpers.isUserExist(id, res)) { + return res + .status(STATUS_CODE.NOT_FOUND) + .send('User with such id not found'); + } + + if (userHelpers.isNameValid(name)) { + return res + .status(STATUS_CODE.BAD_REQUEST) + .send('Error: "name" is required and must be a string.'); + } + + const updatedUser = await userService.update({ id, name }); + + res.status(STATUS_CODE.SUCCESS).send(userHelpers.normalize(updatedUser)); +}; + +module.exports = { + getAll, + create, + remove, + update, + getOne, +}; diff --git a/src/createServer.js b/src/createServer.js index 1ea5542d..9a56f707 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,8 +1,21 @@ 'use strict'; -const createServer = () => { - // your code goes here -}; +const express = require('express'); +const cors = require('cors'); + +const { router: usersRouter } = require('./routes/usersRouter'); +const { router: expensesRouter } = require('./routes/expensesRouter'); + +function createServer() { + const app = express(); + + app.use(express.json()); + app.use(cors()); + app.use('/users', usersRouter); + app.use('/expenses', expensesRouter); + + return app; +} module.exports = { createServer, diff --git a/src/helpers/expense.helper.js b/src/helpers/expense.helper.js new file mode 100644 index 00000000..cb99924b --- /dev/null +++ b/src/helpers/expense.helper.js @@ -0,0 +1,40 @@ +const expenseService = require('../services/expensesService'); + +const isExpenseExist = async (id) => { + if (!(await expenseService.getExpenseById(id))) { + return true; + } +}; + +const validateRequestBodyFields = ({ + userId, + title, + amount, + isCreatingExpense = true, +}) => { + if (isCreatingExpense && !userId) { + throw new Error('provide userId'); + } + + if (!title || typeof title !== 'string') { + throw new Error( + 'Invalid request: "title" is required and must be a string.', + ); + } + + if (typeof amount !== 'number') { + throw new Error('Invalid request: "amount" must be a number.'); + } +}; + +const normalize = ({ dataValues: expense }) => { + const { password, ...rest } = expense; + + return rest; +}; + +module.exports = { + isExpenseExist, + validateRequestBodyFields, + normalize, +}; diff --git a/src/helpers/user.helper.js b/src/helpers/user.helper.js new file mode 100644 index 00000000..21ce00ca --- /dev/null +++ b/src/helpers/user.helper.js @@ -0,0 +1,22 @@ +const userService = require('../services/usersService'); + +const isNameValid = (name) => { + return !name || typeof name !== 'string'; +}; + +const isUserExist = async (id) => { + return !(await userService.getById(id)); +}; + +const normalize = ({ id, name }) => { + return { + id, + name, + }; +}; + +module.exports = { + isNameValid, + isUserExist, + normalize, +}; diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js new file mode 100644 index 00000000..bad819e1 --- /dev/null +++ b/src/middleware/errorHandler.js @@ -0,0 +1,11 @@ +const errorHandler = (action) => async (req, res, next) => { + try { + await action(req, res, next); + } catch (error) { + next(error); + } +}; + +module.exports = { + errorHandler, +}; diff --git a/src/models/Expense.model.js b/src/models/Expense.model.js index 567e1c3e..19ff0c7d 100644 --- a/src/models/Expense.model.js +++ b/src/models/Expense.model.js @@ -1,9 +1,47 @@ 'use strict'; +const { DataTypes } = require('sequelize'); const { sequelize } = require('../db.js'); const Expense = sequelize.define( - // your code goes here + 'Expense', + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + spentAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + title: { + type: DataTypes.STRING, + allowNull: false, + }, + amount: { + type: DataTypes.INTEGER, + allowNull: false, + }, + category: { + type: DataTypes.STRING, + allowNull: true, + }, + note: { + type: DataTypes.STRING, + 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 61861c9e..5455256f 100644 --- a/src/models/User.model.js +++ b/src/models/User.model.js @@ -1,9 +1,31 @@ '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: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + field: 'created_at', + }, + }, + { + tableName: 'users', + updatedAt: false, + }, ); module.exports = { diff --git a/src/routes/expensesRouter.js b/src/routes/expensesRouter.js new file mode 100644 index 00000000..0ecb4d5c --- /dev/null +++ b/src/routes/expensesRouter.js @@ -0,0 +1,19 @@ +const express = require('express'); +const expensesController = require('../controllers/expensesController'); +const { errorHandler } = require('../middleware/errorHandler'); +const router = express.Router(); + +router + .route('/') + .get(errorHandler(expensesController.getAll)) + .post(errorHandler(expensesController.create)); + +router + .route('/:id') + .get(errorHandler(expensesController.getOne)) + .patch(errorHandler(expensesController.update)) + .delete(errorHandler(expensesController.remove)); + +module.exports = { + router, +}; diff --git a/src/routes/usersRouter.js b/src/routes/usersRouter.js new file mode 100644 index 00000000..085f8579 --- /dev/null +++ b/src/routes/usersRouter.js @@ -0,0 +1,18 @@ +const express = require('express'); +const userController = require('../controllers/usersController'); +const { errorHandler } = require('../middleware/errorHandler'); + +const router = express.Router(); + +router + .route('/') + .get(errorHandler(userController.getAll)) + .post(errorHandler(userController.create)); + +router + .route('/:id') + .get(errorHandler(userController.getOne)) + .delete(errorHandler(userController.remove)) + .patch(errorHandler(userController.update)); + +module.exports = { router }; diff --git a/src/services/expensesService.js b/src/services/expensesService.js new file mode 100644 index 00000000..7386c8ca --- /dev/null +++ b/src/services/expensesService.js @@ -0,0 +1,66 @@ +const { Expense } = require('../models/Expense.model'); + +const { Sequelize } = require('sequelize'); + +const getExpenses = async ({ userId, categories, from, to }) => { + const where = {}; + + if (userId) { + where.userId = userId; + } + + if (categories) { + where.category = categories; + } + + if (from || to) { + where.spentAt = {}; + + if (from) { + where.spentAt[Sequelize.Op.gte] = from; + } + + if (to) { + where.spentAt[Sequelize.Op.lte] = to; + } + } + + return Expense.findAll({ + where, + }); +}; + +const getExpenseById = async (id) => { + return Expense.findByPk(id); +}; + +const create = async ({ userId, spentAt, title, amount, category, note }) => { + return Expense.create({ + userId, + spentAt, + title, + amount, + category, + note, + }); +}; + +const remove = async (id) => { + return Expense.destroy({ + where: { id }, + }); +}; + +const update = async ({ id, title }) => { + await Expense.update({ title }, { where: { id } }); + + return Expense.findByPk(id); +}; + +module.exports = { + getExpenses, + getExpenseById, + create, + remove, + update, +}; diff --git a/src/services/usersService.js b/src/services/usersService.js new file mode 100644 index 00000000..0f7e76ea --- /dev/null +++ b/src/services/usersService.js @@ -0,0 +1,33 @@ +const { User } = require('../models/User.model.js'); + +const getAll = async () => { + return User.findAll(); +}; + +const getById = async (id) => { + return User.findByPk(id); +}; + +const create = async (name) => { + return User.create({ name }); +}; + +const remove = async (id) => { + return User.destroy({ + where: { id }, + }); +}; + +const update = async ({ id, name }) => { + await User.update({ name }, { where: { id } }); + + return User.findByPk(id); +}; + +module.exports = { + getAll, + getById, + create, + remove, + update, +}; diff --git a/src/utils/statusCodes.js b/src/utils/statusCodes.js new file mode 100644 index 00000000..a81362e2 --- /dev/null +++ b/src/utils/statusCodes.js @@ -0,0 +1,9 @@ +const STATUS_CODE = { + SUCCESS: 200, + CREATED: 201, + NO_CONTENT: 204, + BAD_REQUEST: 400, + NOT_FOUND: 404, +}; + +module.exports = STATUS_CODE;