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.

Categories
MongoDB

Using MongoDB with Mongoose — Queries and Aggregations

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.

Cursor Timeout

We can set the noCursorTimeout flag to disable cursor timeout.

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: {
      first: String,
      last: String
    },
    occupation: String
  });
  const Person = connection.model('Person', schema);
  const person = new Person({
    name: {
      first: 'james',
      last: 'smith'
    },
    occupation: 'host'
  })
  await person.save();
  const person2 = new Person({
    name: {
      first: 'jane',
      last: 'smith'
    },
    occupation: 'host'
  })
  await person2.save();
  const cursor = Person.find({ occupation: /host/ })
    .cursor()
    .addCursorFlag('noCursorTimeout', true);
  let doc;
  while (doc = await cursor.next()) {
    console.log(doc);
  }
}
run();

We call the addCursorFlag to disable the cursor timeout.

Aggregation

We can call aggregate to do aggregation with the results.

This will return plain JavaScript objects rather than Mongoose documents.

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: {
      first: String,
      last: String
    },
    occupation: String
  });
  const Person = connection.model('Person', schema);
  const person = new Person({
    name: {
      first: 'james',
      last: 'smith'
    },
    occupation: 'host'
  })
  await person.save();
  const person2 = new Person({
    name: {
      first: 'jane',
      last: 'smith'
    },
    occupation: 'host'
  })
  await person2.save();
  const results = await Person.aggregate([{ $match: { 'name.last': 'smith' } }]);
  for (const r of results) {
    console.log(r);
  }
}
run();

We call aggergate to get all the entries that has the name.last property set to 'smith' .

Then we use a for-of loop to loop through the items since it returns a regular JavaScript array.

The aggregate method doesn’t cast its pipeline, so we have to make sure the values we pass in the pipeline have the correct type.

For instance, if we have:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const schema = new Schema({
    name: {
      first: String,
      last: String
    },
    occupation: String
  });
  const Person = connection.model('Person', schema);
  const person = new Person({
    name: {
      first: 'james',
      last: 'smith'
    },
    occupation: 'host'
  })
  await person.save();
  const person2 = new Person({
    name: {
      first: 'jane',
      last: 'smith'
    },
    occupation: 'host'
  })
  await person2.save();
  const doc = await Person.findOne();
  const idString = doc._id.toString();
  const queryRes = await Person.findOne({ _id: idString });
  console.log(queryRes);
}
run();

queryRes isn’t cast to the Person since it’s a plain JavaScript object.

Query Methods

Mongoose comes with the following query methods:

  • Model.deleteMany()
  • Model.deleteOne()
  • Model.find()
  • Model.findById()
  • Model.findByIdAndDelete()
  • Model.findByIdAndRemove()
  • Model.findByIdAndUpdate()
  • Model.findOne()
  • Model.findOneAndDelete()
  • Model.findOneAndRemove()
  • Model.findOneAndReplace()
  • Model.findOneAndUpdate()
  • Model.replaceOne()
  • Model.updateMany()
  • Model.updateOne()

They all return a query object.

Conclusion

We can use Mongoose query methods to query our database.