Skip to content

Commit

Permalink
feat(pagination): Delivers Pagination on articles
Browse files Browse the repository at this point in the history
- Write unit tests
- Modify the Tags and Article relationship
- Add queryHelper.js file
- Add seeds to article seeding file
- Add paginationValidation in ArticleValidation.js
- Add getAllArticles method in ArticleController.js
- Add getUserArticles method in ArticleController.js
- Add getSingleArticle method in ArticleController.js
- Add sendPaginationResponse in ArticleController
- Test endpoints with POSTMAN

[Delivers #159204728]
  • Loading branch information
chukwuemekachm committed Aug 15, 2018
1 parent 558f8c9 commit 4bc9e6f
Show file tree
Hide file tree
Showing 11 changed files with 456 additions and 4 deletions.
115 changes: 115 additions & 0 deletions server/controllers/ArticleController.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import models from '../models';
import randomString from '../helpers/randomString';
import dashReplace from '../helpers/replaceDash';
import queryHelper from '../helpers/queryHelper';

const { Article } = models;
const error = {
message: 'Request can not be processed at the moment, please try again shortly,',
status: 400,
};

/**
* This class contains all the methods responsible for creating and querying
* articles on the app
Expand Down Expand Up @@ -161,4 +167,113 @@ export default class ArticleController {
error: 'Article can not be deleted'
}));
}

/**
* Gets a list of paginated articles
*
* @param {Object} req The HTTP request object
* @param {Object} res The HTTP response object
* @param {Object} next The next middleware to be called
*/
static getAllArticles(req, res, next) {
const limit = req.query.limit || 100;
const offset = req.query.offset || 0;
Article.findAll(
Object.assign({}, queryHelper.allArticles, { offset, limit })
).then((articles) => {
ArticleController.sendPaginationResponse(res, articles, false);
})
.catch(() => next(error));
}

/**
* Gets a list of paginated articles belonging to the user
* in the url route parameter
*
* @param {Object} req The HTTP request object
* @param {Object} res The HTTP response object
* @param {Object} next The next middleware to be called
*/
static getUserArticles(req, res, next) {
const limit = req.query.limit || 100;
const offset = req.query.offset || 0;
let { userId } = req.params;
if (!/[0-9]/.test(userId)) {
return res.status(400).json({
status: 'fail',
errors: { userId: ['userId must be a number.'] }
});
}
userId = Number.parseInt(userId, 10);
Article.findAll(
Object.assign({}, queryHelper.allArticles, { where: { userId }, offset, limit }),
).then((articles) => {
ArticleController.sendPaginationResponse(res, articles, userId);
})
.catch(() => next(error));
}

/**
* Gets a single article with the slug specified
* in the url route parameter
*
* @param {Object} req The HTTP request object
* @param {Object} res The HTTP response object
* @param {Object} next The next middleware to be called
*/
static getSingleArticle(req, res, next) {
const { slug } = req.params;
Article.findOne(
Object.assign({}, queryHelper.allArticles, { where: { slug } }),
).then((article) => {
if (article) {
return res.status(200).json({
status: 'success',
article,
});
}
return res.status(404).json({
status: 'fail',
errors: {
article: [`Article with slug: ${slug} not found.`],
},
});
})
.catch(() => next(error));
}

/**
* Sends the response to the user when a list of paginated articles are requested
* For a given user or random articles
*
* @param {Object} res The HTTP response object
* @param {Object} articles A list of articles returned
* @param {String} userId The userId of the user whose articles are requested
* @returns {Object} The response object returned to the user
*/
static sendPaginationResponse(res, articles, userId) {
// Returns a 20 when a list of articles are requested and found
if (articles[0]) {
return res.status(200).json({
status: 'success',
articles,
});
}
// Returns a 404 when a list of user articles are requested but not found
if (userId) {
return res.status(404).json({
status: 'fail',
errors: {
articles: [`Articles not found for user with userId: ${userId}.`],
},
});
}
// Returns a 404 when a list of articles are requested but not found
return res.status(404).json({
status: 'fail',
errors: {
articles: ['No articles found.'],
},
});
}
}
21 changes: 21 additions & 0 deletions server/helpers/queryHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import models from '../models';

const {
User, Rating, Tag,
} = models;

/*
* Holds the query helper object which contains the various query templates
*/
export default {
allArticles: {
order: [
['createdAt', 'DESC']
],
attributes: ['id', 'slug', 'userId', 'categoryId', 'title', 'body', 'imageUrl', 'createdAt', 'updatedAt'],
include: [
{ model: User, attributes: ['username', 'firstName', 'lastName', 'bio', 'image'] },
{ model: Tag, as: 'tags', attributes: ['id', 'articleId', 'title', 'createdAt', 'updatedAt'] },
],
},
};
24 changes: 24 additions & 0 deletions server/middlewares/validations/ArticleValidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,28 @@ export default class ArticleValidation {
});
});
}

/**
* Validates the offset and limit parameters in the
* url query
* @param {Object} req The HTTP request object
* @param {Object} res The HTTP response object
* @param {Object} next The next middleware on the route
*/
static paginationValidation(req, res, next) {
const queryProperties = {
offset: 'integer|min:0',
limit: 'integer|min:0'
};

const validator = new Validator(req.query, queryProperties);
validator.passes(() => next());
validator.fails(() => {
const errors = validator.errors.all();
return res.status(400).json({
status: 'error',
errors,
});
});
}
}
1 change: 1 addition & 0 deletions server/migrations/20180806205551-create-article.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

