The most +1'ed feature request for mongoose in 2016 was extending discriminators to work with embedded documents. Discriminators are mongoose's built-in schema inheritance mechanism. For example, suppose you have a schema defining events:

const eventSchema = new Schema({ message: String });

Naturally, an 'event' is a nebulous concept. Suppose you want to make some more concrete events, like a ClickedEvent that contains the id of an element that the user clicked and a PurchasedEvent that contains the id of the product purchased. Discriminators let you do this:

const eventSchema = new Schema({ message: String },
  { discriminatorKey: 'kind' });

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

const ClickedEvent = Event.discriminator('Clicked', new Schema({
  element: {
    type: String,
    required: true
  }
}));

const PurchasedEvent = Event.discriminator('Purchased', new Schema({
  product: {
    type: String,
    required: true
  }
}));

Now ClickedEvent and PurchasedEvent are discriminators of Event. In other words, ClickedEvent and PurchasedEvent are mongoose models that share the 'events' collection. When you save a new ClickedEvent instance, mongoose will store it in the 'events' collection with the kind property set to 'Clicked'. The element property is only required for ClickedEvent instances, and the product property is only required for PurchasedEvent instances. Calling ClickedEvent.find() is also equivalent to calling Event.find({ kind: 'Clicked' }). You can read more about discriminators in this article.

One major limitation of discriminators in mongoose ~4.7 is you can only have discriminators in top-level documents. For example, say instead of storing each individual event in the database you store events in batches as shown below:

const eventSchema = new Schema({ message: String },
  { discriminatorKey: 'kind', _id: false });

const batchSchema = new Schema({ events: [eventSchema] });
const Batch = mongoose.model('Batch', batchSchema);

In mongoose 4.7, there's no way to create a discriminator for the events array, because it's embedded in the top-level Batch model. Mongoose 4.8 adds the ability to create discriminators on embedded arrays.

Discriminators for Document Arrays

In mongoose ~4.8 you can define a discriminator by calling a document array's discriminator() function:

const eventSchema = new Schema({ message: String },
  { discriminatorKey: 'kind', _id: false });

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

// `batchSchema.path('events')` gets the mongoose `DocumentArray`
batchSchema.path('events').discriminator('Clicked', new Schema({
  element: {
    type: String,
    required: true
  }
}, { _id: false }));
batchSchema.path('events').discriminator('Purchased', new Schema({
  product: {
    type: String,
    required: true
  }
}, { _id: false }));

const Batch = mongoose.model('Batch', batchSchema);

Now you can now create event batches and mongoose will map kind properties to the correct types:

const batch = {
  events: [
    { kind: 'Clicked', element: 'Test' },
    { kind: 'Purchased', product: 22 }
  ]
};

Batch.create(batch).then(console.log);

// Output
/*
{ __v: 0,
  _id: 589389b84724655fca070f84,
  events:
   [ { element: 'Test', kind: 'Clicked' },
     { product: '22', kind: 'Purchased' } ] }
*/

One neat feature of discriminators is the ability to define methods on each discriminator. In the below example, you'll see you can create a different displayName() method for ClickedEvent and PurchasedEvent instances.

const clickedSchema = new Schema({
  element: {
    type: String,
    required: true
  }
}, { _id: false })

clickedSchema.methods.displayName = function() {
  return `${this.kind}:${this.element}`;
};

batchSchema.path('events').discriminator('Clicked', clickedSchema);

const purchasedSchema = new Schema({
  product: {
    type: String,
    required: true
  }
}, { _id: false });

purchasedSchema.methods.displayName = function() {
  return `${this.kind}:${this.product}`;
};

batchSchema.path('events').discriminator('Purchased', purchasedSchema);

const Batch = mongoose.model('Batch', batchSchema);

// Now actually use the schemas

const batch = {
  events: [
    { kind: 'Clicked', element: 'Test' },
    { kind: 'Purchased', product: 22 }
  ]
};

Batch.create(batch).
  then(batch => console.log(batch.events.map(e => e.displayName())));

// Output
/* [ 'Clicked:Test', 'Purchased:22' ] */

Moving On

Mongoose 4.8.0 includes 13 new features (like support for the MongoDB 3.4 decimal type) in addition to embedded array discriminators. It also has some major performance improvements for documents with large embedded arrays. Make sure you upgrade and take advantage of these new improvements!

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus