Categories
MongoDB

Using MongoDB with Mongoose — Hooks for Operations

To make MongoDB database manipulation easy, we can use the Mongoose NPM package to make working with MongoDB databases easier.

In this article, we’ll look at how to use Mongoose to manipulate our MongoDB database.

Save/Validate Hooks

The save method will trigger validate hooks.

This is because Mongoose calls the pre('save') hook that calls validate .

The pre('validate') and post('validate') hooks are called before any pre('save') hooks.

For example, we can write:

async function run() {  
  const { createConnection, Schema } = require('mongoose');  
  const connection = createConnection('mongodb://localhost:27017/test');  
  const schema = new Schema({ name: String });  
  schema.pre('validate', () => {  
    console.log('1');  
  });  
  schema.post('validate', () => {  
    console.log('2');  
  });  
  schema.pre('save', () => {  
    console.log('3');  
  });  
  schema.post('save', () => {  
    console.log('4');  
  });  
  const User = connection.model('User', schema);  
  new User({ name: 'test' }).save();  
}  
run();

to add the schema hooks.

Then they’ll be called one by one in the same order that they’re listed.

Query Middleware

Pre and post save hooks aren’t run when update methods are run.

For example, if we have:

async function run() {  
  const { createConnection, Schema } = require('mongoose');  
  const connection = createConnection('mongodb://localhost:27017/test');  
  const schema = new Schema({ name: String });  
  schema.pre('updateOne', { document: true, query: false }, function() {  
    console.log('Updating');  
  });  
  const User = connection.model('User', schema);  
  const doc = new User();  
  await doc.updateOne({ $set: { name: 'test' } });  
  await User.updateOne({}, { $set: { name: 'test' } });  
}  
run();

Then when we have query set to false or didn’t add the query property, then the updateOne pre hook won’t run when we run updateOne .

Aggregation Hooks

We can add aggregation hooks.

For example, we can write:

async function run() {  
  const { createConnection, Schema } = require('mongoose');  
  const connection = createConnection('mongodb://localhost:27017/test');  
  const schema = new Schema({  
    name: {  
      type: String,  
      unique: true  
    }  
  });  
  schema.pre('aggregate', function() {  
    this.pipeline().unshift({ $match: { isDeleted: { $ne: true } } });  
  });  
  const User = connection.model('User', schema);  
}  
run();

We listen to the aggregate event.

Error Hooks

We can get errors from hooks.

For example, we can write:

async function run() {  
  const { createConnection, Schema } = require('mongoose');  
  const connection = createConnection('mongodb://localhost:27017/test');  
  const schema = new Schema({  
    name: {  
      type: String,  
      unique: true  
    }  
  });  
  schema.post('update', function (error, res, next) {  
    if (error.name === 'MongoError' && error.code === 11000) {  
      next(new Error('There was a duplicate key error'));  
    } else {  
      next();  
    }  
  });  
  const User = connection.model('User', schema);  
}  
run();

We listen to the update event and get the error from the error parameter.

We can get the name and code to get information about the error.

Synchronous Hooks

Some hooks are always synchronous.

init hooks are always synchronous because the init function is synchronous.

For example, if we have:

async function run() {  
  const { createConnection, Schema } = require('mongoose');  
  const connection = createConnection('mongodb://localhost:27017/test');  
  const schema = new Schema({  
    name: String  
  });  
  schema.pre('init', obj => {  
    console.log(obj);  
  });  
  const User = connection.model('User', schema);  
}  
run();

We added the pre init hook with a callback.

Conclusion

We can add middleware for various events that are emitted when we manipulate data with Mongoose.

Categories
MongoDB

Using MongoDB with Mongoose — Validators on Nested Objects and Updates

To make MongoDB database manipulation easy, we can use the Mongoose NPM package to make working with MongoDB databases easier.

In this article, we’ll look at how to use Mongoose to manipulate our MongoDB database.

Required Validators on Nested Objects

We can define validators on nested objects with Mongoose.

To do that, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const nameSchema = new Schema({
    first: String,
    last: String
  });

  const personSchema = new Schema({
    name: {
      type: nameSchema,
      required: true
    }
  });

  const Person = connection.model('Person', personSchema);
  const doc = new Person({});
  const err = doc.validateSync();
  console.log(err);
}
run();

We created the nameSchema which is embedded in the personSchema so that we can require both the name.first and name.last nested fields.

Now when we create a new Person instance, we’ll see an error because we haven’t added those properties into our document.

Update Validators

Mongoose also supports validation for updating documents with the update , updateOne , updateMany , and findOneAndUpdate methods.

Update validators are off by default. We need the runValidators option to turn it on.

For example, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const toySchema = new Schema({
    color: String,
    name: String
  });
  const Toy = connection.model('Toys', toySchema);
  Toy.schema.path('color').validate(function (value) {
    return /red|green|blue/i.test(value);
  }, 'Invalid color');

  const opts = { runValidators: true };
  Toy.updateOne({}, { color: 'not a color' }, opts, (err) => {
    console.log(err.errors.color.message);
  });
}
run();

