diff --git a/.env b/.env new file mode 100644 index 000000000..d96896343 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +POSTGRES_USER=postgres +POSTGRES_PASSWORD=123 +POSTGRES_DB=postgres +POSTGRES_PORT=5432 diff --git a/.github/workflows/test.yml-template b/.github/workflows/test.yml-template new file mode 100644 index 000000000..03d2b7ff2 --- /dev/null +++ b/.github/workflows/test.yml-template @@ -0,0 +1,44 @@ +name: Test + +on: + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20.x] + services: + postgres: + image: postgres:latest + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: students + POSTGRES_PORT: 5432 + POSTGRES_HOST: localhost + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v2 + - name: Set up Node.js + uses: actions/setup-node@v1 + with: + node-version: '20' + - name: Install dependencies + run: npm install + - name: Run tests + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: students + POSTGRES_HOST: localhost + POSTGRES_PORT: 5432 + run: npm test diff --git a/.gitignore b/.gitignore index ed48a299d..bd6a178a8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,3 @@ node_modules # MacOS .DS_Store - -# env files -*.env -.env* 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/createServer.js b/src/createServer.js index 1ea5542d6..e3bc02bf3 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,8 +1,24 @@ 'use strict'; -const createServer = () => { - // your code goes here -}; +const express = require('express'); +const createExpensesRouter = require('./routes/expenses'); +const createUsersRouter = require('./routes/users'); +const createCategoriesRouter = require('./routes/categories'); + +function createServer() { + const app = express(); + + // Use express to create a server + // Add a routes to the server + // Return the server (express app) + app.use(express.json()); + + app.use('/expenses', createExpensesRouter()); + app.use('/users', createUsersRouter()); + app.use('/categories', createCategoriesRouter()); + + return app; +} module.exports = { createServer, diff --git a/src/models/Category.model.js b/src/models/Category.model.js new file mode 100644 index 000000000..1840171ec --- /dev/null +++ b/src/models/Category.model.js @@ -0,0 +1,16 @@ +'use strict'; + +const { DataTypes } = require('sequelize'); + +const { sequelize } = require('../db.js'); + +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..bd8243dda 100644 --- a/src/models/Expense.model.js +++ b/src/models/Expense.model.js @@ -1,9 +1,41 @@ 'use strict'; const { sequelize } = require('../db.js'); - +const { DataTypes } = require('sequelize'); const Expense = sequelize.define( - // your code goes here + 'Expense', + { + amount: { + type: DataTypes.INTEGER, + allowNull: false, + }, + + title: { + type: DataTypes.STRING, + allowNull: false, + }, + + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + + spentAt: { + type: DataTypes.DATE, + allowNull: false, + }, + + category: { + type: DataTypes.STRING, + allowNull: true, + }, + note: { + type: DataTypes.STRING, + }, + }, + { + timestamps: false, + }, ); module.exports = { diff --git a/src/models/User.model.js b/src/models/User.model.js index 61861c9e4..4fcba696d 100644 --- a/src/models/User.model.js +++ b/src/models/User.model.js @@ -1,10 +1,15 @@ '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..e8dbe5beb 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'); + +// Expense.belongsTo(User); +// User.hasMany(Expense); + +// Expense.belongsTo(Category); якщо я це додам тести не проходитимуть +// Category.hasMany(Expense); module.exports = { models: { User, Expense, + Category, }, }; diff --git a/src/routes/categories.js b/src/routes/categories.js new file mode 100644 index 000000000..8eed9df43 --- /dev/null +++ b/src/routes/categories.js @@ -0,0 +1,60 @@ +'use strict'; + +const express = require('express'); +const { Category } = require('../models/Category.model'); + +function createCategoryRouter() { + const router = express.Router(); + + router.get('/', async (req, res) => { + res.send(await Category.findAll()); + }); + + router.post('/', async (req, res) => { + const category = req.body; + + if (!category.name) { + return res.status(400).send('Name is required'); + } + + res.status(201).send(await Category.create(category)); + }); + + router.get('/:id', async (req, res) => { + const category = await Category.findByPk(Number(req.params.id)); + + if (!category) { + return res.status(404).send('Category not found'); + } + + res.send(category); + }); + + router.delete('/:id', async (req, res) => { + const category = await Category.findByPk(Number(req.params.id)); + + if (!category) { + return res.status(404).send('Category not found'); + } + + await category.destroy(); + + res.status(204).send('Deleted'); + }); + + router.patch('/:id', async (req, res) => { + const category = await Category.findByPk(Number(req.params.id)); + + if (!category) { + return res.status(404).send('Category not found'); + } + + await category.update(req.body); + + res.send(category); + }); + + return router; +} + +module.exports = createCategoryRouter; diff --git a/src/routes/expenses.js b/src/routes/expenses.js new file mode 100644 index 000000000..767ec326e --- /dev/null +++ b/src/routes/expenses.js @@ -0,0 +1,106 @@ +'use strict'; + +const express = require('express'); +const { Expense } = require('../models/Expense.model'); +const { User } = require('../models/User.model'); +const { Op } = require('sequelize'); + +function createExpensesRoute() { + const router = express.Router(); + + router.get('/', async (req, res) => { + const whereClause = {}; + + if (req.query.userId) { + whereClause.userId = Number(req.query.userId); + } + + if (req.query.categories) { + whereClause.category = req.query.categories; + } + + if (req.query.from || req.query.to) { + whereClause.spentAt = {}; + + if (req.query.from) { + whereClause.spentAt[Op.gte] = req.query.from; + } + + if (req.query.to) { + whereClause.spentAt[Op.lte] = req.query.to; + } + } + + const filteredExpenses = await Expense.findAll({ + where: whereClause, + }); + + res.send(filteredExpenses); + }); + + router.post('/', async (req, res) => { + const expense = req.body; + + if (!expense.amount) { + return res.status(400).send('Amount is required'); + } + + if (!expense.title) { + return res.status(400).send('title is required'); + } + + if (!expense.userId) { + return res.status(400).send('userId is required'); + } + + const user = await User.findByPk(expense.userId); + + if (!user) { + return res.status(400).send('Such user doesn`t exist'); + } + + if (!expense.spentAt) { + return res.status(400).send('spentAt is required'); + } + + res.status(201).send(await Expense.create(expense)); + }); + + router.get('/:id', async (req, res) => { + const expense = await Expense.findByPk(Number(req.params.id)); + + if (!expense) { + return res.status(404).send('Expense not found'); + } + + res.send(expense); + }); + + router.delete('/:id', async (req, res) => { + const expense = await Expense.findByPk(Number(req.params.id)); + + if (!expense) { + return res.status(404).send('Expense not found'); + } + + await expense.destroy(); + + res.status(204).send('Deleted'); + }); + + router.patch('/:id', async (req, res) => { + const expense = await Expense.findByPk(Number(req.params.id)); + + if (!expense) { + return res.status(404).send('Expense not found'); + } + + await expense.update(req.body); + + res.send(expense); + }); + + return router; +} + +module.exports = createExpensesRoute; diff --git a/src/routes/users.js b/src/routes/users.js new file mode 100644 index 000000000..17db97edd --- /dev/null +++ b/src/routes/users.js @@ -0,0 +1,60 @@ +'use strict'; + +const express = require('express'); +const { User } = require('../models/User.model'); + +function createUsersRouter() { + const router = express.Router(); + + router.get('/', async (req, res) => { + res.send(await User.findAll()); + }); + + router.post('/', async (req, res) => { + const user = req.body; + + if (!user.name) { + return res.status(400).send('Name is required'); + } + + res.status(201).send(await User.create(user)); + }); + + router.get('/:id', async (req, res) => { + const user = await User.findByPk(Number(req.params.id)); + + if (!user) { + return res.status(404).send('User not found'); + } + + res.send(user); + }); + + router.delete('/:id', async (req, res) => { + const user = await User.findByPk(Number(req.params.id)); + + if (!user) { + return res.status(404).send('User not found'); + } + + await user.destroy(); + + res.status(204).send('Deleted'); + }); + + router.patch('/:id', async (req, res) => { + const user = await User.findByPk(Number(req.params.id)); + + if (!user) { + return res.status(404).send('User not found'); + } + + await user.update(req.body, { silent: true }); + + res.send(user); + }); + + return router; +} + +module.exports = createUsersRouter;