diff --git a/src/createServer.js b/src/createServer.js index 1ea5542d6..0db6917d0 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,9 +1,286 @@ 'use strict'; +const express = require('express'); + +const { User, Expense, Category } = require('./models/models'); + const createServer = () => { - // your code goes here -}; + const app = express(); + + app.use(express.json()); + + app.post('/users', async (req, res) => { + const { name } = req.body; + + if (!name) { + return res.status(400).send('Name is required'); + } + + try { + const newUser = await User.create({ name }); + + res.status(201).json(newUser); + } catch (e) { + res.status(400).send(e.message); + } + }); + + app.get('/users', async (req, res) => { + res.json(await User.findAll()); + }); + + app.get('/users/:id', async (req, res) => { + const user = await User.findByPk(req.params.id); + + if (!user) { + return res.status(404).send('User not found'); + } + res.json(user); + }); + + app.patch('/users/:id', async (req, res) => { + const user = await User.findByPk(req.params.id); + + if (!user) { + return res.status(404).send('Not found'); + } + await user.update(req.body, { silent: true }); + res.json(user); + }); + + app.delete('/users/:id', async (req, res) => { + const user = await User.findByPk(req.params.id); + + if (!user) { + return res.status(404).send('Not found'); + } + await user.destroy(); + res.sendStatus(204); + }); + + app.post('/expenses', async (req, res) => { + const { title, amount, userId, category, note, spentAt } = req.body; + + if (!title || !amount || !userId) { + return res.sendStatus(400); + } + + try { + const [foundCategory] = await Category.findOrCreate({ + where: { name: category || 'Other' }, + }); + + const expense = await Expense.create({ + title, + amount, + userId, + categoryId: foundCategory.id, + note, + + spentAt: spentAt || new Date(), + }); + + const result = expense.toJSON(); + + delete result.categoryId; + + res.status(201).json({ + ...result, + category: foundCategory.name, + }); + } catch (error) { + res.sendStatus(400); + } + }); + + app.get('/expenses', async (req, res) => { + const { userId, categories, from, to } = req.query; + const { Op } = require('sequelize'); + + const where = {}; + + if (userId) { + where.userId = userId; + } + + if (from || to) { + where.spentAt = {}; + + if (from) { + where.spentAt[Op.gte] = from; + } + + if (to) { + where.spentAt[Op.lte] = to; + } + } + + const include = [ + { + model: Category, + as: 'Category', + + ...(categories && { + where: { name: categories }, + required: true, + }), + }, + ]; + + try { + const expenses = await Expense.findAll({ + where, + include, + }); -module.exports = { - createServer, + const result = expenses.map((exp) => { + const data = exp.toJSON(); + + return { + id: data.id, + title: data.title, + amount: data.amount, + spentAt: data.spentAt, + note: data.note, + userId: data.userId, + category: data.Category ? data.Category.name : null, + }; + }); + + res.json(result); + } catch (error) { + res.sendStatus(500); + } + }); + + app.delete('/expenses/:id', async (req, res) => { + const deletedCount = await Expense.destroy({ + where: { id: req.params.id }, + }); + + if (deletedCount === 0) { + return res.sendStatus(404); + } + + res.sendStatus(204); + }); + + app.get('/expenses/:id', async (req, res) => { + const expense = await Expense.findByPk(req.params.id, { + include: [{ model: Category, as: 'Category' }], + }); + + if (!expense) { + return res.sendStatus(404); + } + + const data = expense.toJSON(); + const response = { + ...data, + category: data.Category ? data.Category.name : null, + }; + + delete response.Category; + delete response.categoryId; + + res.json(response); + }); + + app.patch('/expenses/:id', async (req, res) => { + const expense = await Expense.findByPk(req.params.id); + + if (!expense) { + return res.sendStatus(404); + } + + const { category, ...otherData } = req.body; + const updateData = { ...otherData }; + + if (category) { + const [foundCategory] = await Category.findOrCreate({ + where: { name: category }, + }); + + updateData.categoryId = foundCategory.id; + } + + await expense.update(updateData); + + const updatedExpense = await Expense.findByPk(req.params.id, { + include: [{ model: Category, as: 'Category' }], + }); + + const data = updatedExpense.toJSON(); + const response = { + ...data, + category: data.Category ? data.Category.name : null, + }; + + delete response.Category; + delete response.categoryId; + + res.json(response); + }); + + app.get('/categories', async (req, res) => { + try { + const categories = await Category.findAll({ order: [['id', 'ASC']] }); + + res.json(categories); + } catch (e) { + res.status(500).json({ error: e.message }); + } + }); + + app.post('/categories', async (req, res) => { + const { name } = req.body; + + if (!name) { + return res.status(400).send('Name is required'); + } + + try { + const [category, created] = await Category.findOrCreate({ + where: { name }, + }); + + res.status(created ? 201 : 200).json(category); + } catch (e) { + res.status(400).json({ error: e.message }); + } + }); + + app.patch('/categories/:id', async (req, res) => { + try { + const category = await Category.findByPk(req.params.id); + + if (!category) { + return res.sendStatus(404); + } + await category.update(req.body); + res.json(category); + } catch (e) { + res.status(400).json({ error: e.message }); + } + }); + + app.delete('/categories/:id', async (req, res) => { + try { + const category = await Category.findByPk(req.params.id); + + if (!category) { + return res.sendStatus(404); + } + await category.destroy(); + res.sendStatus(204); + } catch (e) { + res + .status(400) + .send('Cannot delete category: it is assigned to expenses.'); + } + }); + + return app; }; + +module.exports = { createServer }; diff --git a/src/db.js b/src/db.js index 1ba3046cc..bd44e4875 100644 --- a/src/db.js +++ b/src/db.js @@ -21,12 +21,12 @@ const { */ const sequelize = new Sequelize({ - database: POSTGRES_DB || 'postgres', + database: POSTGRES_DB || 'students', username: POSTGRES_USER || 'postgres', host: POSTGRES_HOST || 'localhost', dialect: 'postgres', port: POSTGRES_PORT || 5432, - password: POSTGRES_PASSWORD || '123', + password: POSTGRES_PASSWORD || 'postgres', }); module.exports = { diff --git a/src/index.js b/src/index.js index 541737327..ce0e6eb3c 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,24 @@ const { createServer } = require('./createServer'); -createServer().listen(5700, () => { - console.log('Server is running on localhost:5700'); -}); +const { sequelize } = require('./db'); + +const app = createServer(); +const PORT = process.env.PORT || 3000; + +async function start() { + try { + await sequelize.authenticate(); + console.log('Connection to PostgreSQL has been established successfully.'); + + await sequelize.sync({ alter: true }); + + app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); + }); + } catch (error) { + console.error('Unable to connect to the database:', error); + } +} + +start(); diff --git a/src/models/Category.model.js b/src/models/Category.model.js new file mode 100644 index 000000000..0f2c5e23d --- /dev/null +++ b/src/models/Category.model.js @@ -0,0 +1,11 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../db'); + +const Category = sequelize.define('Category', { + name: { + type: DataTypes.STRING, + allowNull: false, + }, +}); + +module.exports = { Category }; diff --git a/src/models/Expense.model.js b/src/models/Expense.model.js index 567e1c3e7..1ca84dfbf 100644 --- a/src/models/Expense.model.js +++ b/src/models/Expense.model.js @@ -1,9 +1,34 @@ 'use strict'; +const { DataTypes } = require('sequelize'); const { sequelize } = require('../db.js'); const Expense = sequelize.define( - // your code goes here + 'Expense', + { + title: { + type: DataTypes.STRING, + allowNull: false, + }, + amount: { + type: DataTypes.INTEGER, + allowNull: false, + }, + + spentAt: { + type: DataTypes.DATE, + allowNull: true, + defaultValue: DataTypes.NOW, + }, + note: { + type: DataTypes.TEXT, + allowNull: true, + }, + }, + { + tableName: 'expenses', + timestamps: false, + }, ); module.exports = { diff --git a/src/models/User.model.js b/src/models/User.model.js index 61861c9e4..e2e2ff33b 100644 --- a/src/models/User.model.js +++ b/src/models/User.model.js @@ -1,9 +1,25 @@ '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, + }, + }, + { + hooks: { + beforeBulkDestroy: (options) => { + if (options.truncate) { + options.truncate = false; + } + }, + }, + }, ); module.exports = { diff --git a/src/models/models.js b/src/models/models.js index b43b55752..ef22a6819 100644 --- a/src/models/models.js +++ b/src/models/models.js @@ -1,11 +1,22 @@ -'use strict'; - const { User } = require('./User.model'); const { Expense } = require('./Expense.model'); +const { Category } = require('./Category.model'); + +User.hasMany(Expense, { foreignKey: 'userId' }); +Expense.belongsTo(User, { foreignKey: 'userId' }); + +Category.hasMany(Expense, { foreignKey: 'categoryId' }); +Expense.belongsTo(Category, { foreignKey: 'categoryId' }); + +const models = { + User, + Expense, + Category, +}; module.exports = { - models: { - User, - Expense, - }, + models, + User, + Expense, + Category, };