Skip to content

Commit

Permalink
feat(notification): implement user notification
Browse files Browse the repository at this point in the history
- create notification modules
- setup in-app, email and push notifications
- write endpoints to manage notifications

[Delivers #167190447]
  • Loading branch information
chuxmykel committed Aug 7, 2019
1 parent e69a1f9 commit fef2e34
Show file tree
Hide file tree
Showing 22 changed files with 791 additions and 15 deletions.
5 changes: 4 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ SERVER_MAIL = noreply@authorsHaven.com
CLOUD_NAME = Your cloudinary cloud name
CLOUD_API_KEY = Your cloudinary api key
CLOUD_API_SECRET = Your cloudinary api secret

PUSHER_APP_ID=YOUR_APP_ID
PUSHER_APP_KEY=YOUR_APP_KEY
PUSHER_APP_SECRET=YOUR_APP_SECRET
PUSHER_APP_CLUSTER=YOUR_APP_CLUSTER
23 changes: 23 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "index.js",
"scripts": {
"start": "node build/index.js",
"dev": "export NODE_ENV=production && DEBUG=dev && nodemon --exec babel-node server/index.js",
"dev": "export DEBUG=dev && nodemon --exec babel-node server/index.js",
"debug": "export DEBUG=dev && nodemon --exec babel-node server/index.js --inspect",
"clean": "rm -rf build && mkdir build && npm run copy-docs",
"build": "npm run clean && npm run migrate && babel -d ./build ./server",
Expand All @@ -16,6 +16,7 @@
"seed:undo": "node_modules/.bin/sequelize db:seed:undo:all",
"generate:model": "node_modules/.bin/sequelize model:generate",
"generate:migration": "node_modules/.bin/sequelize migration:generate",
"generate:seed": "node_modules/.bin/sequelize seed:generate",
"migrate": "node_modules/.bin/sequelize db:migrate",
"migrate:undo": "node_modules/.bin/sequelize db:migrate:undo:all",
"coveralls": "nyc report --reporter=text-lcov | coveralls",
Expand Down Expand Up @@ -52,6 +53,7 @@
"passport-google-oauth2": "^0.2.0",
"pg": "^7.11.0",
"pg-hstore": "^2.3.3",
"pusher": "^2.2.2",
"sequelize": "^5.10.2",
"sequelize-cli": "^5.5.0",
"sequelize-slugify": "^0.7.0",
Expand Down
19 changes: 19 additions & 0 deletions server/controllers/articleController.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import sequelize from 'sequelize';
import models from '../db/models';
import utils from '../helpers/Utilities';
import Paginate from '../helpers/paginate';
import Notification from '../helpers/notifications';

const { Op } = sequelize;
const { paginateArticles } = Paginate;
Expand Down Expand Up @@ -36,6 +37,24 @@ class ArticleController {
image
});
article.tagList = [...article.dataValues.tagList.split(' ')];
const author = await article.getAuthor({
attributes: ['username'],
include: [{
model: models.User,
through: {
attributes: []
},
as: 'followers',
attributes: ['id', 'username', 'email', 'newPostEmailSub'],
}],
});

const payload = {
resourceType: 'article',
resourceId: article.slug,
message: `${author.username} just posted a new article`,
};
Notification.notify(author.followers, payload);
return utils.successStat(res, 201, 'articles', article);
}

Expand Down
10 changes: 10 additions & 0 deletions server/controllers/commentController.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import models from '../db/models';
import helpers from '../helpers';
import Notification from '../helpers/notifications';

const { successStat, errorStat } = helpers;

Expand Down Expand Up @@ -42,6 +43,15 @@ export default class CommentController {
]
}
});
const author = await post.getAuthor({
attributes: ['id', 'username', 'email', 'newPostEmailSub']
});
const payload = {
resourceType: 'comment',
resourceId: post.slug,
message: `${commentResponse.author.username} commented on your article`,
};
Notification.notify([author], payload);
return successStat(res, 201, 'comment', commentResponse);
}
}
76 changes: 76 additions & 0 deletions server/controllers/notificationController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import sequelize from 'sequelize';
import models from '../db/models';
import helpers from '../helpers';

