You are on page 1of 153

Working with MongoDB Data in

Node.js Apps with Mongoose


John Au-Yeung
Working with MongoDB Data in
Node.js Apps with Mongoose
1. Working with MongoDB Data in Node.js Apps with Mongoose
2. Getting Started with Mongoose
3. Defining Schemas
4. Buffer
5. Mixed
6. ObjectIds
7. Boolean
8. Arrays
9. Connections
10. Operation Buffering
11. Error Handling
12. Callback
13. Connection String Options
14. Array Mixed Type
15. Maps
16. Getters
17. SchemaType
18. Schema Types
19. Indexes
20. String Schema Types
21. Number Schema Types
22. Date Schema Types
23. Dates
24. Models
25. Querying
26. Deleting
27. Updating
28. Documents
29. Retrieving
30. Updating
31. Validation Errors
32. Cast Errors
33. Validation
34. Overwriting
35. Subdocuments
36. Validation
37. Modify Nested Documents
38. Adding Subdocuments to Arrays
39. Removing Subdocuments
40. Queries
41. Streaming
42. Cursor Timeout
43. Aggregation
44. Query Methods
45. Parents of Subdocuments
46. Alternative Declaration Syntax for Arrays
47. Alternative Declaration Syntax for Single Nested Subdocuments
48. Required Validators on Nested Objects
49. Update Validators
50. Update Validators and this
51. Save/Validate Hooks
52. Query Middleware
53. Aggregation Hooks
54. Error Hooks
55. Synchronous Hooks
56. Removing Foreign Documents
57. Field Selection
58. Populating Multiple Paths
59. Query Conditions and Other Options
60. Update Validators Only Run On Updated Paths
61. Middleware
62. Pre Middleware
63. Populate
64. Setting Populated Fields
65. Checking Whether a Field is Populated
66. Errors in Pre Hooks
67. Post Middleware
68. Async Post Hooks
69. Define Middleware Before Compiling Models
70. limit vs. perDocumentLimit
71. Refs to Children
72. Dynamic References via refPath
73. Populating an Existing Document
74. Populating Multiple Existing Documents
75. Cross-Database Populate
76. Discriminators
77. Discriminators Save to the Model’s Collection
78. Discriminator Keys
79. Embedded Discriminators in Arrays
80. Recursive Embedded Discriminators in Arrays
81. Discriminators and Queries
82. Discriminators Pre and Post Hooks
83. Handling Custom _id Fields
84. Using Discriminators with Model.create()
85. Populate Virtuals
86. Populate Virtuals: The Count Option
87. Populate in Middleware
88. Discriminators and Single Nested Documents
89. Plugins
90. Conclusion
Working with MongoDB Data in
Node.js Apps with Mongoose
To make MongoDB database manipulation easy, we can use the Mongoose
NPM package to make working with MongoDB databases easier.

In this books, we’ll look at how to use Mongoose to manipulate our


MongoDB database.
Getting Started with Mongoose
We can install the package by running:
npm install mongoose --save

Then we can use it by writing:


const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});

We connect th our MongoDB database with Mongoose by using the


mongoose.connect method.

Then to see if we connected successfully, we can listen to the open event.


Defining Schemas
Now we can define a schema to restrict what we can put on our documents.

This is something that’s not available with the native MongoDB client.

For example, we can write:


const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});

const kittySchema = new mongoose.Schema({


name: String
});

const Kitten = mongoose.model('Kitten', kittySchema);


const baby = new Kitten({ name: 'james' });
baby.save((err, baby) => {
if (err) {
return console.error(err);
}
console.log(baby.name);
});

We create the schema with the mongoose.Schema constructor.

The object has the fields as the keys and the data type as the values.

Then we define the model with the mongoose.model method.

Then we use the Kitten constructor, and then we call save to save the
Kitten object to the database.

The _id will automatically be generated for each inserted entry.


If we want to get the entries, then we can call the find method:
const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});

const kittySchema = new mongoose.Schema({


name: String
});

const Kitten = mongoose.model('Kitten', kittySchema);


const baby = new Kitten({ name: 'james' });
baby.save((err, baby) => {
if (err) {
return console.error(err);
}
console.log(baby.name);
});

Kitten.find((err, kittens) => {


if (err) return console.error(err);
console.log(kittens);
})

If we want to find a specific entry we can pass in an object to the first


argument of find :
const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});

const kittySchema = new mongoose.Schema({


name: String
});

kittySchema.methods.speak = function () {
console.log(`hello ${this.name}`);
}

const Kitten = mongoose.model('Kitten', kittySchema);


const baby = new Kitten({ name: 'james' });
baby.speak();
baby.save((err, baby) => {
if (err) {
return console.error(err);
}
console.log(baby.name);
});

Kitten.find({ name: /^james/ },(err, kittens) => {


if (err) return console.error(err);
console.log(kittens);
})

If we want to add methods, we can write:


const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});

const kittySchema = new mongoose.Schema({


name: String
});

kittySchema.methods.speak = function () {
console.log(`hello ${this.name}`);
}

const Kitten = mongoose.model('Kitten', kittySchema);


const baby = new Kitten({ name: 'baby' });
baby.speak();
baby.save((err, baby) => {
if (err) {
return console.error(err);
}
console.log(baby.name);
});
Kitten.find((err, kittens) => {
if (err) return console.error(err);
console.log(kittens);
})

We added a method to the Kitten model by adding a method to the


methods object property.
Buffer
We can declare the Buffer type with the Buffer constructor:
const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});
const Binary = mongoose.model('binary', { binData: Buffer });
Mixed
We can also add a mixed type to let us add anything as the value for a field:
const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});
const Mixed = mongoose.model('mixed', { any: mongoose.Mixed });

We can’t autodetect and save changes when we use this type since it’s a
schema-less type.
ObjectIds
We can specify the ObjectId type to store object IDs.

For example, we can write:


const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});
const carSchema = new mongoose.Schema({ driver:
mongoose.ObjectId });
const Car = mongoose.model('Car', carSchema);
const car = new Car();
car.driver = new mongoose.Types.ObjectId();
console.log(typeof car.driver)
console.log(car.driver instanceof mongoose.Types.ObjectId);
car.driver.toString();

We created a carSchema to store data about cars.

Inside the schema, we have the driver field, which is of type


mongoose.ObjectId .

We can create the object ID to assign with the


mongoose.Types.ObjectId() method.

Then we can check the types of car.driver . We should get the type is
'object' .

The 2nd console log should be true since driver is of type ObjectId .
Boolean
Another type that we set to fields is the boolean type.

The following values are cast to true :

true
'true'
1
'1'
'yes'

And these values are cast to false :

false
'false'
0
'0'
'no'

For example, if we write:


const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});
const M = mongoose.model('Test', new mongoose.Schema({ b:
Boolean }));
console.log(new M({ b: 'nay' }).b);
console.log(mongoose.Schema.Types.Boolean.convertToFalse);
mongoose.Schema.Types.Boolean.convertToFalse.add('nay');
console.log(new M({ b: 'nay' }).b);

We called mongoose.Schema.Types.Boolean.convertToFalse to let us


cast 'nay' to false .
Arrays
Mongoose has an array type that lets us specify arrays.

For example, we can write:


const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});
const ToySchema = new mongoose.Schema({ name: String });
const ToyBoxSchema = new mongoose.Schema({
toys: [ToySchema],
strings: [String],
});

We have the ToySchema Mongoose schema, which we specify as the type of


the toys array field.

Each entry of the toys array must conform to the ToySchema .


Connections
We can connect to a MongoDB database with Mongoose.

To do that, we write:
const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});

We connect to the server with the URL and the test collection name.
Operation Buffering
We can start using the models immediately without waiting fir Mongoose to
establish a connection.

So we can write:
const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});

async function run() {


const Assignment = mongoose.model('Assignment', { dueDate:
Date });
const assignment = new Assignment({ dueDate: new Date() });
await assignment.save();
Assignment.findOne((err, doc) => {
doc.dueDate.setMonth(3);
doc.save();
doc.markModified('dueDate');
doc.save();
})
}
run();

