Skip to content
Open

task #518

Show file tree
Hide file tree
Changes from 2 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
14 changes: 13 additions & 1 deletion src/createServer.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
'use strict';

const express = require('express');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Category model exists, but you must ensure associations between Category and Expense are defined (e.g., Category.hasMany(Expense, { foreignKey: 'categoryId', constraints: false }) and Expense.belongsTo(Category, { foreignKey: 'categoryId', constraints: false })) in your central models file. Also ensure DB-level FK constraints won't prevent test truncation (use constraints: false or avoid DB-level FK if tests truncate tables) — this ties to checklist items #4, #5 and #7.

const usersRouter = require('./routes/users');
const expensesRouter = require('./routes/expenses');
const categoriesRouter = require('./routes/categories');

const createServer = () => {
// your code goes here
const app = express();

app.use(express.json());
app.use('/users', usersRouter);
app.use('/expenses', expensesRouter);
Comment on lines +12 to +13
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The task requires a CRUD page to manage categories. There is no categories router mounted here. Add and mount a categories router (for example app.use('/categories', categoriesRouter)) so the app exposes endpoints to create/read/update/delete categories.

app.use('/categories', categoriesRouter);

return app;
};

module.exports = {
Expand Down
8 changes: 4 additions & 4 deletions src/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ const {
*/

const sequelize = new Sequelize({
database: POSTGRES_DB || 'postgres',
username: POSTGRES_USER || 'postgres',
host: POSTGRES_HOST || 'localhost',
database: POSTGRES_DB || 'mydb',
username: POSTGRES_USER || 'mykola',
host: POSTGRES_HOST || '20.160.160.166',
dialect: 'postgres',
port: POSTGRES_PORT || 5432,
password: POSTGRES_PASSWORD || '123',
password: POSTGRES_PASSWORD || 'strongpassword',
Comment on lines +26 to +29
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The model currently defines category as a STRING. The task requires using a dedicated Category model and storing a foreign key on Expense instead. Replace this category string field with a categoryId INTEGER (allowNull: true) and rely on associations to link Expense <> Category (see checklist item #5).

});

module.exports = {
Expand Down
22 changes: 22 additions & 0 deletions src/models/Category.model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

const { sequelize } = require('../db.js');
const { DataTypes } = require('sequelize');

const Category = sequelize.define(
'Category',
{
name: {
type: DataTypes.STRING,
allowNull: false,
},
},
{
tableName: 'categories',
timestamps: false,
},
);

module.exports = {
Category,
};
34 changes: 33 additions & 1 deletion src/models/Expense.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,40 @@

const { sequelize } = require('../db.js');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In createServer.js you import users and expenses routers but there is no import for a categories router. The task requires implementing a CRUD page to manage categories — add a categories router (e.g. const categoriesRouter = require('./routes/categories')).

const { DataTypes } = require('sequelize');

const Expense = sequelize.define(
// your code goes here
'Expense',
{
userId: {
type: DataTypes.INTEGER,
allowNull: false,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You register /users and /expenses routes but you don't mount a /categories route. Mount the categories router (e.g. app.use('/categories', categoriesRouter)) so the CRUD endpoints are available as required by the task.

},
spentAt: {
type: DataTypes.DATE,
allowNull: false,
},
title: {
type: DataTypes.STRING,
allowNull: false,
},
amount: {
type: DataTypes.FLOAT,
allowNull: false,
},
category: {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Expense model stores category as a plain string. To properly implement a categories CRUD page you should add a dedicated Category model and relate Expense to it (e.g. categoryId FK and associations) or explicitly document that categories are managed as strings and implement persistence accordingly.

type: DataTypes.STRING,
allowNull: true,
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Expense model currently stores category as a plain string. The task requires introducing a Category model and linking expenses to categories via a foreign key. Replace this category string field with a categoryId integer field (allowNull: true if categories are optional) and add Sequelize associations (Category.hasMany(Expense) and Expense.belongsTo(Category)) elsewhere (e.g., in your models index). Also update routes to accept/validate categoryId (or map a category name to an existing Category) when creating/updating expenses (checklist items #5 and #6).

note: {
type: DataTypes.STRING,
allowNull: true,
},
},
{
tableName: 'expenses',
timestamps: false,
},
);

module.exports = {
Expand Down
14 changes: 13 additions & 1 deletion src/models/User.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,20 @@

const { sequelize } = require('../db.js');

const { DataTypes } = require('sequelize');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are not importing the Category model here. Per the task you should import Category so route handlers can validate/resolve category references and/or include Category in queries (checklist items #5 and #6).

const User = sequelize.define(
// your code goes here
'User',
{
name: {
type: DataTypes.STRING,
allowNull: false,
},
},
{
tableName: 'users',
timestamps: false,
},
);

module.exports = {
Comment on lines +11 to 21
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseCategories treats categories as text values. If you switch to categoryId this parser should convert values to numbers (and validate them). If you keep name-based filtering, ensure you resolve names against the Category table before querying expenses.

Expand Down
5 changes: 5 additions & 0 deletions src/models/models.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Comment on lines +5 to +8
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You import Category but never define associations between Category and Expense. Add Category.hasMany(Expense, { foreignKey: 'categoryId', constraints: false }) and Expense.belongsTo(Category, { foreignKey: 'categoryId', constraints: false }) (keep constraints: false if you must avoid DB-level FK constraints that can break truncation in tests).


module.exports = {
models: {
User,
Expense,
Category,
},
};
73 changes: 73 additions & 0 deletions src/routes/categories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use strict';

const express = require('express');
const {
models: { Category },
} = require('../models/models');
Comment on lines +5 to +6
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You import only Expense and User from models. Per the new design you must also import Category here so you can validate categoryId and/or perform joins when querying/filtering expenses.


const router = express.Router();

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

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

try {
const category = await Category.create({ name });

res.status(201).json(category);
} catch (error) {
res.status(500).json({ error: error.message });
Comment on lines +11 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parseCategories helper parses category names. If you switch to a categoryId relation you should either remove/replace this with parsing numbers (IDs) or change it to map names -> category IDs before using in queries.

}
});

router.get('/', async (req, res) => {
const categories = await Category.findAll();

Comment on lines +26 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The categories filter is mapped to numbers and NaNs are removed, so non-numeric category names (strings) will be ignored. If you intend to support filtering by category name (allowed by the task as an option), map provided names to Category ids before building the where.categoryId filter; otherwise document that only numeric IDs are accepted.

res.status(200).json(categories);
});

router.get('/:id', async (req, res) => {
const { id } = req.params;
const category = await Category.findByPk(id);

if (!category) {
return res.status(404).json({ error: 'Category not found' });
}
res.status(200).json(category);
});

router.delete('/:id', async (req, res) => {
const { id } = req.params;
const category = await Category.findByPk(id);

if (!category) {
return res.status(404).json({ error: 'Category not found' });
Comment on lines +46 to +47
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GET /expenses currently filters with where.category = { [Op.in]: categoryList }. After introducing a relation you must filter by categoryId (e.g., where.categoryId) or perform a join (include: Category) and filter on the Category field instead.

}
await category.destroy();
res.status(204).send();
});

const updateHandler = async (req, res) => {
Comment on lines +50 to +53
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider returning expenses with their Category included (Expense.findAll({ include: Category, ... })) so clients can access category names without extra requests once the relation is added.

const { id } = req.params;
const { name } = req.body;
const category = await Category.findByPk(id);

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

if (name !== undefined) {
category.name = name;
}

await category.save();
res.status(200).json(category);
};

Comment on lines +68 to +69
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

POST handler destructures and accepts category as a string. Update the endpoint to accept categoryId (validate existence with Category.findByPk) or resolve the provided name to an existing Category before creating the expense.

router.patch('/:id', updateHandler);
router.put('/:id', updateHandler);

module.exports = router;
158 changes: 158 additions & 0 deletions src/routes/expenses.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
'use strict';

const express = require('express');
const { Op } = require('sequelize');
const {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You imported Category but did not define Sequelize associations between Category and Expense. Add Category.hasMany(Expense, { foreignKey: 'categoryId', constraints: false }) and Expense.belongsTo(Category, { foreignKey: 'categoryId', constraints: false }) here so the relation exists at the ORM level (checklist item #5).

models: { Expense, User },
} = require('../models/models');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no Category model or any association between Expense and a Category. The task requires CRUD management for categories — add a Category model/table and define associations (e.g. Category.hasMany(Expense) and Expense.belongsTo(Category) or add a categoryId FK on Expense).


Comment on lines +7 to +8
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User<->Expense associations are present; do the same for Category<->Expense here and use constraints: false if you must avoid DB-level FK constraints that interfere with tests.

const router = express.Router();

function parseCategories(categories) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseCategories treats categories as strings which is fine for filtering free-form categories, but to meet the requirement you should persist categories and link expenses to them (so consider parsing incoming category IDs or names depending on chosen design).

if (!categories) {
return [];
Comment on lines +10 to +13
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep userId as a plain INTEGER and avoid adding a DB-level foreign-key here. Tests call User.destroy({ truncate: true }); to avoid failures, keep the DB constraint off and define the association in models.js with constraints: false (you already do that).

}

const values = Array.isArray(categories) ? categories : [categories];

return values
.flatMap((value) => String(value).split(','))
.map((value) => value.trim())
.filter(Boolean);
}

router.get('/', async (req, res) => {
const { userId, from, to, categories } = req.query;
const categoryList = parseCategories(categories);

const where = {};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The model defines category as a STRING. Per the task you must link expenses to the Category model. Replace this field with a categoryId integer foreign key (allowNull: true if optional) and rely on Sequelize associations to relate Expense <> Category (see checklist item #5).


if (userId !== undefined) {
where.userId = userId;
}

if (from || to) {
where.spentAt = {};

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

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

if (categoryList.length > 0) {
where.category = { [Op.in]: categoryList };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filtering uses where.category = { [Op.in]: categoryList } which works for string categories; if you introduce a Category model change filters to use categoryId (and include necessary joins) or query by Category.name via joins.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filtering currently uses where.category = { [Op.in]: categoryList }. After switching to the relation you must filter by categoryId (e.g., where.categoryId = { [Op.in]: ... }) or perform a join on Category to filter by name. Update this logic to use the new relation (checklist item #6).

}

const expenses = await Expense.findAll({
where,
order: [['id', 'ASC']],
});

res.status(200).json(expenses);
});

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

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

router.post('/', async (req, res) => {
const { userId, spentAt, title, amount, category, note } = req.body;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request handlers destructure category from the body. Update handlers to accept/validate categoryId (or accept a category name and map it to an existing Category record) before creating/updating expenses, per checklist item #6.


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

const user = await User.findByPk(userId);

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

try {
const expense = await Expense.create({
userId,
spentAt,
title,
amount,
category,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When creating an expense you accept category as a free string (Expense.create includes category). To support a categories CRUD, accept and validate a categoryId (or map provided category name to an existing Category) and persist a foreign key instead of a free string.

note,
Comment on lines +104 to +111
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When creating an expense you pass the category string into Expense.create. Change this to persist categoryId (or resolve the category name to an id) so expenses reference Category rows instead of storing raw strings.

});

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

const updateHandler = async (req, res) => {
const { id } = req.params;
const { userId, spentAt, title, amount, category, note } = req.body;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update handler still accepts category and assigns expense.category = category. Change this to accept/assign categoryId (or resolve a category name to an id) and save the FK on the expense.

const expense = await Expense.findByPk(id);

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

if (userId !== undefined) {
const user = await User.findByPk(userId);

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

if (spentAt !== undefined) {
expense.spentAt = spentAt;
}

if (title !== undefined) {
expense.title = title;
}

if (amount !== undefined) {
expense.amount = amount;
}

if (category !== undefined) {
expense.category = category;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The update handler also assigns expense.category = category from request body. If you switch to a Category model, update this logic to validate and set categoryId (and handle removal/changes accordingly).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment updates the old category string. After schema change you must assign to expense.categoryId (or perform the mapping to an existing Category id) instead of expense.category.

}

if (note !== undefined) {
expense.note = note;
}

await expense.save();
res.status(200).json(expense);
};

router.patch('/:id', updateHandler);
router.put('/:id', updateHandler);

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

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

module.exports = router;
Loading
Loading