Mongoose: pre, post middleware are not executed on findByIdAndUpdate

Created on 15 Jun 2012  ·  102Comments  ·  Source: Automattic/mongoose

Because a findAndUpdate method is presented to cut down the following code:

 Model.findById(_id, function (err, doc) {
      if (doc) {
          doc.field = 'value';
          doc.save(function (err) {
                 // do something;
          });
      }
 });

to this:

   .findByIdAndUpdate(_id, {$set: {field: 'value'}}, function (err, doc) {
        // do something 
    });

We need to use pre, post middleware exactly the same. At now pre, post middleware are not executed when I make findByIdAndUpdate.

Most helpful comment

Hello,

I know the issue is closed, but I am not entirely satisfied with the answer. Shouldn't there be at least a post-save or post-update middleware for findOneAndUpdate and other similar operations ? Is seems ok since the document is returned. Not feasible for pre middlewares or for Model.update, i agree.

It would greatly improve the capabilities of plugins such as mongoosastic that is currently blind to some operations it should be able to support.

If no middleware, does someone have an idea on how to manage some post update operations in a plugin ?

Thanks

All 102 comments

by design. there are docs involved to call hooks on.

correction, there are no docs to call hooks on.

So? If I want to call pre, post middleware I need to use the first approach?

yes that is correct. Model.update,findByIdAndUpdate,findOneAndUpdate,findOneAndRemove,findByIdAndRemove are all commands executed directly in the database.

This should definitely be clarified in the guide, especially if you're talking about validation in that same page and describing findByIdAndUpdate as "better"

if you click on the link "better" it takes you to the full documentation of
the method where it explains validation etc.

feel free to send a pull request to add something you feel is better. its
pretty easy to do so:
https://github.com/LearnBoost/mongoose/blob/master/CONTRIBUTING.md

On Wed, Oct 31, 2012 at 6:03 PM, Jesse Fulton [email protected]:

This should definitely be clarified in the guidehttp://mongoosejs.com/docs/documents.html,
especially if you're talking about validation in that same page and
describing findByIdAndUpdate as "better"


Reply to this email directly or view it on GitHubhttps://github.com/LearnBoost/mongoose/issues/964#issuecomment-9967865.

Aaron
@aaronheckmann https://twitter.com/#!/aaronheckmann

Added notes to the middleware doc

Pull request: https://github.com/LearnBoost/mongoose/pull/1750

Hello,

I know the issue is closed, but I am not entirely satisfied with the answer. Shouldn't there be at least a post-save or post-update middleware for findOneAndUpdate and other similar operations ? Is seems ok since the document is returned. Not feasible for pre middlewares or for Model.update, i agree.

It would greatly improve the capabilities of plugins such as mongoosastic that is currently blind to some operations it should be able to support.

If no middleware, does someone have an idea on how to manage some post update operations in a plugin ?

Thanks

@albanm certain methods bypass mongoose completely, so you don't get the middleware hooks. AFAIK, the only way to get the hooks to execute is to use separate find() and save() calls as mentioned above.

I get that and it makes sense. Pre middlewares are out of question as there is no fetching prior to certain methods. But still, mongoose should be able to wrap the update operations that also return the updated documents and trigger some post hooks.

I think @albanm is right, People may want the same functionality when they use the same function. How about wrap those 'directly update' methods with some interception of check that if there's any hooks exist? If hook exist, use it, or call original update methods otherwise.

+1

+1

+1

+1

:+1:

+1

:-1:
I get the feature request, but the longer I work with middleware, I find myself needing to bypass middleware often when calling db update scripts and other items that are not routine to my normal operations. By using static methods it becomes very easy to create a custom wrapper that implements the above feature requests already. Since mongoose is now opening to ideas for version 4, it is unlikely that any API change of this magnitude will happen in v3. I request that this be moved to v4 discussion.

I would however, :+1: if I was able to disable middleware on a save call. I often find myself with a mongoose object that was passed from another function and simply want to perform a .save - in this situation, doing a .save is preferable to writing a new query. If this is possible, please point it out

Either way, amazing library. Kudos to awesome maintainers. Not sure how I would operate without it.

+1

A simple way to add hooks for these method is overwrite the embed functions:

_update = UserModel.update
UserModel.update = (conditions, update) ->
  update.updatedAt or= new Date
  _update.apply this, arguments

