Skip to content

Commit

Permalink
164796899-feature: pagination support for articles
Browse files Browse the repository at this point in the history
- modify getAllArticles controller
- write integration tests
- add middlewares

[Delivers #164796899]
  • Loading branch information
chikeozulumba committed Apr 18, 2019
1 parent 3cfe8a5 commit 29f65e3
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 17 deletions.
17 changes: 14 additions & 3 deletions src/controllers/article.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import sequelize from 'sequelize';
import { slug } from '../utils/article';
import { responseHandler, responseFormat, errorResponseFormat } from '../utils';
import { findAndCount } from '../utils/query';
import { Article, Comment, Bookmark, Report, Sequelize } from '../models';

/**
Expand Down Expand Up @@ -209,21 +210,31 @@ export const reportArticle = async (req, res) => {
*/
export const getAllArticles = async (req, res) => {
try {
let { query: { page, limit } } = req;
const regex = new RegExp('^\\d+$');
page = regex.test(page) ? parseInt(page, 10) : 1;
limit = regex.test(limit) ? parseInt(limit, 10) : 10;
const { count } = await findAndCount(Article, { where: {} });
const pages = Math.ceil(count / limit);
const offset = limit * (page - 1);
const paginate = { limit, offset, subQuery: false };

const articles = await Article.findAll({
where: { published: true },
order: [['createdAt', 'ASC']],
group: ['Article.id', 'comments.id'],
...paginate,
include: [{
model: Comment,
as: 'comments',
attributes: [
[sequelize.fn('COUNT', sequelize.col('comments.id')), 'all'],
],
group: 'comments.id',
}]
}],
});
return responseHandler(res, 200, { status: 'success', data: articles });
return responseHandler(res, 200, { status: 'success', data: articles, pages, });
} catch (error) {
console.log(error);
return responseHandler(res, 500, { status: 'error', message: 'An internal server error occured!' });
}
};
Expand Down
33 changes: 31 additions & 2 deletions src/middlewares/articles.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export const articleValidation = async (req, res, next) => {
next();
};


