mongoose - how do I get the elements removed from $pull - javascript

I'm using $pull to pull a subdocument within an array of a document.
Don't know if it matters but in my case the subdocuments contain _id so they are indexed.
Here are JSONs that describes the schemas:
user: {
_id: String,
items: [UserItem]
}
UserItem: {
_id: String,
score: Number
}
Now my problem is this: I am using $pull to remove certain UserItem's from a User.
var delta = {$pull:{}};
delta.$pull.items={};
delta.$pull.items._id = {$in: ["mongoID1", "mongoID2" ...]};
User.findOneAndUpdate(query, delta, {multi: true}, function(err, data){
//whatever...
});
What i get in data here is the User object after the change, when what i wish to get is the items that were removed from the array (satellite data).
Can this be done with one call to the mongo or do I have to do 2 calls: 1 find and 1 $pull?
Thanks for the help.

You really cannot do this, or at least there is nothing that is going to return the "actual" elements that were "pulled" from the array in any response, even with the newer WriteResponse objects available to the newer Bulk Operations API ( which is kind of the way forward ).
The only way you can really do this is by "knowing" the elements you are "intending" to "pull", and then comparing that to the "original" state of the document before it was modified. The basic MongoDB .findAndModify() method allows this, as do the mongoose wrappers of .findByIdAndUpdate() as well and .findOneAndUpdate().
Basic usage premise:
var removing = [ "MongoId1", "MongoId2" ];
Model.findOneAndUpdate(
query,
{ "$pull": { "items._id": { "$in": removing } } },
{ "new": false },
function(err,doc) {
var removed = doc.items.filter(function(item) {
return removing.indexOf(item) != -1;
});
if ( removed.length > 0 )
console.log(removed);
}
);
Or something along those lines. You basically "turn around" the default mongoose .findOneAndUpdate() ( same for the other methods ) behavior and ask for the "original" document before it was modified. If the elements you asked to "pull" were present in the array then you report them, or inspect / return true or whatever.
So the mongoose methods differ from the reference implementation by returning the "new" document by default. Turn this off, and then you can "compare".
Further notes: "multi" is not a valid option here. The method modifies "one" document by definition. Also you make a statement that the array sub-documents conatain an _id. This is done by "mongoose" by default. But those _id values in the array are "not indexed" unless you specifically define an index on that field. The only default index is the "primary document" _id.

Related

How can I add a special Mongo expression to a JSON object?

So I'm aware how I should add any regular string/object to a JSON object - however, I need to add the following Mongo command to my JSON object:
$set : { "author" : req.body.name }
As you can see, it's not as simple as just doing:
myJsonObject.author = "$set..." etc.
Sorry if this is a dupe question - this isn't the easiest subject to Google without tons of unrelated answers popping up.
The reason I need to know this because I want to be able to build a JSON object based on what the user has changed in the form. So a new problem I have encountered is that even if I can build the JSON object with multiple items to change, MongoDB only updates the last item in the object. My example JSON object is here:
updateItem = {
$set : { "name" : "Squid bonobo" },
$set : { "author" : "Mardy Bum" }
};
and the snippet of code which issues the update command is as follows:
updateData.update(searchItem, updateItem, false, true, function(err, results) {
console.log(results);
db.close();
});
I found this solution at the following git site.
Can anybody help with either of these questions?
Thanks for your help :)
Cameron
You need to form your JSON in the below structure:
updateItem = {
$set : { "name":"Squid bonobo","author":"Mardy Bum"}
}
Javascript Objects can be declared with duplicate keys/properties, but their values get over-written, with the latest bound value for the key, in any order.
Here, $set is a key/property which has been set twice to the same object referenced by the variable updateItem, hence only one value, that is last encountered, will be associated to the key. In this case, the value that takes precedence is:
$set:{ "author":"Mardy Bum" }
and the final query that gets executed becomes,
updateItem = {
$set : { "author" : "Mardy Bum" }
};

Removing the sub-document from the array by id and curent user restriction