We create our model and use it without waiting for the connect to complete
since we have the database code outside the callback.

This is because Mongoose buffers the model function calls internally.

However, Mongoose won’t throw any errors if we use models without


connecting.

We can disable buffering by setting bufferCommands to false :


const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
mongoose.set('bufferCommands', false);
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});

We can create collections with the createCollection method:


const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});

async function run() {


const schema = new mongoose.Schema({
name: String
}, {
capped: { size: 1024 },
bufferCommands: false,
autoCreate: false
});

const Model = mongoose.model('Test', schema);


await Model.createCollection();
}
run();

Now we’ll create the tests collection to create the collection.


Error Handling
We can catch any errors that are raised with the catch method or try-catch
with async and await since mongoose.connect returns a promise.

For example, we can write:


const mongoose = require('mongoose');
async function run() {
try {
await mongoose.connect('mongodb://localhost:27017/test', {
useNewUrlParser: true });
} catch (error) {
handleError(error);
}
}
run();

or:
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/test', {
useNewUrlParser: true }).
catch(error => handleError(error));

to connect to a database.

We can also listen to the error event as we did in the example above.
Callback
The mongoose.connect method takes a callback to so we can check for
errors.

For example, we can write:


const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/test', {
useNewUrlParser: true }, function (error) {
console.log(error);
})
Connection String Options
We can add connection options to the connection string.

For example, we can write:


const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/test?
connectTimeoutMS=1000&bufferCommands=false')

We set the connection timeout with the connectTimeoutMS option is set to


1000ms to set.

And we also set bufferCommands option to false .


Array Mixed Type
If we specify an empty array as the type, then the type is considered a
mixed type.

For instance, if we have:


const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});
const Empty1 = new mongoose.Schema({ any: [] });
const Empty2 = new mongoose.Schema({ any: Array });
const Empty3 = new mongoose.Schema({ any:
[mongoose.Schema.Types.Mixed] });
const Empty4 = new mongoose.Schema({ any: [{}] });

then they all have a any field with the mixed type.
Maps
Mongoose version 5.1.0 or later has the Map type to store key-value pairs.

For example, we can write:


const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});
const userSchema = new mongoose.Schema({
socialMediaHandles: {
type: Map,
of: String
}
});

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


console.log(new User({
socialMediaHandles: {
github: 'abc',
twitter: '@abc'
}
}).socialMediaHandles);

We created the userSchema with the Map type.

The of property sets the type of the value.

Then we can pass in the object to specify the key-value pairs for the map.

We can also write:


const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});
const userSchema = new mongoose.Schema({
socialMediaHandles: {
type: Map,
of: String
}
});

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


const user = new User({
socialMediaHandles: {}
});
user.socialMediaHandles.set('github', 'abc');
user.set('socialMediaHandles.twitter', '@abc');
console.log(user.socialMediaHandles.get('github'));
console.log(user.get('socialMediaHandles.twitter'));
user.save();

We can access the socialMediaHandles property or put the property name


in the string in the first argument of the set method.

And we can get the value by the key with the get method.
Getters
We can add getters to our schema fields.

For example, we can write:


const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});
const root = 'https://s3.amazonaws.com/mybucket';

const userSchema = new mongoose.Schema({


name: String,
picture: {
type: String,
get: v => `${root}${v}`
}
});

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

const doc = new User({ name: 'Val', picture: '/123.png' });


console.log(doc.picture);
console.log(doc.toObject({ getters: false }).picture);

to add a getter to the picture field with the get method.

By default the picture property has the return value of the getter.

But we can also set the getters property to false to get the picture field’s
value.
SchemaType
A SchemaType is a configuration object for an individual property.

For example, we can write:


const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});

const kittySchema = new mongoose.Schema({


name: String
});
console.log(kittySchema.path('name') instanceof
mongoose.SchemaType);

to check that the name field in the kittySchema is an instance of


mongoose.SchemaType .

It should return true , so we know that name is a SchemaType .

If we have nested fields in a schema, we also have to set the types for the
nested fields:
const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});

const kittySchema = new mongoose.Schema({


name: { type: String },
nested: {
firstName: { type: String },
lastName: { type: String }
}
});
Schema Types
With Mongoose, we can define a few schema types with our field.

They include:

required: boolean or function, if true adds a required validator for this


property
default: Any or function, sets a default value for the path. If the value
is a function, the return value of the function is used as the default.
select: boolean, specifies default projections for queries
validate: function, adds a validator function for this property
get: function, defines a custom getter for this property using
Object.defineProperty().
set: function, defines a custom setter for this property using
Object.defineProperty().
alias: string, available with Mongoose >= 4.10.0 only. Defines a
virtual with the given name that gets/sets this path.
immutable: boolean, defines path as immutable. Mongoose prevents
you from changing immutable paths unless the parent document has
isNew: true.
transform: function, Mongoose calls this function when you call
Document#toJSON() function, including when you JSON.stringify()
a document.

For example, we can write:


const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});

const schema = new mongoose.Schema({


integerOnly: {
type: Number,
get: v => Math.round(v),
set: v => Math.round(v),
alias: 'i'
}
});

to create a schema with the integerOnly field.

We control how the value is get and set with the get and set methods
respectively.

And we added an alias property to define another name we can access it


with.
Indexes
We can add indexes for fields. For example, we can write:
const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});

const schema = new mongoose.Schema({


test: {
type: String,
index: true,
unique: true
}
});

to add the test schema with the index property set to true to add an index
for the test field.

The unique property set to true will add a unique index.


String Schema Types
We can set various properties for strings schema types.

They include:

lowercase: boolean, whether to always call .toLowerCase() on the


value
uppercase: boolean, whether to always call .toUpperCase() on the
value
trim: boolean, whether to always call .trim() on the value
match: RegExp, creates a validator that checks if the value matches the
given regular expression
enum: Array, creates a validator that checks if the value is in the given
array.
minlength: Number, creates a validator that checks if the value length
is not less than the given number
maxlength: Number, creates a validator that checks if the value length
is not greater than the given number

For example, we can write:


const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});

const schema = new mongoose.Schema({


test: {
type: String,
enum: ['apple', 'orange']
}
});

to make the test field an enum.


Number Schema Types
For number schema types, we can set the following properties for the field:

min: Number, creates a validator that checks if the value is greater than
or equal to the given minimum.
max: Number, creates a validator that checks if the value is less than or
equal to the given maximum.
enum: Array, creates a validator that checks if the value is strictly equal
to one of the values in the given array.
Date Schema Types
For date schema types, we can set:

min: Date
max: Date

We can use the schema types by writing:


const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});

const personSchema = new mongoose.Schema({


name: String
});

const Person = mongoose.model('Person', personSchema);


const person = new Person({ name: { toString: () => 42 } });
person.save();
console.log(person.name);

We have the toString method that converts 42 into a string.

The field can also be rewritten as:


const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});

const personSchema = new mongoose.Schema({


name: 'String'
});

const Person = mongoose.model('Person', personSchema);


const person = new Person({ name: { toString: () => 42 } });
person.save();
console.log(person.name);

The value is 'String' instead of the String constructor.

They do the same thing.


