mongoose 4.0.1: pre "update" middleware this object does not return model object

Created on 30 Mar 2015  ·  40Comments  ·  Source: Automattic/mongoose

Hi, I've a problem with the newest version of Mongoose. I'm creating an API with Express and Mongoose 4.0.1, and I'm not sure if I'm doing anything wrong, but the fact is whenever I try to use the new pre update middleware the same way I use pre save middleware, this object does not return the object being updated, instead it returns the Query object.

Example of what I'm trying to explain:

ExerciseSchema.pre('save', function (next, done) {
        var self = this;
        console.log('exercise:', self); // returns exercise object
        // some validations here
       next();
});
ExerciseSchema.pre('update', function (next, done) {
        var self = this;
        console.log('exercise:', self); // returns Query object instead of exercise object
        // some validations here
       next();
});

This is what I get in this reference object within the middleware, and I don't know how it's useful to me.

{ _mongooseOptions: {},
  mongooseCollection: 
   { collection: { s: [Object] },
     opts: { bufferCommands: true, capped: false },
     name: 'exercises',
     conn: 
     ... },
   ...
}

Looking at the source code, some of its properties are defined inside Query function defined in ./node_modules/mongoose/lib/query.js:

Is this something unexpected or I am doing anything wrong? It would be interesting to have a solution because I don't like the idea of validating within a middleware on object saving and being forced to run validations directly on controller when updating.

The same thing happens with findOneAndUpdate pre middleware, but I don't expect it to return the exercise before finding it. In fact and in my honest opinion, I think it would be interesting that findAndUpdate and findOneAndUpdate pre middlewares triggered find(One) and update middlewares, in this order, instead of having its own middleware.

Thanks in advance for your help.

won't fix

Most helpful comment

Ah ok I see you're using findOneAndUpdate() rather than update(). These are two distinct functions with distinct hooks, so if you want to add update ops to findOneAndUpdate in a pre hook, you should do

schema.pre('findOneAndUpdate', function() {
  this.findOneAndUpdate({}, { $set: { key: 'value' } });
});

All 40 comments

By design - the document being updated might not even be in the server's memory. In order to do that, mongoose would have to do a findOne() to load the document before doing the update(), which is not acceptable.

The design is to enable you to manipulate the query object by adding or removing filters, update params, options, etc. For instance, automatically calling .populate() with find() and findOne(), setting the multi: true option by default on certain models, access control, and other possibilities.

findOneAndUpdate() is a bit of a misnomer, it uses the underlying mongodb findAndModify command, it's not the same as findOne() + update(). As a separate operation, it should have its own middleware.

How would you suggest updating a document field during 'pre' update middleware then?

this.update({ field: val }); will add { $set: { field: val } } to the update operation before it happens.

Thanks for replying, on which version is that supposed to work? because I'm using 4.0.2 and it doesn't. Do I have to call "exec" on the query?

if I run this.update wouldn't I be calling this method http://mongoosejs.com/docs/api.html#query_Query-update?

I ended up doing this.findOneAndUpdate({matcher: "myvalue"}); hope that's the right approach.

Hmm 4.0.2 should work. What does your code look like?

I just did something like this:

var schema = mongoose.Schema({
    name: String,
    description: String,
    matcher: String,

});

var generateMatcherUpdate= function(next) {
     var matcher = "generate matcher function"
        this.update({matcher: matcher});

    next();
};

 schema.pre('update', generateMatcherUpdate);

Ah ok I see you're using findOneAndUpdate() rather than update(). These are two distinct functions with distinct hooks, so if you want to add update ops to findOneAndUpdate in a pre hook, you should do

schema.pre('findOneAndUpdate', function() {
  this.findOneAndUpdate({}, { $set: { key: 'value' } });
});

man mongoose documentation is hatefully bad. Can someone please put this in there?

@askdesigners put what exactly in there?

Sorry I wrote that in a moment of stress :)

Just the brittleness of version compatibility really. It's not an obvious issue, and leaves a lot of people scratching their heads.

No worries. What sort of version compatibility issues are you having?

And what if i want to sanitize a value on pre.update...I cant seem to find a way to do it.
Thanks!

schema.pre('update', function() {
  var v = this.getUpdate().valueToSanitize;
  this.update({}, { $set: { valueToSanitize: sanitize(v) } });
});

Nice, I will try that out!
Thanks!!

Hey @vkarpov15, I've come across something I would say is kind of unexpected behaviour, or at least it should be documented.

If I do something like

MyModel.findOneAndUpdate({a: 1}, {v: '__3'}, function (err, model) {
    console.log(model);
});

And on my model I implement your suggestion

mySchema.pre('findOneAndUpdate', function() {
  var v = this.getUpdate().v;
  this.findOneAndUpdate({}, { $set: { v: sanitize(v) } });
});

The log will output '__3' since the original update query will be executed after the hook. I'm able to change other schema properties but all the fields which are updated on the original query will remain the same even if they're changed in the pre hook.

