Mongoose: Schema pre-validate hook not called when attached after Model creation

Created on 1 Feb 2017  路  9Comments  路  Source: Automattic/mongoose

(disclaimer: I am not sure if this is related directly to mongoose, so if this looks like a bogus bug report, please ignore it. Feels weird that nobody else hasn't hit this issue, so I'm suspecting that it is due to some other part of my system. Posting the issue in here just in case)

What is the current behavior?
On [email protected], where I have code like this:

mySchema.pre('validate', someValidationFunction);

the someValidationFunction is never called (and my tests where that validation function should prevent creation of duplicate entries fails).

When I install explicitly version [email protected], the someValidationFunction is called and my test set passes

What is the expected behavior?
Schema's pre-validate -hook should be called

Versions:

  • node 6.2.2
  • mongoose 4.8.1
  • mongodb: v3.2.9

Most helpful comment

Well a Model is constructed from a schema, so if you mutate the schema after initializing your model, it won't have the changes.

So, for example, this won't work:

const mongoose = require('mongoose');
const co = require('co');
const chalk = require('chalk');

mongoose.Promise = global.Promise;

// Constants
const GITHUB_ISSUE = `gh-4946`;

exec()
  .then(() => {

    console.log(chalk.red(`Should not have gotten here`));
    process.exit(0);
  })
  .catch(error => {
    console.log(chalk.green(`There was an error (as expected): ${error}\n${error.stack}`));
    process.exit(2);
  })
function exec() {
  return co(function* () {
    mongoose.connect(`mongodb://localhost:27017/${GITHUB_ISSUE}`);

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


    const Model = mongoose.model('Model', schema);
    schema.pre('validate', function(next) {
      return this.constructor.findOne({ name: this.name, _id: { $ne: this._id } })
        .then(doc => {
          if (doc) {
            return next(new Error(`Name is not unique yo`))
          } else {
            return next();
          }
        })
        .catch(error => {
          return next(new Error(error));
        });
    });

    yield Model.remove({});
    console.log('MODEL', Model);
    const doc1 = yield Model.create({ name: 'Nosferatu' });
    const doc2 = yield Model.create({ name: 'Nosferatu' });
  });
}

This will work, however:

const mongoose = require('mongoose');
const co = require('co');
const chalk = require('chalk');

mongoose.Promise = global.Promise;

// Constants
const GITHUB_ISSUE = `gh-4946`;

exec()
  .then(() => {

    console.log(chalk.red(`Should not have gotten here`));
    process.exit(0);
  })
  .catch(error => {
    console.log(chalk.green(`There was an error (as expected): ${error}\n${error.stack}`));
    process.exit(2);
  })
function exec() {
  return co(function* () {
    mongoose.connect(`mongodb://localhost:27017/${GITHUB_ISSUE}`);

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

    schema.pre('validate', function(next) {
      return this.constructor.findOne({ name: this.name, _id: { $ne: this._id } })
        .then(doc => {
          if (doc) {
            return next(new Error(`Name is not unique yo`))
          } else {
            return next();
          }
        })
        .catch(error => {
          return next(new Error(error));
        });
    });
    const Model = mongoose.model('Model', schema);


    yield Model.remove({});
    console.log('MODEL', Model);
    const doc1 = yield Model.create({ name: 'Nosferatu' });
    const doc2 = yield Model.create({ name: 'Nosferatu' });
  });
}

All 9 comments

Can you give us some more context?

1) Can I see someValidationFunction?

2) Can i see the test or the context in which this is supposed to run?

Seeing similar issue with pre('save'). Works through 4.7.9. Our mySchema.pre('save', function() {...}) is no longer being called. Of note, we initialize the behavior inside of mySchema.on('init', function(model) {...}) in order to have the model available.

Confirmed. If we move the mySchema.pre('save', ...) out of mySchema.on('init', ...) it works again on 4.8.1.

I digged a little bit and hopefully pinpointed the cause of the error. Not sure if this was misuse of mongoose, but this worked on 4.6.8.

(I am throwing restify-error from the validation hook which is then catched and transformed to JSON error response.)

My schema & model code looks like this:

const mySchema = mongoose.Schema({ ... });

const MyModel = mongoose.model('MyModel', mySchema);

const assertUniqueness = function assertUniqueness(next) {
  MyModel.findByIdentifiers(this.identifier).then((objectWithSameIdentifier) => {
    if (objectWithSameIdentifier && objectWithSameIdentifier.id !== this.id) {
      return next(new restify.ConflictError('Identifier already exists'));
    }
    return next();
  });
};
mySchema.pre('validate', assertUniqueness);


module.exports = MyModel;

the code snippet above worked on 4.6.8 but not in 4.8.1.

