MongoDB User notifications scheme suggestions - mongodb

I was wondering what is the best scheme for user/notifications kind of scenario like the following:
1 | 01-04-2020 | X | John liked your post2
2 | 01-03-2020 | X | Mike and 9 other persons liked your post1
3 | 01-02-2020 | | Rose and 2 other persons liked your post1
4 | 01-01-2020 | | Bernard liked your post1
x = notification has not been read by the user yet
post1 = the same post in all the notifications
Let's say I have a Notification collection like:
_id: ObjectID
receiver: ObjectID (User)
sender : ObjectID (User)
type: String ("post", "comment", etc...)
typeID: ObjectID (Post, Comment, etc...)
message: String
isRead : Boolean
timestamp: Date
A User collection like :
_id: ObjectID
username: String
.
.
.
email: String
A Post Collection like :
_id: ObjectID
postedBy: ObjectID (User)
.
.
.
content: String
A Like collection like :
_id: ObjectID
type: String ("post", "comment", etc...)
typeID: ObjectID (Post, Comment, etc...)
likedBy: ObjectID (User)
On the 01-01-2020, the user opened his notifications panel. Between the last time he checked his notifications and this date, only 1 person liked his post1.
On the 01-02-2020, the user opened his notifications panel. Between the last time he checked his notifications (01-01-2020) and this date, 2 persons liked his post1.
On the 01-04-2020, the user opened his notifications panel. Between the last time he checked his notifications (01-02-2020) and this date, 9 persons liked his post1 and 1 person liked his post2.
I want the user to be able to see all his previous notifications as well as the notifications he hasn't read yet. If the user has several notifications for the same post (X people liked his post since the last time he checked his notifications), I want to group them as 1 notification (I will mark all of them as read once he read that one grouped notification).
How can I do that?
Please let me know if you need more information or if I am being unclear.
Thanks
Edit:
I'm having a hard trying to figure out how to aggregate those notifications. I think I need some kind of read date marker as well to group the notifications that were grouped and read at the same time, but maybe I need another collection to store the grouped notifications?
Notification.aggregate([
{
$group: {
_id: {
typeID : "$typeID",
receiver: "$receiver",
isRead: "$isRead"
// maybe something with a read date?
},
count: {$sum: 1}
}
}
])

I guess this article at Mongo University is a relevant answer to your question.
Use at least two collections: users and notifications also, you your users _id field isn't something like name and you'll allow them to be renamed, then it's perfect to have 3-rd collection likes, instead making likes as a embedded documents in array, like this:
User's schema:
_id: ObjectID,
likes: [{
_id: ObjectID //like_id,
other: "field"
}]
Notifications:
_id: ObjectID
receiver: ObjectID (User)
sender : ObjectID (User)
type: {
type: String,
enum: ["Post", "Comment"] /** Use enum for avaliable values */
},
typeID: {
type: ObjectID, /** It's better if every ID field have an index */
index: true,
unique: true
}
message: String
isRead : Boolean
timestamp: {
type: Date,
default: Date.now /** Don't forget about default values, but check my advice below about $timestamps */
}
Not sure that timestamp field is needed for you, as for me, it's
better to use {timestamps: true} option.
Also, every field with ObjectID should be indexed, it you needed
this fields for aggregation framework. It's a perfect performance
case for $lookup
I want the user to be able to see all his previous notifications as well as the notifications he hasn't read yet.
You needed a compound index for this, like {sender:1, createdAt: -1, isRead: 1}
I want to group them as 1 notification (I will mark all of them as read once he read that one grouped notification).
This is a job for aggregation framework, via:
{
$match: { query_criteria },
},
{
$group: { query_group_by $notification.typeID }
}
So your schema is fine, it's possible to do that. By the way, to test your own queries, you could use MongoPlayground, instead of production DB.
As for the likes schema, it's for you to decide, but maybe it's better to have them as an embedded (child) documents, like:
Post
_id: ObjectID
postedBy: ObjectID (User)
likes: [{
/** Likes Sub-schema */
}]
content: String
Take a look at sub-schema pattern in mongoose.
Hope it will helps you!

Related

Mongoose $push to array inside subdocument

