diff --git a/src/createServer.js b/src/createServer.js index 1ea5542d6..05bba649a 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,7 +1,19 @@ 'use strict'; +const express = require('express'); +const usersRouter = require('./routes/users'); +const expensesRouter = require('./routes/expenses'); +const categoriesRouter = require('./routes/categories'); + const createServer = () => { - // your code goes here + const app = express(); + + app.use(express.json()); + app.use('/users', usersRouter); + app.use('/expenses', expensesRouter); + app.use('/categories', categoriesRouter); + + return app; }; module.exports = { diff --git a/src/db.js b/src/db.js index 1ba3046cc..22e238bc1 100644 --- a/src/db.js +++ b/src/db.js @@ -21,12 +21,12 @@ const { */ const sequelize = new Sequelize({ - database: POSTGRES_DB || 'postgres', - username: POSTGRES_USER || 'postgres', - host: POSTGRES_HOST || 'localhost', + database: POSTGRES_DB || 'mydb', + username: POSTGRES_USER || 'mykola', + host: POSTGRES_HOST || '20.160.160.166', dialect: 'postgres', port: POSTGRES_PORT || 5432, - password: POSTGRES_PASSWORD || '123', + password: POSTGRES_PASSWORD || 'strongpassword', }); module.exports = { diff --git a/src/models/Category.model.js b/src/models/Category.model.js new file mode 100644 index 000000000..24ad7d7c2 --- /dev/null +++ b/src/models/Category.model.js @@ -0,0 +1,22 @@ +'use strict'; + +const { sequelize } = require('../db.js'); +const { DataTypes } = require('sequelize'); + +const Category = sequelize.define( + 'Category', + { + name: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + tableName: 'categories', + timestamps: false, + }, +); + +module.exports = { + Category, +}; diff --git a/src/models/Expense.model.js b/src/models/Expense.model.js index 567e1c3e7..121ed0da0 100644 --- a/src/models/Expense.model.js +++ b/src/models/Expense.model.js @@ -2,10 +2,54 @@ const { sequelize } = require('../db.js'); +const { DataTypes } = require('sequelize'); + const Expense = sequelize.define( - // your code goes here + 'Expense', + { + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + spentAt: { + type: DataTypes.DATE, + allowNull: false, + }, + title: { + type: DataTypes.STRING, + allowNull: false, + }, + amount: { + type: DataTypes.FLOAT, + allowNull: false, + }, + categoryId: { + type: DataTypes.INTEGER, + allowNull: true, + }, + category: { + type: DataTypes.STRING, + allowNull: true, + }, + note: { + type: DataTypes.STRING, + allowNull: true, + }, + }, + { + tableName: 'expenses', + timestamps: false, + }, ); +Expense.prototype.toJSON = function () { + const values = { ...this.get() }; + + delete values.categoryId; + + return values; +}; + module.exports = { Expense, }; diff --git a/src/models/User.model.js b/src/models/User.model.js index 61861c9e4..034e6660d 100644 --- a/src/models/User.model.js +++ b/src/models/User.model.js @@ -2,8 +2,20 @@ const { sequelize } = require('../db.js'); +const { DataTypes } = require('sequelize'); + const User = sequelize.define( - // your code goes here + 'User', + { + name: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + tableName: 'users', + timestamps: false, + }, ); module.exports = { diff --git a/src/models/models.js b/src/models/models.js index b43b55752..d457639b2 100644 --- a/src/models/models.js +++ b/src/models/models.js @@ -2,10 +2,18 @@ const { User } = require('./User.model'); const { Expense } = require('./Expense.model'); +const { Category } = require('./Category.model'); + +User.hasMany(Expense, { foreignKey: 'userId', constraints: false }); +Expense.belongsTo(User, { foreignKey: 'userId', constraints: false }); + +Category.hasMany(Expense, { foreignKey: 'categoryId', constraints: false }); +Expense.belongsTo(Category, { foreignKey: 'categoryId', constraints: false }); module.exports = { models: { User, Expense, + Category, }, }; diff --git a/src/routes/categories.js b/src/routes/categories.js new file mode 100644 index 000000000..27eb6926c --- /dev/null +++ b/src/routes/categories.js @@ -0,0 +1,73 @@ +'use strict'; + +const express = require('express'); +const { + models: { Category }, +} = require('../models/models'); + +const router = express.Router(); + +router.post('/', async (req, res) => { + const { name } = req.body; + + if (!name) { + return res.status(400).json({ error: 'Missing required parameter: name' }); + } + + try { + const category = await Category.create({ name }); + + res.status(201).json(category); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +router.get('/', async (req, res) => { + const categories = await Category.findAll(); + + res.status(200).json(categories); +}); + +router.get('/:id', async (req, res) => { + const { id } = req.params; + const category = await Category.findByPk(id); + + if (!category) { + return res.status(404).json({ error: 'Category not found' }); + } + res.status(200).json(category); +}); + +router.delete('/:id', async (req, res) => { + const { id } = req.params; + const category = await Category.findByPk(id); + + if (!category) { + return res.status(404).json({ error: 'Category not found' }); + } + await category.destroy(); + res.status(204).send(); +}); + +const updateHandler = async (req, res) => { + const { id } = req.params; + const { name } = req.body; + const category = await Category.findByPk(id); + + if (!category) { + return res.status(404).json({ error: 'Category not found' }); + } + + if (name !== undefined) { + category.name = name; + } + + await category.save(); + res.status(200).json(category); +}; + +router.patch('/:id', updateHandler); +router.put('/:id', updateHandler); + +module.exports = router; diff --git a/src/routes/expenses.js b/src/routes/expenses.js new file mode 100644 index 000000000..2c15d0186 --- /dev/null +++ b/src/routes/expenses.js @@ -0,0 +1,189 @@ +'use strict'; + +const express = require('express'); +const { Op } = require('sequelize'); +const { + models: { Expense, User, Category }, +} = require('../models/models'); + +const router = express.Router(); + +function parseCategories(categories) { + if (!categories) { + return []; + } + + const values = Array.isArray(categories) ? categories : [categories]; + + return values + .flatMap((value) => String(value).split(',')) + .map((value) => value.trim()) + .filter(Boolean); +} + +router.get('/', async (req, res) => { + const { userId, from, to, categories } = req.query; + const parsedCategories = parseCategories(categories); + const numericCategoryIds = parsedCategories + .map(Number) + .filter((n) => !isNaN(n)); + const nameCategoryFilters = parsedCategories.filter((v) => isNaN(Number(v))); + + const where = {}; + + if (userId !== undefined) { + where.userId = Number(userId); + } + + if (from || to) { + where.spentAt = {}; + + if (from) { + where.spentAt[Op.gte] = new Date(from); + } + + if (to) { + where.spentAt[Op.lte] = new Date(to); + } + } + + if (numericCategoryIds.length > 0) { + where.categoryId = { [Op.in]: numericCategoryIds }; + } + + if (nameCategoryFilters.length > 0) { + where.category = { [Op.in]: nameCategoryFilters }; + } + + const expenses = await Expense.findAll({ + where, + order: [['id', 'ASC']], + }); + + res.status(200).json(expenses); +}); + +router.get('/:id', async (req, res) => { + const { id } = req.params; + const expense = await Expense.findByPk(id); + + if (!expense) { + return res.status(404).json({ error: 'Expense not found' }); + } + res.status(200).json(expense); +}); + +router.post('/', async (req, res) => { + const { userId, spentAt, title, amount, categoryId, category, note } = + req.body; + + if ( + userId === undefined || + spentAt === undefined || + title === undefined || + amount === undefined + ) { + return res.status(400).json({ error: 'Missing required parameters' }); + } + + const user = await User.findByPk(userId); + + if (!user) { + return res.status(400).json({ error: 'User not found' }); + } + + if (categoryId !== undefined && categoryId !== null) { + const cat = await Category.findByPk(categoryId); + + if (!cat) { + return res.status(400).json({ error: 'Category not found' }); + } + } + + try { + const expense = await Expense.create({ + userId, + spentAt, + title, + amount, + categoryId, + category, + note, + }); + + res.status(201).json(expense); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +const updateHandler = async (req, res) => { + const { id } = req.params; + const { userId, spentAt, title, amount, categoryId, category, note } = + req.body; + const expense = await Expense.findByPk(id); + + if (!expense) { + return res.status(404).json({ error: 'Expense not found' }); + } + + if (userId !== undefined) { + const user = await User.findByPk(userId); + + if (!user) { + return res.status(400).json({ error: 'User not found' }); + } + expense.userId = userId; + } + + if (spentAt !== undefined) { + expense.spentAt = spentAt; + } + + if (title !== undefined) { + expense.title = title; + } + + if (amount !== undefined) { + expense.amount = amount; + } + + if (categoryId !== undefined) { + if (categoryId !== null) { + const categoryExists = await Category.findByPk(categoryId); + + if (!categoryExists) { + return res.status(400).json({ error: 'Category not found' }); + } + } + expense.categoryId = categoryId; + } + + if (category !== undefined) { + expense.category = category; + } + + if (note !== undefined) { + expense.note = note; + } + + await expense.save(); + await expense.reload(); + res.status(200).json(expense); +}; + +router.patch('/:id', updateHandler); +router.put('/:id', updateHandler); + +router.delete('/:id', async (req, res) => { + const { id } = req.params; + const expense = await Expense.findByPk(id); + + if (!expense) { + return res.status(404).json({ error: 'Expense not found' }); + } + await expense.destroy(); + res.status(204).send(); +}); + +module.exports = router; diff --git a/src/routes/users.js b/src/routes/users.js new file mode 100644 index 000000000..50dca0d63 --- /dev/null +++ b/src/routes/users.js @@ -0,0 +1,73 @@ +'use strict'; + +const express = require('express'); +const { + models: { User }, +} = require('../models/models'); + +const router = express.Router(); + +router.post('/', async (req, res) => { + const { name } = req.body; + + if (!name) { + return res.status(400).json({ error: 'Missing required parameter: name' }); + } + + try { + const user = await User.create({ name }); + + res.status(201).json(user); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +router.get('/', async (req, res) => { + const users = await User.findAll(); + + res.status(200).json(users); +}); + +router.get('/:id', async (req, res) => { + const { id } = req.params; + const user = await User.findByPk(id); + + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + res.status(200).json(user); +}); + +router.delete('/:id', async (req, res) => { + const { id } = req.params; + const user = await User.findByPk(id); + + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + await user.destroy(); + res.status(204).send(); +}); + +const updateHandler = async (req, res) => { + const { id } = req.params; + const { name } = req.body; + const user = await User.findByPk(id); + + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + if (name !== undefined) { + user.name = name; + } + + await user.save(); + res.status(200).json(user); +}; + +router.patch('/:id', updateHandler); +router.put('/:id', updateHandler); + +module.exports = router;