I've got a Messages collection with objects like this:
{
"_id": ...,
"author": 123,
"body": "merp",
"comments": [
{
"_id": ...,
"author": 234,
"body": "But what about morp?"
},
{
"_id": ...,
"author": 123,
"body": "You're out of your element, Donnie!"
}
]
}
I'm creating those comment._ids myself when adding them. Anyway, everything works fine, I found out how to add and delete stuff from the comments array etc.
Except now when deleting a comment, I want to check first if the author is the current user. In Meteor, usually you pass the object id to a Meteor-method, which then checks the object and decides whether it's ok to execute. E.g. to delete a message, my method looks like this:
Meteor.methods({
deleteMessage: function(messageId) {
message = Messages.findOne(messageId);
if(!message) {
throw new Meteor.Error("not-found");
}
if(Meteor.userId() != message.author) {
throw new Meteor.Error("not-authorized");
}
Messages.remove(messageId);
}
});
Now how would I do the same for comment objects? I suppose I will have to pass both messageId and commentId, which is not a problem, but then how do I do the permission check? I can't just get a single comment object by _id, can I? At least I've been trying to google this but failed. Should I first retrieve the message object, then manually look through it's comments array to find the one with the correct _id?
So to make this more clear: I know how to use $pull to delete comment objects by _id, what I'm looking for is how do I check whether the object exists in the first place, and that its author is the current user?
Disclaimer: I'm new to Mongo and am fighting the urge to put the comment objects in its own collection, relational-style. From what I've read you're supposed to put everything into one big document. It could well be that I'm missing a crucial point here and in case you see it please point it out to me, even if it does not answer the original question. Thanks! :)
To remove an item from the array then you want to use the $pull operator with .update()
Messages.update(
{
"_id": messageId,
"comments._id": commentId,
"comments.author": Meteor.userId()
},
{ "$pull": { "comments": { "_id": commentId } } }
)
Optionally look for the presence of the "comments._id" as well in the query statement to avoid a match if that comment has already been removed by another request.
To clarify the "method inside the query argument", this is JavaScript, so any function() is evaluated into the object sytax with it's returned value. So before the .update() is executed the Meteor.userId() value is commited with it's return value.
The $pull operator works like a query condition in itself, and will remove any elements matching the conditions from the array. You could also include the same "author" check in there, but it's not really necessary.

MongoDB: incorrect update count

