Skip to content

Commit

Permalink
feature(follow): add endpoint for following a user
Browse files Browse the repository at this point in the history
- write unit test for feature
- create database models and migrations
- write controller logic for adding followers
- create follow route with authorization
- update swagger docs
- add test for database error

[Delivers #167164992]

WIP: users can follow each other
  • Loading branch information
OvieMudi committed Aug 23, 2019
1 parent 850e6f5 commit 16762b4
Show file tree
Hide file tree
Showing 12 changed files with 426 additions and 32 deletions.
108 changes: 105 additions & 3 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ paths:
$ref: '#/components/schemas/Notification'
500:
description: internal server error
description: internal server error
/novels:
post:
tags:
Expand Down Expand Up @@ -293,7 +292,6 @@ paths:
schema:
$ref: '#/components/schemas/StandardServerResponse'

description: internal server error
security:
- token: []
/genres:
Expand Down Expand Up @@ -1057,7 +1055,111 @@ paths:
description: unauthorised request
500:
description: internal server error


/profiles/{userId}/follow:
post:
tags:
- Users
summary: Follow a user
description: Add user to list of followers
parameters:
- name: userId
in: path
required: true
schema:
type: string
example: fb94de4d-47ff-4079-89e8-b0186c0a3be8

responses:
201:
description: user followed
content:
application/json:
schema:
$ref: '#/components/schemas/Profile'
401:
description: unauthorized access
content:
application/json:
schema:
type: object
properties:
error:
type: string
example: invalid token
403:
description: forbidden access
content:
application/json:
schema:
type: object
properties:
errors:
type: string
example: you cannot follow yourself
500:
description: server error
content:
application/json:
schema:
type: object
properties:
error:
type: string
example: error occured

delete:
tags:
- Users
summary: Unfollow a user
description: Remove user from list of followers
parameters:
- name: userId
in: path
required: true
schema:
type: string
example: fb94de4d-47ff-4079-89e8-b0186c0a3be8

responses:
201:
description: user followed
content:
application/json:
schema:
type: string
example: 'successfully unfollowed'
401:
description: unauthorized access
content:
application/json:
schema:
type: object
properties:
error:
type: string
example: invalid token
403:
description: forbidden access
content:
application/json:
schema:
type: object
properties:
errors:
type: string
example: you cannot follow yourself
500:
description: server error
content:
application/json:
schema:
type: object
properties:
error:
type: string
example: error occured

components:
schemas:
token:
Expand Down
76 changes: 74 additions & 2 deletions src/controllers/userController.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const {
authHelper, successResponse, errorResponse, responseMessage, verifyUser
} = helpers;
const { userServices: { findUser, findFollower, getAllUsers } } = services;
const { User } = models;
const { User, Follower } = models;

/**
* user signup controller
Expand Down Expand Up @@ -158,6 +158,78 @@ const listUsers = async (request, response) => {
}
};

/*
* follow users feature
* @param {Object} req - server request
* @param {Object} res - server response
* @return {Object} - custom response
*/
const follow = async (req, res) => {
const { userId } = req.params;
const authUserId = req.user.id;
if (userId === authUserId) {
return responseMessage(res, 403, { error: 'You cannot follow yourself' });
}

try {
const follower = await findFollower(userId, authUserId);
if (follower) {
return responseMessage(res, 409, { error: "You're already a follower" });
}

await Follower.create({
followerId: authUserId,
followeeId: userId
});

const user = await findUser(userId);
const { id, } = user;
const data = {
userId: id,
following: true
};
return responseMessage(res, 201, data);
} catch (error) {
return responseMessage(res, 500, { error: 'an error occured' });
}
};

/*
* unfollow user feature
* @param {Object} req - server request
* @param {Object} res - server response
* @return {Object} - custom response
*/
const unfollow = async (req, res) => {
const { userId } = req.params;
const authUserId = req.user.id;
if (userId === authUserId) {
return responseMessage(res, 403, { error: 'operation not allowed' });
}
try {
const follower = await findFollower(userId, authUserId);
if (!follower) {
return responseMessage(res, 403, { error: "you're not a follower" });
}

await Follower.destroy({
where: {
followerId: authUserId,
followeeId: userId
}
});
} catch (error) {
return responseMessage(res, 500, { error: 'an error occured' });
}


const data = {
message: 'successfully unfollowed',
};

return responseMessage(res, 200, data);
};

export default {
getProfile, editProfile, signUp, login, listUsers
getProfile, editProfile, signUp, login, listUsers, follow, unfollow
};
25 changes: 25 additions & 0 deletions src/database/migrations/20190807094548-create-follower.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const up = (queryInterface, Sequelize) => queryInterface.createTable('Followers', {
id: {
allowNull: false,
primaryKey: true,
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4
},
followeeId: {
allowNull: false,
type: Sequelize.UUID,
},
followerId: {
allowNull: false,
type: Sequelize.UUID,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
export const down = queryInterface => queryInterface.dropTable('Followers');
23 changes: 15 additions & 8 deletions src/database/models/follower.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
export default (sequelize, DataTypes) => {
const Follower = sequelize.define('Follower', {
followeeId: DataTypes.UUID,
followerId: DataTypes.UUID
id: {
allowNull: false,
primaryKey: true,
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4
},
followeeId: {
allowNull: false,
type: DataTypes.UUID,
},
followerId: {
allowNull: false,
type: DataTypes.UUID,
}
}, {});
Follower.associate = (models) => {
Follower.belongsTo(models.User, {
foreignKey: 'followeeId',
onDelete: 'CASCADE'
});

Follower.belongsTo(models.User, {
foreignKey: 'followerId',
foreignKey: 'id',
onDelete: 'CASCADE'
});
};
Expand Down
15 changes: 15 additions & 0 deletions src/database/seeders/20190724090406-user.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,20 @@ export const up = queryInterface => queryInterface.bulkInsert('Users', [{
isSubscribed: true,
createdAt: new Date(),
updatedAt: new Date()
},
{
id: '6dd5a28c-ce96-4866-b3e7-b29aa69aef97',
firstName: 'Anakin',
lastName: 'Skywalker',
email: 'anakin@gmail.com',
password: bcrypt.hashSync('skywalker', 10),
bio: 'Force weilder',
avatarUrl: null,
phoneNo: null,
roleId: 'f2dec928-1ff9-421a-b77e-8998c8e2e720',
isVerified: true,
isSubscribed: true,
createdAt: new Date(),
updatedAt: new Date()
}], {});
export const down = queryInterface => queryInterface.bulkDelete('Users', null, {});
15 changes: 13 additions & 2 deletions src/helpers/validator.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { check } from 'express-validator';
import { check, param } from 'express-validator';

/**
* @param {String} field
* @returns {Object} - Express-validator
Expand Down Expand Up @@ -165,6 +166,15 @@ const isBoolean = field => check(field)
.optional()
.withMessage('isHandled must be a boolean ( true or false )');

/**
* @param {String} paramName
* @returns {Object} - Express-validator
*/
const isValidUUIDParam = paramName => param(paramName)
.trim()
.isUUID()
.withMessage('invalid request');

export default {
isValidEmail,
isValidName,
Expand All @@ -179,5 +189,6 @@ export default {
isValidAvatarUrl,
isValidInt,
isNotTypeOfReport,
isBoolean
isBoolean,
isValidUUIDParam
};
16 changes: 15 additions & 1 deletion src/middlewares/errorHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,18 @@ const validatorError = (req, res, next) => {
next();
};

export default { validatorError };
const paramsValidatorError = (req, res, next) => {
const errors = validationResult(req);

if (!errors.isEmpty()) {
const errorsArray = errors.array({ onlyFirstError: true });
const errorMessage = errorsArray[0].msg;

return res.status(400).send({
error: errorMessage
});
}
next();
};

export default { validatorError, paramsValidatorError };
8 changes: 6 additions & 2 deletions src/middlewares/userValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ const { validators, emptyBody } = helpers;
const {
isValidEmail, isValidName, isValidUUID, isValidPassword,
isValidProfileName, isValidProfilePassword,
isValidAvatarUrl
isValidAvatarUrl, isValidUUIDParam
} = validators;

const { validatorError } = errorHandler;
const { validatorError, paramsValidatorError } = errorHandler;

const userValidator = {
signUpValidator: [
Expand Down Expand Up @@ -43,6 +43,10 @@ const userValidator = {
isValidPassword('newPassword'),
validatorError
],
followUserValidator: [
isValidUUIDParam('userId'),
paramsValidatorError
]
};

export default userValidator;
7 changes: 5 additions & 2 deletions src/routes/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import userController from '../controllers/userController';

const {
userValidator: {
signUpValidator, loginValidator, profileValidator, editProfileValidator
signUpValidator, loginValidator, profileValidator, editProfileValidator, followUserValidator
},
verifyToken,
authorizeUser
} = middlewares;
const {
signUp, login, getProfile, editProfile, listUsers
signUp, login, getProfile, editProfile, listUsers, follow, unfollow
} = userController;
const user = express.Router();

Expand All @@ -32,4 +32,7 @@ user.get(`${PROFILE_URL}/:userId`, verifyToken, authorizeUser(['reader', 'author
// Route to edit a user profile
user.patch(`${PROFILE_URL}`, verifyToken, authorizeUser(['reader', 'author', 'admin', 'superadmin']), editProfileValidator, editProfile);

user.post('/profiles/:userId/follow', followUserValidator, verifyToken, follow);
user.delete('/profiles/:userId/follow', followUserValidator, verifyToken, unfollow);

export default user;
Loading

0 comments on commit 16762b4

Please sign in to comment.