module.exports = {
up: (queryInterface, Sequelize) => queryInterface.createTable('Articles', {
id: {
Expand Down
2 changes: 1 addition & 1 deletion server/models/article.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default (sequelize, DataTypes) => {
onUpdate: 'CASCADE',
});
Article.hasMany(models.Tag, {
foreignKey: 'tagId',
foreignKey: 'articleId',
as: 'tags'
});
Article.hasMany(models.Rating, {
Expand Down
5 changes: 2 additions & 3 deletions server/models/rating.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@

export default (sequelize, DataTypes) => {
const Rating = sequelize.define('Rating', {
userId: {
type: DataTypes.INTEGER,
allowNull: false
},
articleSlug: {
type: DataTypes.TEXT,
articleId: {
type: DataTypes.INTEGER,
allowNull: false
},
value: {
Expand Down
3 changes: 3 additions & 0 deletions server/routes/article.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@ articleRouter.delete('/:slug/comments/:id', isLoggedIn, CommentController.delete

articleRouter.post('/:author/:slug/:rating', isLoggedIn, RatingController.rateArticle);

articleRouter.get('/', ArticleValidation.paginationValidation, ArticleController.getAllArticles);

articleRouter.get('/:slug', ArticleController.getSingleArticle);

export default articleRouter;
8 changes: 8 additions & 0 deletions server/routes/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Router } from 'express';
import PasswordResetController from '../controllers/PasswordResetController';
import UserValidation from '../middlewares/validations/UserValidation';
import checkToken from '../middlewares/checkToken';
import ArticleValidation from '../middlewares/validations/ArticleValidation';
import ArticleController from '../controllers/ArticleController';

const userRouter = Router();

Expand All @@ -22,4 +24,10 @@ userRouter.put(
PasswordResetController.updateUserPassword
);

userRouter.get(
'/:userId/articles',
ArticleValidation.paginationValidation,
ArticleController.getUserArticles
);

export default userRouter;
40 changes: 40 additions & 0 deletions server/seeders/20180808181558-demo-Article.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,46 @@ module.exports = {
createdAt: '2018-08-01 21:54:49',
updatedAt: '2018-08-04 21:54:49'
},
{
title: 'Unmissable steps before, during, and after user research',
slug: 'unmissable-steps-before-74U33U38N3IHF1',
userId: 1,
categoryId: 2,
body: 'As a UX designer, I’m expected to run research studies to mine meaningful insights for my products. I often conduct usability studies to get a pulse of what users think, need or expect from a new product. I was fortunate to work on consumer-facing as well as enterprise products, as a result of which, I gained a diverse experience relevant to conducting user studies in industrial settings. While the commercial products gave us the liberty to find and interview users in person, the enterprise products on the other hand make it difficult to find or access the right users, resulting in remote usability studies.',
imageUrl: 'https://cloudinary/ah/images/miss.jpg',
createdAt: 'NOW()',
updatedAt: 'NOW()'
},
{
title: 'Unmissable steps before, during, and after user research',
slug: 'unmissable-steps-before-74U33U38N3IHF2',
userId: 1,
categoryId: 1,
body: 'As a UX designer, I’m expected to run research studies to mine meaningful insights for my products. I often conduct usability studies to get a pulse of what users think, need or expect from a new product. I was fortunate to work on consumer-facing as well as enterprise products, as a result of which, I gained a diverse experience relevant to conducting user studies in industrial settings. While the commercial products gave us the liberty to find and interview users in person, the enterprise products on the other hand make it difficult to find or access the right users, resulting in remote usability studies.',
imageUrl: 'https://cloudinary/ah/images/miss.jpg',
createdAt: 'NOW()',
updatedAt: 'NOW()'
},
{
title: 'Unmissable steps before, during, and after user research',
slug: 'unmissable-steps-before-74U33U38N3IHF3',
userId: 1,
categoryId: 1,
body: 'As a UX designer, I’m expected to run research studies to mine meaningful insights for my products. I often conduct usability studies to get a pulse of what users think, need or expect from a new product. I was fortunate to work on consumer-facing as well as enterprise products, as a result of which, I gained a diverse experience relevant to conducting user studies in industrial settings. While the commercial products gave us the liberty to find and interview users in person, the enterprise products on the other hand make it difficult to find or access the right users, resulting in remote usability studies.',
imageUrl: 'https://cloudinary/ah/images/miss.jpg',
createdAt: 'NOW()',
updatedAt: 'NOW()'
},
{
title: 'Unmissable steps before, during, and after user research',
slug: 'unmissable-steps-before-74U33U38N3IHF4',
userId: 1,
categoryId: 1,
body: 'As a UX designer, I’m expected to run research studies to mine meaningful insights for my products. I often conduct usability studies to get a pulse of what users think, need or expect from a new product. I was fortunate to work on consumer-facing as well as enterprise products, as a result of which, I gained a diverse experience relevant to conducting user studies in industrial settings. While the commercial products gave us the liberty to find and interview users in person, the enterprise products on the other hand make it difficult to find or access the right users, resulting in remote usability studies.',
imageUrl: 'https://cloudinary/ah/images/miss.jpg',
createdAt: 'NOW()',
updatedAt: 'NOW()'
},
]),
down: () => {
}
Expand Down
Loading

0 comments on commit 4bc9e6f

Please sign in to comment.