Building a Chat Application: Group Chat Backend and Schema with Node Js and MongoDb

in #hive-1693212 years ago

Grey Minimalist Hello Facebook Cover (4).png

This is actually the last major piece in the puzzle. I have been focused on one-one chat all along and as I have finally completed that, I took on the group chat. It needed to have all the features a group chat would have minus the group video call - it could be implemented but too many possible contingencies.

I decided to build the backend first which I think gives me this kind of assurance that most of the issues I'll get after completing the backend will be frontend related and it will help narrow my focus. It would also potentially reduce the way I shuffle between the frontend and backend while working which could save me ample amount of time.

I defined the Group Schema, the routes (which were a lot of posts) and controllers. I only have three collections in my database (Users, Groups and Messages). The messages is a single collection that contains both 1-1 and group messages.

GROUP SCHEMA

const groupSchema = new Schema({ 
     name: {type: String, required: true}, 
     members: [{type: Schema.Types.ObjectId, required: true, ref: 'Users'}], 
      
     creator: {type:Schema.Types.ObjectId, required: true }, 
     admins:[{type:Schema.Types.ObjectId, required: true}], 
     
  
 }, { 
     timestamps: true 
 })

Nothing much going on here. I could have opted to have the group messages as an array in each group but I didn't. I only stopped at having members of the group as an array only. Then the creator, and admins.

GROUP CHAT FEATURES

  • Create Group
module.exports.createGroup = async(req, res)=> { 
     let {name, creator, members} = req.body 
     members.push(creator) 
     if (name && creator && members) { 
          
         let newGroup = new groupModel({ 
             name, 
             creator, 
             members:members, 
             admins: creator 
              
              
              
              
         }) 
          
         newGroup.save().then((message)=> { 
             res.status(201).json(`Successfully created the group ${message}`) 
         }) 
         .catch(err=> res.status(400).json({message: err.message, members: members})) 
     } 
     else { 
         res.status(400).json ({message: "some fields are empty"}) 
     } 
      
 }


Obviously, we need to be able to create group. It takes in the name of the group, the creator which is our userID and then the members of which is an array of members that have been updated to include the creator. The admins is initially solely set to the creator.

  • Update Group
module.exports.updateGroup = async(req,res)=> { 
     const {userId, name, groupId} = req.body 
     if (userId && name, groupId) { 
         const isAdmin = await groupModel.find({_id: groupId, 
         admins: userId 
     }) 
  
     if (isAdmin && isAdmin.length) { 
         groupModel.findByIdAndUpdate(groupId, {name}) 
         .then((result)=> res.status(200).json(`Successfully Updated ${result}`)) 
            .catch((err)=> res.status(400).json({message: err.message})) 
  
     } 
     else { 
         res.status(401).json("Unauthorized") 
     } 
         
     } 
     
     
          else { 
             res.status(400).json("Empty fields") 
          } 
  
 }


We are only updating the name of the group for now, so it takes in the name. It also takes it the userID to check if it is contained in the admin field before authorising the update. The GroupID is also important to know which of the groups we would like to update.

  • Add a New Group Message
module.exports.addGroupMessage = async (req, res)=> { 
     const {groupId, from, message, userId} = req.body 
     if (groupId && from && message && userId) {  
     const isMember = await groupModel.find({ 
         _id: groupId, 
         members: userId 
     }) 
     if (isMember && isMember.length) { 
          
             let newMessage = new messageModel ({ 
                  
                 groupId : groupId, 
                 sender: from, 
                 message,  
                  
         }) 
          
         newMessage.save() 
         .then((message)=> { 
             res.status(201).json(`Successfully created ${message}`) 
         })  
         .catch((err)=>{ 
             res.status(404).json({message: `An error occured ${err.message}`}) 
         }) 
      
          
  
     } 
     else { 
         res.status(401).json({message: "You are not a member of this group"}) 
     } 
 } 
 else { 
     res.status(400).json("Empty fields") 
 } 
  
 }


I know this is not being really validated but its not the crux of this piece. Basically, we are creating a new message model with membership rights. Get the userId, check if it is contained in the members of the groupId before creating a new message with the GroupId as one of the message fields.

  • Get Group Messages
