Stefan
Stefan

Reputation: 4140

mongoose unique:true pre-save hook calls hook before validation

As far as I understand, the mongoose pre-save hooks fire before the document is inserted in the collection but after the validations occur. Therefore if one validation fails, the pre-save hooks won't be called.

In my case, they are called regardless:

What the simple code bellow does is tries to make a schema in which users refer other users by _id when registering. A pre-save hook is added to automatically push the IDs of the new users in their referrals' list.

So user a registers with no referral -> OK

User b registers with a as a referral -> OK

User b2 registers with the same email as b (not OK, is unique) and references a -> SHOULD FAIL and it should not push b2's ID in a.references

Schema:

var userSchema = new Schema({
  email: {type:String, unique:true, required:true},
  isVerified: {type:Boolean, default:false},
  referredBy: {type:Schema.ObjectId, ref:'User'},
  referred: [{type:Schema.ObjectId, ref:'User'}],
});

userSchema.pre('save', function (next) {
  if (!this.isNew) return next();
  if (!this.referredBy) return next();

  User.findById(this.referredBy, function (err, doc) {
    if (err) return next(err);
    if (!doc) return next(new DbError(['referredBy not found: %s', this.referredBy]));
    doc.referred.push(this._id);
    doc.save(next);
  }.bind(this));
});

userSchema.path('referredBy').validate(function (value, respond) {
  User.findById(value, function (err, user) {
    if (err) throw err;
    if (!user) return respond(false);
    respond(true);
  });
}, 'doesntExit');

var User = mongoose.model('User', userSchema);

Test code:

var a = new User();
a.email = 'a';

a.save(function () {
    var b = new User();
    b.email = 'b';
    b.referredBy = a._id;

    b.save(function () {
        var b2 = new User();
        b2.email = 'b';
        b2.referredBy = a._id;

        b2.save(function (err, doc) {
            console.log('error:', err); // duplicate error is thrown, which is OK
            console.log(!!doc);                 // this is false, which is OK
            User.findById(a._id, function (err, result) {
                console.log('# of referrals: ', result.referred.length); // 2, which is BAD
            });
        });
    });
});

Everything else checks out, the errors are thrown, fails happen, but all the pre-save hooks are saved regardless

Any idea how to fix this or if there is a true pre-save after validation hook?

Upvotes: 1

Views: 3288

Answers (1)

heyheyjp
heyheyjp

Reputation: 626

As far as I can tell, if you provide an asynchronous validation function for the referredBy path, it is executed in parallel (effectively) with the pre-save function, rather than serially in such a way that it would prevent the pre-save function's execution.

You might consider combining them into one function, and if you'd like to prevent the update of the referredBy object's referred list until after, for example, the email value's unique constraint has been met (which apparently doesn't get enforced until the actual save attempt), you might want to stick that bit of logic in a post-save hook.

Cheers.

EDIT

I've looked at this a number of ways, and it all seems fairly clear at this point:

1) Custom validation functions are executed before pre-save hooks and can prevent the execution of pre-save hooks (and of course, the save itself) by returning false. Demonstrated with the following:

userSchema.pre('save', function (next) {
    console.log('EXECUTING PRE-SAVE');
    next();
});

userSchema.path('referredBy').validate(function (value, respond) {
    console.log('EXECUTING referredBy VALIDATION')
    respond(false);
}, 'doesntExit');

2) Built-in validators that don't require making a db query to enforce (such as the 'required' constraint) are also executed before pre-save functions and can prevent their execution. Easily demonstrated by commenting out b2's email value assignment instead of assigning a non-unique value:

var b2 = new User();
//b2.email = 'b';
b2.referredBy = a._id;

3) Built-in validators that do require making a db query, such as enforcing uniqueness, will not block pre-save hook execution. Presumably, this is to optimize for the success case, which would otherwise have to involve 1 query to check uniqueness, then another query to make the upsert after passing uniqueness validation.

So, validation (custom or built-in) does occur before pre-save hooks are executed, except in the case of built-in validation which requires a db query to enforce.

Upvotes: 2

Related Questions