Dates
If we have date fields, we can call various methods to change its value:
const mongoose = require('mongoose');
const connection = "mongodb://localhost:27017/test";
mongoose.connect(connection, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', () => console.error('connection error:'));
db.once('open', () => {
console.log('connected')
});

async function run() {


const Assignment = mongoose.model('Assignment', { dueDate:
Date });
const assignment = new Assignment({ dueDate: new Date() });
await assignment.save();
Assignment.findOne((err, doc) => {
doc.dueDate.setMonth(3);
doc.save();
doc.markModified('dueDate');
doc.save();
})
}
run();

We call the setMonth method to set the month.


Models
We can create models so we can use them as templates to create a document
in the MongoDB database.

For example, we can write:


const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/test')
const schema = new mongoose.Schema({ name: 'string', size:
'string' });
const Tank = mongoose.model('Tank', schema);
const small = new Tank({ size: 'small' });
small.save((err) => {
if (err) {
return console.log(err);
}
});

We called mongoose.Schema that has the name and size string fields.

Then we create the model with the schema with the mongoose.model
method.

Next, we use the model class to create the document with the data we want.

Then we call save to save the document.

The tank collection will be created if it’s not already created.

Then the document will be created.

The callback we pass into the save method has the err parameter that will
be defined if there’s an error.

We can also add the create static method to create the document:
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/test')
const schema = new mongoose.Schema({ name: 'string', size:
'string' });
const Tank = mongoose.model('Tank', schema);
Tank.create({ size: 'small' }, (err, small) => {
if (err) {
return console.log(err);
}
console.log(small);
});

The first argument is the document we want to create.

And the 2nd argument is the callback that’s called when the result is
computed.

Also, we can call the insertMany static method on the Tank model:
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/test')
const schema = new mongoose.Schema({ name: 'string', size:
'string' });
const Tank = mongoose.model('Tank', schema);
Tank.insertMany([{ size: 'small' }], (err) => {
if (err) {
console.log(err);
}
});

If we create a custom collection, then we can use the model function to


create the model:
const mongoose = require('mongoose');
const connection =
mongoose.createConnection('mongodb://localhost:27017/test');
const schema = new mongoose.Schema({ name: 'string', size:
'string' });
const Tank = connection.model('Tank', schema);
Tank.insertMany([{ size: 'small' }], (err) => {
if (err) {
console.log(err);
}
});
Querying
We can query documents with the find method.

For example, we can write:


const mongoose = require('mongoose');
const connection =
mongoose.createConnection('mongodb://localhost:27017/test');
const schema = new mongoose.Schema({ name: 'string', size:
'string' });
const Tank = connection.model('Tank', schema);
Tank.find({ size: 'small'
}).where('createdDate').gt(365).exec((err, tanks) => {
if (err) {
return console.log(err)
}
console.log(tanks);
});

We call find with the key and value we’re looking for.

The where method has the field that we want to search for.

gt searches for something that’s greater than.

exec runs the query, and the callback has the results.
Deleting
We can delete items with the deleteOne method:
const mongoose = require('mongoose');
const connection =
mongoose.createConnection('mongodb://localhost:27017/test');
const schema = new mongoose.Schema({ name: 'string', size:
'string' });
const Tank = connection.model('Tank', schema);
Tank.deleteOne({ size: 'large' }, (err) => {
if (err) {
return console.log(err);
}
});

We delete the first item that has the size field equal to 'large' .
Updating
We can update the first with the given key and value with the updateOne
method.

For example, we can write:


const mongoose = require('mongoose');
const connection =
mongoose.createConnection('mongodb://localhost:27017/test');
const schema = new mongoose.Schema({ name: 'string', size:
'string' });
const Tank = connection.model('Tank', schema);
Tank.updateOne({ size: 'small' }, { name: 'small tank' }, (err)
=> {
if (err) {
return console.log(err);
}
});

The first argument is the query for the document.

The 2nd argument is the key-value pair we want to update the document
with.

The 3rd argument is the callback that’s called after the operation is done.
Documents
Mongoose documents are one to one mapping to documents as stored in
MongoDB.

Each document is an instance of its model.

For example, if we have:


const mongoose = require('mongoose');
const connection =
mongoose.createConnection('mongodb://localhost:27017/test');
const schema = new mongoose.Schema({ name: 'string', size:
'string' });
const Tank = connection.model('Tank', schema);
const tank = new Tank({ name: 'big tank' });
console.log(tank instanceof Tank);
console.log(tank instanceof mongoose.Model);
console.log(tank instanceof mongoose.Document);

then we can see all the console logs are true .

The tank document is an instance of the Tank constructor.

It’s also an instance of the Model constructor and the Document constructor.
Retrieving
When we retrieve a document with findOne , it returns a promise that
resolves to a document.

For example, if we have:


const { runInContext } = require('vm');

async function run() {


const mongoose = require('mongoose');
const connection =
mongoose.createConnection('mongodb://localhost:27017/test');
const schema = new mongoose.Schema({ name: 'string', size:
'string' });
const Tank = connection.model('Tank', schema);
const tank = new Tank({ name: 'big tank' });
await tank.save();
const doc = await Tank.findOne();
console.log(doc instanceof Tank);
console.log(doc instanceof mongoose.Model);
console.log(doc instanceof mongoose.Document);
}
run();

We see again that all 3 console logs are true , so we know that documents
are retrieved with findOne .
Updating
We can modify a document and call save to update a document.

For example, we can write:


const { runInContext } = require('vm');

async function run() {


const mongoose = require('mongoose');
const connection =
mongoose.createConnection('mongodb://localhost:27017/test');
const schema = new mongoose.Schema({ name: 'string', size:
'string' });
const Tank = connection.model('Tank', schema);
const tank = new Tank({ name: 'big tank' });
await tank.save();
const doc = await Tank.findOne();
doc.name = 'foo';
await doc.save();
}
run();

We get the document we want to change with the findOne method.

Then we set the property to the given value and the call save to save the
changes.

If the document with the given _id isn’t found, then we’ll get a
DocumentNotFoundError raised:

const { runInContext } = require('vm');

async function run() {


const mongoose = require('mongoose');
const connection =
mongoose.createConnection('mongodb://localhost:27017/test');
const schema = new mongoose.Schema({ name: 'string', size:
'string' });
const Tank = connection.model('Tank', schema);
const tank = new Tank({ name: 'big tank' });
tank.save();
const doc = await Tank.findOne();
try {
await Tank.deleteOne({ _id: doc._id });
doc.name = 'foo';
await doc.save();
} catch (error) {
console.log(error)
}
}
run();

We can also update all documents with the updateMany method:


const { runInContext } = require('vm');

async function run() {


const mongoose = require('mongoose');
const connection =
mongoose.createConnection('mongodb://localhost:27017/test');
const schema = new mongoose.Schema({ name: 'string', size:
'string' });
const Tank = connection.model('Tank', schema);
const tank = new Tank({ name: 'big tank' });
tank.save();
await Tank.updateMany({}, { $set: { name: 'foo' } });
}
run();

We update all documents in the tanks collection by calling the updateMany


method with the 2nd argument being what we want to set.

This is indicated by the $set property.

The first argument of updateMany is the query object. # 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.
Validation
We can validate documents before saving by using the validate method.

For example, we can write:


const { runInContext } = require('vm');

async function run() {


const mongoose = require('mongoose');
const connection =
mongoose.createConnection('mongodb://localhost:27017/test');
const schema = new mongoose.Schema({ name: 'string', size:
'string' });
const Tank = connection.model('Tank', schema);
let t = new Tank({ name: 'foo', size: 'small' });
await t.validate();
let t2 = new Tank({ name: 'foo', size: -1 });
await t2.validate();
}
run();

validate is run internally so we don’t have to run it ourselves.

We can set the runValidators property explicitly to control whether we


want to run the validator:
const { runInContext } = require('vm');

async function run() {


const mongoose = require('mongoose');
const connection =
mongoose.createConnection('mongodb://localhost:27017/test');
const schema = new mongoose.Schema({ name: 'string', size:
'string' });
const Tank = connection.model('Tank', schema);
Tank.updateOne({}, { size: -1 }, { runValidators: true });
}
run();
Overwriting
We can overwrite a document.

For example, we can write:


const { runInContext } = require('vm');

async function run() {


const mongoose = require('mongoose');
const connection =
mongoose.createConnection('mongodb://localhost:27017/test');
const schema = new mongoose.Schema({ name: 'string', size:
'string' });
const Tank = connection.model('Tank', schema);
const doc = await Tank.findOne({ });
doc.overwrite({ name: 'James' });
await doc.save();
}
run();

to call the overwrite method to change the first tanks document with the
name field set to 'James' .
Subdocuments
Subdocuments are documents that are embedded in other documents.

We can nest schemas in other schemas.

For example, we can write:


const { runInContext } = require('vm');

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({ children: [{ name: 'Matt' }, {
name: 'Sarah' }] })
await parent.save();
}
run();

