Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/test.yml-template
Original file line number Diff line number Diff line change
@@ -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
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
62 changes: 31 additions & 31 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,68 +8,68 @@ Implement a REST API server using Node.js that manages **Users** and **Expenses*
## Users API

### POST /users
- Create a user
- Body: `{ name }`
- Returns: `201` with created user
- Returns `400` if `name` is missing
- Create a user
- Body: `{ name }`
- Returns: `201` with created user
- Returns `400` if `name` is missing

### GET /users
- Returns all users (`200`)
- Returns all users (`200`)

### GET /users/:id
- Returns a user (`200`)
- Returns `404` if not found
- Returns a user (`200`)
- Returns `404` if not found

### PATCH /users/:id
- Update user name
- Returns updated user (`200`)
- Returns `404` if not found
- Update user name
- Returns updated user (`200`)
- Returns `404` if not found

### DELETE /users/:id
- Deletes user
- Returns `204`
- Returns `404` if not found
- Deletes user
- Returns `204`
- Returns `404` if not found

---

## Expenses API

### POST /expenses
- Create an expense
- Required: `spentAt`, `title`, `amount`, `userId`
- Optional: `category`, `note`
- Returns: `201` with created expense
- Returns `400` if required fields are missing or user not found
- Create an expense
- Required: `spentAt`, `title`, `amount`, `userId`
- Optional: `category`, `note`
- Returns: `201` with created expense
- Returns `400` if required fields are missing or user not found

### GET /expenses
- Returns all expenses (`200`)
- Returns all expenses (`200`)
- Supports filters:
- `userId`
- `from` / `to` (date range)
- `categories` (comma-separated)

### GET /expenses/:id
- Returns expense (`200`)
- Returns `404` if not found
- Returns expense (`200`)
- Returns `404` if not found

### PATCH /expenses/:id
- Update expense fields
- Returns updated expense (`200`)
- Returns `404` if not found
- Update expense fields
- Returns updated expense (`200`)
- Returns `404` if not found

### DELETE /expenses/:id
- Deletes expense
- Returns `204`
- Returns `404` if not found
- Deletes expense
- Returns `204`
- Returns `404` if not found

---

## General

- Use JSON for all requests/responses
- Set header: `Content-Type: application/json; charset=utf-8`
- Use proper HTTP status codes (`200`, `201`, `204`, `400`, `404`)
- Persist data using a database (e.g. Sequelize)
- Use JSON for all requests/responses
- Set header: `Content-Type: application/json; charset=utf-8`
- Use proper HTTP status codes (`200`, `201`, `204`, `400`, `404`)
- Persist data using a database (e.g. Sequelize)

# Note
- You can sync models by running `npm run test`
Expand Down
204 changes: 201 additions & 3 deletions src/createServer.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,206 @@
'use strict';

const createServer = () => {
// your code goes here
};
const express = require('express');
const { Op } = require('sequelize');
const { User } = require('./models/User.model');
const { Expense } = require('./models/Expense.model');

User.hasMany(Expense, { foreignKey: 'userId', constraints: false });
Expense.belongsTo(User, { foreignKey: 'userId', constraints: false });

function createServer() {
const app = express();

app.use(express.json());

app.post('/users', async (req, res) => {
try {
const { name } = req.body;

if (!name) {
return res.status(400).json({ error: 'Name is required' });
}

const user = await User.create({ name });

res.status(201).json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

app.get('/users', async (req, res) => {
try {
const users = await User.findAll();

res.status(200).json(users);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

app.get('/users/:id', async (req, res) => {
try {
const user = await User.findByPk(req.params.id);

if (!user) {
return res.status(404).json({ error: 'User not found' });
}

res.status(200).json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

app.patch('/users/:id', async (req, res) => {
try {
const { name } = req.body;

if (!name) {
return res.status(400).json({ error: 'Name is required' });
}

const user = await User.findByPk(req.params.id);

if (!user) {
return res.status(404).json({ error: 'User not found' });
}

await user.update({ name });
res.status(200).json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

app.delete('/users/:id', async (req, res) => {
try {
const user = await User.findByPk(req.params.id);

if (!user) {
return res.status(404).json({ error: 'User not found' });
}

await user.destroy();
res.status(204).send();
} catch (error) {
res.status(500).json({ error: error.message });
}
});

app.post('/expenses', async (req, res) => {
try {
const { userId, spentAt, title, amount, category, note } = req.body;

if (!userId || !spentAt || !title || amount === undefined) {
return res.status(400).json({ error: 'Missing required fields' });
}

const user = await User.findByPk(userId);

if (!user) {
return res.status(400).json({ error: 'User not found' });
}

const expense = await Expense.create({
userId,
spentAt,
title,
amount,
category,
note,
});

res.status(201).json(expense);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

app.get('/expenses', async (req, res) => {
try {
const whereCondition = {};

if (req.query.userId) {
whereCondition.userId = req.query.userId;
}

if (req.query.categories) {
whereCondition.category = {
[Op.in]: req.query.categories.split(','),
};
}

if (req.query.from || req.query.to) {
whereCondition.spentAt = {};

if (req.query.from) {
whereCondition.spentAt[Op.gte] = new Date(req.query.from);
}

if (req.query.to) {
whereCondition.spentAt[Op.lte] = new Date(req.query.to);
}
}

const expenses = await Expense.findAll({
where: whereCondition,
});

res.status(200).json(expenses);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

app.get('/expenses/:id', async (req, res) => {
try {
const expense = await Expense.findByPk(req.params.id);

if (!expense) {
return res.status(404).json({ error: 'Expense not found' });
}

res.status(200).json(expense);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

app.patch('/expenses/:id', async (req, res) => {
try {
const expense = await Expense.findByPk(req.params.id);

if (!expense) {
return res.status(404).json({ error: 'Expense not found' });
}

await expense.update(req.body);

res.status(200).json(expense);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

app.delete('/expenses/:id', async (req, res) => {
try {
const expense = await Expense.findByPk(req.params.id);

if (!expense) {
return res.status(404).json({ error: 'Expense not found' });
}

await expense.destroy();
res.status(204).send();
} catch (error) {
res.status(500).json({ error: error.message });
}
});

return app;
}

module.exports = {
createServer,
Expand Down
Loading
Loading