@CMatias this is a case where you need to be careful about using $set or not consistently. Mongoose doesn't necessarily know whether or not to wrap in $set, so in your case you should do this.findOneAndUpdate({}, { v: sanitize(v) });

@vkarpov15 thanks, that worked. Could you briefly explain what's the difference between using $set and not using it? Can't find much information about it.

So if you use the native mongodb driver directly and do coll.findOneAndUpdate({ a: 1 }, { __v: 3 }), mongodb will take the first document with a = 1 and replace it with the document { __v: 3 } modulo _id. In other words, it will overwrite the existing document. In order to just set the key '__v', you need to do coll.findOneAndUpdate({ a: 1 }, { $set: { __v: 3 } }).

This behavior has always been controversial and error-prone, so by default mongoose prevents you from overwriting the document. In other words, MyModel.findOneAndUpdate({ a: 1 }, { __v: 3 }) becomes MyModel.collection.findOneAndUpdate({ a: 1 }, { $set: { __v: 3 } }) _unless_ you set the overwrite: true option. However, you can do strange things like MyModel.update({}, { $set: { b: 2 } }).findOneAndUpdate({ a: 1 }, { __v: 3 }, { overwrite: true }) that make it quite confusing as to what the current state of getUpdate() is, so we just leave the status of update as-is for middleware.

I'm trying to grok all of this but am having a hard time determining if I can modify the doc that will be returned from a find using these middleware methods. Specifically, I'd like to "decorate" the response by adding additional fields.

For instance, if I have a Schema:

const Thing = new mongoose.Schema({
  name: {type: String}
});

Thing.post('find', function(doc, next){
  doc.newfield = "example text";
  next();
}

Is this possible?

The reason newfield isn't defined in the schema is that depending on the values of Thing, different field names are needed.

Thanks!

i need any specific plugin to use schema.pre('update') ?

Nope @jrogatis

@vkarpov15

Looking at the example you gave above ...:

schema.pre('update', function() {
  var v = this.getUpdate().valueToSanitize;
  this.update({}, { $set: { valueToSanitize: sanitize(v) } });
});

This does not work for me. The this.getUpdate().value does not contain my property value as the example suggests. Instead it's available under this.getUpdate().$set.property.

Is it supposed to be like that? Am I understanding something wrong? Did the API change? Is there any documentation about this?

@qqilihq can you open a separate issue with a repro script?

@qqilihq nvm, just saw that you did :P

@varunjayaraman Actually the separate issue I opened is a different one :) But I'll gladly open a new one for the comment above, once I have some spare minutes to compile an example.

@qqilihq ah ok, thanks !

Exactly the same for me as mentioned by @qqilihq

image

So desired behavior could be achieved with this code:

userSchema.pre('update', function (next) {
  const newEmail = this.getUpdate().$set.email
  if (newEmail) {
    this.update({}, {$set: {email: normalizeEmail(newEmail)}})
  }
  next()
})

But it looks like I'm trying to use some fields that are not intended for this.

pre method should not work with this.update, because it is "pre"-save and there is nothing to update yet, while this.newProp = "value" works perfectly, thanks guys.

My case is:

UserSchema.pre('save', function(next) {
    this.created = Date.now()
    next()
})

I understand the reasoning behind having the query as "this" on the "pre" update hook but why is the "post" update hook behaving in a similar manner ? Shouldn't post update have the actual model as "this" ?

So then post find would execute multiple times or have this as an array of there are multiple results? And what if there are no results, should post find not run?

This is all very strange. IMHO The pre hooks should all just behave the same, with a this that is the object being modified and nice methods like isModified to check things.

Actually I think I understand now. I should just be using the pre 'save' method and then the this.isNew method. Thanks!

@ajbraus it is a little strange, but the alternative is massive performance overhead. Suppose you have an updateMany that updates millions of docs - mongoose would have to load those all into memory.

How is one supposed to check for document fields before updates ? Say I need to check whether certain fields are filled to set document as "valid", how can I access the whole document, and not just fields being updated ? this is referring to the query here

@PDS42 you'll have to run a separate query to load the document from mongodb

Is there a particular link in the mongoose documentation where all of this is explained in more depth? I'd love to read up on it and understand this more fully :).

EDIT:

pre update hooks: https://github.com/Automattic/mongoose/issues/2812
middleware: https://mongoosejs.com/docs/middleware.html
query object: https://mongoosejs.com/docs/api.html#Query
queries: https://mongoosejs.com/docs/queries.html

I am having trouble finding documentation on how exactly one should use the queries referenced by 'this' in the update middleware. For example, what happens when:
this.update({field: "value"}, {$set: {updatedAt: new Date()}}); Does this add {field: "value"} to the filter? Or does it have no effect? Is there documentation I'm not seeing?

@ronakvora there's a Query#getConditions() function.

schema.pre('update', function() {
  console.log(this.getConditions()); // Will include `{ field: 'value' }`
});

Gotcha, that will help me play with things :). Thanks!

Ps this is Ronak from above. Apparently I have two accounts logged in on different devices.

Was this page helpful?
0 / 5 - 0 ratings