Skip to content

Commit 53ffb4e

Browse files
author
Reginald Chand
authored
Merge pull request #3 from rschand-dev/development
Add User Connections
2 parents 4cbc6ae + 2d5fcfc commit 53ffb4e

10 files changed

+178
-10
lines changed

PROJECT_STRUCTURE

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ bms-backend-mongodb
77
│ └── eslint.yml
88
├── .gitignore
99
├── CODE_OF_CONDUCT.md
10-
├── eslint.config.mjs
1110
├── LICENSE
12-
├── package.json
13-
├── pnpm-lock.yaml
1411
├── PROJECT_STRUCTURE
1512
├── README.md
13+
├── eslint.config.mjs
14+
├── package.json
15+
├── pnpm-lock.yaml
1616
├── server.mjs
1717
└── src
1818
├── config
@@ -37,7 +37,8 @@ bms-backend-mongodb
3737
│ ├── user.password.reset.email.controller.mjs
3838
│ ├── user.password.update.controller.mjs
3939
│ ├── user.profile.update.controller.mjs
40-
│ └── user.profile.view.controller.mjs
40+
│ ├── user.profile.view.controller.mjs
41+
│ └── user.social.controller.mjs
4142
├── database
4243
│ └── mongodb.database.mjs
4344
├── middleware
@@ -56,8 +57,10 @@ bms-backend-mongodb
5657
│ ├── blog.data.schema.mjs
5758
│ ├── refresh.token.schema.mjs
5859
│ ├── signup.schema.mjs
60+
│ ├── user.connection.schema.mjs
5961
│ ├── user.logout.blacklist.token.schema.mjs
60-
│ └── user.password.reset.blacklist.token.schema.mjs
62+
│ ├── user.password.reset.blacklist.token.schema.mjs
63+
│ └── user.social.schema.mjs
6164
├── store
6265
│ └── redis.client.store.mjs
6366
├── template
@@ -67,7 +70,8 @@ bms-backend-mongodb
6770
│ ├── catch.error.utility.mjs
6871
│ ├── email.sender.utility.mjs
6972
│ ├── jwt.regeneration.utility.mjs
70-
│ └── redis.keys.retrieval.utility.mjs
73+
│ ├── redis.keys.retrieval.utility.mjs
74+
│ └── update.user.connection.utility.mjs
7175
└── validator
7276
├── blog.data.update.validator.mjs
7377
├── blog.data.validator.mjs
@@ -79,4 +83,5 @@ bms-backend-mongodb
7983
├── token.validator.mjs
8084
├── user.password.reset.validator.mjs
8185
├── user.password.update.validator.mjs
82-
└── user.profile.update.validator.mjs
86+
├── user.profile.update.validator.mjs
87+
└── user.social.validator.mjs

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This project is a fully-featured Blog Management System (BMS) backend built with
2222
- Profile view and update
2323
- Blog creation, update, deletion, and view
2424
- Feed view for blogs
25+
- User Connections (Follow/Unfollow)
2526
- CSRF protection
2627
- Redis caching for blog data
2728
- Rate limiting and file upload middleware
@@ -34,9 +35,8 @@ This project is a fully-featured Blog Management System (BMS) backend built with
3435
## Upcoming Features
3536

3637
1. Frontend Development (Website). Using **Next.JS & Tailwind CSS**
37-
2. Followers and following functionality
38-
3. Likes, comments, and shares for blogs
39-
4. Enhanced blog search functionality
38+
2. Likes, comments, and shares for blogs
39+
3. Enhanced blog search functionality
4040

4141
## Installation
4242

@@ -184,6 +184,10 @@ Alternatively you can find the `PROJECT_STRUCTURE` file in the root directory of
184184
- **GET** /user/profile - View user profile (requires JWT)
185185
- **PUT** /user/profile-update - Update user profile (requires JWT)
186186

187+
### User Connection
188+
189+
- **PUT** /user/social/operation - Create and or Update User Connections (Follow/Unfollow) (requires JWT)
190+
187191
### Blog
188192

189193
- **GET** /blog/feeds - View blog feeds (requires JWT)

src/controller/signin.controller.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const signInController = async (request, response) => {
2828
}
2929

