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/page.html b/page.html new file mode 100644 index 000000000..3ee54990f --- /dev/null +++ b/page.html @@ -0,0 +1,566 @@ + + + + + + Accounting App + + + +

Accounting App

+ +
+ +
+

Users

+ +
+ + +
+ + + + + + + + + +
IDNameActions
+
+ + +
+

Expenses

+ +
+ + + + + + + +
+ + + + + + + + + + + + + + +
IDUserTitleAmountCategoryNoteDateActions
+
+
+ + +
+

Categories

+ +
+ + +
+ + + + + + + + + +
IDNameActions
+
+ + + + diff --git a/src/createServer.js b/src/createServer.js index 1ea5542d6..4e77fb1dc 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,7 +1,223 @@ 'use strict'; +const cors = require('cors'); +const express = require('express'); +const path = require('path'); +const { Op } = require('sequelize'); +const { + models: { User, Expense, Category }, +} = require('./models/models'); + const createServer = () => { - // your code goes here + const app = express(); + const usersRouter = express.Router(); + const expensesRouter = express.Router(); + + app.use(cors()); + app.use(express.json()); + + // --- Users --- + + usersRouter.post('/', async (req, res) => { + const { name } = req.body; + + if (!name) { + return res.status(400).json({ message: 'Name is required' }); + } + + const user = await User.create({ name }); + + res.status(201).json(user); + }); + + usersRouter.get('/', async (req, res) => { + const users = await User.findAll(); + + res.json(users); + }); + + usersRouter.get('/:id', async (req, res) => { + const user = await User.findByPk(req.params.id); + + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + res.json(user); + }); + + usersRouter.patch('/:id', async (req, res) => { + const user = await User.findByPk(req.params.id); + + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + await user.update(req.body); + + res.json(user); + }); + + usersRouter.delete('/:id', async (req, res) => { + const user = await User.findByPk(req.params.id); + + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + await user.destroy(); + + res.status(204).send(); + }); + + // --- Expenses --- + + expensesRouter.post('/', async (req, res) => { + const { userId, spentAt, title, amount } = req.body; + + if (!userId || !spentAt || !title || amount === undefined) { + return res.status(400).json({ message: 'Missing required fields' }); + } + + const user = await User.findByPk(userId); + + if (!user) { + return res.status(400).json({ message: 'User not found' }); + } + + const expense = await Expense.create(req.body); + + res.status(201).json(expense); + }); + + expensesRouter.get('/', async (req, res) => { + const { userId, from, to, categories } = req.query; + const where = {}; + + if (userId !== undefined) { + where.userId = Number(userId); + } + + if (from !== undefined || to !== undefined) { + where.spentAt = {}; + + if (from !== undefined) { + where.spentAt[Op.gte] = new Date(from); + } + + if (to !== undefined) { + where.spentAt[Op.lte] = new Date(to); + } + } + + if (categories !== undefined) { + where.category = { [Op.in]: categories.split(',') }; + } + + const expenses = await Expense.findAll({ where }); + + res.json(expenses); + }); + + expensesRouter.get('/:id', async (req, res) => { + const expense = await Expense.findByPk(req.params.id); + + if (!expense) { + return res.status(404).json({ message: 'Expense not found' }); + } + + res.json(expense); + }); + + expensesRouter.patch('/:id', async (req, res) => { + const expense = await Expense.findByPk(req.params.id); + + if (!expense) { + return res.status(404).json({ message: 'Expense not found' }); + } + + await expense.update(req.body); + + res.json(expense); + }); + + expensesRouter.delete('/:id', async (req, res) => { + const expense = await Expense.findByPk(req.params.id); + + if (!expense) { + return res.status(404).json({ message: 'Expense not found' }); + } + + await expense.destroy(); + + res.status(204).send(); + }); + + // --- Categories --- + + const categoriesRouter = express.Router(); + + categoriesRouter.post('/', async (req, res) => { + const { name } = req.body; + + if (!name) { + return res.status(400).json({ message: 'Name is required' }); + } + + const category = await Category.create({ name }); + + res.status(201).json(category); + }); + + categoriesRouter.get('/', async (req, res) => { + const categories = await Category.findAll(); + + res.json(categories); + }); + + categoriesRouter.get('/:id', async (req, res) => { + const category = await Category.findByPk(req.params.id); + + if (!category) { + return res.status(404).json({ message: 'Category not found' }); + } + + res.json(category); + }); + + categoriesRouter.patch('/:id', async (req, res) => { + const category = await Category.findByPk(req.params.id); + + if (!category) { + return res.status(404).json({ message: 'Category not found' }); + } + + await category.update(req.body); + + res.json(category); + }); + + categoriesRouter.delete('/:id', async (req, res) => { + const category = await Category.findByPk(req.params.id); + + if (!category) { + return res.status(404).json({ message: 'Category not found' }); + } + + await category.destroy(); + + res.status(204).send(); + }); + + app.use('/users', usersRouter); + app.use('/expenses', expensesRouter); + app.use('/categories', categoriesRouter); + + app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, '..', 'page.html')); + }); + + return app; }; module.exports = { diff --git a/src/db.js b/src/db.js index 1ba3046cc..7b2592484 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 || 'nodeAcountingappTask', username: POSTGRES_USER || 'postgres', host: POSTGRES_HOST || 'localhost', dialect: 'postgres', port: POSTGRES_PORT || 5432, - password: POSTGRES_PASSWORD || '123', + password: POSTGRES_PASSWORD || 'test123', }); module.exports = { diff --git a/src/index.js b/src/index.js index 541737327..e49c2e2a5 100644 --- a/src/index.js +++ b/src/index.js @@ -3,7 +3,12 @@ 'use strict'; const { createServer } = require('./createServer'); +const { sequelize } = require('./db'); -createServer().listen(5700, () => { - console.log('Server is running on localhost:5700'); +require('./models/models'); + +sequelize.sync().then(() => { + createServer().listen(5700, () => { + console.log('Server is running on localhost:5700'); + }); }); diff --git a/src/models/Category.model.js b/src/models/Category.model.js new file mode 100644 index 000000000..8935025ed --- /dev/null +++ b/src/models/Category.model.js @@ -0,0 +1,21 @@ +'use strict'; + +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../db.js'); + +const Category = sequelize.define( + 'Category', + { + name: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + timestamps: false, + }, +); + +module.exports = { + Category, +}; diff --git a/src/models/Expense.model.js b/src/models/Expense.model.js index 567e1c3e7..716eafe82 100644 --- a/src/models/Expense.model.js +++ b/src/models/Expense.model.js @@ -1,9 +1,37 @@ 'use strict'; +const { DataTypes } = require('sequelize'); const { sequelize } = require('../db.js'); const Expense = sequelize.define( - // your code goes here + 'Expense', + { + spentAt: { + type: DataTypes.DATE, + allowNull: false, + }, + title: { + type: DataTypes.STRING, + allowNull: false, + }, + amount: { + type: DataTypes.FLOAT, + allowNull: false, + }, + category: { + type: DataTypes.STRING, + }, + note: { + type: DataTypes.STRING, + }, + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + }, + { + timestamps: false, + }, ); module.exports = { diff --git a/src/models/User.model.js b/src/models/User.model.js index 61861c9e4..cf0820042 100644 --- a/src/models/User.model.js +++ b/src/models/User.model.js @@ -1,9 +1,19 @@ '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, + }, + }, + { + timestamps: false, + }, ); module.exports = { diff --git a/src/models/models.js b/src/models/models.js index b43b55752..00bbb186f 100644 --- a/src/models/models.js +++ b/src/models/models.js @@ -2,10 +2,15 @@ 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 }); module.exports = { models: { User, Expense, + Category, }, };