Then every update call of mongoose will fix the updatedAt key of data.

You may give a try of this model limbo. It is a simple wrapper of mongoose model, supporting bind static/method/overwrite to all the schemas, and call rpc methods to query the mongodb.

+1, need this feature...

While I sort of understand the reasoning, the lack of hooks on atomic updates IMHO makes Mongoose somewhat pointless in practice. When I use atomic updates any validations, defaults, etc. are not executed, so the entire purpose of using an ODM is defeated. Using find/save will do the job, but is there any guarantee this is always used?

Moreover, usually I would try to avoid find/save since it's not an atomic operation. MongoDB compensates it's lack of transaction support by providing powerful atomic query & update features. So I would use these atomic operations but w/o middleware support Mongoose won't provide much value over the native MongoClient.

Even the examples in http://aaronheckmann.tumblr.com/post/48943525537/mongoose-v3-part-1-versioning would use update, hence bypass middleware. I can use versioning properly or middleware but not combine both? Really, where's the point in having it?

I don't even fully understand the technical reasons: If update & co. wrap around the database operations, why can't we intercept the call and pass the query objects so we can do some validation/customization before we actually do the update?

@joerx +1 would suffice.. :) but your reasoning is flawless.

The 3.9.x branch has support for pre and post hooks for find and findOne - should be easy to add support for findOneAndUpdate and update.

Is this feature merged ?

So the pre('findOneAndUpdate') and post('findOneAndUpdate') hooks are in master, there isn't an update hook just yet. There's no release that contains either of those yet.

So does it triggers pre-save after .update() now?

No. There is a separate update() hook for Query.update(). Save hooks are distinct from query hooks.

@vkarpov15 Can you please link to the supporting documentation for the update hook? Any word on when pre('findOneAndUpdate') and post('findOneAndUpdate') will hit a release?

@karlstanton use 4.0.0-rc2, npm install mongoose@unstable :)

'use strict';

var Promise  = require('bluebird');
var mongoose = Promise.promisifyAll(require('mongoose'));

var counterSchema = new mongoose.Schema({
    total: {
        type:    Number,
        default: 0
    }
});

counterSchema.post('findOneAndUpdate', function (doc) {
    console.log(doc.total);
});

var Counter = mongoose.model('Counter', counterSchema);

Promise.coroutine(function *() {
    yield mongoose.connectAsync(process.env.MONGODB_URI);
    console.log('Connected');
    let counter = yield Counter.createAsync({});
    console.log(`${counter.total}`);
    for (let i = 0; i < 10; i++) {
        yield Counter.findOneAndUpdateAsync({ _id: counter.id }, { $inc: { total: 1} });
    }
})();
0
0
1
2
3
4
5
6
7
8
9

Seems like it's a step behind. What am I missing?

AH, the new flag default breaking change. While there's something to say about consistency with the underlying driver, I have to say it's really counter-intuitive, especially when considering this new hook.

@neverfox :)

Yeah @neverfox good catch. This change is documented in the release notes and you can see more discussion why this was changed in #2262.

The reason is that new is false by default in the mongodb node driver, the mongodb shell, the actual mongodb server, etc. and wrapper layers setting non-standard defaults makes life difficult for devs. My canonical example of a module that gets this horribly wrong is gulp-uglify, which overrides a bunch of uglify-js's defaults.

I see that the issue is closed, but is the functionality in place in version 4.0.2 out is it just still in the unstable version? It doesn't seem to execute on findOneAndUpdate with Scema.pre('update') or Schema.pre('findOneAndUpdate') with the 4.0.2 version. Am I missing something that I have to pass to the function?

How are you declaring the pre hook @CaptainStaplerz ?