Since we have the runValidators property set to true in the opts object, we’ll get validator when we call the updateOne method.

Then we should see the ‘Invalid color’ message logged in the console log in the callback.

Update Validators and this

The value of this for update validators and document validators.

In document validators, this refers to the document itself.

However, when we’re updating a document, the document that’s updated may not be in memory itself.

Therefore, this is not defined by default.

The context optioin lets us set the value of this in update validators.

For example, we can write:

const { captureRejectionSymbol } = require('events');

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const toySchema = new Schema({
    color: String,
    name: String
  });
  toySchema.path('color').validate(function (value) {
    if (this.getUpdate().$set.name.toLowerCase().indexOf('red') !== -1) {
      return value === 'red';
    }
    return true;
  });

  const Toy = connection.model('Toy', toySchema);
  const update = { color: 'blue', name: 'red car' };
  const opts = { runValidators: true, context: 'query' };
  Toy.updateOne({}, update, opts, (error) => {
    console.log(error.errors['color']);
  });
}
run();

to set the context property in the opts object to 'query' to make this defined in the validator when we do updates.

Conclusion

We can add validators to updates operations in Mongoose. It’s not enabled by default.

Categories
MongoDB

Using MongoDB with Mongoose — Pre Middleware Errors and Post Middlewares

To make MongoDB database manipulation easy, we can use the Mongoose NPM package to make working with MongoDB databases easier.

In this article, we’ll look at how to use Mongoose to manipulate our MongoDB database.

Errors in Pre Hooks

We can raise errors in pre hooks in various ways.

For example, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const schema  = new Schema({
    name: { type: String, required: true },
    age: Number
  });
  schema.pre('save', (next) => {
    const err = new Error('error');
    next(err);
  });
  const Kitten = connection.model('Kitten', kittenSchema);
}
run();

to pass an Error instance in the next method.

Also, we can return a promise:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const kittenSchema = new Schema({
    name: { type: String, required: true },
    age: Number
  });
  schema.pre('save', (next) => {
    const err = new Error('error');
    return Promise.reject(err);
  });
  const Kitten = connection.model('Kitten', kittenSchema);
}
run();

Or we can throw an error:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const schema = new Schema({
    name: { type: String, required: true },
    age: Number
  });
  schema.pre('save', (next) => {
    throw new Error('error');
  });
  const Kitten = connection.model('Kitten', kittenSchema);
}
run();

Post Middleware

Post middlewares are run after the hooked method and all its pre middleware are run.

For example, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const schema = new Schema({
    name: { type: String, required: true },
    age: Number
  });
  schema.post('init', (doc) => {
    console.log('%s has been initialized from the db', doc._id);
  });
  schema.post('validate', (doc) => {
    console.log('%s has been validated (but not saved yet)', doc._id);
  });
  schema.post('save', (doc) => {
    console.log('%s has been saved', doc._id);
  });
  schema.post('remove', (doc) => {
    console.log('%s has been removed', doc._id);
  });
  const Kitten = connection.model('Kitten', kittenSchema);
}
run();

We added post middlewares for the init , validate , save , and remove operations that are run after the given document operations.

Async Post Hooks

We can add async post hooks. We just need to call next to proceed to the next post hook:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const schema = new Schema({
    name: { type: String, required: true },
    age: Number
  });
  schema.post('save', (doc, next) => {
    setTimeout(() => {
      console.log('post1');
      next();
    }, 10);
  });

  schema.post('save', (doc, next) => {
    console.log('post2');
    next();
  });
  const Kitten = connection.model('Kitten', schema);
}
run();

We call the next function in the setTimeout callback to proceed to the next post middleware.

Define Middleware Before Compiling Models

We have to define middleware before we create the model with the schema for it to fire.

For example, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const schema = new Schema({ name: String });
  schema.pre('save', () => console.log('pre save called'));
  const User = connection.model('User', schema);
  new User({ name: 'test' }).save();
}
run();

We created the schema and then added a save pre middleware right after we defined the schema and before the model is created.

This way, the callback in the pre method will be run when we create a document with the model.

Conclusion

We can add post middlewares and be careful where we define the model.

Categories
MongoDB

Using MongoDB with Mongoose — Middlewares

To make MongoDB database manipulation easy, we can use the Mongoose NPM package to make working with MongoDB databases easier.

In this article, we’ll look at how to use Mongoose to manipulate our MongoDB database.

Update Validators Only Run On Updated Paths

We can add validators to run only on updated paths.

For instance, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const kittenSchema = new Schema({
    name: { type: String, required: true },
    age: Number
  });
  const Kitten = connection.model('Kitten', kittenSchema);

  const update = { color: 'blue' };
  const opts = { runValidators: true };
  Kitten.updateOne({}, update, opts, (err) => {
    console.log(err)
  });

  const unset = { $unset: { name: 1 } };
  Kitten.updateOne({}, unset, opts, (err) => {
    console.log(err);
  });
}
run();

then only the second updateOne callback will have its err parameter defined because we unset the name field when it’s required.