We created the Child and Parent schemas with the Child schema
embedded in the Parent schema.

The children property has an array of documents that fie the Child
schema.

When we call save on the parent , everything embedded inside will also be
saved.

We can watch the validate and save events for each schema.

For example, we can write:


const { runInContext } = require('vm');

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
});

childSchema.pre('validate', function (next) {


console.log('2');
next();
});

childSchema.pre('save', function (next) {


console.log('3');
next();
});

parentSchema.pre('validate', function (next) {


console.log('1');
next();
});

parentSchema.pre('save', function (next) {


console.log('4');
next();
});
const Child = await connection.model('Child', childSchema);
const Parent = await connection.model('Parent',
parentSchema);
const parent = new Parent({ children: [{ name: 'Matt' }, {
name: 'Sarah' }] })
await parent.save();
}
run();

We see that they have the number logged.

They should run in the order in the same order the numbers are in.

So first the parent is validated, then the children are validated.


The children documents are saved, and then the parent is saved.
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 username 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.
Modify Nested Documents
We can modify nested documents by accessing the path and then set the
value for it.

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({ children: \[{ name: 'Matt' }, {
name: 'Sarah' }\] })
await parent.save();
parent.children\[0\].name = 'Mary';
await parent.save();
console.log(parent);
}
run();

We get the children subarray’s first entry’s name property and set it to
'Mary' .

Then we call save on the parent to save everything.

We can also call the set method to do the same thing.

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({ children: \[{ name: 'Matt' }, {
name: 'Sarah' }\] })
await parent.save();
parent.set({ children: \[{ name: 'Mary' }\] });
await parent.save();
console.log(parent);
}
run();

We replaced the children array with the given entry.

Then we save the parent and child entries with the save method.
Adding Subdocuments to Arrays
We can add subdocuments to arrays.

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({ children: \[{ name: 'Matt' }, {
name: 'Sarah' }\] })
parent.children.push({ name: 'Jane' });
await parent.save();
console.log(parent);
}
run();

We call push on the children array to append an entry to the children


subarray.
Removing Subdocuments
To remove subdocuments, we can call the remove method.

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({ children: \[{ name: 'Matt' }, {
name: 'Sarah' }\] })
await parent.children\[0\].remove();
await parent.save();
console.log(parent);
}
run();

to remove a document from the children array.

To remove an object, we can call the remove method on the object:


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' } })
await parent.child.remove();
await parent.save();
console.log(parent);
}
run();
Queries
Mongoose comes with various query methods.

We can use the findOne method to return the first entry that matches the
query.

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: 'waiter'
})
await person.save();
const p = await Person.findOne({ 'name.last': 'smith' },
'name occupation');
console.log(p);
}
run();

We create the Person schema and save a document that’s created from the
Person constructor.

Then we call findOne to find an entry.


The first argument is an object with the query. 'name.last' is the path to
the nested field.

The 2nd argument is a string with the columns that we want to select.

Then we can get the result from the thenable object.

Even though we can use async and await , with the findOne method, it
doesn’t return a promise.

Then then method is provided so that we can use the async and await
syntax.

We should not mix promises and callbacks.

For example, we shouldn’t write code like:


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: 'waiter'
})
await person.save();
const update = { name: { first: ['alex'] } };
const p = await Person.updateOne({ 'name.first': 'james' },
update, (err, res) => {
console.log(res);
});
console.log(p)
}
run();
Streaming
We can stream query results from MongoDB.

To do that, we call the cursor method:


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();
let doc;
while (doc = await cursor.next()) {
console.log(doc);
}
}
run();

We created 2 Person documents.


Then we call find with the query to return the ones with the occupation
field that matches the /host/ regex pattern.

Then we use the while loop to get the cursor.next() method to get the
next item from the cursor object.

We can also use the for-await-of loop to do the same thing:


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 query = Person.find({ occupation: /host/ });
for await (const doc of query) {
console.log(doc);
}
}
run();

We don’t need the cursor method anymore since find and other query
methods return the cursor when we use it with for-await-of .
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.


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.
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.
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.


Removing Foreign Documents
If we remove foreign documents, then when we try to reference a linked
foreign document, it’ll return null .

For instance, if we have:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const connection =
createConnection('mongodb://localhost:27017/test');
const personSchema = Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});
const storySchema = Schema({
author: { type: Schema.Types.ObjectId, ref: 'Person' },
title: String,
fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});
const Story = connection.model('Story', storySchema);
const Person = connection.model('Person', personSchema);
const author = new Person({
_id: new Types.ObjectId(),
name: 'James Smith',
age: 50
});
await author.save();
const story1 = new Story({
title: 'Mongoose Story',
author: author._id
});
await story1.save();
await Person.deleteMany({ name: 'James Smith' });
const story = await Story.findOne({ title: 'Mongoose Story'
}).populate('author');
console.log(story)
}
run();
We created a Story document that is linked to an Author .

Then we deleted the Person with the name 'James Smith' .

Now when we retrieve the latest value of the story with the author with
populate , we’ll see that story is null .
Field Selection
We can select a few fields instead of selecting all the fields when we call
populate .

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const connection =
createConnection('mongodb://localhost:27017/test');
const personSchema = Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});
const storySchema = Schema({
author: { type: Schema.Types.ObjectId, ref: 'Person' },
title: String,
fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});
const Story = connection.model('Story', storySchema);
const Person = connection.model('Person', personSchema);
const author = new Person({
_id: new Types.ObjectId(),
name: 'James Smith',
age: 50
});
await author.save();
const story1 = new Story({
title: 'Mongoose Story',
author: author._id
});
await story1.save();
const story = await Story.findOne({ title: 'Mongoose Story'
})
.populate('author', 'name')
.exec();
console.log(story)
}
run();
We save the author and story1 documents into our database.

Then we call findOne to get the story document with the author and the
name properties.
Populating Multiple Paths
If we call populate multiple times, then only the last one will take effect.
Query Conditions and
Other Options
The populate method can accept query conditions.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const connection =
createConnection('mongodb://localhost:27017/test');
const personSchema = Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});
const storySchema = Schema({
author: { type: Schema.Types.ObjectId, ref: 'Person' },
title: String,
fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});
const Story = connection.model('Story', storySchema);
const Person = connection.model('Person', personSchema);
const author = new Person({
_id: new Types.ObjectId(),
name: 'James Smith',
age: 50
});
await author.save();
const fan = new Person({
_id: new Types.ObjectId(),
name: 'Fan Smith',
age: 50
});
await fan.save();
const story1 = new Story({
title: 'Mongoose Story',
author: author._id,
fans: [fan._id]
});
await story1.save();
const story = await Story.findOne({ title: 'Mongoose Story'
})
.populate({
path: 'fans',
match: { age: { $gte: 21 } },
select: 'name -_id'
})
.exec();
console.log(story.fans)
}
run();

We add a Person entry to the fans array.

Then when we call populate with an object that finds all the fans with age
greater than or equal to 21, we should get our fan entry.

The select property has a string that lets us select the name field but not the
_id as indicated by the - sign before the _id .

So the console log should show [{“name”:”Fan Smith”}] .


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();
Populate
We can use the populate method to join 2 models together.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const connection =
createConnection('mongodb://localhost:27017/test');
const personSchema = Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});

const storySchema = Schema({


author: { type: Schema.Types.ObjectId, ref: 'Person' },
title: String,
fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});

const Story = connection.model('Story', storySchema);


const Person = connection.model('Person', personSchema);
const author = new Person({
_id: new Types.ObjectId(),
name: 'James Smith',
age: 50
});

