diff --git a/src/createServer.js b/src/createServer.js index 1ea5542d6..ad3a8ce16 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,7 +1,467 @@ 'use strict'; +const express = require('express'); +const { Op } = require('sequelize'); +const { User } = require('./models/User.model'); +const { Expense } = require('./models/Expense.model'); +const { Category } = require('./models/Category.model'); + +const prepareExpense = (expense) => { + const { createdAt, updatedAt, ...preparedExpense } = expense.toJSON(); + + return preparedExpense; +}; + const createServer = () => { - // your code goes here + const app = express(); + + app.use(express.json()); + + app.get('/users', async (req, res) => { + const result = await User.findAll(); + + res.send(result); + }); + + app.post('/users', async (req, res) => { + const { name } = req.body; + + if (typeof name !== 'string') { + res.sendStatus(400); + + return; + } + + const user = await User.create({ name }); + + res.status(201).json(user); + }); + + app.get('/users/:id', async (req, res) => { + const id = Number(req.params.id); + + if (Number.isNaN(id)) { + res.sendStatus(400); + + return; + } + + const user = await User.findByPk(id); + + if (!user) { + res.sendStatus(404); + + return; + } + + res.json(user); + }); + + app.patch('/users/:id', async (req, res) => { + const id = Number(req.params.id); + const { name } = req.body; + + if (Number.isNaN(id)) { + return res.sendStatus(400); + } + + if (typeof name !== 'string') { + res.sendStatus(400); + + return; + } + + const user = await User.findByPk(id); + + if (!user) { + res.sendStatus(404); + + return; + } + + await user.update({ name }, { silent: true }); + + res.json(user); + }); + + app.delete('/users/:id', async (req, res) => { + const id = Number(req.params.id); + + if (Number.isNaN(id)) { + res.sendStatus(400); + + return; + } + + const deletedCount = await User.destroy({ + where: { id }, + }); + + if (deletedCount === 0) { + res.sendStatus(404); + + return; + } + + res.sendStatus(204); + }); + + app.get('/expenses', async (req, res) => { + const { userId, categories, from, to } = req.query; + + const where = {}; + + if (userId !== undefined) { + const parsedUserId = Number(userId); + + if (Number.isNaN(parsedUserId)) { + res.sendStatus(400); + + return; + } + + where.userId = parsedUserId; + } + + if (categories !== undefined) { + const categoriesArray = Array.isArray(categories) + ? categories + : [categories]; + + where.category = { + [Op.in]: categoriesArray, + }; + } + + if (from !== undefined || to !== undefined) { + where.spentAt = {}; + + if (from !== undefined) { + const fromDate = new Date(from); + + if (isNaN(fromDate.getTime())) { + res.sendStatus(400); + + return; + } + + where.spentAt[Op.gte] = fromDate; + } + + if (to !== undefined) { + const toDate = new Date(to); + + if (isNaN(toDate.getTime())) { + res.sendStatus(400); + + return; + } + + where.spentAt[Op.lte] = toDate; + } + } + + const expenses = await Expense.findAll({ where }); + + res.json(expenses.map(prepareExpense)); + }); + + app.post('/expenses', async (req, res) => { + const { userId, spentAt, title, amount, category, note } = req.body; + + if ( + typeof userId !== 'number' || + typeof spentAt !== 'string' || + typeof title !== 'string' || + typeof amount !== 'number' + ) { + res.sendStatus(400); + + return; + } + + if (category !== undefined && typeof category !== 'string') { + res.sendStatus(400); + + return; + } + + const user = await User.findByPk(userId); + + if (!user) { + res.sendStatus(400); + + return; + } + + const expense = await Expense.create({ + userId, + spentAt, + title, + amount, + ...(category !== undefined ? { category } : {}), + ...(note !== undefined ? { note } : {}), + }); + + return res.status(201).json(prepareExpense(expense)); + }); + + app.get('/expenses/:id', async (req, res) => { + const id = Number(req.params.id); + + if (Number.isNaN(id)) { + res.sendStatus(400); + + return; + } + + const expense = await Expense.findByPk(id); + + if (!expense) { + res.sendStatus(404); + + return; + } + + res.json(prepareExpense(expense)); + }); + + app.patch('/expenses/:id', async (req, res) => { + const id = Number(req.params.id); + const { spentAt, title, amount, category, note } = req.body; + + if (Number.isNaN(id)) { + res.sendStatus(400); + + return; + } + + const expense = await Expense.findByPk(id); + + if (!expense) { + res.sendStatus(404); + + return; + } + + const hasNoFields = + spentAt === undefined && + title === undefined && + amount === undefined && + category === undefined && + note === undefined; + + if (hasNoFields) { + res.sendStatus(400); + + return; + } + + if (spentAt !== undefined) { + if (typeof spentAt !== 'string') { + res.sendStatus(400); + + return; + } + + const spentAtDate = new Date(spentAt); + + if (Number.isNaN(spentAtDate.getTime())) { + res.sendStatus(400); + + return; + } + } + + if (title !== undefined && typeof title !== 'string') { + res.sendStatus(400); + + return; + } + + if (amount !== undefined && typeof amount !== 'number') { + res.sendStatus(400); + + return; + } + + if (category !== undefined && typeof category !== 'string') { + res.sendStatus(400); + + return; + } + + if (note !== undefined && note !== null && typeof note !== 'string') { + res.sendStatus(400); + + return; + } + + const updatedFields = {}; + + if (spentAt !== undefined) { + updatedFields.spentAt = spentAt; + } + + if (title !== undefined) { + updatedFields.title = title; + } + + if (amount !== undefined) { + updatedFields.amount = amount; + } + + if (category !== undefined) { + updatedFields.category = category; + } + + if (note !== undefined) { + updatedFields.note = note; + } + + await expense.update(updatedFields); + + res.json(prepareExpense(expense)); + }); + + app.delete('/expenses/:id', async (req, res) => { + const id = Number(req.params.id); + + if (Number.isNaN(id)) { + res.sendStatus(400); + + return; + } + + const deletedCount = await Expense.destroy({ + where: { id }, + }); + + if (deletedCount === 0) { + res.sendStatus(404); + + return; + } + + res.sendStatus(204); + }); + + app.get('/categories', async (req, res) => { + const categories = await Category.findAll(); + + res.json(categories); + }); + + app.post('/categories', async (req, res) => { + const { name } = req.body; + + if (typeof name !== 'string' || !name.trim()) { + res.sendStatus(400); + + return; + } + + const existingCategory = await Category.findOne({ + where: { name: name.trim() }, + }); + + if (existingCategory) { + res.sendStatus(400); + + return; + } + + const category = await Category.create({ + name: name.trim(), + }); + + res.status(201).json(category); + }); + + app.get('/categories/:id', async (req, res) => { + const id = Number(req.params.id); + + if (Number.isNaN(id)) { + res.sendStatus(400); + + return; + } + + const category = await Category.findByPk(id); + + if (!category) { + res.sendStatus(404); + + return; + } + + res.json(category); + }); + + app.patch('/categories/:id', async (req, res) => { + const id = Number(req.params.id); + const { name } = req.body; + + if (Number.isNaN(id)) { + res.sendStatus(400); + + return; + } + + if (typeof name !== 'string' || !name.trim()) { + res.sendStatus(400); + + return; + } + + const category = await Category.findByPk(id); + + if (!category) { + res.sendStatus(404); + + return; + } + + const existingCategory = await Category.findOne({ + where: { name: name.trim() }, + }); + + if (existingCategory && existingCategory.id !== id) { + res.sendStatus(400); + + return; + } + + await category.update({ + name: name.trim(), + }); + + res.json(category); + }); + + app.delete('/categories/:id', async (req, res) => { + const id = Number(req.params.id); + + if (Number.isNaN(id)) { + res.sendStatus(400); + + return; + } + + const deletedCount = await Category.destroy({ + where: { id }, + }); + + if (deletedCount === 0) { + res.sendStatus(404); + + return; + } + + res.sendStatus(204); + }); + + return app; }; module.exports = { diff --git a/src/db.js b/src/db.js index 1ba3046cc..8226c6fc0 100644 --- a/src/db.js +++ b/src/db.js @@ -26,7 +26,7 @@ const sequelize = new Sequelize({ host: POSTGRES_HOST || 'localhost', dialect: 'postgres', port: POSTGRES_PORT || 5432, - password: POSTGRES_PASSWORD || '123', + password: POSTGRES_PASSWORD, }); module.exports = { diff --git a/src/models/Category.model.js b/src/models/Category.model.js new file mode 100644 index 000000000..53ebcb9af --- /dev/null +++ b/src/models/Category.model.js @@ -0,0 +1,23 @@ +'use strict'; + +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../db.js'); + +const Category = sequelize.define( + 'Category', + { + name: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + }, + { + tableName: 'categories', + timestamps: false, + }, +); + +module.exports = { + Category, +}; diff --git a/src/models/Expense.model.js b/src/models/Expense.model.js index 567e1c3e7..0f83fbd10 100644 --- a/src/models/Expense.model.js +++ b/src/models/Expense.model.js @@ -1,10 +1,34 @@ 'use strict'; +const { DataTypes } = require('sequelize'); const { sequelize } = require('../db.js'); -const Expense = sequelize.define( - // your code goes here -); +const Expense = sequelize.define('Expense', { + 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, + allowNull: true, + }, + note: { + type: DataTypes.STRING, + allowNull: true, + }, +}); module.exports = { Expense, diff --git a/src/models/User.model.js b/src/models/User.model.js index 61861c9e4..d159c5182 100644 --- a/src/models/User.model.js +++ b/src/models/User.model.js @@ -1,10 +1,14 @@ 'use strict'; +const { DataTypes } = require('sequelize'); const { sequelize } = require('../db.js'); -const User = sequelize.define( - // your code goes here -); +const User = sequelize.define('User', { + name: { + type: DataTypes.STRING, + allowNull: false, + }, +}); module.exports = { User, 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, }, };