Update validators only run on some operations, they include:

  • $set
  • $unset
  • $push (>= 4.8.0)
  • $addToSet (>= 4.8.0)
  • $pull (>= 4.12.0)
  • $pullAll (>= 4.12.0)

Middleware

Middlewares are functions that are passed control during the execution of async functions.

They are useful for writing plugins.

There are several types of middleware. They include:

  • validate
  • save
  • remove
  • updateOne
  • deleteOne
  • init (init hooks are synchronous)

Query middleware are supported for some model and query functions. They include:

  • count
  • deleteMany
  • deleteOne
  • find
  • findOne
  • findOneAndDelete
  • findOneAndRemove
  • findOneAndUpdate
  • remove
  • update
  • updateOne
  • updateMany

Aggregate middleware is for the aggregate method.

They run when we call exec on the aggregate object.

Pre Middleware

We can one or more pre middleware for an operation.

They are run one after the other by each middleware calling next .

For example, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const kittenSchema = new Schema({
    name: { type: String, required: true },
    age: Number
  });
  kittenSchema.pre('save', (next) => {
    next();
  });
  const Kitten = connection.model('Kitten', kittenSchema);
}
run();

to add a pre middleware for the that runs before the save operation.

Once the next function is run, the save operation will be done.

We can also return a promise in the callback instead of calling next in Mongoose version 5 or later.

For example, we can write:

const { captureRejectionSymbol } = require('events');

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const kittenSchema = new Schema({
    name: { type: String, required: true },
    age: Number
  });
  kittenSchema.pre('save', async () => {
    return true
  });
  const Kitten = connection.model('Kitten', kittenSchema);
}
run();

If we call next in our callback, calling next doesn’t stop the rest of the middleware code from running.

For example, if we have:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const kittenSchema = new Schema({
    name: { type: String, required: true },
    age: Number
  });
  kittenSchema.pre('save', async () => {
    if (true) {
      console.log('calling next');
      next();
    }
    console.log('after next');
  });
  const Kitten = connection.model('Kitten', kittenSchema);
}
run();

Then both console logs will be displayed.

We should add return to stop the code below the if block from running:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const kittenSchema = new Schema({
    name: { type: String, required: true },
    age: Number
  });
  kittenSchema.pre('save', async () => {
    if (Math.random() < 0.5) {
      console.log('calling next');
      return next();
    }
    console.log('after next');
  });
  const Kitten = connection.model('Kitten', kittenSchema);
}
run();

Conclusion

We can add middleware for various operations to run code before them.

Categories
MongoDB

Using MongoDB with Mongoose — Async Validators and Validation Errors

To make MongoDB database manipulation easy, we can use the Mongoose NPM package to make working with MongoDB databases easier.

In this article, we’ll look at how to use Mongoose to manipulate our MongoDB database.

Async Custom Validators

We can add custom validators that are async.

For example, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const userSchema = new Schema({
    email: {
      type: String,
      validate: {
        validator(v) {
          return Promise.resolve(/(.+)@(.+){2,}.(.+){2,}/.test(v));
        },
        message: props => `${props.value} is not a email!`
      },
      required: [true, 'Email is required']
    }
  });
  const User = connection.model('User', userSchema);
  const user = new User();
  user.email = 'test';
  try {
    await user.validate();
  } catch (error) {
    console.log(error);
  }
}
run();

to add the validator method to our method that returns a promise instead of a boolean directly.

Then we can use the validate method to validate the values we set.

And then we can catch validation errors with the catch block.

We can get the message from the errors property in the error object.

Validation Errors

Errors returned after validation has an errors object whose values are ValidatorError objects.

ValidatorError objects have kind , path , value , and message properties.

They may also have a reason property.

If an error is thrown in the validator, the property will have the error that was thrown.

For example, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const toySchema = new Schema({
    color: String,
    name: String
  });

  const validator = function (value) {
    return /red|white|gold/i.test(value);
  };
  toySchema.path('color').validate(validator,
    'Color `{VALUE}` not valid', 'Invalid color');
  toySchema.path('name').validate((v) => {
    if (v !== 'special toy') {
      throw new Error('I want special toy');
    }
    return true;
  }, 'Name `{VALUE}` is not valid');
  const Toy = connection.model('Toy', toySchema);
  const toy = new Toy();
  toy.color = 'green';
  toy.name = 'abc';
  toy.save((err) => {
    console.log(err);
  })

}
run();

We have the validator function that returns true or false depending on the validity of the value.

The name value also has a validator added to it by passing a callback into the validate method to validate the name field.

Cast Errors

Mongoose tries to coerce values into the correct type before validators are run.

If data coercion fails, then the error.errors object will have a CastError object.

For example, if we have:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const vehicleSchema = new Schema({
    numWheels: { type: Number, max: 18 }
  });
  const Vehicle = connection.model('Vehicle', vehicleSchema);
  const doc = new Vehicle({ numWheels: 'abc' });
  const err = doc.validateSync();
  console.log(err);
}
run();

Since we set numWheels to a non-numeric string, we’ll get a CastError as the value of the err object.

Conclusion

There are many ways to do validation with Mongoose schema fields with Mongoose.