author.save(function (err) {
if (err) return handleError(err);

const story1 = new Story({


title: 'Mongoose Story',
author: author._id
});

story1.save(function (err) {
if (err) {
return console.log(err);
}

Story.
findOne({ title: 'Mongoose Story' }).
populate('author').
exec(function (err, story) {
if (err) {
return console.log(err);
}
console.log('author', story.author.name);
});
});
});
}
run();

We created the Story and Person models.

The Story model references the Person model by setting author._id as the
value of the author field.

Then we save both the author and story.

Then in the callback for story1.save , we get the Story entry with the
exec method with a callback to get the data.

Then we can access the author’s name of the story with the
story.author.name property.

The documents created with the models will be saved in their own
collections.

Therefore the story.author.name ‘s value is 'James Smith' .


Setting Populated Fields
We can also set the author value directly by writing:
async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const connection =
createConnection('mongodb://localhost:27017/test');
const personSchema = Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});

const storySchema = Schema({


author: { type: Schema.Types.ObjectId, ref: 'Person' },
title: String,
fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});

const Story = connection.model('Story', storySchema);


const Person = connection.model('Person', personSchema);
const author = new Person({
_id: new Types.ObjectId(),
name: 'James Smith',
age: 50
});

author.save(function (err) {
if (err) return handleError(err);

const story1 = new Story({


title: 'Mongoose Story',
});

story1.save(function (err) {
if (err) {
return console.log(err);
}

Story.findOne({ title: 'Mongoose Story' }, function


(error, story) {
if (error) {
return console.log(error);
}
story.author = author;
console.log(story.author.name);
});
});
});
}
run();

We set story.author to author to link the author to the story .

And now story.author.name is 'James Smith' .


Checking Whether a Field is
Populated
We can check whether a field is populated with the populated method.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const connection =
createConnection('mongodb://localhost:27017/test');
const personSchema = Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});

const storySchema = Schema({


author: { type: Schema.Types.ObjectId, ref: 'Person' },
title: String,
fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});

const Story = connection.model('Story', storySchema);


const Person = connection.model('Person', personSchema);
const author = new Person({
_id: new Types.ObjectId(),
name: 'James Smith',
age: 50
});

author.save(function (err) {
if (err) return handleError(err);

const story1 = new Story({


title: 'Mongoose Story',
});

story1.save(function (err) {
if (err) {
return console.log(err);
}

Story.findOne({ title: 'Mongoose Story' }, function


(error, story) {
if (error) {
return console.log(error);
}
story.author = author;
console.log(story.populated('author'));
});
});
});
}
run();

We get the ID of the author that the store is populated with.

We call depopulate to unlink the author and the story .

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const connection =
createConnection('mongodb://localhost:27017/test');
const personSchema = Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});

const storySchema = Schema({


author: { type: Schema.Types.ObjectId, ref: 'Person' },
title: String,
fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});

const Story = connection.model('Story', storySchema);


const Person = connection.model('Person', personSchema);
const author = new Person({
_id: new Types.ObjectId(),
name: 'James Smith',
age: 50
});

author.save(function (err) {
if (err) return handleError(err);

const story1 = new Story({


title: 'Mongoose Story',
});

story1.save(function (err) {
if (err) {
return console.log(err);
}

Story.findOne({ title: 'Mongoose Story' }, function


(error, story) {
if (error) {
return console.log(error);
}
story.author = author;
console.log(story.populated('author'));
story.depopulate('author');
console.log(story.populated('author'));
});
});
});
}
run();

We call the depopulate method to unlink the author and story.

Now when we log the value of populated again, we see undefined .


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.
limit vs. perDocumentLimit
Populate has a limit option, but it doesn’t support limit on a per-document
basis.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const connection =
createConnection('mongodb://localhost:27017/test');
const personSchema = Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});
const storySchema = Schema({
author: { type: Schema.Types.ObjectId, ref: 'Person' },
title: String,
fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});
const Story = connection.model('Story', storySchema);
const Person = connection.model('Person', personSchema);
const author = new Person({
_id: new Types.ObjectId(),
name: 'James Smith',
age: 50
});
await author.save();
const fan = new Person({
_id: new Types.ObjectId(),
name: 'Fan Smith',
age: 50
});
await fan.save();
const story1 = new Story({
title: 'Mongoose Story',
author: author._id,
fans: [fan._id]
});
await story1.save();
const story = await Story.findOne({ title: 'Mongoose Story'
})
.populate({
path: 'fans',
options: { limit: 2 }
})
.exec();
console.log(story.fans)
}
run();

to query up to numDocuments * limit .

Mongoose 5.9.0 or later supports the perDocumentLimit property to add a


per-document limit.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const connection =
createConnection('mongodb://localhost:27017/test');
const personSchema = Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});
const storySchema = Schema({
author: { type: Schema.Types.ObjectId, ref: 'Person' },
title: String,
fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});
const Story = connection.model('Story', storySchema);
const Person = connection.model('Person', personSchema);
const author = new Person({
_id: new Types.ObjectId(),
name: 'James Smith',
age: 50
});
await author.save();
const fan = new Person({
_id: new Types.ObjectId(),
name: 'Fan Smith',
age: 50
});
await fan.save();
const story1 = new Story({
title: 'Mongoose Story',
author: author._id,
fans: [fan._id]
});
await story1.save();
const story = await Story.findOne({ title: 'Mongoose Story'
})
.populate({
path: 'fans',
perDocumentLimit: 2
})
.exec();
console.log(story.fans)
}
run();
Refs to Children
If we call push to items to children, then we can get the refs to the child
items.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const connection =
createConnection('mongodb://localhost:27017/test');
const personSchema = Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});
const storySchema = Schema({
author: { type: Schema.Types.ObjectId, ref: 'Person' },
title: String,
fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});
const Story = connection.model('Story', storySchema);
const Person = connection.model('Person', personSchema);
const author = new Person({
_id: new Types.ObjectId(),
name: 'James Smith',
age: 50
});
await author.save();
const fan = new Person({
_id: new Types.ObjectId(),
name: 'Fan Smith',
age: 50
});
await fan.save();
const story1 = new Story({
title: 'Mongoose Story',
author: author._id,
});
story1.fans.push(fan);
await story1.save();
const story = await Story.findOne({ title: 'Mongoose Story'
})
.populate('fans')
.exec();
console.log(story.fans)
}
run();

We call push on story.fans to add an entry to the fans array field.