const { Op } = sequelize;
const { successStat } = helpers;

/**
* @Module NotificationController
* @description Controlls comments made by users
*/
class NotificationController {
/**
* @static
* @param {*} req Request object
* @param {*} res Response object
* @returns {Object} server response
* @memberof NotificationController
*/
static async getNotifications(req, res) {
const { user: { id } } = req;
const user = await models.User.findByPk(id);
return successStat(res, 200, 'notifications', await user.getNotifications());
}

/**
* @static
* @param {*} req Request object
* @param {*} res Response object
* @returns {Object} server response
* @memberof NotificationController
*/
static async markAsRead(req, res) {
const { user: { id }, params, } = req;
await models.Notification.update({
read: true,
}, {
where: {
[Op.and]: [{ id: params.id }, { userId: id }]
}
});
return successStat(res, 200, 'message', 'marked as read');
}

/**
* @static
* @param {*} req Request object
* @param {*} res Response object
* @returns {Object} server response
* @memberof NotificationController
*/
static async markAllAsRead(req, res) {
const { id } = req.user;
await models.Notification.update({ read: true }, {
where: {
userId: id,
}
});
return successStat(res, 200, 'message', 'all notifications marked as read');
}

/**
* @static
* @param {*} req Request object
* @param {*} res Response object
* @returns {Object} server response
* @memberof NotificationController
*/
static async emailSubscribe(req, res) {
const { user: { id }, method } = req;
await models.User.update({ newPostEmailSub: method === 'PATCH', }, { where: { id } });
return successStat(res, 200, 'message', `email notification ${method === 'PATCH' ? 'enabled' : 'disabled'}`);
}
}