/**
*@name verifyArticle
*@description Middleware for verifying that article exists
Expand All @@ -40,7 +39,6 @@ export const verifyArticle = async (req, res, next) => {
next();
};


/**
*@name isAuthor
*@description Middleware for checking if an author has the privilege to edit an article
Expand All @@ -56,6 +54,7 @@ export const isAuthor = async (req, res, next) => {
if (authorId !== userId && role !== 'admin') { return responseHandler(res, 403, { status: 'error', message: 'You don\'t have access to manage this article!', }); }
next();
};

/**
* @function articleReportValidation
* @param {Request} req
Expand Down Expand Up @@ -107,3 +106,33 @@ export const getArticleHandler = async (req, res, next) => {
default: return getAllArticles(req, res, next);
}
};

/**
*@name getArticleHandler
*@description Middleware for handling requests for an article
* @param {object} req Request object
* @param {object} res Response object
* @param {function} next Calls the necxt function/action
* @returns {function} Calls next function/action
*/
export const checkQueryParams = async (req, res, next) => {
const queryParams = req.query;
const allowedQueryFields = ['page', 'limit'];
if (typeof queryParams !== 'object') { return next(); }
// eslint-disable-next-line no-restricted-syntax
for (const query in queryParams) {
// eslint-disable-next-line no-prototype-builtins
if (queryParams.hasOwnProperty(query)) {
if (!allowedQueryFields.includes(query)) {
return responseHandler(res, 400, { status: 'fail', message: 'Invalid request parameter supplied!' });
}
if (query === 'page' || query === 'limit') {
const param = parseInt(queryParams[query], 10);
if (param < 1) {
return responseHandler(res, 400, { status: 'fail', message: `${query.toUpperCase()} should not be less than 1!` });
}
}
}
}
return next();
};
5 changes: 3 additions & 2 deletions src/routers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ import {
isAuthor,
getArticleHandler,
articleReportValidation,
articleTagValidation
articleTagValidation,
checkQueryParams,
} from '../middlewares/articles';
import { checkParam } from '../middlewares/checkParam';
import checkEditBody from '../middlewares/editProfileValidator';
Expand Down Expand Up @@ -61,7 +62,7 @@ router
*/
router
.route('/articles/:id?')
.get(getArticleHandler)
.get(checkQueryParams, getArticleHandler)
.post(Auth.authenticateUser, articleValidation, addArticle)
.delete(checkParam, Auth.authenticateUser, verifyArticle, isAuthor, deleteArticle)
.put(checkParam, Auth.authenticateUser, articleValidation, verifyArticle, isAuthor, editArticle);
Expand Down
11 changes: 3 additions & 8 deletions src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,9 @@ export const validateParameters = async (body) => {
* @param {object} payload The response data to the user
* @returns {object} Returns the response object
*/
export const responseHandler = (res, code, payload) => {
const { data, status, message } = payload;
return res.status(code).json({
status,
message,
data,
});
};
export const responseHandler = (res, code, payload) => res.status(code).json({
...payload,
});

/**
* @function errorResponseFormat
Expand Down
19 changes: 19 additions & 0 deletions src/utils/query.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
/**
* @name findById
* @description This is function for getting records by primary key
* @param {object} Model The object model
* @param {string} id The primary key
* @returns {object} Returns record query
*/
export const findById = async (Model, id) => {
const record = await Model.findByPk(id);
return record;
};

/**
* @name findAndCount
* @description This is function for counting records via query
* @param {object} Model The object model
* @param {object|null|void} param The certain conditionals for the query to run
* @returns {object} Returns record query
*/
export const findAndCount = async (Model, param = null) => {
const record = await Model.findAndCountAll(param);
return record;
};

export default findById;
52 changes: 50 additions & 2 deletions tests/integration/article.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ describe('GET /api/v1/articles/:id?', () => {
it('Should return status: 200 for getting all articles', (done) => {
agent.get('/api/v1/articles')
.set('Authorization', JWT_TOKEN)
.send(ARTICLE)
.end((_err, res) => {
expect(res).to.have.status(200);
done();
Expand All @@ -31,13 +30,53 @@ describe('GET /api/v1/articles/:id?', () => {
it('Should return status: 200 for getting a single article', (done) => {
agent.get('/api/v1/articles/979eaa2e-5b8f-4103-8192-4639afae2ba7')
.set('Authorization', JWT_TOKEN)
.send(ARTICLE)
.end((_err, res) => {
expect(res).to.have.status(200);
done();
});
});

it('Should return status: 200 for getting all articles in paginated format when valid query parameters are supplied', (done) => {
agent.get('/api/v1/articles?page=1&limit=5')
.set('Authorization', JWT_TOKEN)
.end((_err, res) => {
expect(res).to.have.status(200);
expect(res.body.status).to.equal('success');
done();
});
});

it('Should return status: 400 for getting all articles in paginated format when invalid query parameters value are supplied', (done) => {
agent.get('/api/v1/articles?page=1&limit=0')
.set('Authorization', JWT_TOKEN)
.end((_err, res) => {
expect(res).to.have.status(400);
expect(res.body.status).to.equal('fail');
done();
});
});

it('Should return status: 400 for getting all articles in paginated format when invalid query parameters are supplied', (done) => {
agent.get('/api/v1/articles?page=1&limit=0&atabolebo=1000000')
.set('Authorization', JWT_TOKEN)
.end((_err, res) => {
expect(res).to.have.status(400);
expect(res.body.status).to.equal('fail');
done();
});
});

it('Should return status: 200 with an empty array when page parameter exceeds available records number', (done) => {
agent.get('/api/v1/articles?page=200&limit=5')
.set('Authorization', JWT_TOKEN)
.end((_err, res) => {
expect(res).to.have.status(200);
expect(res.body.data.length).to.equal(0);
done();
});
});


after(async (done) => {
app.close();
app = null;
Expand Down Expand Up @@ -73,6 +112,15 @@ describe('POST /api/v1/articles', () => {
});
});

it('Should return status: 400 when authorization token is invalid', (done) => {
agent.post('/api/v1/articles')
.send(ARTICLE)
.end((_err, res) => {
expect(res).to.have.status(400);
done();
});
});

after(async (done) => {
app.close();
app = null;
Expand Down
2 changes: 2 additions & 0 deletions tests/mock/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,5 @@ export const editPayload = {
bio: 'I Love Javascript',
imageUrl: 'http://waterease.herokuapp.com/images/board/comfort.com'
};

export const FAKE_USER = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ijg4ZDkwOGQwLTdmYWYtNDZiNC1hZTQ2LWNhZWE0MzVlY2I4MiIsImZ1bGxOYW1lIjoiY2hpa2UgYXJ0aHVyIiwiYmlvIjpudWxsLCJlbWFpbCI6ImFuZGVsYVV0b2JpbGlibGl5YUBnbWFpbC5jb20iLCJ1c2VybmFtZSI6ImFydGh1cnRvIiwicm9sZSI6ImF1dGhvciIsImlhdCI6MTU1NTYwMDg3MywiZXhwIjoxNTU1Njg3MjczfQ.YrQFpEPJP4fqLSALXwdwQZtDAogruEdH9EtoLz1qZPA';

0 comments on commit 29f65e3

Please sign in to comment.