I have a companies model which has payment info stored like this:
let paymentSchema = new Schema({
date: Date,
chargeId: String
amount: Number,
receipt_url: String,
currency: String,
type: String
});
paymentInfo: {
lastPayment: {type: paymentSchema},
paymentHistory: [{type: paymentSchema}],
paymentMethod: paymentMethod,
customerId: customerId
}
)};
The paymentInfo is a subdocument and I am using schema there for some validation purposes.
While trying to update the a company's info using the following query, it updates everything except the paymentHistory :
Companies.updateOne(
{_id: companyId},
{
paymentInfo: {
paymentMethod: paymentMethod,
customerId: customerId,
lastPayment : paymentObject,
$push: {paymentHistory: paymentObject }
}
One could say why save the same paymentObject in lastPayment and also push it to paymentHistory - that's required for a feature we need and that's not causing any trouble.
I also tried the following update query and it gives a conflict error - updates in the following code is a valid object and without $push it works fine
{$set: updates, $push: {'paymentInfo.paymentHistory': paymentObject}}
Conflict Error :
{"driver":true,"name":"MongoError","index":0,"code":40,"errmsg":"Updating the path 'paymentInfo' would create a conflict at 'paymentInfo'"}
What do I want?
I want to update the company document in a single database call - looking at that error it seems like I am supposed to make 2 calls, one for $set and other for $push? Is there anything wrong with the first update query??
Possible Solution
One way to solve this is to take out the paymentHistory out of paymentInfo so that it's part of main company schema - that solves the issue but I want to keep all payment related stuff inside paymentInfo.
Any help would be appreciated. Thanks

populate or aggregate 2 collection with sorting and pagination

I just use mongoose recently and a bit confused how to sort and paginate it.
let say I make some project like twitter and I had 3 schema. first is user second is post and third is post_detail. user schema contains data that user had, post is more like fb status or twitter tweet that we can reply it, post_detail is like the replies of the post
user
var userSchema = mongoose.Schema({
username: {
type: String
},
full_name: {
type: String
},
age: {
type: Number
}
});
post
var postDetailSchema = mongoose.Schema({
message: {
type: String
},
created_by: {
type: String
}
total_reply: {
type: Number
}
});
post_detail
var postDetailSchema = mongoose.Schema({
post_id: {
type: String
}
message: {
type: String
},
created_by: {
type: String
}
});
the relation is user._id = post.created_by, user._id = post_detail.created_by, post_detail.post_id = post._id
say user A make 1 post and 1000 other users comment on that posts, how can we sort the comment by the username of user? user can change the data(full_name, age in this case) so I cant put the data on the post_detail because the data can change dynamically or I just put it on the post_detail and if user change data I just change the post_detail too? but if I do that I need to change many rows because if the same users comment 100 posts then that data need to be changed too.
the problem is how to sort it, I think if I can sort it I can paginate it too. or in this case I should just use rdbms instead of nosql?
thanks anyway, really appreciate the help and guidance :))
Welcome to MongoDB.
If you want to do it in the way you describe, just don't go for Mongo.
You are designing the schema based on relations and not in documents.
Your design requires to do joins and this does not work well in mongo because there is not an easy/fast way of doing this.
First, I would not create a separate entity for the post details but embedded in the Post document the post details as a list.
Regarding your question:
or I just put it on the post_detail and if user change data I just
change the post_detail too?
Yes, that is what you should do. If you want to be able to sort the documents by the userName you should denormalize it and include in the post_details.
If I had to design the schema, it would be something like this:
{
"message": "blabl",
"authorId" : "userId12",
"total_reply" : 100,
"replies" : [
{
"message" : "okk",
"authorId" : "66234",
"authorName" : "Alberto Rodriguez"
},
{
"message" : "test",
"authorId" : "1231",
"authorName" : "Fina Lopez"
}
]
}
With this schema and using the aggregation framework, you can sort the comments by username.
If you don't like this approach, I rather would go for an RDBMS as you mentioned.

Trying to design Facebook-like notification in mongoose. Need help aggregating

