Spektor?.dev

How To Add Defaults In Mongoose Nested Schemas

July 10, 2020

Recently, I needed to update an existing mongoose schema (for those unfamiliar mongoose is a Javascript ORM for MongoDB) with a nested rating object. During the task I needed to define default values for nested mongoose schemas and update the existing documents with those which turned out a bit tricky. Read further for tips on how to do it.

The schema was for Brand object and already had a lot of fields. For the purposes of the article let’s define the Brand schema like so:

const mongoose = require("mongoose")
const Schema = mongoose.Schema
const Brand = new Schema({
  meta: {
    name: {
      type: String,
      required: true,
      unique: true,
    },
    slug: {
      type: String,
      required: true,
      unique: true,
    },
  },
  contact: {
    email: String,
    address: String,
  },
})

module.exports = mongoose.model("Brand", Brand, "Brand")

The new rating object consisted of multiple sub-fields of objects. The lowest object level consisted of the fields score whose default value should be 0 and maxScore which can vary with each object field. Also the rating object is required. An intuitive implemention which doesn’t implement defaults and mandatory fields might look like this:

const mongoose = require("mongoose")
const Schema = mongoose.Schema
const Brand = new Schema({
  meta: {
    name: {
      type: String,
      required: true,
      unique: true,
    },
    slug: {
      type: String,
      required: true,
      unique: true,
    },
  },
  contact: {
    email: String,
    address: String,
  },
  rating: {
    userExperience: {
      overall: {
        score: Number,
        maxScore: Number,
      },
    },
    independentReviews: {
      overall: {
        score: Number,
        maxScore: Number,
      },
    },
    overall: {
      score: Number,
      maxScore: Number,
    },
  },
})

module.exports = mongoose.model("Brand", Brand, "Brand")

As you can see the fields score and maxScore appear a lot in the schema so it will be easier to create a helper function getRating which receives a maxScore and returns an object with the relevant score and maxScore fields. In addition we’ll create a new type called BrandRating so that we can mark it as required on Brand object:

const mongoose = require("mongoose")
const Schema = mongoose.Schema
const dbUrl = "mongodb://localhost:27017/local"

const main = async () => {
  mongoose.connect(dbUrl, {
    useNewUrlParser: true,
    socketTimeoutMS: 3000000,
    keepAlive: 3000000,
    reconnectTries: 30,
    reconnectInterval: 10000,
  })

  const minScore = 0
  const getRating = ({ maxScore }) => ({
    type: new Schema(
      {
        score: {
          type: Number,
          min: minScore,
          max: maxScore,
        },
        maxScore: Number,
      },
      { _id: false }
    ),
    required: true,
    default: {
      score: minScore,
      maxScore,
    },
  })

  const BrandRating = new Schema(
    {
      userExperience: {
        overall: getRating({ maxScore: 50 }),
      },
      independentReviews: {
        overall: getRating({ maxScore: 50 }),
      },
      overall: getRating({ maxScore: 100 }),
    },
    { _id: false }
  )

  const Brand = new Schema({
    meta: {
      name: {
        type: String,
        required: true,
        unique: true,
      },
      slug: {
        type: String,
        required: true,
        unique: true,
      },
    },
    rating: {
      type: BrandRating,
      required: true,
      default: {},
    },
  })
  const BrandModel = mongoose.model("Brand", Brand, "Brand")
  const CurbYourEnthusiasm = new BrandModel({
    meta: {
      name: "Curb Your Enthusiasm",
      slug: "cye",
    },
    contact: {
      email: "cye@example.com",
      address: "California, USA",
    },
  })
  await CurbYourEnthusiasm.validate()
  await CurbYourEnthusiasm.save()
}

main().catch(error => console.error(error))

The code above can be run as is and will create a CurbYourEnthusiasm document complete with the rating object and all of the defaults. In case you already had a lot of documents and you just need to update them with the new rating system then it will just take a one-liner:

await BrandModel.updateMany({}, { $set: { rating: {} } }).lean()

A Few Side Notes:

  • Setting the default for rating as empty object is crucial in order to enable creation of defaults. Without it default score values will not be created on nested schemas.
  • Setting _id: false option is handy because id’s are not needed on nested schemas because they’re just an abstraction to enable validation (required fields) and default values.
  • Calling .lean() on mongoose model methods is an optimization technique which return a plain Javascript object of the document instead of a mongoose object which has mongoose-specific metadata and internal state sometimes making the lean document 10 times smaller than the mongoose object.