27

I'd like to compare the new/incoming value of a property with the previous value of that property (what is currently saved in the db) within a pre('save') middleware.

Does Mongoose provide a facility for doing this?

franzlorenzon
  • 5,845
  • 6
  • 36
  • 58
smabbott
  • 792
  • 1
  • 7
  • 13

5 Answers5

36

The accepted answer works very nicely. An alternative syntax can also be used, with the setter inline with the Schema definition:

var Person = new mongoose.Schema({
  name: {
    type: String,
    set: function(name) {
      this._previousName = this.name;
      return name;
    }
});

Person.pre('save', function (next) {
  var previousName = this._previousName;
  if(someCondition) {
    ...
  }
  next();
});
Tom Spencer
  • 7,816
  • 4
  • 54
  • 50
30

Mongoose allows you to configure custom setters in which you do the comparison. pre('save') by itself won't give you what you need, but together:

schema.path('name').set(function (newVal) {
  var originalVal = this.name;
  if (someThing) {
    this._customState = true;
  }
});
schema.pre('save', function (next) {
  if (this._customState) {
    ...
  }
  next();
})
aaronheckmann
  • 10,625
  • 2
  • 40
  • 30
  • 3
    Do I need to do this to access previous value in **validators**? Or there is more straightforward way in case of validators? – eagor Oct 23 '14 at 20:07
  • @aaronheckmann sorry for resurrecting an old thread, I guess this will not work, if we have multiple node server behind a load balancer. – Saurabh Jan 06 '16 at 14:22
  • @aaronheckmann if you are using nginx, there is a value to specify the host, and send it to the same sever it was sent before.. or how did you solved it ? – Carlos.V Jul 30 '19 at 23:04
  • 2
    I don't know if this used to work but it doesn't work anymore. this.name is undefined – Josh Woodcock Oct 16 '19 at 00:15
  • I know that it's been 4 years since you wrote this @JoshWoodcock, but I'm having the same problem. – shankie_san Aug 14 '20 at 10:14
  • I encountered the same problem as @JoshWoodcock while working with PATCH request, when I do not send the field which is using a custom setter(in this case this.name). In that case it comes up as undefined. – Kanwarbir Singh May 31 '21 at 17:19
2

I was looking for a solution to detect changes in any one of multiple fields. Since it looks like you can't create a setter for the full schema, I used a virtual property. I'm only updating records in a few places so this is a fairly efficient solution for that kind of situation:

Person.virtual('previousDoc').get(function() {
  return this._previousDoc;
}).set(function(value) {
    this._previousDoc = value;
});

Let's say your Person moves and you need to update his address:

const person = await Person.findOne({firstName: "John", lastName: "Doe"});
person.previousDoc = person.toObject();  // create a deep copy of the previous doc
person.address = "123 Stack Road";
person.city = "Overflow";
person.state = "CA";
person.save();

Then in your pre hooks, you would just need to reference properties of _previousDoc such as:

// fallback to empty object in case you don't always want to check the previous state
const previous = this._previousDoc || {};

if (this.address !== previous.address) {
    // do something
}

// you could also assign custom properties to _previousDoc that are not in your schema to allow further customization
if (previous.userAddressChange) {

} else if (previous.adminAddressChange) {

}
SuperCodeBrah
  • 2,874
  • 2
  • 19
  • 33
0

Honestly, I tried the solutions posted here, but I had to create a function that would store the old values in an array, save the values, and then see the difference.

// Stores all of the old values of the instance into oldValues
const oldValues = {};
for (let key of Object.keys(input)) {
    if (self[key] != undefined) {
        oldValues[key] = self[key].toString();
    }

    // Saves the input values to the instance
    self[key] = input[key];
}

yield self.save();


for (let key of Object.keys(newValues)) {
    if (oldValues[key] != newValues[key]) {
       // Do what you need to do
    }
}
Raza
  • 3,147
  • 2
  • 31
  • 35
  • 1
    this doesn't use middleware, so it is not an answer to the question that was asked – tar Apr 29 '19 at 18:20
0

What I do is use this.constructor within the pre-save route to access the current value in the database.

const oldData = this.constructor.findById(this.id)

You can then grab the specific key you're looking for from the oldData to work with as you see fit :)

let name = oldData.name

Note that this works well for simple data such as strings, but I have found that it does not work well for subschema, as mongoose has built in functionality that runs first. Thus, sometimes your oldData will match your newData for a subschema. This can be resolved by giving it it's own pre-save route!