Like Facebook, I would like to aggregate the results. But I can't figure out how to go about it.
Example:
Let's say 10 users like my posts.
I don't want to get 10 notifications. 1 is of course enough.
This is my schema:
var eventLogSchema = mongoose.Schema({
//i.e. Somebody commented, sombody liked, etc.
event: String,
//to a comment, to a post, to a profile, etc.
toWhat: String,
//who is the user we need to notify?
toWho: {type: mongoose.Schema.Types.ObjectId, ref:'User'},
//post id, comment id, whatever..
refID: {type: mongoose.Schema.Types.ObjectId},
//who initiated the event.
whoDid: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
// when the event happened
date: {type:Date, default: Date.now()},
//whether the user already saw this notification or not.
seen: {type:Boolean, default: false}
})
so I need to count the times
Ex.1: event='liked' and toWhat="post" and refID=myPostID and seen=false
But at the same time, I would like to populate the last event with this parameters on the 'who' path so I could display "Michael and 9 other people liked your post(link to post)"
Every way I can think of doing this is clunky and requires multiple queries that feel like they would cost a lot of system resources and I am wondering if there's a simple way to do it.
Actually it gets more complicated then that.
I do not want to specify values like I did in Ex.1.
Instead I would like to say
aggregate all events with similar 'event', 'toWhat',
'refID' with value seen=false and populate the last one on the 'who' path.
Would love some reading materials, links, advice, or anything.
Thanks!
Managed to solve it like this.
Not sure if it's optimal, but it works.
//The name of my Schema
Notification.aggregate([
{
$match: {
//I only want to display new notifications
seen: {$ne: true}
//to query a specific user add
// toWho: UserID
}
},
{
$group: {
//groups when event, toWhat, and refID are similar
_id: {
event: '$event',
toWhat: '$toWhat',
refID: '$refID',
},
//gets the total number of this type of notification
howMany: {$sum: 1},
//gets the date of the last document in this query
date: {$max: '$date'},
//pulls the user ID of the last user in this query
user: {$last: '$whoDid'}
}
}
]).exec(function (err, results) {
if (err) throw err;
if (results) {
//after I get the results, I want to populate my user to get his name.
Notification.populate(results, {path: 'user', model: "User"}, function (err, notifications) {
if (err) throw err;
if (notifications) res.send(notifications);
})
}
})
I'm not sure whether it's possible to populate the aggregated result in one query, I assume that if it's possible it would be optimal, but so far, this seems acceptable for my needs.
Hope this helps.

return specific properties of array element - MongoDB/ Meteor

I have documents in games collection.Each document is responsible for holding the data that requires to run the game. Here's my document structure
{
_id: 'xxx',
players: [
user:{} // Meteor.users object
hand:[] //array
scores:[]
calls:[]
],
table:[],
status: 'some string'
}
Basically this is a structure of my card game(call-bridge). Now what I want for the publication is that the player will have his hand data in his browser( minimongo ) along with other players user, scores, calls fields. So the subscription that goes down to the browser will be like this.
{
_id: 'xxx',
players: [
{
user:{} // Meteor.users object
hand:[] //array
scores:[]
calls:[]
},
{
user:{} // Meteor.users object
scores:[]
calls:[]
},
// 2 more player's data, similar to 2nd player's data
],
table:[],
status: 'some string'
}
players.user object has an _id property which differentiates the user. and in the meteor publish method, we have access to this.userId which returns the userId who is requesting the data.It means I want the nested hand array of that user whose _id matches with this.userId. I hope this explanations help you write more accurate solution.
What you need to do is "normalize" your collection. Instead of having hand,scores, calls in the players field in the Games collection, what you can do is create a separate collection to hold that data and use the user _id as the "Key" then only reference the user _id in the players field. For example.
Create a GameStats collection(or whichever name you want)
{
_id: '2wiowew',
userId: 1,
hand:[],
scores:[],
calls:[],
}
Then in the Games collection
{
_id: 'xxx',
players: [userId],
table:[],
status: 'some string'
}
So if you want to get the content of the current user requesting the data
GameStats.find({userId: this.userId}).hand
EDIT
They do encourage denormalization in certain situations, but in the code you posted above, array is not going to work. Here is an example from the mongoDB docs.
{
_id: ObjectId("5099803df3f4948bd2f98391"),
name: { first: "Alan", last: "Turing" },
birth: new Date('Jun 23, 1912'),
death: new Date('Jun 07, 1954'),
contribs: [ "Turing machine", "Turing test", "Turingery" ],
views : NumberLong(1250000)
}
To get a specific property from an array element you may write something as in the below line db.games.aggregate([{$unwind:"$players"},{$project:{"players.scores":1}}]); this gives us only the id and scores fields