when I move the pre-validation hook to a row _before_ the model creation, the code works in 4.8.1:

let MyModel = null;

const mySchema = mongoose.Schema({ ... });

const assertUniqueness = function assertUniqueness(next) {
  MyModel.findByIdentifiers(this.identifier).then((objectWithSameIdentifier) => {
    if (objectWithSameIdentifier && objectWithSameIdentifier.id !== this.id) {
      return next(new restify.ConflictError('Identifier already exists'));
    }
    return next();
  });
};
mySchema.pre('validate', assertUniqueness);


MyModel = mongoose.model('MyModel', mySchema);

module.exports = MyModel;

I altered my code to work with the latest version of mongoose.

Was this a bug or misuse of mongoose?

Well a Model is constructed from a schema, so if you mutate the schema after initializing your model, it won't have the changes.

So, for example, this won't work:

const mongoose = require('mongoose');
const co = require('co');
const chalk = require('chalk');

mongoose.Promise = global.Promise;

// Constants
const GITHUB_ISSUE = `gh-4946`;

exec()
  .then(() => {

    console.log(chalk.red(`Should not have gotten here`));
    process.exit(0);
  })
  .catch(error => {
    console.log(chalk.green(`There was an error (as expected): ${error}\n${error.stack}`));
    process.exit(2);
  })
function exec() {
  return co(function* () {
    mongoose.connect(`mongodb://localhost:27017/${GITHUB_ISSUE}`);

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


    const Model = mongoose.model('Model', schema);
    schema.pre('validate', function(next) {
      return this.constructor.findOne({ name: this.name, _id: { $ne: this._id } })
        .then(doc => {
          if (doc) {
            return next(new Error(`Name is not unique yo`))
          } else {
            return next();
          }
        })
        .catch(error => {
          return next(new Error(error));
        });
    });

    yield Model.remove({});
    console.log('MODEL', Model);
    const doc1 = yield Model.create({ name: 'Nosferatu' });
    const doc2 = yield Model.create({ name: 'Nosferatu' });
  });
}

This will work, however:

const mongoose = require('mongoose');
const co = require('co');
const chalk = require('chalk');

mongoose.Promise = global.Promise;

// Constants
const GITHUB_ISSUE = `gh-4946`;

exec()
  .then(() => {

    console.log(chalk.red(`Should not have gotten here`));
    process.exit(0);
  })
  .catch(error => {
    console.log(chalk.green(`There was an error (as expected): ${error}\n${error.stack}`));
    process.exit(2);
  })
function exec() {
  return co(function* () {
    mongoose.connect(`mongodb://localhost:27017/${GITHUB_ISSUE}`);

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

    schema.pre('validate', function(next) {
      return this.constructor.findOne({ name: this.name, _id: { $ne: this._id } })
        .then(doc => {
          if (doc) {
            return next(new Error(`Name is not unique yo`))
          } else {
            return next();
          }
        })
        .catch(error => {
          return next(new Error(error));
        });
    });
    const Model = mongoose.model('Model', schema);


    yield Model.remove({});
    console.log('MODEL', Model);
    const doc1 = yield Model.create({ name: 'Nosferatu' });
    const doc2 = yield Model.create({ name: 'Nosferatu' });
  });
}

Thanks to both of you. That explains and solves our situation as well. The example with this.constructor.findOne is exactly the case where we thought we needed a model, and we thought we were being clever grabbing it from 'init'. We will use that approach going forward.

No problem, I'm going to close this issue out. Let me know if you're still having problems with your pre('validate') hooks

re-commenting to this old issue since I think there might be some regression between 4.8.2 and 4.8.4.

Given my "correct" example code, copypasted in here again from earlier comment:

let MyModel = null;

const mySchema = mongoose.Schema({ ... });

const assertUniqueness = function assertUniqueness(next) {
  MyModel.findByIdentifiers(this.identifier).then((objectWithSameIdentifier) => {
    if (objectWithSameIdentifier && objectWithSameIdentifier.id !== this.id) {
      return next(new restify.ConflictError('Identifier already exists'));
    }
    return next();
  });
};
mySchema.pre('validate', assertUniqueness);


MyModel = mongoose.model('MyModel', mySchema);

module.exports = MyModel;

It seems that in 4.8.4 that assertUniqueness-function is NOT called when creating new documents. In 4.8.2 it gets called (and it blocks new documents being created which fails the uniqueness check).

Has anyone encountered similar behaviour, or is this yet again myself misusing Mongoose? :)

Cheers for your help in advance!

Hmm @miro how are you creating new documents?

Also, you'd probably want to use a unique index instead of running a separate query to assert uniqueness, because that has a race condition

Was this page helpful?
0 / 5 - 0 ratings