What to do with ENUM values in Mongoose?

18,039

Solution 1

You can use admin panel to add more country to the country collection. As you are saying that COUNTRIES array can grow, you can use another collection to add more countries on demand from admin panel.

And when you are going to add/save a new record into the survey you can trigger a pre-save hook to mongo for validation.

suppose we have another schema for countries like this.

{
 countries: [String]
}

Here is a sample code for the scenario.

const mongoose = require("mongoose");

const GENDERS = ["M", "F"];

const surveySchema = {
    subject: { type: String, required: true },
    country: { type: String},
    target: {
        gender: { type: String, enum: GENDERS }
    }
};

var Survey = new mongoose.Schema(surveySchema);

Survey.pre('save',function(next){
  var me = this;
  CountryModel.find({},(err,docs)=>{
    (docs.countries.indexOf(me.country) >=0) ? next() : next(new Error('validation failed'));
  });
});

This way you can handle dynamic country add without changing the country array and redeploying your whole server.

USING CUSTOM VALIDATOR

const mongoose = require("mongoose");

const GENDERS = ["M", "F"];

const surveySchema = {
        subject: {
            type: String,
            required: true
        },
        country: {
            type: String,
            validate: {
                isAsync: true,
                validator: function(arg, cb) {
                    CountryModel.find({}, (err, docs) => {
                                if (err) {
                                    cb(err);
                                } else {
                                    cb(docs.countries.indexOf(arg) >= 0);
                                }
                            }
                        },
                        message: '{VALUE} is not a valid country'
                }
            },
            target: {
                gender: { type: String, enum: GENDERS }
            }
        };

you will get an error while saving the survey data in the callback ..

ServeyModel.save((err,doc)=>{
if(err){
console.log(err.errors.country.message);
//Error handle
}else {
//TODO
}
});

Solution 2

Just wanted to add one thing, @Shawon's answer is great. Do know that there are two ways to make a validator async. One is shown above where if you specify the isAsync flag and then Mongoose will pass that callback into your validator function to be called at the end of your validation, but you should only use that flag if you're not returning a promise. If you return a promise from your custom validator, Mongoose will know it is async and the isAsync flag is no longer required.

The Mongoose async custom validator doc is found here which shows these both examples really well, great docs. http://mongoosejs.com/docs/validation.html#async-custom-validators

Share:
18,039
Flame_Phoenix
Author by

Flame_Phoenix

I have been programming all my life since I was a little boy. It all started with Warcraft 3 and the JASS and vJASS languages that oriented me into the path of the programmer. After having that experience I decided that was what I wanted to do for a living, so I entered University (FCT UNL ftw!) where my skills grew immensely and today, after several years of studying, professional experience and a master's degree, I have meddled with pretty much every language you can think of: from NASM to Java, passing by C# Ruby, Python, and so many others I don't even have enough space to mention them all! But don't let that make you think I am a pro. If there is one thing I learned, is that there is always the new guy can teach me. What will you teach me?

Updated on July 13, 2022

Comments

  • Flame_Phoenix
    Flame_Phoenix almost 2 years

    Background

    I have a Mongoose schema that defines a set of possible values a given object can have.

    const mongoose = require("mongoose");
    
    const COUNTRIES = ["ES", "PT", "US", "FR", "UK"];
    const GENDERS = ["M", "F"];
    
    const surveySchema = {
        subject: { type: String, required: true },
        country: { type: String, enum: COUNTRIES },
        target: {
            gender: { type: String, enum: GENDERS }
        }
    };
    
    module.exports = new mongoose.Schema(surveySchema);;
    module.exports.modSchema = surveySchema;
    

    Why I don't like ENUM

    I don't personally like ENUM values because if I add another value to the ENUM, I have to recompile the entire application again and deploy.

    I guess that with an ENUM such as gender, that will never change, this is not a problem.

    However, with countries, my SQL side tells me I should store them because if you have a growing business, you are likely to expand to other countries.

    Problem

    My problem here is that I don't know how to tell Mongoose, at a schema level, that the only allowed values for the countries have to be ["ES", "PT", "US", "FR", "UK"].

    I guess I could create a collection countries, but then I lack the knowledge on how I would connect them. Would I have to use async validators?

    How would you deal with an ENUM that can change?

    • Khurram
      Khurram about 7 years
      ENUM is always at schema level. you can add remove countries at any time in your enum list. just need to restart the app. after adding it to the enum.
    • Flame_Phoenix
      Flame_Phoenix about 7 years
      Or in a Production environment, deploy. Which is a big no no. If you are gonna deploy a new version of your app every time you need an extra country, product, book, etc, you will be doomed.
  • Talha Awan
    Talha Awan about 7 years
    async custom validator hook would be more appropriate here than save. That allows validation on object level without saving it to database.
  • Shawon Kanji
    Shawon Kanji about 7 years
    That will do the trick too. But one way or another you have to validate the values somewhere.One advantage here in pre-save is that the error will be handled directly by the callback error handler of the save method. And in the case of custom validation, you have to pass the error manually to the next waterfall or other error handlers.
  • Flame_Phoenix
    Flame_Phoenix about 7 years
    Could you post a solution with async validators as to compare?
  • Shawon Kanji
    Shawon Kanji about 7 years
    Sorry for the late reply. I have edited my answer with a custom validator. **All the validators are registered as a pre-save hook by default.
  • Flame_Phoenix
    Flame_Phoenix about 7 years
    Thanks for the response !