Mongoose / MongoDB User notifications scheme suggestions

I was wondering what is the best scheme for user/notifications kind of scenario like the following :
You have multiple users.
You have multiple notifications that might be for a single user, for some users or for all users.
You need a notification "read" entry in the storage, to know if the user has read the notification or not.
Option One
Embedded notifications scheme
Notifications = new Schema ( {
message : String,
date : { type : Date, default: Date.now() }
read : { type: Boolean, default : false }
});
User = new Schema( {
username : String,
name : String,
notifications : [Notifications]
});
Pros :
It is very easy to display the data, since calling User.find() will display notifications as array object.
Cons :
When you create a notification for every user, you need to do .push to every embedded Notifications
Multiple notifications entries for every user ( multiple data in the database )
Giant embedded document ( I read something about <4MB limit of those )
Since it is a Embedded Document - ( mongoose DocumentArray ) you can't search or skip. You load every notifications everytime you access user.
Option Two
Populate (DBRef like) objects
Notification = new Schema ({
message : String,
date : { type : Date, default : Date.now() }
});
UserNotification = new Schema ({
user : { type : Schema.ObjectId, ref : 'User' },
notification : { type : Schema.ObjectId, ref : 'Notification' },
read : { type : Boolean, default : false }
});
User = new Schema( {
username : String,
name : String,
notifications : [ { type : Schema.ObjectID, ref : 'UserNotification' } ]
});
Pros :
Optimal for queries
No duplicate data
Supporting large amount of notifications
Cons :
You have 3 collections, instead of one in ( Option One has only one collection )
You have 3 queries every time you access the collection.
Questions
What do you think is the best scheme from those two?
Am I missing something or some kind of basic NoSQL knowledge?
Can someone propose better scheme?
Thank you, in advance and I'm sorry for the long post, but I think I can't explain it simpler.
Option 1 looks like it will probably result in a lot of excessive document growth and moves, which would be bad for performance, since most of your writes will be going to the embedded doc (Notifications).
Option 2 I'm not totally clear on your strategy - it seems redundant to have those 3 collections but also embed a list of notifications by objectId if you are already referencing user by ID in the notifications table. You could index on user in Notifications table, and then eliminate the nested array in the Users table.
(EDIT)
Here's another strategy to consider.
Three collections that look like this:
Users:
_id: objectid
username : string
name: string
Notifications:
_id: objectid
to (indexed): objectid referencing _id in "users" collection
read: boolean
Global Notifications:
_id: objectid
read_by: [objectid referencing _id in users]
For notifications meant for a single user, insert into Notifications for that user. For multiple users, insert one for each user (alternately, you could make the "to" field an array and store _ids of all the recipients, and then maintain another list of all the recipients who have read it). To send a notification to all users, insert into Global Notifications collection. When the user reads it, add their user's _id to the read_by field.
So, to get a list of all the unread notifications of a user, you do two queries: one to notifications, and one to global notifications.
// namespace like a channel
// we have a notification from specific channel or namespace
const NamespaceSchema = new Schema({
name: {
type: String,
unique: true,
required: true
},
author: {
type: String,
required: true
},
createdAt: {
type: Boolean,
default: Date.now()
},
notifications: [{
type: Schema.Types.ObjectId,
ref: 'Notification',
}]
});
// the notification schema have a subscribers to specific notification (objectId)
//
const NotificationSchema = new Schema({
title: {
type: String
},
message: {
type: String
},
read: {
type: Boolean,
default: false
},
subscribers: [{
type: Schema.Types.ObjectId,
ref: 'Subscriber'
}],
createdAt: {
type: Date,
default: Date.now()
}
});
// subscribers subscribe to a namespace or channel
const SubscriberSchema = new Schema({
subscriber: {
type: String,
required: true
},
namespaces: [{
type: Schema.Types.ObjectId,
ref: 'Namespace',
}]
});