diff --git a/.codex b/.codex new file mode 100644 index 000000000..e69de29bb 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/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..37b5ca6ee --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +services: + db: + image: postgres:18 + container_name: project-db-local + restart: unless-stopped + ports: + - '${POSTGRES_PORT:-5432}:5432' + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - pg_data:/var/lib/postgresql + +volumes: + pg_data: diff --git a/package-lock.json b/package-lock.json index cd25728ae..2d75f58b5 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", @@ -66,6 +66,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -1474,10 +1475,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", @@ -1543,7 +1545,6 @@ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", "dev": true, - "peer": true, "engines": { "node": ">= 18" } @@ -1572,7 +1573,6 @@ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", "dev": true, - "peer": true, "dependencies": { "@octokit/types": "^13.0.0", "universal-user-agent": "^7.0.2" @@ -1586,7 +1586,6 @@ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz", "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==", "dev": true, - "peer": true, "dependencies": { "@octokit/request": "^9.0.0", "@octokit/types": "^13.0.0", @@ -1600,8 +1599,7 @@ "version": "22.2.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@octokit/plugin-paginate-rest": { "version": "2.21.3", @@ -1663,7 +1661,6 @@ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", "dev": true, - "peer": true, "dependencies": { "@octokit/endpoint": "^10.0.0", "@octokit/request-error": "^6.0.1", @@ -1679,7 +1676,6 @@ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.4.tgz", "integrity": "sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==", "dev": true, - "peer": true, "dependencies": { "@octokit/types": "^13.0.0" }, @@ -1867,7 +1863,6 @@ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", "dev": true, - "peer": true, "dependencies": { "@octokit/openapi-types": "^22.2.0" } @@ -2239,6 +2234,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2682,8 +2678,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", - "dev": true, - "peer": true + "dev": true }, "node_modules/body-parser": { "version": "1.20.2", @@ -2762,6 +2757,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001640", "electron-to-chromium": "^1.4.820", @@ -3583,6 +3579,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3638,6 +3635,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -3728,6 +3726,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, + "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -3805,6 +3804,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", "dev": true, + "peer": true, "dependencies": { "eslint-plugin-es": "^3.0.0", "eslint-utils": "^2.0.0", @@ -3855,6 +3855,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.3.1.tgz", "integrity": "sha512-bY2sGqyptzFBDLh/GMbAxfdJC+b0f23ME63FOE4+Jao0oZ3E1LEwFtWJX/1pGMJLiTtrSSern2CRM/g+dfc0eQ==", "dev": true, + "peer": true, "engines": { "node": ">=6" } @@ -3878,6 +3879,7 @@ "url": "https://feross.org/support" } ], + "peer": true, "peerDependencies": { "eslint": ">=5.0.0" } @@ -5422,6 +5424,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -7757,6 +7760,7 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", + "peer": true, "dependencies": { "pg-connection-string": "^2.6.4", "pg-pool": "^3.6.2", @@ -7983,6 +7987,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -9175,8 +9180,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", - "dev": true, - "peer": true + "dev": true }, "node_modules/universalify": { "version": "2.0.1", diff --git a/package.json b/package.json index bdf079250..6704f99c9 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,16 @@ "test:only": "mate-scripts test", "update": "mate-scripts update", "postinstall": "npm run update", - "test": "npm run lint && npm run test:only" + "test": "npm run lint && npm run test:only", + "db:up": "docker compose up -d", + "db:down": "docker compose down", + "db:stop": "docker compose stop", + "db:logs": "docker compose logs -f db", + "db:reset": "docker compose down -v", + "build": "sequelize generate && tsc", + "test:watch": "jest --watch", + "test:user:watch": "npx jest tests/user.test.js --runInBand --verbose --watch ", + "test:expense:watch": "npx jest tests/expense.test.js --runInBand --verbose --watch " }, "author": "Mate academy", "license": "GPL-3.0", @@ -23,7 +32,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/controllers/categories.controller.js b/src/controllers/categories.controller.js new file mode 100644 index 000000000..e6f80417b --- /dev/null +++ b/src/controllers/categories.controller.js @@ -0,0 +1,102 @@ +const { + models: { Category }, +} = require('../models/models'); + +function handleControllerError(res, action, error) { + // eslint-disable-next-line no-console + console.error(`Smth bad with: ${action}`, error); + res.sendStatus(500); +} + +async function getAllCategories(req, res) { + try { + const categories = await Category.findAll({ + order: [['id', 'ASC']], + }); + + res.send(categories); + } catch (error) { + handleControllerError(res, 'get', error); + } +} + +async function getCategoryById(req, res) { + try { + const { id } = req.params; + const category = await Category.findByPk(Number(id)); + + if (!category) { + return res.sendStatus(404); + } + + res.status(200).send(category); + } catch (error) { + handleControllerError(res, 'get', error); + } +} + +async function create(req, res) { + try { + const { name } = req.body; + + if (!name) { + return res.sendStatus(400); + } + + const category = await Category.create({ name }); + + res.status(201).send(category); + } catch (error) { + handleControllerError(res, 'create', error); + } +} + +async function remove(req, res) { + try { + const { id } = req.params; + const category = await Category.findByPk(Number(id)); + + if (!category) { + return res.status(404).json({ error: 'Not found' }); + } + + await category.destroy(); + + return res.sendStatus(204); + } catch (error) { + handleControllerError(res, 'remove', error); + } +} + +async function update(req, res) { + try { + const { id } = req.params; + const { name } = req.body; + + const category = await Category.findByPk(Number(id)); + + if (!category) { + return res.status(404).json({ error: 'Not found' }); + } + + if (!name) { + return res.status(400).json({ error: 'Bad Request' }); + } + + category.name = name; + + await category.save(); + + return res.status(200).send(category); + } catch (error) { + handleControllerError(res, 'update', error); + } +} + +module.exports = { + getAllCategories, + getCategoryById, + remove, + create, + update, +}; diff --git a/src/controllers/expenses.controller.js b/src/controllers/expenses.controller.js new file mode 100644 index 000000000..c0977cc38 --- /dev/null +++ b/src/controllers/expenses.controller.js @@ -0,0 +1,218 @@ +const { Op } = require('sequelize'); + +const { + models: { Expense, User, Category }, +} = require('../models/models'); + +function handleControllerError(res, action, error) { + // eslint-disable-next-line no-console + console.error(`Smth bad with: ${action}`, error); + res.sendStatus(500); +} + +function serializeExpense(expense) { + const plainExpense = expense.get({ plain: true }); + + if (plainExpense.Category) { + plainExpense.category = plainExpense.Category.name; + } + + delete plainExpense.Category; + delete plainExpense.categoryId; + + return plainExpense; +} + +async function getAllExpenses(req, res) { + try { + const { userId, categoryId, categories, from, to } = req.query; + + const where = {}; + + if (userId) { + where.userId = Number(userId); + } + + if (categoryId) { + where.categoryId = Number(categoryId); + } + + if (categories) { + const category = await Category.findOne({ + where: { name: categories }, + }); + + if (!category) { + return res.send([]); + } + + where.categoryId = category.id; + } + + if (from || to) { + where.spentAt = {}; + + if (from) { + where.spentAt[Op.gte] = new Date(from); + } + + if (to) { + where.spentAt[Op.lte] = new Date(to); + } + } + + const expenses = await Expense.findAll({ + where, + include: Category, + order: [['id', 'ASC']], + }); + + res.send(expenses.map(serializeExpense)); + } catch (error) { + handleControllerError(res, 'getAllExpenses', error); + } +} + +async function getExpenseById(req, res) { + try { + const { id } = req.params; + const expense = await Expense.findByPk(Number(id), { + include: Category, + }); + + if (!expense) { + return res.sendStatus(404); + } + + res.status(200).send(serializeExpense(expense)); + } catch (error) { + handleControllerError(res, 'getExpenseById', error); + } +} + +async function remove(req, res) { + try { + const { id } = req.params; + const expense = await Expense.findByPk(Number(id)); + + if (!expense) { + return res.status(404).json({ error: 'Not found' }); + } + + await expense.destroy(); + + return res.sendStatus(204); + } catch (error) { + handleControllerError(res, 'remove', error); + } +} + +async function create(req, res) { + try { + const { userId, spentAt, title, amount, category, categoryId, note } = + req.body; + + if (userId === undefined || !spentAt || !title || amount === undefined) { + return res.status(400).json({ error: 'Bad Request' }); + } + + const user = await User.findByPk(Number(userId)); + + if (!user) { + return res.sendStatus(400); + } + + let resolvedCategoryId = categoryId; + + if (category) { + const [categoryModel] = await Category.findOrCreate({ + where: { name: category }, + }); + + resolvedCategoryId = categoryModel.id; + } + + if (resolvedCategoryId !== undefined) { + const categoryModel = await Category.findByPk(Number(resolvedCategoryId)); + + if (!categoryModel) { + return res.sendStatus(400); + } + } + + const expense = await Expense.create({ + userId, + spentAt, + title, + amount, + categoryId: resolvedCategoryId, + note, + }); + + const createdExpense = await Expense.findByPk(expense.id, { + include: Category, + }); + + res.status(201).send(serializeExpense(createdExpense)); + } catch (error) { + handleControllerError(res, 'create', error); + } +} + +async function update(req, res) { + try { + const { id } = req.params; + const { title, category, categoryId } = req.body; + + const expense = await Expense.findByPk(Number(id), { + include: Category, + }); + + if (!expense) { + return res.status(404).json({ error: 'Not found' }); + } + + if (!title) { + return res.status(400).json({ error: 'Bad Request' }); + } + + let resolvedCategoryId = categoryId; + + if (category) { + const [categoryModel] = await Category.findOrCreate({ + where: { name: category }, + }); + + resolvedCategoryId = categoryModel.id; + } + + if (resolvedCategoryId !== undefined) { + const categoryModel = await Category.findByPk(Number(resolvedCategoryId)); + + if (!categoryModel) { + return res.sendStatus(400); + } + + expense.categoryId = resolvedCategoryId; + } + + expense.title = title; + await expense.save(); + + const updatedExpense = await Expense.findByPk(expense.id, { + include: Category, + }); + + return res.status(200).send(serializeExpense(updatedExpense)); + } catch (error) { + handleControllerError(res, 'update', error); + } +} + +module.exports = { + getAllExpenses, + getExpenseById, + remove, + create, + update, +}; diff --git a/src/controllers/users.controller.js b/src/controllers/users.controller.js new file mode 100644 index 000000000..81c2fc2f1 --- /dev/null +++ b/src/controllers/users.controller.js @@ -0,0 +1,100 @@ +const { + models: { User }, +} = require('../models/models'); + +function handleControllerError(res, action, error) { + // eslint-disable-next-line no-console + console.error(`Smth bad with: ${action}`, error); + res.sendStatus(500); +} + +async function getAllUsers(req, res) { + try { + const users = await User.findAll(); + + res.send(users); + } catch (error) { + handleControllerError(res, 'get', error); + } +} + +async function getUserById(req, res) { + try { + const { id } = req.params; + const user = await User.findByPk(Number(id)); + + if (!user) { + return res.sendStatus(404); + } + + res.status(200).send(user); + } catch (error) { + handleControllerError(res, 'get', error); + } +} + +async function create(req, res) { + try { + const { name } = req.body; + + if (!name) { + return res.sendStatus(400); + } + + const user = await User.create({ name }); + + res.status(201).send(user); + } catch (error) { + handleControllerError(res, 'create', error); + } +} + +async function remove(req, res) { + try { + const { id } = req.params; + const user = await User.findByPk(Number(id)); + + if (!user) { + return res.status(404).json({ error: 'Not found' }); + } + + await user.destroy(); + + return res.sendStatus(204); + } catch (error) { + handleControllerError(res, 'remove', error); + } +} + +async function update(req, res) { + try { + const { id } = req.params; + const { name } = req.body; + + const user = await User.findByPk(Number(id)); + + if (!user) { + return res.status(404).json({ error: 'Not found' }); + } + + if (!name) { + return res.status(400).json({ error: 'Bad Request' }); + } + + user.name = name; + + await user.save(); + + return res.status(200).send(user); + } catch (error) { + handleControllerError(res, 'update', error); + } +} + +module.exports = { + getAllUsers, + getUserById, + remove, + create, + update, +}; diff --git a/src/createServer.js b/src/createServer.js index 1ea5542d6..ddfda5150 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,8 +1,23 @@ 'use strict'; -const createServer = () => { - // your code goes here -}; +const express = require('express'); +const cors = require('cors'); +const expensesRouter = require('./routers/expenses.router.js'); +const usersRouter = require('./routers/users.router.js'); +const categoriesRouter = require('./routers/categories.router.js'); + +function createServer() { + const app = express(); + + app.use(cors()); + app.use(express.json()); + + app.use('/users', usersRouter); + app.use('/expenses', expensesRouter); + app.use('/categories', categoriesRouter); + + 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..2618cd145 --- /dev/null +++ b/src/models/Category.model.js @@ -0,0 +1,27 @@ +'use strict'; + +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../db.js'); + +const Category = sequelize.define( + 'Category', + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + 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..dadca90b8 100644 --- a/src/models/Expense.model.js +++ b/src/models/Expense.model.js @@ -1,9 +1,45 @@ 'use strict'; +const { DataTypes } = require('sequelize'); const { sequelize } = require('../db.js'); const Expense = sequelize.define( - // your code goes here + 'Expense', + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + spentAt: { + type: DataTypes.DATE, + allowNull: false, + }, + title: { + type: DataTypes.STRING, + allowNull: false, + }, + amount: { + type: DataTypes.INTEGER, + allowNull: false, + }, + categoryId: { + type: DataTypes.INTEGER, + allowNull: true, + }, + note: { + type: DataTypes.STRING, + allowNull: true, + }, + }, + { + tableName: 'expenses', + timestamps: false, + }, ); module.exports = { diff --git a/src/models/User.model.js b/src/models/User.model.js index 61861c9e4..2dc8dceba 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', + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + 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..65c8e0d48 100644 --- a/src/models/models.js +++ b/src/models/models.js @@ -2,10 +2,17 @@ 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/routers/categories.router.js b/src/routers/categories.router.js new file mode 100644 index 000000000..a1513f385 --- /dev/null +++ b/src/routers/categories.router.js @@ -0,0 +1,18 @@ +const express = require('express'); +const controller = require('../controllers/categories.controller.js'); + +const router = express.Router(); + +router.get('/', (req, res) => { + controller.getAllCategories(req, res); +}); + +router.get('/:id', controller.getCategoryById); + +router.post('/', controller.create); + +router.delete('/:id', controller.remove); + +router.patch('/:id', controller.update); + +module.exports = router; diff --git a/src/routers/expenses.router.js b/src/routers/expenses.router.js new file mode 100644 index 000000000..12dec9884 --- /dev/null +++ b/src/routers/expenses.router.js @@ -0,0 +1,18 @@ +const express = require('express'); +const controller = require('../controllers/expenses.controller.js'); + +const router = express.Router(); + +router.get('/', (req, res) => { + controller.getAllExpenses(req, res); +}); + +router.get('/:id', controller.getExpenseById); + +router.post('/', controller.create); + +router.delete('/:id', controller.remove); + +router.patch('/:id', controller.update); + +module.exports = router; diff --git a/src/routers/users.router.js b/src/routers/users.router.js new file mode 100644 index 000000000..cb8b37cda --- /dev/null +++ b/src/routers/users.router.js @@ -0,0 +1,18 @@ +const express = require('express'); +const controller = require('../controllers/users.controller.js'); + +const router = express.Router(); + +router.get('/', (req, res) => { + controller.getAllUsers(req, res); +}); + +router.get('/:id', controller.getUserById); + +router.post('/', controller.create); + +router.delete('/:id', controller.remove); + +router.patch('/:id', controller.update); + +module.exports = router; diff --git a/src/utils/sequelize.setup.js b/src/utils/sequelize.setup.js new file mode 100644 index 000000000..fe921c68f --- /dev/null +++ b/src/utils/sequelize.setup.js @@ -0,0 +1,7 @@ +'use strict'; + +const { sequelize } = require('../db'); + +require('../models/models'); + +sequelize.sync().then(() => sequelize.close());