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

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.

Related

MongoDB User notifications scheme suggestions

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!

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

Accessing nested documents within nested documents

I'm having a problem that is really bugging me. I don't even want to use this solution I don't think but I want to know if there is one.
I was creating a comment section with mongodb and mongoose and keeping the comments attached to the resource like this:
const MovieSchema = new mongoose.Schema({
movieTitle: {type: String, text: true},
year: Number,
imdb: String,
comments: [{
date: Date,
body: String
}]
})
When editing the comments body I understood I could access a nested document like this:
const query = {
imdb: req.body.movie.imdb,
"comments._id": new ObjectId(req.body.editedComment._id)
}
const update = {
$set: {
"comments.$.body": req.body.newComment
}
}
Movie.findOneAndUpdate(query, update, function(err, movie) {
//do stuff
})
I then wanted to roll out a first level reply to comments, where every reply to a comment or to another reply just appeared as an array of replies for the top level comment (sort of like Facebook, not like reddit). At first I wanted to keep the replies attached to the comments just as I had kept the comments attachted to the resource. So the schema would look something like this:
const MovieSchema = new mongoose.Schema({
movieTitle: {type: String, text: true},
year: Number,
imdb: String,
comments: [{
date: Date,
body: String,
replies: [{
date: Date,
body: String
}]
}]
})
My question is how would you go about accessing a nested nested document. For instance if I wanted to edit a reply it doesn't seem I can use two $ symbols. So how would I do this in mongodb, and is this even possible?
I'm pretty sure I'm going to make Comments have its own model to simplify things but I still want to know if this is possible because it seems like a pretty big drawback of mongodb if not. On the other hand I'd feel pretty stupid using mongodb if I didn't figure out how to edit a nested nested document...
according to this issue: https://jira.mongodb.org/browse/SERVER-27089
updating nested-nested elements can be done this way:
parent.update({},
{$set: {“children.$[i].children.$[j].d”: nuValue}},
{ arrayFilters: [{ “i._id”: childId}, { “j._id”: grandchildId }] });
this is included in MongoDB 3.5.12 development version, in the MongoDB 3.6 production version.
according to https://github.com/Automattic/mongoose/issues/5986#issuecomment-358065800 it's supposed to be supported in mongoose 5+
if you're using an older mongodb or mongoose versions, there are 2 options:
find parent, edit result's grandchild, save parent.
const result = await parent.findById(parentId);
const grandchild = result.children.find(child => child._id.equals(childId))
.children.find(grandchild => grandchild._id.equals(grandchildId));
grandchild.field = value;
parent.save();
know granchild's index "somehow", findByIdAndUpdate parent with:
parent.findByIdAndUpdate(id,
{ $set: { [`children.$.children.${index}.field`]: value }});

Using TTL in MongoDB [duplicate]

I have a very certain thing i want to accomplish, and I wanted to make sure it is not possible in mongoose/mongoDB before I go and code the whole thing myself.
I checked mongoose-ttl for nodejs and several forums and didn't find quite what I need.
here it is:
I have a schema with a date field createDate. Now i wish to place a TTL on that field, so far so good, i can do it like so (expiration in 5000 seconds):
createDate: {type: Date, default: Date.now, expires: 5000}
but I would like my users to be able to "up vote" documents they like so those documents will get a longer period of time to live, without changing the other documents in my collection.
So, Can i change a TTL of a SINGLE document somehow once a user tells me he likes that document using mongoose or other existing npm related modules?
thank you
It has been more than a year, but this may be useful for others, so here is my answer:
I was trying accomplish this same thing, in order to allow a grace period after an entry deletion, so the user can cancel the operation afterwards.
As stated by Mike Bennett, you can use a TTL index making documents expire at a specific clock time.
Yo have to create an index, setting the expireAfterSeconds to zero:
db.yourCollection.createIndex({ "expireAt": 1 }, { expireAfterSeconds: 0 });
This will not affect any of the documents in your collection, unless you set expireAfterSeconds on a particular document like so:
db.log_events.insert( {
"expireAt": new Date('July 22, 2013 14:00:00'),
"logEvent": 2,
"logMessage": "Success!"
} )
Example in mongoose
Model
var BeerSchema = new Schema({
name: {
type: String,
unique: true,
required: true
},
description: String,
alcohol: Number,
price: Number,
createdAt: { type: Date, default: Date.now }
expireAt: { type: Date, default: undefined } // you don't need to set this default, but I like it there for semantic clearness
});
BeerSchema.index({ "expireAt": 1 }, { expireAfterSeconds: 0 });
Deletion with grace period
Uses moment for date manipulation
exports.deleteBeer = function(id) {
var deferred = q.defer();
Beer.update(id, { expireAt: moment().add(10, 'seconds') }, function(err, data) {
if(err) {
deferred.reject(err);
} else {
deferred.resolve(data);
}
});
return deferred.promise;
};
Revert deletion
Uses moment for date manipulation
exports.undeleteBeer = function(id) {
var deferred = q.defer();
// Set expireAt to undefined
Beer.update(id, { $unset: { expireAt: 1 }}, function(err, data) {
if(err) {
deferred.reject(err);
} else {
deferred.resolve(data);
}
});
return deferred.promise;
};
You could use the expire at clock time feature in mongodb. You will have to update the expire time each time you want to extend the expiration of a document.
http://docs.mongodb.org/manual/tutorial/expire-data/#expire-documents-at-a-certain-clock-time

MongoDB: Get all mentioned items

I've got two relations in my Mongoose/MongoDB-Application:
USER:
{
name: String,
items: [{ type: mongoose.Schema.ObjectId, ref: 'Spot' }]
}
and
ITEM
{
title: String,
price: Number
}
As you can see, my user-collection containing a "has-many"-relation to the item-collection.
I'm wondering how to get all Items which are mentioned in the items-field of on specific user.
Guess its very common question, but I haven't found any solution on my own in the Docs or elsewhere. Can anybody help me with that?
If you are Storing items reference in user collection,then fetch all items from user,it will give you a array of object ids of items and then you can access all items bases on their ids
var itemIdsArray = User.items;
Item.find({
'_id': { $in: itemIdsArray}
}, function(err, docs){
console.log(docs);
});
You can get the items at the same time you query for the user, by using Mongoose's support for population:
User.findOne({_id: userId}).populate('items').exec(function(err, user) {
// user.items contains the referenced docs instead of just the ObjectIds
});