Use req.user instead of User.findOne when updating MongoDB sub-documents? - node.js

I am using Passport.js for Express, which already holds the current user in the session variable req.user.
Yet when I try to update a MongoDB database I can only use req.user to identify the current user in certain cases. In others, I have to first do a root search to identify the correct user. Why?
I'm updating a database with this structure:
User A
Stocks [array]
Notes [array]
User B
Stocks [array]
Notes [array]
User C
...
Let's say we have already populated the database with 'stocks', and with 'notes' belonging to these 'stocks'. Now the user clicks a button to delete an existing 'note'.
This code works fine:
await User.updateOne (
{ $and: [ { email: req.user.email }, { 'stocks.ticker': req.body.ticker } ]},
{ $pull: { 'stocks.$.notes': { _id: req.body.id }}}
)
This query contains both the email to identify the user, and the stock's ticker to identify the relevant stock. And then the note is pulled from that stock. As said, it works fine.
But since I already know the current user (set in Passport's req.user variable) I don't want to have to include the email in the query and waste resources finding the current user.
I want to reduce to this:
await req.user.updateOne (
{ 'stocks.ticker': req.body.ticker },
{ $pull: { 'stocks.$.notes': { _id: req.body.id }}}
)
But this does not work, and I don't understand why.
If I use the req.user variable for a simpler pull action, it does work. I.e. the following code works to pull a stock document from the user's database:
await req.user.updateOne (
{ $pull: { stocks: { ticker: req.body.ticker }}}
)
But if I include a query before the pull action, it does not. I have tried everything I can think of to solve this. No joy.

Related

MongoDB: How to perform a second match using the results (an array of ObjectIds) of the previous match in aggregation pipeline

I have a MongoDB collection called users with documents that look like:
{
_id: ObjectId('123'),
username: "abc",
avatar: "avatar/long-unique-random-string.jpg",
connections: [ObjectId('abc'), ObjectId('xyz'), ObjectId('lmn'), ObjectId('efg')]
}
This document belongs to the users collection.
What I want to do:
First, find one document from the users' collection that matches _id -> '123'.
Project the connections field received from step 1, which is an array of ObjectIds of other users within the same collection.
Find all documents of users from the array field projected in step 2.
Project and return an array of only the username and avatar of all those users from step 3.
While I know that I can do this in two separate queries. First using findOne which returns the friends array. Then, using find with the results of findOne to get all the corresponding usernames and avatars.
But, I would like to do this in one single query, using the aggregation pipeline.
What I want to know, is it even possible to do this in one query using aggregation?
If so, what would the query look like?
What, I currently have:
await usersCollection
.aggregate([
{ $match: { _id: new ObjectId(userId) } },
{ $project: { ids: "$connections" } },
{ $match: { _id: { $in: "ids" } } },
{
$project: {
username: "$username",
avatar: { $ifNull: ["$avatar", "$$REMOVE"] },
},
},
])
.toArray()
I know this is wrong because each aggregation stage receives the results from the previous stage. So, the second match cannot query on the entire users' collection, as far as I know.
I'm using MongoDB drivers for nodeJS. And I would like to avoid $lookup for possible solutions.

Mongodb check if value is in a nested array

I have a collection in my database that contains a field which is composed of 3 arrays, like this :
use_homepage: {
home: [Array],
hidden: [Array],
archive: [Array]
}
This field represents the homepage of a user.
Each array contains an ObjectID that identifies projects shown on the user homepage.
I would like to check if my project id is in use_homepage.home or use_homepage.hidden, and if it is, remove the id from the array that match.
Can I do this with 1 (or 2 max) requests or do I have to make a request each time I have to check in another array ?
In case you expect to update one document at most, you can try this:
db.entities.findAndModify({
query: { $or : [
{ home: ObjectId('<HERE YOUR ID TO BE FOUND>') },
{ hidden: ObjectId('<HERE YOUR ID TO BE FOUND>') }
]},
update: { $pull: {
home: ObjectId('<HERE YOUR ID TO BE DELETED>'),
hidden: ObjectId('<HERE YOUR ID TO BE DELETED>')
}
}
});
As you can see, in general, you can search for some value and delete some other value.
The statement returns the original matching document (i.e. before the deletion is performed). If you want the modified document you can add the following attribute:
new: true
In case you search for many documents to update, this solution does not work, since findAndModify() works just on the first document matching the query condition.
Finaly, i used to make 2 requests to do the job :
db.User.find({"use_homepage.home": id}, {_id: 1}).toArray(function(err, result) {
// If some users have the id in the array home
db.User.updateMany({_id: {$in: users_match_ids}}, {
$pull: {"use_homepage.home": id}
}
});
// Do the same with 'hidden' array
If anyone see this post and have a better solution, I take it :)

Mongoose: How to find documents by sub-collection's document property value

I’m using Mongoose version 4.6.8 and MongoLab (MLab). I have a Mongoose schema called “Group” that has a collection of User subdocuments called “teachers”:
var GroupSchema = new Schema({
//…more properties here…//
teachers: [{
type: Schema.ObjectId,
ref: 'User'
}]
});
This is a document from the “groups” collection on MongoLab:
{
//…more properties here…//
"teachers": [
{
"$oid": "5799a9c759feea9c208c004c"
}
]
}
And this is a document from the “users” collection on MongoLab:
{
//…more properties here…//
"username": "bob"
}
But if I want to get a list of Groups that have a particular teacher (User) with the username of “bob”, this doesn’t work (the list of groups is empty):
Group.find({"teachers.username": "bob"}).exec(callback);
This also returns no items:
Group.find().where('teachers.username').equals('bob').exec(callback);
How can I achieve this?
Without some more knowledge of your set up (specifically whether you want anybody named Bob or a specific Bob whose id you could pick up first) - this might be some help although I think it would require you to flatten your teachers array to just their ID's, not single-key objects.
User.findById(<Id of Bob>, function(err, user){
Group.find({}, function(err, groups){
var t = groups.map(function(g){
if(g['teachers'].indexOf(user.id))
return g
})
// Do something with t
})
})
You can use populate to do that.
Try this:
Group.find({})
.populate({
path : 'teachers' ,
match : { username : "bob" }
})
.exec(callback);
populate will populate based on the teachers field (given path) and match will return only those who have username bob.
For more information on mongoose populate options, Please read Mongoose populate documentation.
I think the solution in this case is to get a teacher’s groups through the User module instead of my first inclination which was to go through the Groups module. This makes sense because it is in line with how modern APIs represent a one-to-many relationship.
As an example, in Behance’s API, an endpoint for a user’s projects is:
GET /v2/users/user/projects
And a request to this endpoint (where the User’s username is “matiascorea”) would look like this:
https://api.behance.net/v2/users/matiascorea/projects?client_id=1234567890
So in my case, instead of finding the groups by teacher, I would need to simply find the User (teacher) by username, populate the teacher’s groups, and use them:
User.findOne({username: 'bob'})
.populate('groups')
.exec(callback);
And the API call for this would be:
GET /api/users/user/groups
And a request to this endpoint would look like this:
https://example.com/api/users/bob/groups

Easy way to only allow one item per user in mongoose schema array

I'm trying to implement a rating system and I'm struggling to only allow one rating per user in a reasonable way.
Simply put, i have an array of ratings in my schema, containing the "rater" and the rating, as such:
var schema = new Schema({
//...
ratings: [{
by: {
type: Schema.Types.ObjectId
},
rating: {
type: Number,
min: 1,
max: 5,
validate: ratingValidator
}
}],
//...
});
var Model = mongoose.model('Model', schema);
When i get a request, i wish to add the users rating to the array if the user has not already voted this document, otherwise i wish to update the rating (you should not be able to give more than one rating)
One way to do this is to find the document, "loop through" the array of ratings and search for the user. If the user has got already a rating in the array, the rating is changed, otherwise a new rating is pushed. As such:
Model.findById(id)
.select('ratings')
.exec(function(err, doc) {
if(err) return next(err);
if(doc) {
var rated = false;
var ratings = doc.ratings;
for(var i = 0; i < ratings.length; i++) {
if(ratings[i].by === user.id) {
ratings[i].rating = rating;
rated = true;
break;
}
}
if(!rated) {
ratings.push({
by: user.id,
rating: rating
});
}
doc.markModified('ratings');
doc.save();
} else {
//Not found
}
});
Is there an easier way? A way to let mongodb do this automatically?
The mongodb $addToSet operator could be an alternative, however i have not managed to use it for this, since that could allow two ratings with different scores from the same user.
As you note the $addToSet operator will not work in this case as indeed a userId with a different vote value would be a different value and it's own unique member of the set.
So the best way to do this is to actually issue two update statements with complementary logic. Only one will actually be applied depending on the state of the document:
async.series(
[
// Try to update a matching element
function(callback) {
Model.update(
{ "_id": id, "ratings.by": user.id },
{ "$set": { "ratings.$.rating": rating } },
callback
);
},
// Add the element where it does not exist
function(callback) {
Model.update(
{ "_id": id, "ratings.by": { "$ne": user.id } },
{ "$push": { "ratings": { "by": user.id, "rating": rating } }},
callback
);
}
],
function(err,result) {
// all done
}
);
The principle is simple, try to match the userId present in the ratings array for the document and update the entry. If that condition is not met then no document is updated. In the same way, try to match the document where there is no userId present in the ratings array, if there is a match then add the element, otherwise there will be no update.
This does bypass the built in schema validation of mongoose, so you would have to apply your constraints manually ( or inspect the schema validation rules and apply manually ) but it is better than you current approach in one very important aspect.
When you .find() the document and call it back to your client application to modify using code as you are, then there is no guarantee that the document has not changed on the server from another process or request. So when you issue .save() the document on the server may no longer be in the state that it was when it was read and any modifications can overwrite the changes made there.
Hence while there are two operations to the server and not one ( and your current code is two operations anyway ), it is the lesser of two evils to manually validate than to possibly cause a data inconsistency. The two update approach will respect any other updates issued to the document possibly occurring at the same time.

Extracting value from nested array in Mongoose

I'm working with Mongoose for the first time and I'm trying to accomplish what seems to be a simple task. I have a users document that contains a clients property, which consists of an array of client id's. As an example, my document looks like this:
{
email: "nick#movementstrategy.com",
password: "$2a$10$xZVzMYgyoyT.biOMDrBlRe3HNHY5A6lXga6uc8b/cnIAX/khQ7ep2",
modified: ISODate("2013-01-16T00:13:56.894Z"),
created: ISODate("2013-01-16T00:13:56.894Z"),
_id: ObjectId("50f5f0c4d6bbbcc6ce000002"),
clients: [
"50f6e118e0ccf9a1e9000001",
"50f6e12be0ccf9a1e9000002"
],
__v: 0
}
I've created a middleware that removes dependencies for a client when I call remove();
clientSchema.pre('remove', function(next) {
Sweepstakes.remove({client_id: this._id}).exec();
Submission.remove({client_id: this._id}).exec();
// find users with this._id present in clients, and remove from array
next();
});
Now, I simply need to locate all users who have the client id present in clients, remove the id, and update the user.
I know that I could easily query for all users with the id present, and then loop through and save out each user individually... But that seems inefficient, and my gut tells me that there is a better way to accomplish what I am trying to do -- just having a hard time locating it in the documentation.
What is the most efficient way to do this using Mongoose?
Probably something like this:
Users.update({condition}, {$pull : { clients: this._id} }, function(err, numAffected) {
//handle errors and whatever
next();
});
You can add clients : {$in : [this._id]} as condition to limit the update.

Resources