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.

Categories
MongoDB

Using MongoDB with Mongoose — Model Validation

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.

Validation

Mongoose comes with validation features for schemas.

All SchemaTypes have a built-in validator.

Numbers have min and max validators.

And strings have enum, match, minlength, and maxlength validators.

For example, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const breakfastSchema = new Schema({
    eggs: {
      type: Number,
      min: [6, 'too few eggs'],
      max: 12
    },
    bacon: {
      type: Number,
      required: [true, 'too few bacon']
    },
    drink: {
      type: String,
      enum: ['orange juice', 'apple juice'],
      required() {
        return this.bacon > 3;
      }
    }
  });
  const Breakfast = connection.model('Breakfast', breakfastSchema);
  const badBreakfast = new Breakfast({
    eggs: 2,
    bacon: 0,
    drink: 'Milk'
  });
  let error = badBreakfast.validateSync();
  console.log(error);
}
run();

We create the Breakfast schema with some validators.

The eggs field have the min and max validators.

The 2nd entry of the min array has the error message.

We have similar validation with the bacon field.

The drink field has more validation. We have the required method to check other fields to make this field required only if this.bacon is bigger than 3.

enum has the valid values for the drink field.

Therefore, when we create the Breakfast instance with invalid values as we in the code above, we’ll see the errors after we run the validateSync method.

The messages are in the message property in the errors object.

For example, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const breakfastSchema = new Schema({
    eggs: {
      type: Number,
      min: [6, 'too few eggs'],
      max: 12
    },
    bacon: {
      type: Number,
      required: [true, 'too few bacon']
    },
    drink: {
      type: String,
      enum: ['orange juice', 'apple juice'],
      required() {
        return this.bacon > 3;
      }
    }
  });
  const Breakfast = connection.model('Breakfast', breakfastSchema);
  const badBreakfast = new Breakfast({
    eggs: 2,
    bacon: 0,
    drink: 'Milk'
  });
  let error = badBreakfast.validateSync();
  console.log(error.errors['eggs'].message === 'too few eggs');
}
run();

to get the error as we did in the last line of the run function.

The unique option isn’t a validator. It lets us add unique indexes to a field.

For example, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const uniqueUsernameSchema = new Schema({
    username: {
      type: String,
      unique: true
    }
  });
}
run();

to add a unique index to the usernamd field.

We can also add a custom validator. For instance, 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 /(.+)@(.+){2,}.(.+){2,}/.test(v);
        },
        message: props => `${props.value} is not a email!`
      },
      required: [true, 'Email is required']
    }
  });
  const User = connection.model('User', userSchema);
}
run();

We add the email field to with the validate method with the validator function to add validation for the email field.

The message method is a function that returns the error message if validation fails.

Conclusion

We can add validation in various ways with Mongoose.

Categories
MongoDB

Using MongoDB with Mongoose — Subdocuments

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.

Parents of Subdocuments

We can get the parent document from a child document.

For example, we can write:

async function run() {
  const mongoose = require('mongoose');
  const connection = mongoose.createConnection('mongodb://localhost:27017/test');
  const childSchema = new mongoose.Schema({ name: 'string' });
  const parentSchema = new mongoose.Schema({
    children: [childSchema],
    child: childSchema
  });
  const Child = await connection.model('Child', childSchema);
  const Parent = await connection.model('Parent', parentSchema);
  const parent = new Parent({ child: { name: 'Matt' }, children: [{ name: 'Matt' }] })
  await parent.save();
  console.log(parent === parent.child.parent());
  console.log(parent === parent.children[0].parent());
}
run();

Then both console log statements are true because the parent method returns the parent object of the child and a subdocument in the children fields.

If we have a deeply nested subdocument, we can call the ownerDocument method to get the root document:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const parentSchema = new Schema({
    level1: new Schema({
      level2: new Schema({
        test: String
      })
    })
  });
  const Parent = await connection.model('Parent', parentSchema);
  const doc = new Parent({ level1: { level2: { test: 'test' } } });
  await doc.save();
  console.log(doc === doc.level1.level2.ownerDocument());
}
run();

We call the ownerDocument method with on the level2 subdocument to get the root document.

Therefore, the console log should log true .

Alternative Declaration Syntax for Arrays

We can declare nested documents in more than one way.

One way is to put the array straight into the schema object:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const parentSchema = new Schema({
    children: [{ name: 'string' }]
  });
  const Parent = await connection.model('Parent', parentSchema);
  const doc = new Parent({ children: { name: 'test' } });
  await doc.save();
}
run();

We can also create a schema with the Schema constructor:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const parentSchema = new Schema({
    children: [new Schema({ name: 'string' })]
  });
  const Parent = await connection.model('Parent', parentSchema);
  const doc = new Parent({ children: { name: 'test' } });
  await doc.save();
}
run();

Alternative Declaration Syntax for Single Nested Subdocuments

There are also 2 ways to declare subdocuments schema.

One way is to write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const schema = new Schema({
    nested: {
      prop: String
    }
  });
  const Parent = await connection.model('Parent', schema);
}
run();

or:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const schema = new Schema({
    nested: {
      type: new Schema({ prop: String }),
      required: true
    }
  });
  const Parent = await connection.model('Parent', schema);
}
run();

Both ways will set the nested subdocument with the data type string.

Conclusion

We can work with MongoDB subdocuments in different ways with Mongoose.