export default NotificationController;
45 changes: 45 additions & 0 deletions server/db/migrations/20190806103204-create-notification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
module.exports = {
up: (queryInterface, Sequelize) => queryInterface.createTable('Notifications', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
userId: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'Users',
key: 'id',
},
onDelete: 'CASCADE'
},
resourceType: {
type: Sequelize.STRING,
allowNull: false,
},
resourceId: {
type: Sequelize.STRING,
allowNull: false,
},
message: {
type: Sequelize.STRING,
allowNull: false,
},
read: {
type: Sequelize.BOOLEAN,
allowNull: false,
default: false,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
}),
down: queryInterface => queryInterface.dropTable('Notifications')
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
up: (queryInterface, Sequelize) => queryInterface.addColumn('Users', 'newPostEmailSub', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: true
}),
down: queryInterface => queryInterface.removeColumn('Users', 'newPostEmailSub')
};
16 changes: 16 additions & 0 deletions server/db/models/notification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = (sequelize, DataTypes) => {
const Notification = sequelize.define('Notification', {
userId: DataTypes.INTEGER,
resourceType: DataTypes.STRING,
resourceId: DataTypes.STRING,
message: DataTypes.STRING,
read: DataTypes.BOOLEAN,
}, {});
Notification.associate = (models) => {
Notification.belongsTo(models.User, {
as: 'userNotification',
foreignKey: 'userId',
});
};
return Notification;
};
2 changes: 2 additions & 0 deletions server/db/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module.exports = (sequelize, DataTypes) => {
image: DataTypes.STRING,
socialId: DataTypes.STRING,
verified: DataTypes.BOOLEAN,
newPostEmailSub: DataTypes.BOOLEAN,
}, {});
User.associate = (models) => {
User.belongsToMany(models.User, {
Expand All @@ -31,6 +32,7 @@ module.exports = (sequelize, DataTypes) => {
});
User.hasMany(models.Article, { as: 'authorId', foreignKey: 'authorId', onDelete: 'CASCADE' });
User.hasMany(models.Comment, { foreignKey: 'authorId', onDelete: 'CASCADE' });
User.hasMany(models.Notification, { foreignKey: 'userId', onDelete: 'CASCADE' });
};
return User;
};
12 changes: 12 additions & 0 deletions server/db/seeders/20190731122700-demo-user.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module.exports = {
password: '$2a$06$loVt9DxXF97PGJxkjyfJj.PVHNz5FUjNhU4yXIzTK4HQ2EesmuoPi',
image: null,
bio: null,
newPostEmailSub: true,
createdAt: new Date(),
updatedAt: new Date(),
},
Expand All @@ -19,6 +20,7 @@ module.exports = {
password: '$2a$06$loVt9DxXF97PGJxkjyfJj.PVHNz5FUjNhU4yXIzTK4HQ2EesmuoPi',
image: null,
bio: null,
newPostEmailSub: false,
createdAt: new Date(),
updatedAt: new Date(),
},
Expand All @@ -30,6 +32,7 @@ module.exports = {
password: '$2a$06$loVt9DxXF97PGJxkjyfJj.PVHNz5FUjNhU4yXIzTK4HQ2EesmuoPi',
image: null,
bio: null,
newPostEmailSub: true,
createdAt: new Date(),
updatedAt: new Date(),
},
Expand All @@ -41,6 +44,7 @@ module.exports = {
password: '$2a$06$loVt9DxXF97PGJxkjyfJj.PVHNz5FUjNhU4yXIzTK4HQ2EesmuoPi',
image: null,
bio: null,
newPostEmailSub: true,
createdAt: new Date(),
updatedAt: new Date(),
},
Expand All @@ -52,6 +56,7 @@ module.exports = {
password: '$2a$06$loVt9DxXF97PGJxkjyfJj.PVHNz5FUjNhU4yXIzTK4HQ2EesmuoPi',
image: null,
bio: null,
newPostEmailSub: true,
createdAt: new Date(),
updatedAt: new Date(),
},
Expand All @@ -63,6 +68,7 @@ module.exports = {
password: '$2a$06$loVt9DxXF97PGJxkjyfJj.PVHNz5FUjNhU4yXIzTK4HQ2EesmuoPi',
image: null,
bio: null,
newPostEmailSub: false,
createdAt: new Date(),
updatedAt: new Date(),
},
Expand All @@ -74,6 +80,7 @@ module.exports = {
password: '$2a$06$loVt9DxXF97PGJxkjyfJj.PVHNz5FUjNhU4yXIzTK4HQ2EesmuoPi',
image: null,
bio: null,
newPostEmailSub: true,
createdAt: new Date(),
updatedAt: new Date(),
},
Expand All @@ -85,6 +92,7 @@ module.exports = {
password: '$2a$06$loVt9DxXF97PGJxkjyfJj.PVHNz5FUjNhU4yXIzTK4HQ2EesmuoPi',
image: null,
bio: null,
newPostEmailSub: false,
createdAt: new Date(),
updatedAt: new Date(),
},
Expand All @@ -96,6 +104,7 @@ module.exports = {
password: '$2a$06$loVt9DxXF97PGJxkjyfJj.PVHNz5FUjNhU4yXIzTK4HQ2EesmuoPi',
image: null,
bio: null,
newPostEmailSub: false,
createdAt: new Date(),
updatedAt: new Date(),
},
Expand All @@ -107,6 +116,7 @@ module.exports = {
password: '$2a$06$loVt9DxXF97PGJxkjyfJj.PVHNz5FUjNhU4yXIzTK4HQ2EesmuoPi',
image: null,
bio: null,
newPostEmailSub: false,
createdAt: new Date(),
updatedAt: new Date(),
},
Expand All @@ -118,6 +128,7 @@ module.exports = {
password: '$2a$06$loVt9DxXF97PGJxkjyfJj.PVHNz5FUjNhU4yXIzTK4HQ2EesmuoPi',
image: null,
bio: null,
newPostEmailSub: false,
createdAt: new Date(),
updatedAt: new Date(),
},
Expand All @@ -129,6 +140,7 @@ module.exports = {
password: '$2a$06$loVt9DxXF97PGJxkjyfJj.PVHNz5FUjNhU4yXIzTK4HQ2EesmuoPi',
image: null,
bio: null,
newPostEmailSub: false,
createdAt: new Date(),
updatedAt: new Date(),
},
Expand Down
Loading

0 comments on commit fef2e34

Please sign in to comment.