8

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?

Flame_Phoenix
  • 16,489
  • 37
  • 131
  • 266
  • 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. – Khurram Apr 01 '17 at 19:44
  • 1
    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. – Flame_Phoenix Apr 02 '17 at 10:36

3 Answers3

16

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
}
});
Shawon Kanji
  • 710
  • 5
  • 13
  • 1
    [async custom validator](http://mongoosejs.com/docs/validation.html) hook would be more appropriate here than save. That allows validation on object level without saving it to database. – Talha Awan Apr 01 '17 at 20:44
  • 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. – Shawon Kanji Apr 02 '17 at 06:37
  • Could you post a solution with async validators as to compare? – Flame_Phoenix Apr 02 '17 at 10:34
  • 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. – Shawon Kanji Apr 03 '17 at 06:05
  • Thanks for the response ! – Flame_Phoenix Apr 07 '17 at 10:18
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

0

just use an enum array like this in your schema

country: { type: String, enum: ["ES", "PT", "US", "FR", "UK"]},
Suraj Rao
  • 29,388
  • 11
  • 94
  • 103