Now when we query the story, we get the fans array with the fan in it.
Dynamic References via refPath
We can join more than one model with dynamic references and the refPath
property.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const db =
createConnection('mongodb://localhost:27017/test');
const commentSchema = new Schema({
body: { type: String, required: true },
subject: {
type: Schema.Types.ObjectId,
required: true,
refPath: 'subjectModel'
},
subjectModel: {
type: String,
required: true,
enum: ['BlogPost', 'Product']
}
});
const Product = db.model('Product', new Schema({ name: String
}));
const BlogPost = db.model('BlogPost', new Schema({ title:
String }));
const Comment = db.model('Comment', commentSchema);
const book = await Product.create({ name: 'Mongoose for
Dummies' });
const post = await BlogPost.create({ title: 'MongoDB for
Dummies' });
const commentOnBook = await Comment.create({
body: 'Great read',
subject: book._id,
subjectModel: 'Product'
});
await commentOnBook.save();
const commentOnPost = await Comment.create({
body: 'Very informative',
subject: post._id,
subjectModel: 'BlogPost'
});
await commentOnPost.save();
const comments = await
Comment.find().populate('subject').sort({ body: 1 });
console.log(comments)
}
run();

We have the commentSchema that has the subject and subjectModel


properties.

The subject is set to an object ID. We have the refPath property that
references the model that it can reference.

The refPath is set to the subjectModel , and the subjectModel references


the BlogPost and Product models.

So we can link comments to a Product entry or a Post entry.

To do the linking to the model we want, we set the subject and


subjectModel when we create the entry with the create method.

Then we call populate with subject to get the subject field’s data.

Equivalently, we can put the related items into the root schema.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const db =
createConnection('mongodb://localhost:27017/test');
const commentSchema = new Schema({
body: { type: String, required: true },
product: {
type: Schema.Types.ObjectId,
required: true,
ref: 'Product'
},
blogPost: {
type: Schema.Types.ObjectId,
required: true,
ref: 'BlogPost'
}
});
const Product = db.model('Product', new Schema({ name: String
}));
const BlogPost = db.model('BlogPost', new Schema({ title:
String }));
const Comment = db.model('Comment', commentSchema);
const book = await Product.create({ name: 'Mongoose for
Dummies' });
const post = await BlogPost.create({ title: 'MongoDB for
Dummies' });
const commentOnBook = await Comment.create({
body: 'Great read',
product: book._id,
blogPost: post._id,
});
await commentOnBook.save();
const comments = await Comment.find()
.populate('product')
.populate('blogPost')
.sort({ body: 1 });
console.log(comments)
}
run();

Then we set the product and blogPost in the same object.

We rearranged the commentSchema to have the products and blogPost


references.

Then we call populate on both fields so that we can get the comments.
Populating an Existing Document
We can call populate on an existing document.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const connection =
createConnection('mongodb://localhost:27017/test');
const personSchema = Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});
const storySchema = Schema({
author: { type: Schema.Types.ObjectId, ref: 'Person' },
title: String,
fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});
const Story = connection.model('Story', storySchema);
const Person = connection.model('Person', personSchema);
const author = new Person({
_id: new Types.ObjectId(),
name: 'James Smith',
age: 50
});
await author.save();
const fan = new Person({
_id: new Types.ObjectId(),
name: 'Fan Smith',
age: 50
});
await fan.save();
const story1 = new Story({
title: 'Mongoose Story',
author: author._id,
});
story1.fans.push(fan);
await story1.save();
const story = await Story.findOne({ title: 'Mongoose Story'
})
await story.populate('fans').execPopulate();
console.log(story.populated('fans'));
console.log(story.fans[0].name);
}
run();

We created the story with the fans and author field populated.

Then we get the entry with the Story.findOne method.

Then we call populate in the resolved story object and then call
execPopulate to do the join.

Now the console log should see the fans entries displayed.
Populating Multiple Existing
Documents
We can populate across multiple levels.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const connection =
createConnection('mongodb://localhost:27017/test');
const userSchema = new Schema({
name: String,
friends: [{ type: Schema.Types.ObjectId, ref: 'User' }]
});
const User = connection.model('User', userSchema);
const user1 = new User({
_id: new Types.ObjectId(),
name: 'Friend',
});
await user1.save();
const user = new User({
_id: new Types.ObjectId(),
name: 'Val',
});
user.friends.push(user1);
await user.save();
const userResult = await User.
findOne({ name: 'Val' }).
populate({
path: 'friends',
populate: { path: 'friends' }
});
console.log(userResult);
}
run();

We have a User schema that is self-referencing.

The friends property references the User schema itself.


Then when we query the User , we call populate with the path to query.

We can query across multiple levels with the populate property.


Cross-Database Populate
We can populate across databases.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const db1 =
createConnection('mongodb://localhost:27017/db1');
const db2 =
createConnection('mongodb://localhost:27017/db2');

const conversationSchema = new Schema({ numMessages: Number


});
const Conversation = db2.model('Conversation',
conversationSchema);

const eventSchema = new Schema({


name: String,
conversation: {
type: Schema.Types.ObjectId,
ref: Conversation
}
});
const Event = db1.model('Event', eventSchema);
const conversation = new Conversation({ numMessages: 2 });
conversation.save();
const event = new Event({
name: 'event',
conversation
})
event.save();
const events = await Event
.findOne({ name: 'event' })
.populate('conversation');
console.log(events);
}
run();
We create 2 models with the Coversation and Event models that are linked
to different database connections.

We can create the Conversation and Event entries and link them together.

And then we can call findOne on Event to get the linked data.
Discriminators
Discriminators are a schema inheritance mechanism.

They let us enable multiple models to have overlapping schemas on top of


the same MongoDB collection.

For example, we can use them as follows:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const db =
createConnection('mongodb://localhost:27017/test');
const options = { discriminatorKey: 'kind' };

const eventSchema = new Schema({ time: Date }, options);


const Event = db.model('Event', eventSchema);
const ClickedLinkEvent = Event.discriminator('ClickedLink',
new Schema({ url: String }, options));
const genericEvent = new Event({ time: Date.now(), url:
'mongodb.com' });
console.log(genericEvent)

const clickedEvent =
new ClickedLinkEvent({ time: Date.now(), url: 'mongodb.com'
});
console.log(clickedEvent)
}
run();

We created an Event model from the eventSchema .

It has the discriminatorKey so that we get can discriminate between the 2


documents we create later.

To create the ClickedLinkEvent model, we call Event.discriminator to


create a model by inheriting from the Event schema.

We add the url field to the ClickedLinkEvent model.


Then when we add the url to the Event document and the
ClickedLinkEvent document, only the clickedEvent object has the url
property.

We get:
{ _id: 5f6f78f17f83ca22408eb627, time: 2020-09-26T17:22:57.690Z
}

as the value of genericEvent and:


{
_id: 5f6f78f17f83ca22408eb628,
kind: 'ClickedLink',
time: 2020-09-26T17:22:57.697Z,
url: 'mongodb.com'
}

as the value of clickedEvent .


Discriminators Save to the Model’s
Collection
We can save different kinds of events all at once.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const db =
createConnection('mongodb://localhost:27017/test');
const options = { discriminatorKey: 'kind' };

const eventSchema = new Schema({ time: Date }, options);


const Event = db.model('Event', eventSchema);

const ClickedLinkEvent = Event.discriminator('ClickedLink',


new Schema({ url: String }, options));

const SignedUpEvent = Event.discriminator('SignedUp',


new Schema({ user: String }, options));

const event1 = new Event({ time: Date.now() });


const event2 = new ClickedLinkEvent({ time: Date.now(), url:
'mongodb.com' });
const event3 = new SignedUpEvent({ time: Date.now(), user:
'mongodbuser' });
await Promise.all([event1.save(), event2.save(),
event3.save()]);
const count = await Event.countDocuments();
console.log(count);
}
run();

We created the eventSchema as an ordinary schema.

And the rest of the models are created from the Event.discriminator
method.
Then we created the models and saved them all.

And finally, we called Event.countDocuments to get the number of items


saved under the Event model.

Then count should be 3 since ClickedLinkEvent and SignedUpEvent both


inherit from Event itself.
Discriminator Keys
We can tell the difference between each type of models with the __t
property by default.

For instance, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const db =
createConnection('mongodb://localhost:27017/test');

const eventSchema = new Schema({ time: Date });


const Event = db.model('Event', eventSchema);

const ClickedLinkEvent = Event.discriminator('ClickedLink',


new Schema({ url: String }));

const SignedUpEvent = Event.discriminator('SignedUp',


new Schema({ user: String }));

const event1 = new Event({ time: Date.now() });


const event2 = new ClickedLinkEvent({ time: Date.now(), url:
'mongodb.com' });
const event3 = new SignedUpEvent({ time: Date.now(), user:
'mongodbuser' });
await Promise.all([event1.save(), event2.save(),
event3.save()]);
console.log(event1.__t);
console.log(event2.__t);
console.log(event3.__t);
}
run();

to get the type of data that’s saved from the console logs. We should get:
undefined
ClickedLink
SignedUp

logged.
We can add the discriminatorKey in the options to change the
discriminator key.

So we can write:
async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const db =
createConnection('mongodb://localhost:27017/test');
const options = { discriminatorKey: 'kind' };
const eventSchema = new Schema({ time: Date }, options);
const Event = db.model('Event', eventSchema);