module.exports.messages = async (req,res)=> { 
     const {groupId, userId} = req.body 
     if (groupId && userId){ 
         const isMember = await groupModel.find({ 
             _id: groupId, 
             members: userId 
         })   
         if (isMember && isMember.length){ 
                 let messages = await messageModel.find({ 
                     groupId: groupId 
                 }) 
                  
                 messages = await messageModel.populate(messages, {path: "sender", select: "nickname avatarImage"}) 
                 messages = messages.map((msg)=> { 
                     return { 
                         fromSelf: msg.sender._id.toString() === userId, 
                         message: msg.message, 
                         details: msg.sender 
                     } 
                 }) 
                 res.status(200).json(messages) 
                  
             } 
         else { 
             res.status(401).json({message: "Not a member"}) 
         } 
  
     } 
     else { 
         res.status(400).json({message: "Empty fields"}) 
     } 
  
  
 } 


We are performing quite a number of operations here. We only need the UserId and the groupID to check if the user is a member of that group. Then we populate the 'sender' path with the nickname and avatar of that user. Kindly note that for the populate method to work, the path to be populated need to reference another model. In this case, my message model has a 'sender' field that references the 'Users' model.

const messageSchema = new Schema ({ 
  
  message: {type: String, required: true}, 
  users: {type: Array, required:true, default: null},   groupId: {type: Schema.Types.ObjectId,  ref: "Groups", default: null}, 
 sender: {type: Schema.Types.ObjectId, ref: "Users", required: true}, 
 to: {type: Schema.Types.ObjectId, ref: "Users", default: null}, 
  
  
  
 }


After populating, we then modify our result by returning a new map of the result that contains an added field. The field 'fromSelf' returns true if the sender of that message is equal to the userId. This will help us when we get to our frontend to identify and style our own messages differently.

  • Add new Admin with Admin rights
module.exports.addAdmins = async(req,res)=> { 
     const {groupId, admins, userId} = req.body 
     if (userId && groupId && admins) { 
         const isCreator = await groupModel.find({_id: groupId, 
             creator: userId 
         }) 
         if (isCreator && isCreator.length) { 
                 groupModel.updateOne({_id: groupId}, { 
                     $push: {admins} 
                 }).then((result)=> res.status(200).json(result)) 
                 .catch(err=> res.status(404).json({message: err.message})) 
         } 
         else { 
             res.status(401).json("Unauthorized") 
         } 
          
  
     } 
     else { 
         res.status(400).json({message: "Empty Fields"}) 
     } 
      
      
 }

We are trying to make sure that only the creator is able to add new admins. The 'admins' is a new array that gets pushed into the admin array field.

  • Remove Admins with Creator rights
module.exports.removeAdmins  = async(req, res)=> { 
     const {userId, admins, groupId} = req.body 
     if (userId && admins) { 
  
         const isCreator = await groupModel.find({_id: groupId, 
             creator: userId 
             }) 
             if (isCreator && isCreator.length) { 
                 groupModel.updateOne({_id: groupId}, {  
                 $pull: {admins: {$in: admins}} 
                 }).then((result)=> { 
                     res.status(200).json({result}) 
                 }).catch(err=> res.status(400).json({message: err.message})) 
             }  
             else { 
                 res.status(401).json({message: "Unauthorized"}) 
             } 
         
     } 
     else { 
         res.status(400).json({message: "Empty fields"}) 
     } 
 } 

We use the Mongodb Update one function to pull admins from the admins field. This only works only if the user executing this call is the creator.

  • Add Members with Admin rights
module.exports.addMembers = async(req, res)=> { 
     const {groupId, members, userId} = req.body 
     if (userId && groupId && members ) { 
         const isAdmin = await groupModel.find({ 
             _id: groupId, 
             admins: userId 
         }) 
         if (isAdmin && isAdmin.length) { 
             groupModel.updateOne({_id: groupId}, { 
                 $push:  {members} 
             }) 
             .then((result)=> res.status(200).json(`Successfully Updated ${result}`)) 
                .catch((err)=> res.status(400).json({message: err.message})) 
              
         }    
  
         else { 
             res.status(401).json({message: "Unauthorized"}) 
         } 
           
          } 
          else { 
             res.status(400).json({message: "Empty fields"}) 
         } 
 } 
 

  • Remove Members with Admin rights
module.exports.removeMembers = async(req, res)=> { 
     const {groupId, userId, members} = req.body 
     if (groupId && id && members) { 
        const isAdmin = await groupModel.find({ 
         _id: groupId, 
         admins: userId 
        }) 
        
           
         if (isAdmin && isAdmin.length) { 
             try{ 
              
                 const updatedMembers =  await groupModel.updateOne({_id: groupId}, { 
                     $pull: {members: {$in: members}} 
                 }) 
                 res.status(200).json(updatedMembers) 
             } 
             catch(err) { 
                 res.status(400).json({message: err.message}) 
             } 
         } 
         else { 
             res.status(401).json({message: "Not authorized"}) 
         } 
          } 
          else { 
             res.status(400).json({message: "Empty fields"}) 
         } 
 }


  • Get User Groups along with the groups last message to be displayed in Chat
module.exports.getGroups = async (req,res)=> { 
     const { userId} = req.body 
     if (userId) { 
  
         let groups = await  groupModel.find({ 
             members: userId 
         })  
         if (groups && groups.length){ 
             
              
         const projectedGroupMsgs = await Promise.all(groups.map( async (group)=>  { 
              
             let messages= await messageModel.find({ 
                 groupId: group._id 
             }) 
                 if (messages && messages.length) { 
  
                     return await messageModel.aggregate([ 
                        {$match: {groupId: ObjectId(group._id)}}, 
                        {$sort: { 
                            "timeStamp": -1 
                        }}, 
                        { 
                            $group: { 
                                _id: "$groupId", 
         
                                "name": { 
                                   "$first": `${group.name}` 
                                }, 
                                "lastMessage": { 
                                    "$last": "$message" 
                                }, 
                                "messages": { 
                                    "$push" : "$message" 
                                }, 
                                "groupId": { 
                                    "$first": "$groupId" 
                                } 
                            } 
                        } 
                    ]) 
                 } 
                 else { 
                     return await groupModel.aggregate([ 
                         {$match: {_id: ObjectId(group._id)}}, 
                         { 
                             "$project": { 
                                 _id: 1, 
                                 name: 1, 
                                  
                               
          
                             } 
                         }, 
                     ]) 
                 } 
              
             
         
                  
              
  
         })) 
      
         return res.status(200).json(projectedGroupMsgs) 
         
     } 
     else { 
         res.status(404).json({message: "User does not belong to any group"}) 
     } 
         
     } 
      
     else { 
         res.status(400).json({message: "Empty fields"}) 
     } 
  
 }


We only need the userId but we are performing a lot of operations. First we need to check for the groups that the user belongs to any group, map through those groups and perform an aggregation. Before that, we need to check if the group contains any messages, if it does we aggregate the message model, match the messages that contains the groupId of each of those mapped groups and group those messages by the groupId.

Also worthy to note that we sneaked in a field that contains the group name coming from our map.

If the group does not contain any message, instead of returning an empty array without either of our group name and the messages. We just return the aggregated group model instead with just the name and id fields projected.

*** I included the messages array in order to use the search functionality to search through messages and get the results on our chat page.

And that's that. I have started the frontend part of the chat group. Here's a little sneak peak.

bandicam 2022-10-24 23-05-59-141.jpg

bandicam 2022-10-24 23-02-06-995.jpg

Follow for more❤️

Information: Cover designed on Canva

Sort:  

Dear @marvel1206, sorry to jump in a bit off-topic.
May I ask you to review and support the new proposal (https://peakd.com/me/proposals/240) so I can continue to improve and maintain this service?
You can support the new proposal (#240) on Peakd, Ecency, Hive.blog or using HiveSigner.

Thank you!


The rewards earned on this comment will go directly to the people sharing the post on Twitter as long as they are registered with @poshtoken. Sign up at https://hiveposh.com.

🍕 PIZZA !
@marvel1206! The Hive.Pizza team manually upvoted your post.

Join us in Discord!

Thank you