Skip to content

Instantly share code, notes, and snippets.

@devdbrandy
Last active February 1, 2023 15:01
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save devdbrandy/df7f88b96edd51df71fa94ae774d51bc to your computer and use it in GitHub Desktop.
Save devdbrandy/df7f88b96edd51df71fa94ae774d51bc to your computer and use it in GitHub Desktop.
User followers implementation with Sequelize ORM and ExpressJS
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;
};
@sauldeleon
Copy link

my savior!

@devdbrandy
Copy link
Author

Hey @sauldeleon, glad to be of help 😄

@sauldeleon
Copy link

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)
        })
    }

@devdbrandy
Copy link
Author

devdbrandy commented Apr 30, 2020

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!

@mom3d
Copy link

mom3d commented Jan 3, 2022

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 !!

@devdbrandy
Copy link
Author

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.

@mom3d
Copy link

mom3d commented Jan 4, 2022

thank you so much @devdbrandy, but where i could find it in the docs ?

@devdbrandy
Copy link
Author

devdbrandy commented Jan 4, 2022

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment