-
-
Save devdbrandy/df7f88b96edd51df71fa94ae774d51bc to your computer and use it in GitHub Desktop.
import createError from 'http-errors'; | |
import models from '../models'; | |
import Response from '../helpers/responseHandler'; // a wrapper for request.json | |
import { MESSAGE } from '../helpers/constants'; // an object constant used accross the codebase | |
/** | |
* Class handling followers operation | |
* | |
* @class FollowersController | |
*/ | |
class FollowersController { | |
/** | |
* User followers handler | |
* | |
* @static | |
* @param {object} request - Express Request object | |
* @param {object} response - Express Response object | |
* @returns {object} Response object | |
* @param {Function} next - Express NextFunction | |
* @memberof FollowersController | |
*/ | |
static async follow(request, response, next) { | |
const { user, params: { username } } = request; | |
try { | |
const { followable, follower } = await FollowersController.validateFollowable(user, username); | |
await followable.addFollower(follower); | |
return Response.send(response, 200, followable, `${MESSAGE.FOLLOW_SUCCESS} ${username}`); | |
} catch (error) { | |
next(error); | |
} | |
} | |
/** | |
* User unfollow handler | |
* | |
* @static | |
* @param {object} request - Express Request object | |
* @param {object} response - Express Response object | |
* @returns {object} Response object | |
* @param {Function} next - Express NextFunction | |
* @memberof FollowersController | |
*/ | |
static async unfollow(request, response, next) { | |
const { user, params: { username } } = request; | |
try { | |
const { followable, follower } = await FollowersController.validateFollowable(user, username); | |
const existingFollower = await followable.hasFollowers(follower); | |
if (!existingFollower) { | |
next(createError(400, MESSAGE.UNFOLLOW_ERROR)); | |
} | |
await followable.removeFollower(follower); | |
return Response.send(response, 200, followable, `${MESSAGE.UNFOLLOW_SUCCESS} ${username}`); | |
} catch (error) { | |
next(error); | |
} | |
} | |
/** | |
* Validate users to follow | |
* | |
* @static | |
* @param {object} user - Authenticated user object | |
* @param {object} username - Username of the user to follow | |
* @returns {object} Object holding the information of the followable and follower | |
* @memberof FollowersController | |
*/ | |
static async validateFollowable(user, username) { | |
try { | |
const follower = await models.User.findOne({ | |
where: { id: user.id } | |
}); | |
const profile = await models.Profile.findOne({ where: { username } }); | |
if (follower.id === profile.userId) { | |
throw createError(400, MESSAGE.FOLLOW_ERROR); | |
} | |
const followable = await profile.getUser(); | |
if (followable.deletedAt !== null) { | |
throw createError(404, MESSAGE.FOLLOW_ERROR); | |
} | |
return { followable, follower }; | |
} catch (error) { | |
throw error; | |
} | |
} | |
/** | |
* Fetch user followers (handler) | |
* | |
* @static | |
* @param {object} request - Express Request object | |
* @param {object} response - Express Response object | |
* @returns {object} Response object | |
* @param {Function} next - Express NextFunction | |
* @memberof FollowersController | |
*/ | |
static async followers(request, response, next) { | |
const { user } = request; | |
const routePath = request.path.split('/')[2]; | |
let followers; | |
try { | |
const authUser = await models.User.findOne({ | |
where: { id: user.id } | |
}); | |
if (routePath === 'followers') { | |
followers = await authUser.getFollowers(); | |
} else { | |
followers = await authUser.getFollowing(); | |
} | |
return Response.send(response, 400, followers); | |
} catch (error) { | |
next(error); | |
} | |
} | |
} | |
export default FollowersController; |
router.post( | |
'/profiles/:username/follow', | |
middlewares.authenticate, | |
followersController.follow | |
); | |
router.post( | |
'/profiles/:username/unfollow', | |
middlewares.authenticate, | |
followersController.unfollow | |
); | |
router.get( | |
'/profiles/followers', | |
middlewares.authenticate, | |
followersController.followers | |
); | |
router.get( | |
'/profiles/following', | |
middlewares.authenticate, | |
followersController.followers | |
); |
import bcrypt from 'bcryptjs'; | |
/** | |
* A model class representing user resource | |
* | |
* @param {Sequelize} sequelize - Sequelize object | |
* @param {Sequelize.DataTypes} DataTypes - A convinient object holding data types | |
* @return {Sequelize.Model} - User model | |
*/ | |
export default (sequelize, DataTypes) => { | |
/** | |
* @type {Sequelize.Model} | |
*/ | |
const User = sequelize.define('User', { | |
email: { | |
type: DataTypes.STRING, | |
allowNull: false, | |
unique: true, | |
validate: { | |
isEmail: { msg: 'Must be a valid email address' } | |
} | |
}, | |
password: { | |
type: DataTypes.STRING, | |
set(value) { | |
this.setDataValue('password', bcrypt.hashSync(value, 10)); | |
} | |
}, | |
isConfirmed: { | |
type: DataTypes.BOOLEAN, | |
defaultValue: false | |
}, | |
createdAt: { | |
type: DataTypes.DATE, | |
defaultValue: sequelize.NOW | |
}, | |
updatedAt: { | |
type: DataTypes.DATE, | |
defaultValue: sequelize.NOW, | |
onUpdate: sequelize.NOW | |
}, | |
deletedAt: { | |
allowNull: true, | |
type: DataTypes.DATE, | |
} | |
}, {}); | |
User.associate = (models) => { | |
User.hasOne(models.Profile, { foreignKey: 'userId' }); | |
User.belongsToMany(models.User, { | |
foreignKey: 'userId', | |
as: 'followers', | |
through: models.UserFollowers | |
}); | |
User.belongsToMany(models.User, { | |
foreignKey: 'followerId', | |
as: 'following', | |
through: models.UserFollowers | |
}); | |
}; | |
/** | |
* Validate user password | |
* | |
* @param {Object} user - User instance | |
* @param {string} password - Password to validate | |
* @returns {boolean} Truthy upon successful validation | |
*/ | |
User.comparePassword = (user, password) => bcrypt.compareSync(password, user.password); | |
return User; | |
}; |
Yed, a lot of help indeed.
Also, I added my current version of a toggle method, instead of having 2 different methods, which gives flexibility to a rest api, but for my specific use case I only needed one controller which was calling to one service, like this.
Maybe you can add the same method to your code, and have the 3 options, followUser, unFollowUser and toggleFollow. This is my current approach, using ids instead of the object. Your approach is better as you use the build in Sequelize methods for creating the relationships thought
const userIsFollowing = (followerId, followableId) => {
if (followerId === followableId) throw new Error('Cant follow itself')
return Promise.all([
Users.findOne({ where: { id: followerId } }),
Users.findOne({ where: { id: followableId } }),
])
.then(([follower, followed]) => {
if (!follower) throw new Error('Follower does not exist')
if (!followed) throw new Error('Followed does not exist')
return sequelize.models.FollowerFolloweds.findOne({
where: { userId: followableId, followerId: followerId },
}).then((followerFollowed) => (followerFollowed ? true : false))
})
.catch((err) => {
throw new Error('Could not perform the operation. ' + err.message)
})
}
toggleFollow: (followerId, followableId) => {
return userIsFollowing(followerId, followableId)
.then((userAlreadyFollows) => {
const FollowerFolloweds = sequelize.models.FollowerFolloweds
return userAlreadyFollows
? FollowerFolloweds.destroy({
where: { userId: followableId, followerId: followerId },
})
: FollowerFolloweds.create({ userId: followableId, followerId: followerId })
})
.catch((err) => {
throw Error('Could not perform toggle operation. ' + err.message)
})
}
Nice! ๐
Yeah, I totally agree with you. It's better having a single method to handle both scenarios for follow and unfollow.
Great job mate!
when i try
const followers = await user.getFollowers()
users always have a property called UserFollowers that contain an object like this
"UserFollowers": { "id": 67, "userId": 496, "followerId": 500, "createdAt": "2021-12-04T11:26:45.000Z", "updatedAt": "2021-12-04T11:26:45.000Z" }
and i have to manually remove it before sending it to the user !!
Hi @mom3d, you can use joinTableAttributes
option to remove the association attributes from the output, like so:
const followers = await user.getFollowers({ joinTableAttributes: [] })
Hope you find this helpful.
thank you so much @devdbrandy, but where i could find it in the docs ?
@mom3d you can look up the Associating Objects
section on the Docs, depending on your version of sequelize.
https://sequelize.org/v5/manual/associations.html#associating-objects
Hey @sauldeleon, glad to be of help ๐