Skip to content

Commit

Permalink
Merge 49f4cab into 3d1496b
Browse files Browse the repository at this point in the history
  • Loading branch information
micah-akpan committed Apr 26, 2019
2 parents 3d1496b + 49f4cab commit 83d2d1c
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"no-param-reassign": 0,
"comma-dangle": 0,
"camelcase": 0,
"no-console": 0,
"no-console": 1,
"import/prefer-default-export": 0,
"function-paren-newline": 0,
"object-curly-newline": 0,
Expand Down
57 changes: 52 additions & 5 deletions src/controllers/article.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import sequelize from 'sequelize';
import { Article, Comment, Bookmark, Report, Rating, Sequelize } from '../models';
import {
Article,
Comment,
Bookmark,
Report,
Rating,
Sequelize,
ArticleReadHistory
} from '../models';
import { slug, userAuthoredThisArticle } from '../utils/article';
import {
responseHandler,
responseFormat,
errorResponseFormat,
omitProps,
sendResponse,
handleDBErrors
handleDBErrors,
addArticleToReadHistory
} from '../utils';
import { findAndCount } from '../utils/query';

Expand Down Expand Up @@ -227,18 +236,19 @@ export const rateArticle = async (req, res) => {
articleId,
value: userRating
},
attributes: ['id', 'articleId', 'userId', 'value']
});

if (!created) {
const updatedRating = await rating.update({ value: userRating }, { returning: true });
return sendResponse(res, 200, {
responseType: 'success',
data: omitProps(updatedRating.dataValues, ['createdAt', 'updatedAt']),
data: omitProps(updatedRating.get({ plain: true }), ['createdAt', 'updatedAt']),
});
}
return sendResponse(res, 201, {
responseType: 'success',
data: omitProps(rating.dataValues, ['createdAt', 'updatedAt']),
data: rating,
});
} catch (error) {
handleDBErrors(error, { req, Sequelize }, message => sendResponse(res, 500, {
Expand Down Expand Up @@ -282,7 +292,6 @@ export const getAllArticles = async (req, res) => {
});
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 All @@ -309,8 +318,46 @@ export const getAnArticleByID = async (req, res) => {
}]
});
if (!article) { return responseHandler(res, 404, { status: 'fail', message: 'Article not found!' }); }
// This article will be added to this user read history
await addArticleToReadHistory(id, req.user.id);
return responseHandler(res, 200, { status: 'success', data: article });
} catch (error) {
return responseHandler(res, 500, { status: 'error', message: 'An internal server error occured!' });
}
};

/**
* @description This gets the reading stats of the user
* @function getReadingStats
* @param {Request} req
* @param {Response} res
* @returns {object} Returns response object
*/
export const getReadingStats = async (req, res) => {
try {
const userReadHistory = await ArticleReadHistory.findAll({
where: { userId: req.user.id },
include: [{
model: Article,
as: 'article',
attributes: ['id', 'title', 'slug']
}],
attributes: { exclude: ['createdAt', 'updatedAt'] }
});

const readArticles = userReadHistory.map(history => history.article);

return res.status(200).json({
status: 'success',
data: {
totalArticleReadCount: readArticles.length,
articles: readArticles,
}
});
} catch (error) {
return res.status(500).json({
status: 'error',
message: 'internal server error occurred'
});
}
};
42 changes: 42 additions & 0 deletions src/migrations/20190421143711-create-article-read-history.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@