3030
const userData = {
31+
_id: existingUser._id,
3132
firstName: existingUser.firstName,
3233
lastName: existingUser.lastName,
3334
userName: existingUser.userName,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { SocialModel, UserModel } from "../model/model.mjs";
2+
import { catchErrorUtility } from "../utility/catch.error.utility.mjs";
3+
import { updateUserConnectionUtility } from "../utility/update.user.connection.utility.mjs";
4+
import { userSocialValidator } from "../validator/user.social.validator.mjs";
5+
6+
export const userSocialController = async (request, response) => {
7+
const { error, value } = userSocialValidator.validate(request.body);
8+
9+
if (error) {
10+
return response.status(400).json({ responseData: error.message });
11+
}
12+
13+
const { email, userId, userData } = value;
14+
15+
try {
16+
const [existingUser, existingUserToFollowOrUnfollow] = await Promise.all([
17+
UserModel.findOne({
18+
email: { $eq: userData.email },
19+
}),
20+
21+
UserModel.findOne({
22+
$and: [{ email: { $eq: email } }, { _id: { $eq: userId } }],
23+
}),
24+
]);
25+
26+
if (!existingUser || !existingUserToFollowOrUnfollow) {
27+
return response.status(404).json({
28+
responseData:
29+
"OOPS! Something happened on our end. Please wait for a while and try again!",
30+
});
31+
}
32+
33+
await SocialModel.findOneAndUpdate(
34+
{ _id: existingUser._id },
35+
{ _id: existingUser._id },
36+
{ upsert: true }
37+
);
38+
39+
await updateUserConnectionUtility(
40+
SocialModel,
41+
email,
42+
userId,
43+
existingUser,
44+
existingUserToFollowOrUnfollow,
45+
response
46+
);
47+
} catch (error) {
48+
catchErrorUtility(error, response);
49+
}
50+
};

src/model/model.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { blogDataSchema } from "../schema/blog.data.schema.mjs";
22
import mongoose from "mongoose";
33
import { signUpSchema } from "../schema/signup.schema.mjs";
4+
import { userSocialSchema } from "../schema/user.social.schema.mjs";
45

56
export const UserModel = mongoose.model("User", signUpSchema);
67
export const BlogModel = mongoose.model("Blog", blogDataSchema);
8+
export const SocialModel = mongoose.model("Social", userSocialSchema);

src/router/router.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { userPasswordResetEmailController } from "../controller/user.password.re
1818
import { userPasswordUpdateController } from "../controller/user.password.update.controller.mjs";
1919
import { userProfileUpdateController } from "../controller/user.profile.update.controller.mjs";
2020
import { userProfileViewController } from "../controller/user.profile.view.controller.mjs";
21+
import { userSocialController } from "../controller/user.social.controller.mjs";
2122
import { verifyJwtMiddleware } from "../middleware/verify.jwt.middleware.mjs";
2223

2324
export const router = express.Router();
@@ -55,6 +56,7 @@ router.put(
5556
verifyJwtMiddleware,
5657
userProfileUpdateController
5758
);
59+
router.put("/user/social/operation", verifyJwtMiddleware, userSocialController);
5860

5961
router.get("/blog/feeds", verifyJwtMiddleware, feedViewController);
6062
router.get("/blog/:blogSlug", verifyJwtMiddleware, blogViewController);

src/schema/user.connection.schema.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import mongoose from "mongoose";
2+
3+
export const userConnectionSchema = new mongoose.Schema(
4+
{
5+
email: { type: String, required: true, unique: true, ref: "User" },
6+
},
7+
{ timestamps: true }
8+
);

src/schema/user.social.schema.mjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import mongoose from "mongoose";
2+
import { userConnectionSchema } from "./user.connection.schema.mjs";
3+
4+
export const userSocialSchema = new mongoose.Schema(
5+
{
6+
_id: { type: mongoose.Types.ObjectId, ref: "User" },
7+
followers: [userConnectionSchema],
8+
followings: [userConnectionSchema],
9+
followerCount: { type: Number, default: 0 },
10+
followingCount: { type: Number, default: 0 },
11+
},
12+
{ timestamps: true }
13+
);
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* @description Function to add or remove user (Follow/Unfollow) (Concurrent updates to users connecting with each other).
3+
* @param {mongoose.Model} SocialModel The social model.
4+
* @param {string} email The email of the user to follow.
5+
* @param {string} userId The id of the user to follow.
6+
* @param {mongoose.Document} existingUser The existing user aka currently logged in users required MongoDB documents.
7+
* @param {mongoose.Document} existingUserToFollowOrUnfollow The user to follow required MongoDB documents.
8+
* @param {Expression.Response} response Express response message.
9+
* @returns Express response message.
10+
*/
11+
export const updateUserConnectionUtility = async (
12+
SocialModel,
13+
email,
14+
userId,
15+
existingUser,
16+
existingUserToFollowOrUnfollow,
17+
response
18+
) => {
19+
const userIsAConnection = await SocialModel.exists({
20+
"followers.email": { $eq: email },
21+
});
22+
23+
if (!userIsAConnection) {
24+
// Follow User.
25+
await SocialModel.bulkWrite([
26+
{
27+
updateOne: {
28+
filter: { _id: { $eq: existingUser._id } },
29+
update: {
30+
$push: { followers: { email } },
31+
$inc: { followerCount: 1 },
32+
},
33+
},
34+
},
35+
{
36+
updateOne: {
37+
filter: { _id: { $eq: userId } },
38+
update: {
39+
$push: { followings: { email: existingUser.email } },
40+
$inc: { followingCount: 1 },
41+
},
42+
},
43+
},
44+
]);
45+
46+
return response.status(201).json({
47+
responseData: `Successfully followed ${existingUserToFollowOrUnfollow.firstName} ${existingUserToFollowOrUnfollow.lastName}`,
48+
});
49+
}
50+
51+
// Unfollow User.
52+
await SocialModel.bulkWrite([
53+
{
54+
updateOne: {
55+
filter: { _id: { $eq: existingUser._id } },
56+
update: {
57+
$pull: { followers: { email } },
58+
$inc: { followerCount: -1 },
59+
},
60+
},
61+
},
62+
{
63+
updateOne: {
64+
filter: { _id: { $eq: userId } },
65+
update: {
66+
$pull: { followings: { email: existingUser.email } },
67+
$inc: { followingCount: -1 },
68+
},
69+
},
70+
},
71+
]);
72+
73+
return response.status(200).json({
74+
responseData: `Successfully unfollowed ${existingUserToFollowOrUnfollow.firstName} ${existingUserToFollowOrUnfollow.lastName}`,
75+
});
76+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Joi from "joi";
2+
3+
export const userSocialValidator = Joi.object({
4+
email: Joi.string().email().required(),
5+
userId: Joi.string().required(),
6+
userData: Joi.object().required(),
7+
}).unknown();

0 commit comments

Comments
 (0)