Skip to content

Commit

Permalink
feat(notification): add user notification
Browse files Browse the repository at this point in the history
- Add event listener and emitter on a new article created
- Add option for followers to receive notification when a new articles created
- Add functionalities of choosing preferred notificastions
- Add unit tests
- Add swagger documentation
[Delivers #165413114]
  • Loading branch information
Niyitangasam committed May 16, 2019
1 parent 3436d23 commit 4aaed8f
Show file tree
Hide file tree
Showing 19 changed files with 745 additions and 5 deletions.
3 changes: 3 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import session from 'express-session';
import routes from './routes';
import registerApiDocEndpoint from './config/swagger';
import pass from './config/passport/localstrategy';
import initNotification from './helpers/utils/eventHandlers';

initNotification();

const isProduction = process.env.NODE_ENV === 'production';

Expand Down
1 change: 1 addition & 0 deletions controllers/article.controller.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ArticleHelper from '../helpers/article';

/**
* @author Samuel Niyitanga
* @exports ArticleController
* @class ArticleController
* @description Handles all related articles functioanlities
Expand Down
44 changes: 44 additions & 0 deletions controllers/notification.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import NotificationHelper from '../helpers/notification';

/**
* @author Samuel Niyitanga
* @exports ArticleController
* @class ArticleController
* @description Handles all related notifications functioanlities
* */
class NotificationController {
/**
* @param {object} req - Request object
* @param {object} res - Response object
* @returns {object} response
* @static
*/
static async setNotification(req, res) {
await NotificationHelper.setOption(req);
return res.status(200).send({ Message: 'Notification option setted.' });
}

/**
* @param {object} req - Request object
* @param {object} res - Response object
* @returns {object} response
* @static
*/
static async unsetNotification(req, res) {
await NotificationHelper.UnsetOption(req);

return res.status(200).send({ Message: 'Notification successfully unsetted.' });
}

/**
* @param {object} req - Request object
* @param {object} res - Response object
* @returns {object} response
* @static
*/
static async getNotificationByUser(req, res) {
const notification = await NotificationHelper.getByUser(req);
return res.status(200).send({ notification });
}
}
export default NotificationController;
17 changes: 12 additions & 5 deletions helpers/article.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import uniqid from 'uniqid';
import Joi from 'joi';
import PassportHelper from './passport';
import db from '../models';
import emitter from './eventEmitters';

const { Article, User, Like } = db;

/**
* @author Samuel Niyitanga
* @exports ArticleHelper
* @class ArticleHelper
* @description Article Helper
Expand Down Expand Up @@ -49,6 +51,11 @@ class ArticleHelper {
slug,
authorId: articleAuthor.id
});
emitter.emit('onFollowPublish', {
title,
authorId: articleAuthor.id,
slug
});
const values = {
userData,
article
Expand Down Expand Up @@ -145,7 +152,7 @@ class ArticleHelper {
}

/**
* Check article owner
* Update article
* @param {object} req - an object
* @return {object} Returns response
* @static
Expand All @@ -164,10 +171,10 @@ class ArticleHelper {
}

/**
* Return if the user is owner
* Delete article
* @param {object} req - an object
* @param {object} res - an object
*@return {boolean} Return true if user matche
*@return {boolean} Return true if deleted
*/
static async deleteArticle(req) {
const currentSlug = req.params.slug;
Expand All @@ -177,7 +184,7 @@ class ArticleHelper {
}

/**
* Return if the user is owner
* Return all article
*@return {object} Return all articles
*/
static async getAllArticles() {
Expand All @@ -195,7 +202,7 @@ class ArticleHelper {
/**
* Return one article
* @param {object} req - an object
*@return {object} Return all articles
*@return {object} Return article
*/
static async getOneArticle(req) {
const article = await Article.findOne({
Expand Down
5 changes: 5 additions & 0 deletions helpers/eventEmitters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { EventEmitter } from 'events';

const emitter = new EventEmitter();

export default emitter;
238 changes: 238 additions & 0 deletions helpers/notification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import db from '../models';
import passportHelper from './passport';
import mailSender from './utils/mail-sender';

const {
User, Follows, Notification
} = db;

/**
* @author Samuel Niyitanga
* @exports NotificationHelper
* @class NotificationHelper
* @description Notification Helper
* */
class NotificationHelper {
/**
* Set notification option
* @param {object} req - an object
* @return {object} Returns response
* @static
*/
static async setOption(req) {
const { option } = req.params;
const user = await User.findOne({ where: { id: req.user.id } });
const notifsArray = user.dataValues.notification;
notifsArray.push(option);
const optionSet = await user.update({ notification: notifsArray });
return optionSet;
}

/**
* Unset notification option
* @param {object} req - an object
* @return {object} Returns response
* @static
*/
static async UnsetOption(req) {
const { option } = req.params;
const user = await User.findOne({ where: { id: req.user.id } });
const notifsArray = user.dataValues.notification;
const optionIndex = notifsArray.indexOf(option);
notifsArray.splice(optionIndex, 1);
const optionUnsetted = await user.update({ notification: notifsArray });
return optionUnsetted;
}

/**
* Check if option exists
* @param {object} req - an object
* @param {object} res - an object
* @param {object} next - an object
* @return {boolean} Returns if true if it is valid else return false
* @static
*/
static async isOptionAvailable(req, res, next) {
const user = await User.findOne({ where: { id: req.user.id } });
const index = user.dataValues.notification
.findIndex(notification => notification === req.params.option);
if (index !== -1) {
return res.status(422).send({ status: 422, Error: 'You already have this Option set' });
}
next();
}

/**
* Check if option exists
* @param {object} req - an object
* @param {object} res - an object
* @param {object} next - an object
* @return {boolean} Returns if true if it is valid else return false
* @static
*/
static async isOptionIn(req, res, next) {
const user = await User.findOne({ where: { id: req.user.id } });
const index = user.dataValues.notification
.findIndex(notification => notification === req.params.option);
if (index === -1) {
return res.status(422).send({ status: 422, Error: 'You don\'t have this Option set' });
}
next();
}

/**
* Check if it is valid params
* @param {object} req - an object
* @param {object} res - an object
* @param {object} next - an object
* @return {boolean} Returns if true if it is valid else return false
* @static
*/
static isValidOption(req, res, next) {
const validValues = ['email', 'in-app', 'follower', 'articleFavorite'];
if (validValues.indexOf(req.params.option) === -1) {
return res.status(422).send({ status: 422, Error: 'Only option must be email,in-app,follower or articleFavorite' });
}
next();
}

/**
* @param {integer} authorId
* @returns {object} return list of followers
*/
static async getFollowersList(authorId) {
const lists = await Follows.findAll({
where: { follower: authorId },
attributes: ['following']
});
return lists;
}

/**
* @param {object} userIds
* @param {string} userType
* @returns {Array} return array of object
*/
static async serializeUsers(userIds, userType) {
const type = userType === 'follower' ? 'follower' : 'following';
const userList = [];
await Promise.all(userIds.map(async (currentId) => {
const userInfo = await passportHelper.findRecord(User, currentId[type]);
userList.push({
id: currentId[type],
username: userInfo.dataValues.username,
bio: userInfo.dataValues.bio,
image: userInfo.dataValues.image,
email: userInfo.dataValues.email,
notification: userInfo.dataValues.notification
});
}));
return userList;
}

/**
* @param {object} followers
* @returns {object} return emails
*/
static getfollowersWithEmailOption(followers) {
const followersWithEmails = [];
followers.forEach((follower) => {
if (follower.notification.indexOf('email') !== -1) {
followersWithEmails.push(follower);
}
});
return followersWithEmails;
}

/**
* @param {object} followers
* @param {integer} authorId
* @param {string} articleSlug
* @returns {object} return followers
*/
static getfollowersWithAppOption(followers) {
const followersWithApp = [];
followers.forEach((follower) => {
if (follower.notification.indexOf('in-app') !== -1) {
followersWithApp.push(follower);
}
});
return followersWithApp;
}

/**
* Send notification email when users they follow publish new articles
* @param {string } email - A user aemail
* @param {string} author
* @param {string} title
* @returns {object} return email to send
*/
static sendEmailToFollowers(email, author, title) {
return mailSender.send({
email,
subject: `${author} created a new article`,
html: `<html>You can check <strong>${title}</strong> created on Author Haven</html>`
});
}

/**
* Send notification email when articles they favorited have new interaction
* @param {string } email - A user aemail
* @param {string} user - People who interacted with an article
* @param {string} title
* @returns {object} return email to send
*/
static sendEmailOnInteraction(email, user, title) {
return mailSender.send({
email,
subject: `Reaction on Article by ${user} on Author Haven`,
html: `<html>On Author Haven<strong>${title}</strong> had a new interaction </html>`
});
}

/**
* @param {object} followers followers object
* @param {string} title
* @param {string} username
* @returns {array} return array of notification object
*/
static inAppNotifications(followers, title, username) {
const notifications = [];
followers.forEach((follower) => {
if (follower.notification.indexOf('follower') !== -1) {
const singleNotification = {
userId: follower.id,
notificationMessage: `New article called ${title} was created by ${username} `,
};
notifications.push(singleNotification);
return;
}
if (follower.notification.indexOf('articleFavorite') !== -1) {
const singleNotification = {
userId: follower.id,
notificationMessage: `Reaction on Article called ${title} by ${username} `,
};
notifications.push(singleNotification);
}
});
return notifications;
}

/**
* Return one article
* @param {object} req - an object
*@return {object} Return article
*/
static async getByUser(req) {
const article = await Notification.findAll({
where: { userId: req.user.id },
include: [{
model: User,
as: 'user',
attributes: ['username', 'bio', 'image']
}]
});
return article;
}
}
export default NotificationHelper;
Loading

0 comments on commit 4aaed8f

Please sign in to comment.