In my database, I have a field called 'fruits' which is a simple array. When inserting elements in this array, I use $addToSet to only insert elements that do not exist already in this array.
I want to know whether or not the element I insert really modified this field. However, the docModified parameter in the callback always returns 1 even when I try to add an element that already exists.
model.update (
{ username: username }, // find the document
{ $addToSet : { fruits: "apple" } }, // add "apple" to fruits field
function (err, docModified) {
console.log(docModified);
// !PROBLEM: this will print "1" no matter what
}
);
Does anyone know why? Thanks a lot! (btw I'm using Mongoose)
The current method implementations in mongoose use the legacy write concern API to determine the count of modified documents. As you note, even if there is no actual change to the content such as an $addToSet operation that does not add a new member to the set, the modified count will be returned.
As long as your MongoDB server version is recent enough ( needs to be MongoDB 2.6 or greater ) and your mongoose version is recent enough an bundles a recent mongodb driver, then you can get the correct numbers from the Bulk Operations API responses.
To do this with mongoose you call the .collection accessor on the model, which returns a native driver collection object:
var bulk = Model.collection.initializeOrderedBulkOp();
bulk.find({ username: username })
.updateOne({ $addToSet : { fruits: "apple" } });
bulk.execute(function(err,result) {
if (err) throw err;
console.log( JSON.stringify( result, undefined, 4 ) );
})
The "result" that is returned is an object conforming to the BulkWriteResult() specification, which more or less will look like this:
{
"writeErrors" : [ ],
"writeConcernErrors" : [ ],
"nInserted" : 2,
"nUpserted" : 0,
"nMatched" : 3,
"nModified" : 3,
"nRemoved" : 1,
"upserted" : [ ]
}
But specifically, where you use $addToSet and the member already exists, the response will contain "nMatched": 1 and "nModified": 0, which is the result you want to see, confirming that nothing was in fact added to the set.
In the MongoDB 2.6 shell, all of the update and insert methods try to use this API where it is available and only fallback to the legacy implementation when connecting to an older server. So If you did this in a modern shell with a modern server you would see the same results.
Hopefully mongoose itself will be altered to also use these methods where available and provide access to the same response. It seems to be the way of the future, but the codebase is yet to catch up and utilize this.
Note: The one thing to be careful of when using any of the native driver methods after accessing the collection object is to make sure that mongoose has an active connection to the database at the time this is called. Mongoose hides this by queuing up any request until a connection is actually established.
So if you are going straight to the collection object methods, then you might want to make sure you are waiting for the "open" event on the connection somewhere before that code executes.
"number affected is the number of docs updated, even if the new values are identical. this comes straight from mongo." I got this from this forum post: https://github.com/LearnBoost/mongoose/issues/867
This means you'll have to come up with a different way to determine if the element was missing from the array before you update. I would recommend pull all the documents and iterating through them before the update. Obviously it's not ideal, but i don't think there's any other way to do it.
Hope this helps.

Saving a modified document (with a modified subdocument array) in Mongoose model

The code I currently have is:
User.findOne(
{
"subUsers.email" : userEmail
},
{
subUsers : {
$elemMatch: {
email : userEmail }
}
},
function(err, user){
if(user){
var information = user.subUsers[0].information.id(id);
information.arrayA.push({someId : "something"});
user.save(callback(err)); // Also tried information.save()
// without luck
}
callback(err);
}
);
This doesn't return any kind of error, but when I check the DB, the new array element hasn't been pushed (the whole document is intact).
Any help would be much appreciated. Thanks!
You should probably check out the first faq, here: http://mongoosejs.com/docs/faq.html
Mongoose doesn't create getters/setters for array indexes; without them mongoose never gets
notified of the change and so doesn't know to persist the new value. The work-around is to
use [MongooseArray set][1] available in Mongoose >= 3.2.0.
So in your case, you want to add this third line
var information = user.subUsers[0].information.id(id);
information.arrayA.push({someId : "something"});
user.subUsers.set(0, information);
Or something like that.
As of today, Mongoose is currently not prepared for multilevel nesting in an atomic way.
Therefore, even if it's going back to a kind-of relational database, in this case it's better to split the nesting into at least 2 collections, and reference using the automatically generated ObjectId.

MongoDB findAndModify() adds query to update clause

I'm creating an application in Node that has some CRUD components. On one of my data objects, I have a save() method that is meant to update a record if the object has an id that is found in the collection, or upsert a new document if not. Additionally, if doing an upsert I'd like to get back the _id for the document generated by Mongo.
It seems that findAndModify would do this, and, indeed, it does return an _id value from the database. In my query clause, I am filtering by _id. If my data object doesn't have an id, Mongo correctly does an upsert, however, no matter what _id value it returns, in addition to the the keys I am setting in the update clause, it also sets the _id on the document based on what value I used in the query clause. Some code for clarity:
User.prototype.save = function(callback) {
var that = this;
var args = {
'query' : { _id: this.getId() }, //getId() returns empty string if not set
'update' : { $set : {
firstName : this.firstName,
lastName : this.lastName,
email : this.email
//_id : this.getId()
// which is blank, is magically getting added due to query clause
}},
'new' : true,
'upsert' : true,
'fields' : {'_id' : true}
};
this.db.collection(dbName).findAndModify(args, function(err, doc){
if(!that.getId()) {
that.setId(doc._id);
}
if (typeof(callback) === "function"){
callback.call(that);
}
});
}
I'm simply looking for the semantics of update that also happens to return a Mongo-generated _id on upsert. I do not want the values of the query clause to additionally be appended as if they were in the update map. Is there any way to achieve what I am getting at?
You can generate the _id client side, with new new require('mongodb').ObjectID()
Then you can just do a regular upsert (no need to do findandmodify) because you already have the _id.
However, if you are using findAndModify, keep in mind that the node driver accepts the arguments to this function positionally, not as an object (like in the regular mongo shell).
The correct format to do findandmodify with the node driver looks like this:
collection.findAndModify(criteria, sort, update[, options, callback])
(options and callback are optional params). Full docs here:
https://github.com/mongodb/node-mongodb-native/blob/master/docs/insert.md

Categories