Mongoose Private Chat Message Model - node.js

I'm trying to add private messaging between users into my data model. I've been going back and forth between two possible ways of doing this.
1) Each user has an array of user_id, chat_id pairs which correspond to chats they are participating in. Chat model just stores chat_id and array of messages.
2) Don't store chats with user at all and just have the Chat model store a pair of user_ids and array of messages.
The issue with option (1) is whenever a user joins or starts a chat, I would need to look first through the array for the user to see if the user_id, chat_id pair already exists. And then do a second find for the chat_id in Chat. If it doesn't exist, I would need to create the user_id, chat_id pair in two different places for both users who are participating.
With option (2) I would search through the Chat model for the user_id1, user_id2 pair, and if I find it I'm done, if not I would create a new Chat record for that pair and done.
Based on this option (2) does seem like the better way of handling this. However, I'm running into issues figuring out how to model the "pair" of user ids in a way that they are easily searchable in the chat model. i.e. how do I make sure I can find the chat record even if the user_ids are passed in the wrong order, i.e. user_id2, user_id1. What would be the best way to model this in Mongoose?
var chatSchema = mongoose.Schema({
messages: [{
text: {
type: String,
max: 2000
},
sender: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}
}],
participant1: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}]
participant2: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}]
});
If it's something like above, how would I search for a participant pair? Could I order the participant IDs in some way so that they are always participant1 < participant2 for example, making search simpler?