const ClickedLinkEvent = Event.discriminator('ClickedLink',


new Schema({ url: String }, options));

const SignedUpEvent = Event.discriminator('SignedUp',


new Schema({ user: String }, options));

const event1 = new Event({ time: Date.now() });


const event2 = new ClickedLinkEvent({ time: Date.now(), url:
'mongodb.com' });
const event3 = new SignedUpEvent({ time: Date.now(), user:
'mongodbuser' });
await Promise.all([event1.save(), event2.save(),
event3.save()]);
console.log(event1.kind);
console.log(event2.kind);
console.log(event3.kind);
}
run();

to set the options for each model and then access the kind property instead
of __t and get the same result as before.
Embedded Discriminators
in Arrays
We can add embedded discriminators into arrays.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const db =
createConnection('mongodb://localhost:27017/test');
const eventSchema = new Schema({ message: String },
{ discriminatorKey: 'kind', _id: false });

const batchSchema = new Schema({ events: [eventSchema] });


const docArray = batchSchema.path('events');
const clickedSchema = new Schema({
element: {
type: String,
required: true
}
}, { _id: false });
const Clicked = docArray.discriminator('Clicked',
clickedSchema);
const Purchased = docArray.discriminator('Purchased', new
Schema({
product: {
type: String,
required: true
}
}, { _id: false }));

const Batch = db.model('EventBatch', batchSchema);


const batch = {
events: [
{ kind: 'Clicked', element: '#foo', message: 'foo' },
{ kind: 'Purchased', product: 'toy', message: 'world' }
]
};
const doc = await Batch.create(batch);
console.log(doc.events);
}
run();

We add the events array into the eventSchema .

Then we create the docArray by calling the batchSchema.path method so


that we can create the discriminator with the docArray method.

Then we create the discriminators by calling docArray.discriminator


method with the name of the model and the schema.

Next, we create the Batch model from the batchSchema so that we can
populate the model.

We call Batch.create with the batch object that has an events array
property to add the items.

The kind property has the type of object we want to add.

Then doc.events has the event entries, which are:


[
{ kind: 'Clicked', element: '#foo', message: 'foo' },
{ kind: 'Purchased', product: 'toy', message: 'world' }
]
Recursive Embedded
Discriminators in Arrays
We can also add embedded discriminators recursively in arrays.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const db =
createConnection('mongodb://localhost:27017/test');
const singleEventSchema = new Schema({ message: String },
{ discriminatorKey: 'kind', _id: false });

const eventListSchema = new Schema({ events:


[singleEventSchema] });

const subEventSchema = new Schema({


subEvents: [singleEventSchema]
}, { _id: false });

const SubEvent = subEventSchema.path('subEvents').


discriminator('SubEvent', subEventSchema);
eventListSchema.path('events').discriminator('SubEvent',
subEventSchema);

const Eventlist = db.model('EventList', eventListSchema);


const list = {
events: [
{ kind: 'SubEvent', subEvents: [{ kind: 'SubEvent',
subEvents: [], message: 'test1' }], message: 'hello' },
{ kind: 'SubEvent', subEvents: [{ kind: 'SubEvent',
subEvents: [{ kind: 'SubEvent', subEvents: [], message: 'test3'
}], message: 'test2' }], message: 'world' }
]
};

const doc = await Eventlist.create(list)


console.log(doc.events);
console.log(doc.events[1].subEvents);
console.log(doc.events[1].subEvents[0].subEvents);
}
run();

We create the singleEventSchema .

Then we use as the schema for the objects in the events array property.

Next, we create the subEventSchema the same way.

Then we create the SubEvent model calling the path and the
discriminator methods.

This will create the schema for the subEvents property.

Then we link that to the events property with the:


eventListSchema.path('events').discriminator('SubEvent',
subEventSchema);

call.

Now we have can subEvents properties in events array entrys that are
recursive.

Next, we create the list object with the events array that has the
subEvents property added recursively.

Then we call Eventlist.create to create the items.

The last 2 console logs should get the subevents from the 2nd event entry.
Discriminators and Queries
Queries are smart enough to take into account discriminators.

For example, if we have:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const db =
createConnection('mongodb://localhost:27017/test');
const options = { discriminatorKey: 'kind' };
const eventSchema = new Schema({ time: Date }, options);
const Event = db.model('Event', eventSchema);

const ClickedLinkEvent = Event.discriminator('ClickedLink',


new Schema({ url: String }, options));

const SignedUpEvent = Event.discriminator('SignedUp',


new Schema({ user: String }, options));

const event1 = new Event({ time: Date.now() });


const event2 = new ClickedLinkEvent({ time: Date.now(), url:
'mongodb.com' });
const event3 = new SignedUpEvent({ time: Date.now(), user:
'mongodbuser' });
await Promise.all([event1.save(), event2.save(),
event3.save()]);
const clickedLinkEvent = await ClickedLinkEvent.find({});
console.log(clickedLinkEvent);
}
run();

We called the ClickedLinkEvent.find method.

Therefore, we’ll get all the ClickedLinkEvent instances.


Discriminators Pre and Post Hooks
We can add pre and post hooks to schemas created with discriminators.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const db =
createConnection('mongodb://localhost:27017/test');
const options = { discriminatorKey: 'kind' };
const eventSchema = new Schema({ time: Date }, options);
const Event = db.model('Event', eventSchema);

const clickedLinkSchema = new Schema({ url: String },


options)
clickedLinkSchema.pre('validate', (next) => {
console.log('validate click link');
next();
});
const ClickedLinkEvent = Event.discriminator('ClickedLink',
clickedLinkSchema);

const event1 = new Event({ time: Date.now() });


const event2 = new ClickedLinkEvent({ time: Date.now(), url:
'mongodb.com' });
await event2.validate();
}
run();

to add a pre hook for the validate operation to the clickedLinkSchema .


Handling Custom _id Fields
If an _id field is set on the base schema, then it’ll always override the
discriminator’s _id field.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const db =
createConnection('mongodb://localhost:27017/test');
const options = { discriminatorKey: 'kind' };

const eventSchema = new Schema({ _id: String, time: Date },


options);
const Event = db.model('BaseEvent', eventSchema);

const clickedLinkSchema = new Schema({


url: String,
time: String
}, options);

const ClickedLinkEvent = Event.discriminator('ChildEventBad',


clickedLinkSchema);

const event1 = new ClickedLinkEvent({ _id: 'custom id', time:


'12am' });
console.log(typeof event1._id);
console.log(typeof event1.time);
}
run();

And from the console log, we can see that both the _id and time fields of
event1 are strings.

So the _id field is the same one as the eventSchema , but the
ClickedLinkEvent field has the same type as the one in
clickedLinkSchema .
Using Discriminators with
Model.create()
We can use discriminators with the Model.create method.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const db =
createConnection('mongodb://localhost:27017/test');
const shapeSchema = new Schema({
name: String
}, { discriminatorKey: 'kind' });

const Shape = db.model('Shape', shapeSchema);

const Circle = Shape.discriminator('Circle',


new Schema({ radius: Number }));
const Square = Shape.discriminator('Square',
new Schema({ side: Number }));

const shapes = [
{ name: 'Test' },
{ kind: 'Circle', radius: 5 },
{ kind: 'Square', side: 10 }
];
const [shape1, shape2, shape3] = await Shape.create(shapes);
console.log(shape1 instanceof Shape);
console.log(shape2 instanceof Circle);
console.log(shape3 instanceof Square);
}
run();

We created 3 schemas for shapes with the discriminator method.

Then we called Shape.create with an array of different shape objects.


We specified the type with the kind property since we set that as the
discriminator key.

Then in the console log, they should all log true since we specified the type
of each entry.

If it’s not specified, then it has the base type.


Populate Virtuals
We can control how 2 models are joined together.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const db =
createConnection('mongodb://localhost:27017/test');
const PersonSchema = new Schema({
name: String,
band: String
});

const BandSchema = new Schema({


name: String
});