Is findOneAndUpdate middleware hook available in 4.0.2? I upgraded from 3.8 to latest mongoose 4.0.2 to use this and the middlware Document.schema.post('findOneAndUpdate', function (doc) is not getting triggered like save() or remove() does

@honitus show me your code

@vkarpov15 - Thanks for quick response, here you go

blog.controller,js

// Updates an existing Blog in the DB, adds comment
exports.update = function(req, res) {

Blog.findOneAndUpdate(
     {"_id": req.body._id,},
      {"$push": {"comments": buildComment}},
     {safe: true, upsert: true}, function (err, workspace) {
      if (err) {
         return handleError(res, err);
       }
       return res.send(200);
      }
   );

}

blog.socket.js

/**
 * Broadcast updates to client when the model changes
 */

'use strict';

var Blog= require('./blog.model');

exports.register = function(socket) {
//SAVE WORKS
  Blog.schema.post('save', function (doc) {
    onSave(socket, doc);
  });

// IS NOT TRIGGERED :(
 Blog.schema.post('findOneAndUpdate', function (doc) {
    onComment(socket, doc);
  });

  Blog.schema.post('remove', function (doc) {
    onRemove(socket, doc);
  });
}

//SAVE WORKS when a new blog is created
function onSave(socket, doc, cb) {
  socket.emit('blog:save', doc);
}

// IS NOT TRIGGERED :(
function onComment(socket, doc, cb) {
  socket.emit('blog:findOneAndUpdate', doc);
}

function onRemove(socket, doc, cb) {
  socket.emit('blog:remove', doc);
}

@vkarpov15 Thanks for reopening. Does this mean hook for findOneAndUpdate isn't there in 4.0.2 and you plan to include in 4.0.3.

@vkarpov15 Here's the code where I declare the hooks:

...
var TodoSchema = new mongoose.Schema({
  name: {type: String, required: true},
  note: String,
  completed: {type: Boolean, default: false},
  updatedAt: {type: Date, default: Date.now},
  user: {
    type: mongoose.Schema.ObjectId,
    ref: 'Users'
  }
});
...
// Not executed
TodoSchema.pre('update', function() {
  console.log('------------->>>>>> update updatedAt')
  this.updatedAt = Date.now();
});
// Not executed
TodoSchema.pre('findOneAndUpdate', function() {
  console.log('------------->>>>>> update updatedAt')
  this.updatedAt = Date.now();
});

And here's where I call the update:

...
router.route('/:id')
.put(function(req, res, next) {
  TodoModel.findOneAndUpdate({_id: req.params.id, user: req.user.id}, req.body, {new: true}, function(err, post) {
    if(err) return next(err);
    if(post) {
      res.status(200).json(post);
    }
    else {
      next(newSystemError(errorCodes.TODO_NOT_FOUND, undefined, req.params.id));
    }
  });
});

As far as I know the findOneAndUpdate() hook should be in there, if it's not working that's a bug

@CaptainStaplerz try:

TodoSchema.pre('update', function(next) {
  console.log('------------->>>>>> update updatedAt')
  this.updatedAt = Date.now();
  next();
});

Also, is the console.log statement executing, or is it just the Date.now() part that's giving unexpected results?

@vkarpov15
I changed my source code, adding the changes you made in Add implementation #964 https://github.com/Automattic/mongoose/commit/e98ef98e857965c4b2ae3339fdd7eefd2a5a9913

It works like charm now. So I think the fix is not checked into main

@honitus are you sure you're using mongoose 4.0.2? That change is in fact committed to 4.0.0 and up.

honitus$ npm view mongoose version
4.0.2

@honitus what you're doing incorrectly is that you're adding hooks to the schema after compiling your model. Model.schema.pre('remove'); is not expected to work, see "Compiling your first model" on the model docs. Attach the hooks to the schema first and then things should work - that's the only difference I see between your code and our tests.

@CaptainStaplerz the only way I can find to repro your code is with an empty update. The below code works

var mongoose = require('mongoose');
mongoose.set('debug', true);
var util = require('util');

mongoose.connect('mongodb://localhost:27017/gh964');

var TodoSchema = new mongoose.Schema({
  name: {type: String, required: true},
  note: String,
  completed: {type: Boolean, default: false},
  updatedAt: {type: Date, default: Date.now},
  user: {
    type: mongoose.Schema.ObjectId,
    ref: 'Users'
  }
});
TodoSchema.pre('update', function() {
  console.log('------------->>>>>> update updatedAt')
  this.updatedAt = Date.now();
});
TodoSchema.pre('findOneAndUpdate', function() {
  console.log('------------->>>>>> update updatedAt')
  this.updatedAt = Date.now();
});

var Todo = mongoose.model('Todo', TodoSchema);

Todo.update({}, { note: "1" }, function(err) {
  if (err) {
    console.log(err);
  }
  console.log('Done');
  process.exit(0);
});

But once you make the update empty:

var mongoose = require('mongoose');
mongoose.set('debug', true);
var util = require('util');

mongoose.connect('mongodb://localhost:27017/gh964');

var TodoSchema = new mongoose.Schema({
  name: {type: String, required: true},
  note: String,
  completed: {type: Boolean, default: false},
  updatedAt: {type: Date, default: Date.now},
  user: {
    type: mongoose.Schema.ObjectId,
    ref: 'Users'
  }
});
TodoSchema.pre('update', function() {
  console.log('------------->>>>>> update updatedAt')
  this.updatedAt = Date.now();
});
TodoSchema.pre('findOneAndUpdate', function() {
  console.log('------------->>>>>> update updatedAt')
  this.updatedAt = Date.now();
});

var Todo = mongoose.model('Todo', TodoSchema);

Todo.update({}, { }, function(err) {
  if (err) {
    console.log(err);
  }
  console.log('Done');
  process.exit(0);
});

The pre update hook no longer executes. Is this consistent with what you're seeing?

@vkarpov15
Model.schema.pre('remove'); is one the stubs automatically created by angular full stack
I added implementation #964 e98ef98
All I did is add one line below
this._schema.s_.hooks.wrap('findOneAndUpdate', Query.base.findOneAndUpdate,
See Line 1526 in 4.0.2 - this.model.hooks.wrap('findOneAndUpdate', Query.base.findOneAndUpdate,

Replace Line 1526 with- this.schema.s.hooks.wrap('findOneAndUpdate', Query.base.findOneAndUpdate,
and it works.

That's by design. Mongoose is not supposed to allow modifying the schema hooks after the model has been compiled (that is, the mongoose.model() call), which is why that no longer works.

@vkarpov15 I managed to get the hook working now with the following, it must have been a stupid typo or something else:

TodoSchema.pre('findOneAndUpdate', function(next) {
  console.log('------------->>>>>> update updatedAt: ', this.updatedAt);
  this.updatedAt = Date.now();
  next();
});

However 'this' doesn't seem to refer to the updated model (but it does in the 'save' hook?), thereby this.updatedAt refers to undefined.

How do I update the 'updatedAt' -field in the 'findOneAndUpdate' -hook?

I should add more docs clarifying this - the document that's being updated may not exist in memory when you call findOneAndUpdate, so the this object refers to the query rather than the document in query middleware. Try this.update({ $set: { updatedAt: Date.now() } });

@vkarpov15 I could not get this.update({ $set: { updatedAt: Date.now() } }); to work, but I was able to successfully update the doc on the findOneAndUpdate hook with this: this._update['$setOnInsert'].updatedAt=Date.now();.

I would highly recommend not depending on tweaking internal state like that. I'm pretty surprised that you couldn't get this.update() to work - can you show me what your hook looks like?

Sure! (Note that I'm using 4.0.2)

tagSchema.pre('findOneAndUpdate',function(next){
  var self = this;

  //NOTE THAT 'this' in the findOneAndUpdate hook refers to the query, not the document
  //https://github.com/Automattic/mongoose/issues/964

  geoData.country.findOne({'_id':self._update['$setOnInsert'].countryCode}).select('_id name cca2 cca3 ccn3').lean().exec(function(err,country){
    if (err){throw err;}
    if (!country){throw 'no coutnry';}
    self._update['$setOnInsert'].country=country;
    next();
  });
});

I could obviously handle this when I initialize the document elsewhere in my app, but it's nice to have it all contained right there in Mongoose. Welcome any thoughts!

Yeah I was confused and thought you were using update. The below script

var mongoose = require('mongoose');
mongoose.set('debug', true);
var util = require('util');

mongoose.connect('mongodb://localhost:27017/gh964');

var TodoSchema = new mongoose.Schema({
  name: {type: String, required: true},
  note: String,
  completed: {type: Boolean, default: false},
  updatedAt: {type: Date, default: Date.now},
  user: {
    type: mongoose.Schema.ObjectId,
    ref: 'Users'
  }
});
TodoSchema.pre('findOneAndUpdate', function() {
  this.findOneAndUpdate({}, { updatedAt: Date.now() });
});

var Todo = mongoose.model('Todo', TodoSchema);

Todo.findOneAndUpdate({}, { note: "1" }, function(err) {
  if (err) {
    console.log(err);
  }
  console.log('Done');
  process.exit(0);
});

Works correctly and executes the desired query:

Mongoose: todos.findAndModify({}) [] { '$set': { note: '1', updatedAt: new Date("Thu, 07 May 2015 20:36:39 GMT") } } { new: false, upsert: false }
Done

So please use

TodoSchema.pre('findOneAndUpdate', function() {
  this.findOneAndUpdate({}, { updatedAt: Date.now() });
});

Instead of manually manipulating mquery's internal state - that's usually a bad idea unless you really really know what you're doing.

Is this working or not ? Only hook that works for me is 'save', rest is completely ignored. I'm running on 4.0.6. Thanks

@agjs provide code example please.

UserController.prototype.updateAvatar = function (req, res) {
    return new Promise(function (resolve, reject) {
        CompanyDetails.update({
            _author: req.user._id
        }, {
            avatarPath: req.files.file
        }, function (error, updated) {
            if (error) {
                reject(error);
            } else {
                resolve(updated);
            }
        });
    }).then(function (resolved) {
        res.sendStatus(204).send(updated);
    }).catch(function (error) {
        next(error);
    })

};

CompanyAvatarSchema.pre('update', function (next) {
    console.log('pre save');
    let VirtualModel = this,
        parent = this.ownerDocument(),
        PATH = path.normalize('./public/images/uploads/avatars/' + parent._id);

    mkdirp(PATH, function (error) {
        if (error) {
            next(error);
        } else {
            fs.rename(VirtualModel.path, path.join(PATH, VirtualModel.name), function (error2) {
                if (error2) {
                    next(error2);
                } else {
                    next();
                }
            });
        }
    });

});

I have another pre-hook on another model with model.create and pre-save and it works normally.

With update whatsoever, It simply doesn't see the hook, doesn't even console.log it. I tried findOneAndUpdate too etc, doesn't really work... Funny thing I've went through this entire thread and as I usually do, checked official documentation and you guys claim even there that it works.

What's the relationship between CompanyAvatarSchema and CompanyDetails in the above code?

Company details have CompanyAvatar.schema as a subdocument

avatarPath: {
        type: [CompanyAvatar.schema],
        required: true
    }

Also, its not only pre-hook but validation gets ignored completely as well. This subdocument gets populated but ignores both, validation and pre-hook. I've googled everything, also tried THIS but nothing seams to work. When I just for test change my query to create and invoke model with new aka var parent = new Parent(), it does work.

You're calling CompanyDetails.update() but the pre hook is defined on a separate schema. Query middleware doesn't fire the nested schema's pre('update') atm.

Also, please provide a more thorough code example for your 'validation gets ignored completely' case as well.

Here is my company avatar schema made for validation and pre-hooks for users who are updating their profile (avatar photo):

'use strict';

let mongoose = require('mongoose'),
    mkdirp = require('mkdirp'),
    fs = require('fs'),
    path = require('path'),
    Schema = mongoose.Schema;

let CompanyAvatarSchema = new Schema({
    name: String,
    width: Number,
    height: Number,
    size: Number,
    type: String
});


CompanyAvatarSchema.path('type').validate(function (type) {
    return /^image\//.test(type);
}, 'Image type not allowed!');

CompanyAvatarSchema.path('size').validate(function (size) {
    return size < 5;
}, 'Image too big!');



CompanyAvatarSchema.virtual('path').set(function (path) {
    return this._path = path;
}).get(function () {
    return this._path;
});


CompanyAvatarSchema.virtual('public_path').get(function () {
    var parent = this.ownerDocument();
    var PATH = path.normalize('images/uploads/avatars/' + parent._id);
    if (this.name) {
        return path.join(PATH, this.name);
    }
});

CompanyAvatarSchema.set('toJSON', {
    getters: true
});

CompanyAvatarSchema.pre('findOneAndUpdate', function (next) {
    console.log('pre save');
    let VirtualModel = this,
        parent = this.ownerDocument(),
        PATH = path.normalize('./public/images/uploads/avatars/' + parent._id);

    mkdirp(PATH, function (error) {
        if (error) {
            next(error);
        } else {
            fs.rename(VirtualModel.path, path.join(PATH, VirtualModel.name), function (error2) {
                if (error2) {
                    next(error2);
                } else {
                    next();
                }
            });
        }
    });

});


let runValidatorsPlugin = function (schema, options) {
    schema.pre('findOneAndUpdate', function (next) {
        this.options.runValidators = true;
        next();
    });
};

CompanyAvatarSchema.plugin(runValidatorsPlugin);

let CompanyAvatar = mongoose.model('CompanyAvatar', CompanyAvatarSchema);
module.exports = CompanyAvatar;

Here is the company_details schema where company_avatar is a subdocument :

let CompanyDetailsSchema = new mongoose.Schema({
    _author: [{
        type: Schema.Types.ObjectId,
        ref: 'CompanyAccount'
    }],
    company_name: {
        type: String,
        es_indexed: true,
        es_boost: 2.0
    },
    contact_email: {
        type: String,
        es_indexed: true
    },
    website: {
        type: String,
        es_indexed: true
    },
    country: {
        type: String,
        es_indexed: true
    },
    industry: {
        type: String,
        es_indexed: true
    },
    address: {
        type: String,
        es_indexed: true
    },
    about: {
        type: String,
        es_indexed: true
    },
    avatarPath: {
        type: [CompanyAvatar.schema],

    }
});

And here is update profile controller and the avatarPath which should be validated / hooked before this update performs:

UserController.prototype.updateAvatar = function (req, res, next) {
    let updates = {
        $set: {
            avatarPath: req.files.file
        }
    };
    return new Promise(function (resolve, reject) {
        CompanyDetails.findOneAndUpdate({
            _author: req.user._id
        }, updates, function (error) {
            if (error) {
                reject(error);
            } else {
                resolve('done');
            }
        });
    }).then(function () {
        res.sendStatus(204);
    }).catch(function (error) {
        next(error);
    });

};

Basically, my mongodb gets populated with the fields from req.files.file but other then that, validation gets ignored and no hooks are working.

The issue is that the pre('findOneAndUpdate') middleware is defined on a nested schema. Right now, mongoose only fires query middleware for top level schemas, so middlewares defined on CompanyDetailsSchema will get fired re: #3125

+1

@vkarpov15 I get that

TodoSchema.pre('findOneAndUpdate', function() {
  this.findOneAndUpdate({}, { updatedAt: Date.now() });
});

works to set a property to a hard value, but how would I _read and change_ a property, e.g. hash a password?

TodoSchema.pre('findOneAndUpdate', function() {
  this.findOneAndUpdate({}, { password: hashPassword(.....?....) });
});

Any thoughts? This is a pretty common usecase, right? Or do people usually find() and then separate save() ?

TodoSchema.pre('findOneAndUpdate', function() {
  this.findOneAndUpdate({}, { password: hashPassword(this.getUpdate().$set.password) });
});

That should work @willemmulder.

@vkarpov15 perfect, thanks! I'll be trying that tonight. That should also work for pre('update') right?

@vkarpov15 So I just tried it using

schema.pre('update', function(next) {
this.update({}, { $set : { password: bcrypt.hashSync(this.getUpdate().$set.password) } });
next();
});

and if I console.log this I do get

_update: { '$set': { password: '$2a$10$CjLYwXFtx0I94Ij0SImk0O32cyQwsShKnWh1248BpYsJLIHh7jb66', postalAddress: [Object], permissions: [Object], firstName: 'Willem', lastName: 'Mulder', email: '...@...', _id: 55ed4e8b6de4ff183c1f98e8 } },

which seems fine (and I even tried setting that property directly before) but in the end, it doesn't actually write the hashed value to the database, but simply the 'raw' value. Is there anything I could try?

That's strange. You can try enabling mongoose debug mode with require('mongoose').set('debug', true); and see what the query that's getting sent to the db is, that could shed some light.

Thanks for the suggestion. Just did that:

I run this:

schema.pre('update', function(next) {
    this.update({}, { password: bcrypt.hashSync(this.getUpdate().$set.password) } );
    console.log(this.getUpdate());
    next();
});

which returns this for the console.log

{ '$set':
   { password: '$2a$10$I1oXet30Cl5RUcVMxm3GEOeTFOLFmPWaQvXbr6Z5368zbfpA8nFEK',
     postalAddress: { street: '', houseNumber: '', zipCode: '', city: '', country: '' },
     permissions: [ '' ],
     __v: 0,
     lastName: '',
     firstName: '',
     email: '[email protected]',
     _id: 563b0410bd07ce2030eda26d } }

and then this for the Mongoose debug

Mongoose: users.update({ _id: ObjectId("563b0410bd07ce2030eda26d") }) { '$set': { password: 'test', postalAddress: { street: '', houseNumber: '', zipCode: '', city: '', country: '' }, permissions: [ '\u001b[32m\'\'\u001b[39m' ], __v: 0, lastName: '', firstName: '', email: '[email protected]', _id: ObjectId("563b0410bd07ce2030eda26d") } } { overwrite: false, strict: true, multi: false, upsert: false, safe: true }
Mongoose: users.findOne({ _id: ObjectId("563b0410bd07ce2030eda26d") }) { fields: { password: 0 } }

Any clue?

Not sure, opened up a new issue to track.

@vkarpov15 Thanks, will track the other issue.

@vkarpov15 I think the right way to set options for the ongoing query in pre hook would be something like:

  finishSchema.pre('findOneAndUpdate', function (next) {
    this.setOptions({
      new: true,
      runValidators: true
    });
    this.update({}, {
      lastEdited: Date.now()
    });
    next();
  });

but the documentation, http://mongoosejs.com/docs/api.html#query_Query-setOptions doesn't mention any of those options. If this considered to be a hackish solutions, what would be more appropriate?

That's an issue with the docs, that code you describe looks like it should work at first glance

Can you open up a separate issue for that?

@vkarpov15 Yes, it does work. I think I wasn't clear enough.

setOptions applies new and runValidators correctly, I was merely asking if setting those options through setOptions should be preferred over this.options.

setOptions() is preferable IMO, but both should work. Or you could just do

this.update({}, { lastEdited: Date.now() }, { new: true, runValidators: true });
schema.pre('update', function(next) {
this.update({}, { $set : { password: bcrypt.hashSync(this.getUpdate().$set.password) } });
next();
});

This will update password on every update() call. So if i just change the value of other properties, i.e. name or age, then password will be updated also which is not correct!?

@nlonguit I guess it will. But you can access through this the fields that are going to be updated and you could do something like:

if (this._fields.password) { // <- I'm sure about this one, check in debugger the properties of this 
    this.update({}, { $set : { password: bcrypt.hashSync(this.getUpdate().$set.password) } });
}

if (this._update.$set.password) { this.update({}, { $set: { password: bcrypt.hashSync(this.getUpdate().$set.password)} }); }

This code is working well for me. Thanks @akoskm

I wonder if it would be possible to add a pre hook for findByIdAndUpdate as well. Would be nice to have both hooks available.

I did it this way and it is working: Just findById then save without updating any fields then use the findByIdAndUpdate method:

dbModel.findById(barId, function (err, bar) {
        if (bar) {

            bar.save(function (err) {
                if (err) throw err;
            });
        }
    });
    dbModel.findByIdAndUpdate(barId, {$set:req.body}, function (err, bar) {
        if (err) throw err;
        res.send('Updated');
    });`

I'm trying to set a property to have the length of an array.

schema.post('findOneAndUpdate', function(result) {
    console.log(result.comments.length);
    this.findOneAndUpdate({}, { totalNumberOfComments: result.comments.length });
});

The correct length is logged, although the query never sets totalNumberOfComments, and the field stays at 0 (since the schema references default: 0).

When I console.log(this) at the end of the hook, I can see that my query contains the following:

_update: { '$push': { comments: [Object] }, totalNumberOfComments: 27 }

Although when I turn on debug mode, a query is never logged by Mongoose.

Is there something I'm doing wrong, or is this a bug?

@zilions this.findOneAndUpdate({}, { totalNumberOfComments: result.comments.length }).exec(); need to actually execute the query :) Just be careful, you're gonna get an infinite recursion there because your post save hook will trigger another post save hook

@vkarpov15 Ahhhh right! Then I can just use this.update({} {....}).exec() instead :)
Question though, when using this, it sets the totalNumberOfComments field perfectly, although performs the original update of the findOneAndUpdate too.

For example:

Post.findOneAndUpdate({_id: fj394hri3hfj}, {$push: {comments: myNewComment}})

Will trigger the following hook:

schema.post('findOneAndUpdate', function(result) {
    this.update({}, {
        totalNumberOfComments: result.comments.length
    }).exec();
}));

Although, the hook will $push to comments the myNewComment again, therefore making a duplicate entry.

Because you're technically executing the same query -

schema.post('findOneAndUpdate', function(result) {
    this.update({}, {
        totalNumberOfComments: result.comments.length
    }).exec();
}));

is essentially the same as

var query = Post.findOneAndUpdate({_id: fj394hri3hfj}, {$push: {comments: myNewComment}});
query.update({}, {
        totalNumberOfComments: result.comments.length
    }).exec();
query.findOneAndUpdate().exec();

If you want to create a new query from scratch, just do

schema.post('findOneAndUpdate', function(result) {
    this.model.update({}, { // <--- `this.model` gives you access to the `Post` model
        totalNumberOfComments: result.comments.length
    }).exec();
}));

just dont touch your pre save hook,

router.put('/:id', jsonParser, function(req, res, next) {

  currentCollection.findByIdAndUpdate(req.params.id, req.body, function (err, item) {
    if (err) {
        res.status(404);
        return res.json({'error': 'Server Error', 'trace': err});
    }
    item.save(); // <=== this is were you save your data again which triggers the pre hook :)
    res.status(200); 
    return res.json({'message': 'Saved successfully'});
  });
});

I found that the order matters in which you define a model and define a pre hook. Allow me to demonstrate:

Does not work:

// Create Model
let model = Database.Connection.model(`UserModel`, this._schema, `users`);

// Attach Pre Hook
this._schema.pre(`findOneAndUpdate`, function(next) {
    console.log('pre update');
    return next();
});

Does work:

// Attach Pre Hook
this._schema.pre(`findOneAndUpdate`, function(next) {
    console.log('pre update');
    return next();
});

// Create Model
let model = Database.Connection.model(`UserModel`, this._schema, `users`);

Hope this helps anyone!

I just found out the same thing as @nicky-lenaers.

It works just fine with 'safe'. 'delete'. etc. if you define the hooks after the model is defined.

Is there a workaround do define a 'findOneAndUpdate' hook after the model is defined?

@albert-92 no not at the moment

For anyone trying to get something that used to be like

SCHEMA.pre('validate', function(done) {
    // and here use something like 
    this.yourNestedElement 
    // to change a value or maybe create a hashed character    
    done();
});

This should work

SCHEMA.pre('findOneAndUpdate', function(done){
    this._update.yourNestedElement
    done();
});

I can't get post hooks to update the document in the collection.

`module.exports = function (mongoose) {
var mySchema = mongoose.Schema({
id: { type: Number, index: { unique: true } },
field1: { type: String },
field2: { type: String}
}, {
collection: "mySchema",
versionKey: false
});

mySchema.post('findOneAndUpdate', function (result) {
    this.model.update({}, {
        field2: 'New Value'
    }).exec();
});
return mySchema;

}`

mySchema.findOneAndUpdate({id: 1}, {field1: 'test'}, {new: true});

Sets field in collection to { id:1, field1: 'test' ) but should be {id: 1, field1: 'test', field2:'New Value'}
Not sure what I am doing wrong

I can change change the result of the findOneAndUpdate by doing this
mySchema.post('findOneAndUpdate', function (result) { result.field2 = 'something' });

I think it might be that you are trying to update the model with an element that already exists on the model. Or possibly that you are selecting it wrong. Try printing out "this" in your mySchema.post. Also you don't seem to have a done() or next() in your post. I'm not very knowledgable on the subject but I know printing out this will at least give you an idea of what you are dealing with.

Isn't the point of update to change an existing document in your model?

this is a query object

You don't need done or next in post hooks as far as I understand.

Well you have this.model.update which is the schema not the model of the object. I think.. Which means you would have to use

mySchema.post('findOneAndUpdate', function (result) {
    this.model.update({}, {
        $set: { field2: 'New Value'}
    }).exec();
});
return mySchema;

This seems a little backwards to be calling a model function inside of the model. Since you could just use parts of the "this" object that is given to you. you might be better off just using the findOneAndUpdate instead of calling it and then calling another model function on top of it.
in a manager

var data = yourNewData;
self.findOneAndUpdate({id: something._id}, data, {safe: false, new: true})
    .exec()
    .then(resolve)
    .catch(reject);

In my example above I used this._update because that was the update object that needed to be used off of this.

I have tried using $set. It still doesn't change my document in the collection.

Where can I find the all available pre and post hooks?

+1

Was this page helpful?
0 / 5 - 0 ratings