Skip to content

Commit d59c4e5

Browse files
authored
Merge pull request #5 from foyzulkarim/feature/03-request-validation
[Refactor, Test] Enhance Logging, Testing, and Request Validation
2 parents f2a1892 + 68ae738 commit d59c4e5

File tree

12 files changed

+430
-81
lines changed

12 files changed

+430
-81
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
},
1010
"scripts": {
1111
"test": "jest",
12-
"test:watch": "jest --watch",
12+
"test:watch": "jest --watch --verbose",
1313
"start": "nodemon src/start.js"
1414
},
1515
"keywords": [],

src/domains/product/api.js

+69-21
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,95 @@
11
const express = require('express');
22
const logger = require('../../libraries/log/logger');
3+
const { AppError } = require('../../libraries/error-handling/AppError');
34

45
const {
56
createProduct,
6-
getAllProducts,
7+
filterProducts,
78
getProductById,
89
updateProductById,
910
deleteProductById,
1011
} = require('./service');
1112

13+
const { createSchema, updateSchema, idSchema } = require('./request');
14+
const { validateRequest } = require('../../middlewares/request-validate');
15+
const { logRequest } = require('../../middlewares/log');
16+
1217
// CRUD for product entity
1318
const routes = () => {
1419
const router = express.Router();
15-
16-
router.get('/', async (req, res, next) => {
17-
logger.info('GET /api/v1/products', { query: req.query });
20+
logger.info('Setting up product routes');
21+
router.get('/', logRequest({}), async (req, res, next) => {
1822
try {
19-
const products = await getAllProducts();
23+
// TODO: Add pagination and filtering
24+
const products = await filterProducts(req.query);
2025
res.json(products);
2126
} catch (error) {
2227
next(error);
2328
}
2429
});
2530

26-
router.post('/', async (req, res) => {
27-
logger.info('POST /api/v1/products', { body: req.body });
28-
const product = await createProduct(req.body);
29-
res.status(201).json(product);
30-
});
31+
router.post(
32+
'/',
33+
logRequest({}),
34+
validateRequest({ schema: createSchema }),
35+
async (req, res, next) => {
36+
try {
37+
const product = await createProduct(req.body);
38+
res.status(201).json(product);
39+
} catch (error) {
40+
next(error);
41+
}
42+
}
43+
);
3144

32-
router.get('/:id', async (req, res) => {
33-
logger.info('GET /api/v1/products/:id', { params: req.params });
34-
const product = await getProductById(req.params.id);
35-
res.status(200).json(product);
36-
});
45+
router.get(
46+
'/:id',
47+
logRequest({}),
48+
validateRequest({ schema: idSchema, isParam: true }),
49+
async (req, res, next) => {
50+
try {
51+
const product = await getProductById(req.params.id);
52+
if (!product) {
53+
throw new AppError('Product not found', 'Product not found', 404);
54+
}
55+
res.status(200).json(product);
56+
} catch (error) {
57+
next(error);
58+
}
59+
}
60+
);
3761

38-
router.put('/:id', async (req, res) => {
39-
res.json({ status: 'UP' });
40-
});
62+
router.put(
63+
'/:id',
64+
logRequest({}),
65+
validateRequest({ schema: idSchema, isParam: true }),
66+
validateRequest({ schema: updateSchema }),
67+
async (req, res, next) => {
68+
try {
69+
const product = await updateProductById(req.params.id, req.body);
70+
if (!product) {
71+
throw new AppError('Product not found', 'Product not found', 404);
72+
}
73+
res.status(200).json(product);
74+
} catch (error) {
75+
next(error);
76+
}
77+
}
78+
);
4179

42-
router.delete('/:id', async (req, res) => {
43-
res.json({ status: 'UP' });
44-
});
80+
router.delete(
81+
'/:id',
82+
logRequest({}),
83+
validateRequest({ schema: idSchema, isParam: true }),
84+
async (req, res, next) => {
85+
try {
86+
await deleteProductById(req.params.id);
87+
res.status(204).json({ message: 'Product deleted' });
88+
} catch (error) {
89+
next(error);
90+
}
91+
}
92+
);
4593

4694
return router;
4795
};

src/domains/product/request.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const Joi = require('joi');
2+
const mongoose = require('mongoose');
3+
4+
const createSchema = Joi.object().keys({
5+
name: Joi.string().required(),
6+
price: Joi.number().required(),
7+
description: Joi.string().optional(),
8+
inStock: Joi.boolean().optional(),
9+
});
10+
11+
const updateSchema = Joi.object().keys({
12+
name: Joi.string(),
13+
price: Joi.number(),
14+
description: Joi.string(),
15+
inStock: Joi.boolean(),
16+
});
17+
18+
const idSchema = Joi.object().keys({
19+
id: Joi.string()
20+
.custom((value, helpers) => {
21+
if (!mongoose.Types.ObjectId.isValid(value)) {
22+
return helpers.error('any.invalid');
23+
}
24+
return value;
25+
}, 'ObjectId validation')
26+
.required(),
27+
});
28+
29+
module.exports = { createSchema, updateSchema, idSchema };

src/domains/product/service.js

+27-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const logger = require('../../libraries/log/logger');
2+
13
const Product = require('./schema');
24
const { AppError } = require('../../libraries/error-handling/AppError');
35

@@ -6,18 +8,34 @@ const createProduct = async (data) => {
68
try {
79
const product = new Product(data);
810
const savedProduct = await product.save();
11+
logger.info('createProduct(): Product created', { id: savedProduct._id });
912
return savedProduct;
1013
} catch (error) {
14+
logger.error('createProduct(): Failed to create product', error);
1115
throw new AppError('Failed to create product', error.message);
1216
}
1317
};
1418

1519
// Get all products
16-
const getAllProducts = async () => {
20+
const filterProducts = async (query) => {
1721
try {
18-
const products = await Product.find();
22+
const { keyword } = query ?? {};
23+
// search by keyword on name and description fields
24+
const filter = {};
25+
if (keyword) {
26+
filter.$or = [
27+
{ name: { $regex: keyword, $options: 'i' } },
28+
{ description: { $regex: keyword, $options: 'i' } },
29+
];
30+
}
31+
const products = await Product.find(filter);
32+
logger.info('getAllProducts(): Products fetched', {
33+
filter,
34+
count: products.length,
35+
});
1936
return products;
2037
} catch (error) {
38+
logger.error('getAllProducts(): Failed to get products', error);
2139
throw new AppError('Failed to get products', error.message, 400);
2240
}
2341
};
@@ -26,8 +44,10 @@ const getAllProducts = async () => {
2644
const getProductById = async (id) => {
2745
try {
2846
const product = await Product.findById(id);
47+
logger.info('getProductById(): Product fetched', { id });
2948
return product;
3049
} catch (error) {
50+
logger.error('getProductById(): Failed to get product', error);
3151
throw new AppError('Failed to get product', error.message);
3252
}
3353
};
@@ -36,8 +56,10 @@ const getProductById = async (id) => {
3656
const updateProductById = async (id, data) => {
3757
try {
3858
const product = await Product.findByIdAndUpdate(id, data, { new: true });
59+
logger.info('updateProductById(): Product updated', { id });
3960
return product;
4061
} catch (error) {
62+
logger.error('updateProductById(): Failed to update product', error);
4163
throw new AppError('Failed to update product', error.message);
4264
}
4365
};
@@ -46,15 +68,17 @@ const updateProductById = async (id, data) => {
4668
const deleteProductById = async (id) => {
4769
try {
4870
await Product.findByIdAndDelete(id);
71+
logger.info('deleteProductById(): Product deleted', { id });
4972
return true;
5073
} catch (error) {
74+
logger.error('deleteProductById(): Failed to delete product', error);
5175
throw new AppError('Failed to delete product', error.message);
5276
}
5377
};
5478

5579
module.exports = {
5680
createProduct,
57-
getAllProducts,
81+
filterProducts,
5882
getProductById,
5983
updateProductById,
6084
deleteProductById,

src/middlewares/log/index.js

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const logger = require('../../libraries/log/logger');
2+
3+
// Middleware to log the request.
4+
// Logic: by default it will log req.params and req.query if they exist.
5+
// for the req.body, if no specific fields are provided in the fields, it will log the entire body.
6+
const logRequest = ({ fields = {} }) => {
7+
return (req, res, next) => {
8+
const logData = {};
9+
if (req.params) {
10+
logData.params = req.params;
11+
}
12+
if (req.query) {
13+
logData.query = req.query;
14+
}
15+
if (req.body) {
16+
if (fields && fields.length) {
17+
fields.forEach((field) => {
18+
logData[field] = req.body[field];
19+
});
20+
} else {
21+
logData.body = req.body;
22+
}
23+
}
24+
logger.info(`${req.method} ${req.originalUrl}`, logData);
25+
26+
// Store the original end method
27+
const oldEnd = res.end;
28+
// Override the end method
29+
res.end = function (...args) {
30+
// Log the status code after the original end method is called
31+
logger.info(`${req.method} ${req.originalUrl}`, {
32+
statusCode: res.statusCode,
33+
});
34+
oldEnd.apply(this, args);
35+
};
36+
37+
next();
38+
};
39+
};
40+
41+
module.exports = { logRequest };
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const logger = require('../../libraries/log/logger');
2+
3+
function validateRequest({ schema, isParam = false }) {
4+
return (req, res, next) => {
5+
const input = isParam ? req.params : req.body;
6+
const validationResult = schema.validate(input, { abortEarly: false });
7+
8+
if (validationResult.error) {
9+
logger.error(`${req.method} ${req.originalUrl} Validation failed`, {
10+
errors: validationResult.error.details.map((detail) => detail.message),
11+
});
12+
// Handle validation error
13+
return res.status(400).json({
14+
errors: validationResult.error.details.map((detail) => detail.message),
15+
});
16+
}
17+
18+
// Validation successful - proceed
19+
next();
20+
};
21+
}
22+
23+
module.exports = { validateRequest };

src/server.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const createExpressApp = () => {
2020

2121
expressApp.use((req, res, next) => {
2222
// Log an info message for each incoming request
23-
logger.info(`Received a ${req.method} request for ${req.url}`);
23+
logger.info(`${req.method} ${req.originalUrl}`);
2424
next();
2525
});
2626

@@ -69,7 +69,6 @@ function defineErrorHandlingMiddleware(expressApp) {
6969
error.isTrusted = true;
7070
}
7171
}
72-
console.log('error', error);
7372

7473
errorHandler.handleError(error);
7574
res.status(error?.HTTPStatus || 500).end();

test/app.test.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@ const { createExpressApp } = require('../src/server');
44

55
let app = null;
66
beforeAll(async () => {
7-
console.log('1 - beforeAll');
87
app = await createExpressApp();
98
});
109
afterAll(async () => {
11-
console.log('1 - afterAll');
1210
app = null;
1311
});
14-
beforeEach(async () => console.log('1 - beforeEach'));
15-
afterEach(async () => console.log('1 - afterEach'));
12+
13+
// beforeEach(async () => console.log('1 - beforeEach'));
14+
// afterEach(async () => console.log('1 - afterEach'));
1615

1716
// Test App module
1817
// Test API up and running

0 commit comments

Comments
 (0)