BandSchema.virtual('members', {
ref: 'Person',
localField: 'name',
foreignField: 'band',
justOne: false,
options: { sort: { name: -1 }, limit: 5 }
});

const Person = db.model('Person', PersonSchema);


const Band = db.model('Band', BandSchema);
const person = new Person({ name: 'james', band: 'superband'
});
await person.save();
const band = new Band({ name: 'superband' });
await band.save();
const bands = await Band.find({}).populate('members').exec();
console.log(bands[0].members);
}
run();

We create the PersonSchema as usual, but the BandSchema is different.


We call the virtual method with the join field call members to get the
persons with the band name set to a given name.

The ref property is the name of the model we want to join.

localField is the field of the BandSchema that we want to join with


PersonSchema .

The foreignField is the field of PersonSchema that we want to join with


the BandSchema .

justOne means we only return the first entry of the join.

options has the options for querying.

Virtuals aren’t included in the toJSON() output by default.

If we want populate virtual to show when using functions that rely on


JSON.stringify() , then add the virtuals option and set it to true .

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const db =
createConnection('mongodb://localhost:27017/test');
const PersonSchema = new Schema({
name: String,
band: String
});

const BandSchema = new Schema({


name: String
}, { toJSON: { virtuals: true } });

BandSchema.virtual('members', {
ref: 'Person',
localField: 'name',
foreignField: 'band',
justOne: false,
options: { sort: { name: -1 }, limit: 5 }
});
const Person = db.model('Person', PersonSchema);
const Band = db.model('Band', BandSchema);
const person = new Person({ name: 'james', band: 'superband'
});
await person.save();
const band = new Band({ name: 'superband' });
await band.save();
const bands = await Band.find({}).populate('members').exec();
console.log(bands[0].members);
}
run();

to add the virtuals option to the BandSchema .

If we use populate projections, then foreignField should be included in


the projection:
async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const db =
createConnection('mongodb://localhost:27017/test');
const PersonSchema = new Schema({
name: String,
band: String
});

const BandSchema = new Schema({


name: String
}, { toJSON: { virtuals: true } });

BandSchema.virtual('members', {
ref: 'Person',
localField: 'name',
foreignField: 'band',
justOne: false,
options: { sort: { name: -1 }, limit: 5 }
});

const Person = db.model('Person', PersonSchema);


const Band = db.model('Band', BandSchema);
const person = new Person({ name: 'james', band: 'superband'
});
await person.save();
const band = new Band({ name: 'superband' });
await band.save();
const bands = await Band.find({}).populate({ path: 'members',
select: 'name band' }).exec();
console.log(bands[0].members);
}
run();

We call populate with an object with the path to get the virtual field and
the select property has a string with the field names that we want to get
separated by a space.
Populate Virtuals: The
Count Option
We can add the count option when we create a populate virtual to get the
count of the number of children we retrieved.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const db =
createConnection('mongodb://localhost:27017/test');
const PersonSchema = new Schema({
name: String,
band: String
});

const BandSchema = new Schema({


name: String
}, { toJSON: { virtuals: true } });

BandSchema.virtual('numMembers', {
ref: 'Person',
localField: 'name',
foreignField: 'band',
count: true
});

const Person = db.model('Person', PersonSchema);


const Band = db.model('Band', BandSchema);
const person = new Person({ name: 'james', band: 'superband'
});
await person.save();
const band = new Band({ name: 'superband' });
await band.save();
const doc = await Band.findOne({ name: 'superband' })
.populate('numMembers');
console.log(doc.numMembers);
}
run();
We created the PersonSchema and BandSchema schema objects.

We call the virtual method on the BandSchema schema with several


options.

The ref property is the model that the Band model is referencing.

localField is the field in the BandSchema we want to join with


PeronSchema .

foreignField is the field in the ForeignSchema we want to join with


PersonSchema .

count set to true means that we get the count. numMembers is the field name
that we get the count from.

Then we save a Person and Band document with the same name.

Then we call populate with the numMembers field to get the number of
members.

And finally, we get the numMembers field from the retrieved result to get
how many Person children entries are in the Band .
Populate in Middleware
We can add pre or post hooks to populate operations.

For example, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const db =
createConnection('mongodb://localhost:27017/test');
const PersonSchema = new Schema({
name: String,
band: String
});

const BandSchema = new Schema({


name: String
}, { toJSON: { virtuals: true } });

BandSchema.virtual('numMembers', {
ref: 'Person',
localField: 'name',
foreignField: 'band',
count: true
});

BandSchema.pre('find', function () {
this.populate('person');
});

BandSchema.post('find', async (docs) => {


for (const doc of docs) {
await doc.populate('person').execPopulate();
}
});

const Person = db.model('Person', PersonSchema);


const Band = db.model('Band', BandSchema);
const person = new Person({ name: 'james', band: 'superband'
});
await person.save();
const band = new Band({ name: 'superband' });
await band.save();
const doc = await Band.findOne({ name: 'superband' })
.populate('numMembers');
console.log(doc.numMembers);
}
run();

to add the pre and post hooks to listen to the find operation.

We should always call populate with the given field to do the population.
Discriminators and Single Nested
Documents
We can define discriminators on single nested documents.

For instance, we can write:


async function run() {
const { createConnection, Types, Schema } =
require('mongoose');
const db =
createConnection('mongodb://localhost:27017/test');
const shapeSchema = Schema({ name: String }, {
discriminatorKey: 'kind' });
const schema = Schema({ shape: shapeSchema });

schema.path('shape').discriminator('Circle', Schema({ radius:


String }));
schema.path('shape').discriminator('Square', Schema({ side:
Number }));

const Model = db.model('Model', schema);


const doc = new Model({ shape: { kind: 'Circle', radius: 5 }
});
console.log(doc)
}
run();

We call discriminator on the shape property with:


schema.path('shape').discriminator('Circle', Schema({ radius:
String }));
schema.path('shape').discriminator('Square', Schema({ side:
Number }));

We call the discriminator method to add the Circle and Square


discriminators.

Then we use them by setting the kind property when we create the entry.
Plugins
We can add plugins to schemas.

Plugins are useful for adding reusable logic into multiple schemas.

For example, we can write:


const loadedAtPlugin = (schema, options) => {
schema.virtual('loadedAt').
get(function () { return this._loadedAt; }).
set(function (v) { this._loadedAt = v; });

schema.post(['find', 'findOne'], function (docs) {


if (!Array.isArray(docs)) {
docs = [docs];
}
const now = new Date();
for (const doc of docs) {
doc.loadedAt = now;
}
});
};

async function run() {


const { createConnection, Types, Schema } =
require('mongoose');
const db =
createConnection('mongodb://localhost:27017/test');
const gameSchema = new Schema({ name: String });
gameSchema.plugin(loadedAtPlugin);
const Game = db.model('Game', gameSchema);

const playerSchema = new Schema({ name: String });


playerSchema.plugin(loadedAtPlugin);
const Player = db.model('Player', playerSchema);

const player = new Player({ name: 'foo' });


const game = new Game({ name: 'bar' });
await player.save()
await game.save()
const p = await Player.findOne({});
const g = await Game.findOne({});
console.log(p.loadedAt);
console.log(g.loadedAt);
}
run();

We created th loadedAtPlugin to add the virtual loadedAt property to the


retrieved objects after we call find or findOne .

We call schema.post in the plugin to listen to the find and findOne events.

Then we loop through all the documents and set the loadedAt property and
set that to the current date and time.

In the run function, we add the plugin by calling the plugin method on
each schema.

This has to be called before we define the model .

Otherwise, the plugin won’t be added.

Now we should see the timestamp when we access the loadedAt property
after calling findOne .
Conclusion
To make manipulating Mongodb data easier in Node.js apps, we can use the
Mongoose library.

It comes with features like schemas and models to restrict the data types
that can be in a document.

Also, we can watch for operations with hooks.

We can also create models that inherit from other models with
discriminators.

You might also like