Well, there is no correct answer to this question, But definitely, the approaches you have mentioned are not the best at all!
Firstly, when you are thinking about designing a "chat" model, you need to take into account that there would be millions of messages between the users, so you need to care about performance when you want to fetch the chats.
Storing the messages into an array is not a good idea at all, your model's size will be large by the time and you have to consider that MongoDB's document size limit is currently 16 MB per document.
https://docs.mongodb.com/manual/reference/limits/
Secondly, You have to consider pagination aspect because it will affect the performance when the chat is large, when you retrieve the chat between 2 users you won't request all the chats since the beginning of the time, you will just request the most recent ones, and then you can request the older ones if the user scroll the chat, this aspect is very important and can't be neglected due to its effect on performance.
My approach will be to store each message in a separated document
First of all, storing each message in a single document will boost your performance during fetching the chats, and the document size will be very small.
This is a very simple example, you need to change the model according to your needs, it is just to represent the idea:
const MessageSchema = mongoose.Schema({
message:{
text: { type:String, required:true }
// you can add any other properties to the message here.
// for example, the message can be an image ! so you need to tweak this a little
}
// if you want to make a group chat, you can have more than 2 users in this array
users:[{
user: { type:mongoose.Schema.Types.ObjectId, ref:'User', required:true }
}]
sender: { type:mongoose.Schema.Types.ObjectId, ref:'User', required:true },
read: { type:Date }
},
{
timestamps: true
});
you can fetch the chats by this query:
Message.find(({ users: { "$in" : [#user1#,#user2#]} })
.sort({ updatedAt: -1 })
.limit(20)
Easy and clean!
as you see, pagination becomes very easy with this approach.

A few suggestions.
First - why store Participant1 and 2 as arrays? There is one specific sender, and one (or more) recipients (depending on if you want group messages).
Consider the following Schema:
var ChatSchema = new Schema({
sender : {
type : mongoose.Schema.Types.ObjectId,
ref : 'User'
},
messages : [
{
message : String,
meta : [
{
user : {
type : mongoose.Schema.Types.ObjectId,
ref : 'User'
},
delivered : Boolean,
read : Boolean
}
]
}
],
is_group_message : { type : Boolean, default : false },
participants : [
{
user : {
type : mongoose.Schema.Types.ObjectId,
ref : 'User'
},
delivered : Boolean,
read : Boolean,
last_seen : Date
}
]
});
This schema allows one chat document to store all messages, all participants, and all statuses related to each message and each participant.
the Boolean is_group_message is just a shorter way to filter which are direct / group messages, maybe for client side viewing or server-side processing. Direct messages are obviously easier to work with query-wise, but both are pretty simple.
the meta array lists the delivered/read status, etc, for each participant of a single message. If we weren't handling group messages, this wouldn't need to be an array, but we are, so that's fine.
the delivered and read properties on the main document (not the meta subdocument) are also just shorthand ways of telling if the last message was delivered/read or not. They're updated on each write to the document.
This schema allows us to store everything about a chat in one document. Even group chats.

Related

How can I count the view of a Specific post by a User ? Count every User just Once

We have user and news model, in the news model we have e viewsCount field, I want to increment this view count when a GET request is made by a User.
When a specific user makes a GET request, the view count will increment one, every user just one view.
const NEWSModel = new Schema({
viewesCount: { type: Number },
Publisher: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
LikesCount: { type: Number },
DislikeCount: { type: Number },
Comments: CommenTs
});
Every user can view the news as many times as wants, but just can make one view. How can I do that?
you Can change your model like and then whenever you get a news just push the user id to the viewedBy field.
news.viewedBy.push(user id)
viewedBy: [{
type: mongoose.Schema.Types.ObjectId,
ref: "User"
}]
}); ```
If you have not a lot of users, you can add additional field to news model like users_viewed which would be array of unique user ids.
And make additional check before incrementing views count.
If user, who requested news is already in this users_viewed array, you skip any additional actions.
If don't, increment views counter.
But if you do have a lot of users, it's better to store views counter in Redis to skip request to database and increment in memory counter.
The logic for storing and showing data would be the same, but you'll reduce load on your database and speed up the whole process.
[UPDATE] According to your comment, about number of users.
To make things work you can use this package.
First of all, after request for a news from a client, you can store all the news data in your cache (to reduce number of requests to your database).
Now you have few possible ways to handle number of views.
I think, the easiest to implement would be to add user unique identifier to SET. And return number of users in SET using SCARD;
In this solution you wouldn't need to check if user already watched the news, because set data structure holds only unique values (the same reason why do we need to use user's unique identifier).
And you just use 2 redis requests, which is pretty good for heavy load services.
You can have another field called viewedBy of type array in which you can store users ids. Then it will be easier to check if a user already viewed your post or to count them.
File: news.model.js
const News = new Schema({
viewedBy: [{
type: mongoose.Schema.Types.ObjectId,
ref: "User"
}],
// other properties...
});
File: news.controller.js
const user = User.find({...}); // get current user
const news = News.find({...}); // get a news
/*
Update views count by adding the current user id if it's not already added
Thanks to '$addToSet', the update() function will do nothing if the user id it's already there)
*/
news.update({ $addToSet: { viewedBy: user._id } });
// Getting the views count
console.log('Total views:', news.viewedBy.length);
More about $addToSet: https://docs.mongodb.com/manual/reference/operator/update/addToSet/

Create View from multiple collections MongoDB

I have following Mongo Schemas(truncated to hide project sensitive information) from a Healthcare project.
let PatientSchema = mongoose.Schema({_id:String})
let PrescriptionSchema = mongoose.Schema({_id:String, patient: { type: Number, ref: 'Patient', createdAt:Date }})
let ReportSchema = mongoose.Schema({_id:String, patient: { type: Number, ref: 'Patient', createdAt:Date }})
let EventsSchema = mongoose.Schema({_id:String, patient: { type: Number, ref: 'Patient', createdAt:Date }})
There is ui screen from the mobile and web app called Health history, where I need to paginate the entries from prescription, reports and events sorted based on createAt. So I am building a REST end point to get this heterogeneous data. How do I achieve this. Is it possible to create a "View" from multiple schema models so that I won't load the contents of all 3 schema to fetch one page of entries. The schema of my "View" should look like below so that I can run additional queries on it (e.g. find last report)
{recordType:String,/* prescription/report/event */, createdDate:Date, data:Object/* content from any of the 3 tables*/}
I can think of three ways to do this.
Imho the easiest way to achieve this is by using an aggregation something like this:
db.Patients.aggregate([
{$match : {_id: <somePatientId>},
{
$lookup:
{
from: Prescription, // replicate this for Report and Event,
localField: _id,
foreignField: patient,
as: prescriptions // or reports or events,
}
},
{ $unwind: prescriptions }, // or reports or events
{ $sort:{ $createDate : -1}},
{ $skip: <positive integer> },
{ $limit: <positive integer> },
])
You'll have to adapt it further, to also get the correct createdDate. For this, you might want to look at the $replaceRoot operator.
The second option is to create a new "meta"-collection, that holds your actual list of events, but only holds a reference to your patient as well as the actual event using a refPath to handle the three different event types. This solution is the most elegant, because it makes querying your data way easier, and probably also more performant. Still, it requires you to create and handle another collection, which is why I didn't want to recommend this as the main solution, since I don't know if you can create a new collection.
As a last option, you could create virtual populate fields in Patient, that automatically fetch all prescriptions, reports and events. This has the disadvantage that you can not really sort and paginate properly...

self referencing schema in mongoose

I've been through several tutorials. I'm still wondering what the best approach for my problem would be. I got the following Schema:
var userSchema = new Schema({
_id : Number,
first_name : String,
last_name : String,
friends : [ Number ],
messages : [{
from: Number,
body : String,
date : { type : Date, default: Date.now}
}]
}, { collection : "user"});
In friends I want to store the ids of user's friends in an array. In message.from I want to store the sender's id of a message.
Ideally I want those ids in friends and message.from to be only ids of valid user entries.
Unfortunately mongodb doesn't enforce referential integrity.
This functionality must be provided by your application.
So in your case: when a user is deleted your application must also remove references to that user in all other user's friends arrays and message fields.

MongoDB collection/Express.js for "friend request" functionality

I have an idea for how to store the relationships. Each user has a friends Array filled with IDs. However, how should I initiate a friend request in my Express.js app in MongoDB?
I'm thinking about creating a "notifications" collection with:
_id, userId, type, friendId, read
So when the requested friend logs in, they can see all of their own notifications to deal with...
Or is that ridiculous?
For such notifications what I did, is as follows:
var notificationSchema = mongoose.Schema({
to:{
type: mongoose.Schema.Types.ObjectId, ref: 'User'
},
from:{
type: mongoose.Schema.Types.ObjectId, ref: 'User'
},
for: {
type: String
},
status: {
type: String,
default: "Not Seen"
},
description:{
type: String
}
},{ timestamps: { createdAt: 'created_at' }
});
where I have saved _id of to and from (users) where I used for field for notification type like following, liked etc and used description field as optional.
Sounds reasonable enough. But my approach will be a little different. I would store the notifications in the user db itself. Something like
username
dob
...
notifications[[type: 'friend', read: 0, request_from: '22sd300sdf45003425asz'], ...]
...
This way, you don't have to make a db call on every page load. As soon as you initialize a session (I use passport), it will be there, ready already for templates. After a valid action from the user, I can delete it or whatever.
But again, its dependent on the need. Do what suits you best!
If you store it in the user passport session (solution mentioned earlier) you will not be able to receive anymore notifications since it is static information in the header and not connected directly to the document store.
The best way to do it would to have let it have it's own store.
{Id:ObjectId, for_user:String, sent from:String, status:Boolean}
Perhaps you can initially set the status to null then set it to true or false when a user accepts or denies it. After create the user to user friend relationship. That's more or less the way I would go about it.

Mongoose: populate() / DBref or data duplication?

I have two collections:
Users
Uploads
Each upload has a User associated with it and I need to know their details when an Upload is viewed. Is it best practice to duplicate this data inside the the Uploads record, or use populate() to pull in these details from the Users collection referenced by _id?
OPTION 1
var UploadSchema = new Schema({
_id: { type: Schema.ObjectId },
_user: { type: Schema.ObjectId, ref: 'users'},
title: { type: String },
});
OPTION 2
var UploadSchema = new Schema({
_id: { type: Schema.ObjectId },
user: {
name: { type: String },
email: { type: String },
avatar: { type: String },
//...etc
},
title: { type: String },
});
With 'Option 2' if any of the data in the Users collection changes I will have to update this across all associated Upload records. With 'Option 1' on the other hand I can just chill out and let populate() ensure the latest User data is always shown.
Is the overhead of using populate() significant? What is the best practice in this common scenario?
If You need to query on your Users, keep users alone. If You need to query on your uploads, keep uploads alone.
Another question you should ask yourself is: Every time i need this data, do I need the embedded objects (and vice-versa)? How many time this data will be updated? How many times this data will be read?
Think about a friendship request:
Each time you need the request you need the user which made the request, then embed the request inside the user document.
You will be able to create an index on the embedded object too, and your search will be mono query / fast / consistent.
Just a link to my previous reply on a similar question:
Mongo DB relations between objects
I think this post will be right for you http://www.mongodb.org/display/DOCS/Schema+Design
Use Cases
Customer / Order / Order Line-Item
Orders should be a collection. customers a collection. line-items should be an array of line-items embedded in the order object.
Blogging system.
Posts should be a collection. post author might be a separate collection, or simply a field within posts if only an email address. comments should be embedded objects within a post for performance.
Schema Design Basics
Kyle Banker, 10gen
http://www.10gen.com/presentation/mongosf2011/schemabasics
Indexing & Query Optimization
Alvin Richards, Senior Director of Enterprise Engineering
http://www.10gen.com/presentation/mongosf-2011/mongodb-indexing-query-optimization
**These 2 videos are the bests on mongoddb ever seen imho*
Populate() is just a query. So the overhead is whatever the query is, which is a find() on your model.
Also, best practice for MongoDB is to embed what you can. It will result in a faster query. It sounds like you'd be duplicating a ton of data though, which puts relations(linking) at a good spot.
"Linking" is just putting an ObjectId in a field from another model.
Here is the Mongo Best Practices http://www.mongodb.org/display/DOCS/Schema+Design#SchemaDesign-SummaryofBestPractices
Linking/DBRefs http://www.mongodb.org/display/DOCS/Database+References#DatabaseReferences-SimpleDirect%2FManualLinking

Resources