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
+
+
+
+
+
+ | ID |
+ Name |
+ Actions |
+
+
+
+
+
+
+
+
+
Expenses
+
+
+
+
+
+ | ID |
+ User |
+ Title |
+ Amount |
+ Category |
+ Note |
+ Date |
+ Actions |
+
+
+
+
+
+
+
+
+
+
Categories
+
+
+
+
+
+ | ID |
+ Name |
+ Actions |
+
+
+
+
+
+
+
+
+
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,
},
};