export default {
up: (queryInterface, Sequelize) => queryInterface.createTable('article_read_histories', {
id: {
type: Sequelize.UUID,
allowNull: false,
primaryKey: true,
defaultValue: Sequelize.UUIDV4
},
articleId: {
type: Sequelize.UUID,
allowNull: false,
references: {
model: 'articles',
key: 'id',
deferrable: Sequelize.Deferrable.INITIALLY_IMMEDIATE,
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
userId: {
type: Sequelize.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id',
deferrable: Sequelize.Deferrable.INITIALLY_IMMEDIATE
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
}),
down: queryInterface => queryInterface.dropTable('article_read_histories')
};
46 changes: 46 additions & 0 deletions src/models/article_read_history.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@

/**
* @function
* @param {sequelize} sequelize
* @param {DataTypes} DataTypes
* @returns {Model} Returns a database model
*/
export default (sequelize, DataTypes) => {
const ArticleReadHistory = sequelize.define(
'ArticleReadHistory',
{
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
},

articleId: {
type: DataTypes.UUID,
allowNull: true,
},

userId: {
type: DataTypes.UUID,
allowNull: true,
},
},
{
tableName: 'article_read_histories'
}
);
ArticleReadHistory.associate = (models) => {
const { Article, User } = models;
ArticleReadHistory.belongsTo(Article, {
foreignKey: 'articleId',
as: 'article',
onDelete: 'CASCADE',
});
ArticleReadHistory.belongsTo(User, {
foreignKey: 'userId',
as: 'user',
onDelete: 'CASCADE',
});
};
return ArticleReadHistory;
};
6 changes: 4 additions & 2 deletions src/routers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
reportArticle,
bookmarkArticle,
editArticleTag,
rateArticle
rateArticle,
getReadingStats
} from '../controllers/article';
import { addComment } from '../controllers/comment';
import checkFields from '../middlewares/auth/loginValidator';
Expand Down Expand Up @@ -71,7 +72,7 @@ router.route('/articles/:id/highlight').post(Auth.authenticateUser, checkParam,
*/
router
.route('/articles/:id?')
.get(checkQueryParams, getArticleHandler)
.get(checkQueryParams, Auth.authenticateUser, getArticleHandler)
.post(Auth.authenticateUser, articleValidation, addArticle)
.delete(checkParam, Auth.authenticateUser, verifyArticle, isAuthor, deleteArticle)
.put(checkParam, Auth.authenticateUser, articleValidation, verifyArticle, isAuthor, editArticle);
Expand Down Expand Up @@ -164,6 +165,7 @@ router.get('/authors', Auth.authenticateUser, getAuthors);

router.post('/articles/:articleId/ratings', Auth.authenticateUser, articleRatingValidation, rateArticle);

router.get('/user-reading-stats', Auth.authenticateUser, getReadingStats);

router.all('*', (req, res) => {
res.status(404).json({
Expand Down
20 changes: 20 additions & 0 deletions src/seeders/20190421151513-article-read-history.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export default {
up: queryInterface => queryInterface.bulkInsert('article_read_histories', [
{
id: '979eaa2e-5b8f-4103-8192-4639afae2ba1',
articleId: '979eaa2e-5b8f-4103-8192-4639afae2ba8',
userId: '979eaa2e-5b8f-4103-8192-4639afae2ba9',
createdAt: new Date(),
updatedAt: new Date()
},

{
id: '979eaa2e-5b8f-4103-8192-4639afae2ba2',
articleId: '979eaa2e-5b8f-4103-8192-4639afae2ba4',
userId: '979eaa2e-5b8f-4103-8192-4639afae2ba9',
createdAt: new Date(),
updatedAt: new Date()
}
], {}),
down: queryInterface => queryInterface.bulkDelete('article_read_histories', null, {})
};
28 changes: 28 additions & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Validator from 'validatorjs';
import isUUID from 'validator/lib/isUUID';
import { ArticleReadHistory } from '../models';

export const responseFormat = (response) => {
const { data, status, message } = response;
Expand Down Expand Up @@ -153,6 +154,14 @@ export const omitProps = (obj, props) => {
return filtered;
};

/**
* @description Handles Database errors thrown during a DB operation
* @function handleDBErrors
* @param {object} error A Database error instance
* @param {object} opts An hash of request and Sequelize object
* @param {function} cb callback function that sends back the error response
* @returns {*} Returns the result of calling the `cb` function
*/
export const handleDBErrors = (error, { req, Sequelize }, cb) => {
let errorResponseMessage = '';
if (error instanceof Sequelize.ForeignKeyConstraintError) {
Expand All @@ -162,3 +171,22 @@ export const handleDBErrors = (error, { req, Sequelize }, cb) => {
}
return cb(errorResponseMessage);
};

/**
* @function addToReadHistory
* @param {string} articleId id of the article to be added
* @param {string} userId id of the user read stats to be added
* @returns {ArticleReadHistory} returns a the newly created article read history
*/
export const addArticleToReadHistory = async (articleId, userId) => {
try {
let articleReadHistory = await ArticleReadHistory.findOne({ where: { articleId, userId } });
if (!articleReadHistory) {
articleReadHistory = await ArticleReadHistory.create({ articleId, userId });
return articleReadHistory;
}
return articleReadHistory;
} catch (error) {
throw error;
}
};
5 changes: 2 additions & 3 deletions tests/integration/article.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,9 @@ describe('DELETE /api/v1/articles/:id', () => {
});
});

after(async (done) => {
app.close();
after(async () => {
await app.close();
app = null;
done();
});
});

Expand Down
51 changes: 51 additions & 0 deletions tests/integration/readingStats.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import 'chai/register-should';
import chai from 'chai';
import chaiHttp from 'chai-http';
import { startServer } from '../../src/server';
import Auth from '../../src/middlewares/authenticator';

chai.use(chaiHttp);


describe('User Reading Stats API Test', () => {
let app, agent = null;

beforeEach(async () => {
app = await startServer(9999);
agent = chai.request(app);
});

it('should return the reading stats of a user', (done) => {
agent
.get('/api/v1/user-reading-stats')
.set('Authorization', Auth.generateToken({ id: '979eaa2e-5b8f-4103-8192-4639afae2ba9' }))
.end((err, res) => {
if (err) return done(err);
const { body } = res;
res.should.have.status(200);
body.status.should.equal('success');
body.data.should.have.property('articles');
body.data.should.have.property('totalArticleReadCount');
body.data.totalArticleReadCount.should.be.a('number');
done();
});
});

it('should return an error if no user is provided', (done) => {
agent
.get('/api/v1/user-reading-stats')
.set('Authorization', Auth.generateToken({ id: '979eaa2e-5b8f-4103-8192-4639afae2ba30' }))
.end((err, res) => {
if (err) return done(err);
const { body } = res;
res.should.have.status(500);
body.status.should.equal('error');
done();
});
});

afterEach(async () => {
app = await app.close();
agent = null;
});
});

0 comments on commit 83d2d1c

Please sign in to comment.