Sequelize: Model Validation on Client Side

Created on 26 Mar 2016  路  3Comments  路  Source: sequelize/sequelize

This question maybe was answered before but I can't find it.

It possible to have the Model creation and validation on client side? I would like to use the same definitions and validation on the client side too (DRY)

Thanks!

Most helpful comment

Hello again, at the end I did a parser (kind sort)


Shared code between Client and Server

First Created a new data-types files that don't depend of the Database

    const ABSTRACT = {
      type: 'ABSTRACT', // this match with the attribute name of the sequelize ex. of the Sequelize.ABSTRACT
      validate: (value) => (value && true),
    };
    const STRING = {
      type: 'STRING',
      validate: (value) => {
        if (Object.prototype.toString.call(value) !== '[object String]') {
          if (isNumber(value)) {
            return true;
          }
          throw new ValidationError({
            message: format(' %j is not a valid  string', value),
          });
        }
    return true;
          },
    };
    ....
    const dataTypes = {
      ABSTRACT,
      STRING,
      CHAR,
      TEXT,
      NUMBER,
      INTEGER,
      BIGINT,
      FLOAT,
      TIME,
      DATE,
      DATEONLY,
      BOOLEAN,
      NOW,
      BLOB,
      DECIMAL,
      NUMERIC,
      UUID,
      UUIDV1,
      UUIDV4,
      HSTORE,
      JSON: JSONTYPE,
      JSONB,
      VIRTUAL,
      ARRAY,
      NONE,
      ENUM,
      RANGE,
      REAL,
      DOUBLE,
      'DOUBLE PRECISION': DOUBLE,
      GEOMETRY,
      GEOGRAPHY,
    };

    export default dataTypes;

Later Create the Model using object like this

const UserModel = {
  name: 'user',
  model: {
    id: {
      type: DataTypes.INTEGER, //DataTypes come from the previous code not the sequelize DataTypes
      primaryKey: true,
      autoIncrement: true,
    },
    username: {
      type: DataTypes.STRING,
      get: function get() {
        return `name: ${this.getDataValue('username')}`;
      },
      // allowNull: false,
      validate: {
        isEmail: { msg: 'wrong email format' },
        len: {
          msg: 'len between 15-25',
          args: [15, 25],
        },
      },
    },
    password: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    alias: {
      type: DataTypes.STRING,
      set: function set() {
        this.setDataValue('alias', `${this.getDataValue('username')}-${this.getDataValue('id')}`);
      },
    },
  },
  options: {
    freezeTableName: true,
    classMethods: {},
    underscored: true,
  },
};

Server Side

Parse model to valid sequelize models

    .readdirSync(path.resolve(process.cwd(), 'src', 'shared', 'model'))// folder where the models are storaged
    .filter(file => file.endsWith('.js')) // only parse javascript files 
    .map((file) => {
      const modelDefinition = require(`model/${file}`).default;
      const { name, options } = modelDefinition;
      const model = Object.keys(modelDefinition.model).reduce((newModel, key) => {
        const modelField = modelDefinition.model[key].type;
        const field = Object.assign({}, modelDefinition.model[key], {
          type: Sequelize[modelField.type], //override the type from the custom type to the sequelize type
        });
        newModel[key] = field;
        return newModel;
      }, {});
      const Model = this._sequelize.define(name, model, options);
      // create the Model Constructor ex. you  can later use this.User.create({...}) NOTE the Capitalize first letter
      this[capitalizeFirstLetter(name)] = Model; 
      return modelDefinition;
    }).forEach(modelDefinition => {
      const { name, relations } = modelDefinition;
      Object.keys(relations || {}).forEach(relation => {
        this[capitalizeFirstLetter(name)][relation](this[capitalizeFirstLetter(relations[relation])]);
      });
    });
    return this._sequelize.sync({ force: true }).then(() => Promise.resolve(this));

Client Side (Validation)

Create a Validation function that use the Model and the object to be validated as parameter (this probably will change to a class or something)

class CustomValidator {
  static notEmpty(str) {
    return !str.match(/^[\s\t\r\n]*$/);
  }
  static len(str, min, max) {
    console.log('??', str, min, max);
    return Validator.isLength(str, min, max);
  }
  static isUrl(str) {
    return Validator.isURL(str);
  }
  static isIPv6(str) {
    return Validator.isIP(str, 6);
  }
  static isIPv4(str) {
    return Validator.isIP(str, 4);
  }
  static notIn(str, values) {
    return !Validator.isIn(str, values);
  }
  static regex(str, pattern, modifiers) {
    str += '';
    if (Object.prototype.toString.call(pattern).slice(8, -1) !== 'RegExp') {
      pattern = new RegExp(pattern, modifiers);
    }
    return str.match(pattern);
  }
  static notRegex(str, pattern, modifiers) {
    return !Validator.regex(str, pattern, modifiers);
  }
  static isDecimal(str) {
    return str !== '' && str.match(/^(?:-?(?:[0-9]+))?(?:\.[0-9]*)?(?:[eE][\+\-]?(?:[0-9]+))?$/);
  }
  static min(str, val) {
    const number = parseFloat(str);
    return isNaN(number) || number >= val;
  }
  static max(str, val) {
    const number = parseFloat(str);
    return isNaN(number) || number <= val;
  }
  static not(str, pattern, modifiers) {
    return Validator.notRegex(str, pattern, modifiers);
  }
  static contains(str, elem) {
    return str.indexOf(elem) >= 0 && !!elem;
  }
  static notContains(str, elem) {
    return !Validator.contains(str, elem);
  }
  static is(str, pattern, modifiers) {
    return Validator.regex(str, pattern, modifiers);
  }
}

const defaultOptions = {
  exact: true,
  onlyIntanciated: false,
};

export default function validator(Model, instance, opts = defaultOptions) {
  const options = Object.assign({}, defaultOptions, opts);
  const { model } = Model;
  const errors = [];
  try {
    Object.keys(instance).forEach(key => {
      if(!model[key]) {
        throw new ValidationError({
          message: `${key} is not a field of this model`,
        });
      }
    });
  } catch(e) {
    errors.push(e);
  }
  Object.keys(model)
  .forEach(key => {
    const m = model[key];
    const value = instance[key];
    try {
      if(options.exact &&
        !value &&
        !m.autoIncrement &&
        !m.defaultValue &&
        m.allowNull === false
      ) {
        throw new ValidationError({
          field: key,
          message: `${key} is required`,
        });
      }
      if(value) {
        m.type.validate(value);
      }
      if(m.validate) {
        Object.keys(m.validate).forEach(rule => {
          const validationRule = m.validate[rule];
          if(validationRule === true ||
            (typeof validationRule === 'object' &&
            !Array.isArray(validationRule))
          ) {
            const values = [value].concat(validationRule.args || []);
            if((Validator[rule] && !Validator[rule].apply(null, values)) ||
              (CustomValidator[rule] && !CustomValidator[rule].apply(null, values))) {
              throw new ValidationError({
                field: key,
                message: validationRule.msg || `Validate ${rule} failed (${value})`,
              });
            }
          }
        });
      }
    }catch(e) {
      errors.push(e);
    }
  });
  return errors;
}

I throw a custom exception ValidationError that extends from Error

export default class ValidationError extends Error {
  constructor(props = {}) {
    super(props);
    this.name = 'Validation Error';
    this.title = 'Validation Error';
    this.field = props.field;
    this.message = props.message || 'default error message';
  }
}

Now we can use on the server side as usual and on the client side this

// this will return an array with 2 errors: [Invalid email, test is not on the model]
Validator(UserModel, { username: 'email', test: 'test' });

this is only a raw implementation that can be improved, but I think at some point can be putted on the core of the library.

Thanks TL;DR;

All 3 comments

I'm using SchemaForm to generate forms for models CRUD screens, and what I did was a converter to create a JSON schema from each model's metadata and added this schemas to the object I send when there is a successful authentication, then the frontend could use these schemas when generating forms. It would be nice to have in sequelize support for this.

//First I break model definitions in two parts, one which contains only DB metadata as 
//attributes and validations, etc (which is maintained by the DB designer) and the other
//contains the logic, mainly the options object of the definition. This way I could
//require the first module to obtain metadata only, and to avoid large definitions file
//sizes. i.e each model definition consist of two files, one with DB related metadata
//and the other with logic, which are both merged in module load time to create a
//valid sequelize definition, both files have the name of the model.

//Generates a JSON schema from models metadata and attach it to them
var addJSONSchema = function (models) {

    var Sequelize = models.Sequelize;
    //require underscore
    models = _.flatten([_.filter(_.values(models), function(model){
      return model instanceof Model
    })]);

    //Here I'm getting the metadata part of the definitions and passing it a fake
    //DataTypes object which do translate sequelize types to JSON Schema types
    var modelAttribs = require('./models/attribs')({
        BIGINT: function(){
            return 'integer';
        },
        INTEGER: function(){
            return 'integer';
        },
        FLOAT: function(){
            return 'number';
        },
        DECIMAL: function(){
            return 'number';
        },
        DOUBLE: function(){
            return 'number';
        },
        BOOLEAN: function(){
            return 'boolean';
        },
        TEXT: function(){
            return 'string';
        },
        ENUM: function(){
            var args = Array.prototype.slice.call(arguments);
            return 'string' + '['+ args.join(',') +']';
        },
        DATE: function(){
            return 'string' + '#date';
        },
        BINARY: function(){
            return 'binary';
        },
        STRING: function(len){
            return 'string'+ (len ? '('+len+')' : '');
        }
    }, Sequelize);

    //For each model I generate and attach the JSONSchema for it
    models.forEach(function(model) {
        var modelDef = modelAttribs[model.name][0];

        // Regexp used on validations
        var urlRegEx = "^(https?:\\\/\\/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})([\/\\w \\.-]*)*\/?$";
        var IPv4RegEx = "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
        var IPv6RegEx = "^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$";
        var emailRegEx = "^\\S+@\\S+$";
        var lowercaseRegEx = "[^A-Z]*$";
        var uppercaseRegEx = "[^a-z]*$";
        var alphaRegEx = "^[a-zA-Z\\s]*$";
        var numericRegEx = "^[-+]?[0-9]+$";
        var floatRegEx = "^(?:[-+]?(?:[0-9]+))?(?:\.[0-9]*)?(?:[eE][\+\-]?(?:[0-9]+))?$";
        var decimalRegEx = "^[-+]?([0-9]+|\.[0-9]+|[0-9]+\.[0-9]+)$";
        var intRegEx = "^(?:[-+]?(?:0|[1-9][0-9]*))$";
        var alphanumericRegEx = "^[a-zA-Z0-9\\s]*$";
        var creditCardRegEx = "^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$";

        var keys = Object.keys( modelDef );
        var required = [];
        var properties = {};

        keys.forEach( function( key ) {

            if( !modelDef[key].title ) return;

            properties[key] = {
                title: modelDef[key].title,
                type: typeof modelDef[key].type == 'function' ? modelDef[key].type() : modelDef[key].type
            };

            var matchResult = properties[key].type.match( /\((\d+)\)/ );
            if( matchResult != null ) {
                properties[key].type = properties[key].type.replace( /\((\d+)\)/, '' );
                properties[key].maxLength = parseInt( matchResult[1] );
            }

            var matchResult = properties[key].type.match( /\[(.+?)\]/ );
            if( matchResult != null ) {
                properties[key].type = properties[key].type.replace( /\[(.+?)\]/, '' );
                properties[key].enum = matchResult[1].split( "," );
            }

            var matchResult = properties[key].type.match( /#(.+)/ );
            if( matchResult != null ) {
                properties[key].type = properties[key].type.replace( /#(.+)/, '' );
                properties[key].format = matchResult[1] ;
            }

            if( modelDef[key].description )
                properties[key].description = modelDef[key].description;

            if( modelDef[key].defaultValue )
                properties[key].default = modelDef[key].defaultValue;

            if( modelDef[key].allowNull === false )
                required.push( key );

            if( modelDef[key].validate ) {
                var validate = modelDef[key].validate;

                if( typeof modelDef[key].allowNull == 'undefined' && validate.notNull ) {
                    required.push( key );
                }

                if( typeof modelDef[key].allowNull == 'undefined' && validate.isNull === false ) {
                    required.push( key );
                }

                if( validate.notEmpty ) {
                    properties[key].minLength = 1;
                }

                if( validate.len ) {
                    if( validate.len instanceof Array ){
                        properties[key].minLength = parseInt( validate.len[0] );
                        if( validate.len.length > 1 ){
                            properties[key].maxLength = parseInt( validate.len[1] );
                        }
                    } else {
                        properties[key].minLength = parseInt( validate.len );
                    }
                }

                if( validate.isIn ) {
                    properties[key].enum = validate.isIn[0];
                }

                if( validate.notIn ) {
                    properties[key].not = { "enum" : validate.notIn[0] };
                }

                if( validate.isUrl ) {
                    properties[key].pattern = urlRegEx;
                }

                if( validate.isIP ) {
                    properties[key].anyOf  = [
                        { "pattern" : IPv4RegEx },
                        { "pattern" : IPv6RegEx }
                    ]
                }

                if( validate.isIPv4 ) {
                    properties[key].pattern = IPv4RegEx;
                }

                if( validate.isIPv6 ) {
                    properties[key].pattern = IPv6RegEx;
                }

                if( validate.isEmail ) {
                    properties[key].pattern = emailRegEx;
                }

                if( validate.max ) {
                    properties[key].maximum = parseInt( validate.max );
                }

                if( validate.min ) {
                    properties[key].minimum = parseInt( validate.min );
                }

                if( validate.is ) {
                    if( validate.is instanceof RegExp ) {
                        properties[key].pattern = validate.is.toString().slice( 1,-1 );
                    } else if( validate.is instanceof Array ){
                        properties[key].pattern = validate.is[0];
                    } else {
                        properties[key].pattern = validate.is
                    }
                }

                if( validate.isLowercase ) {
                    properties[key].pattern = lowercaseRegEx;
                }

                if( validate.equals ) {
                    properties[key].pattern = "^" + validate.equals + "$";
                }

                if( validate.isUppercase ) {
                    properties[key].pattern = uppercaseRegEx;
                }

                if( validate.isDate ) {
                    properties[key].format = "date"
                }

                if( validate.isAlpha ) {
                    properties[key].pattern = alphaRegEx;
                }

                if( validate.notContains ) {
                    properties[key].not = { "pattern":  validate.notContains };
                }

                if( validate.contains ) {
                    properties[key].pattern = validate.contains;
                }

                if( validate.isNumeric ) {
                    properties[key].pattern = numericRegEx;
                }

                if( validate.isFloat ) {
                    properties[key].pattern = floatRegEx;
                }

                if( validate.isDecimal ) {
                    properties[key].pattern = decimalRegEx;
                }

                if( validate.isInt ) {
                    properties[key].pattern = intRegEx;
                }

                if( validate.isAlphanumeric ) {
                    properties[key].pattern = alphanumericRegEx;
                }

                if( validate.isCreditCard ) {
                    properties[key].pattern = creditCardRegEx;
                }

                if( validate.not ) {
                    if( validate.not instanceof RegExp ) {
                        properties[key].not = {
                            pattern : validate.not.toString().slice( 1,-1 )
                        }
                    } else if( validate.not instanceof Array ){
                        properties[key].not = {
                            pattern : validate.not[0]
                        }
                    } else {
                        properties[key].not = {
                            pattern : validate.not
                        }
                    }
                }
            }

        });

        model._JSONSchema = {
            type: "object",
            title: model.name,
            properties: properties,
            required : required
        };
    });
};

Probably not impossible but we have no official way of doing it.
In theory if you have no validators that hit the database you could pack the code/library and run the validator function.

Hello again, at the end I did a parser (kind sort)


Shared code between Client and Server

First Created a new data-types files that don't depend of the Database

    const ABSTRACT = {
      type: 'ABSTRACT', // this match with the attribute name of the sequelize ex. of the Sequelize.ABSTRACT
      validate: (value) => (value && true),
    };
    const STRING = {
      type: 'STRING',
      validate: (value) => {
        if (Object.prototype.toString.call(value) !== '[object String]') {
          if (isNumber(value)) {
            return true;
          }
          throw new ValidationError({
            message: format(' %j is not a valid  string', value),
          });
        }
    return true;
          },
    };
    ....
    const dataTypes = {
      ABSTRACT,
      STRING,
      CHAR,
      TEXT,
      NUMBER,
      INTEGER,
      BIGINT,
      FLOAT,
      TIME,
      DATE,
      DATEONLY,
      BOOLEAN,
      NOW,
      BLOB,
      DECIMAL,
      NUMERIC,
      UUID,
      UUIDV1,
      UUIDV4,
      HSTORE,
      JSON: JSONTYPE,
      JSONB,
      VIRTUAL,
      ARRAY,
      NONE,
      ENUM,
      RANGE,
      REAL,
      DOUBLE,
      'DOUBLE PRECISION': DOUBLE,
      GEOMETRY,
      GEOGRAPHY,
    };

    export default dataTypes;

Later Create the Model using object like this

const UserModel = {
  name: 'user',
  model: {
    id: {
      type: DataTypes.INTEGER, //DataTypes come from the previous code not the sequelize DataTypes
      primaryKey: true,
      autoIncrement: true,
    },
    username: {
      type: DataTypes.STRING,
      get: function get() {
        return `name: ${this.getDataValue('username')}`;
      },
      // allowNull: false,
      validate: {
        isEmail: { msg: 'wrong email format' },
        len: {
          msg: 'len between 15-25',
          args: [15, 25],
        },
      },
    },
    password: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    alias: {
      type: DataTypes.STRING,
      set: function set() {
        this.setDataValue('alias', `${this.getDataValue('username')}-${this.getDataValue('id')}`);
      },
    },
  },
  options: {
    freezeTableName: true,
    classMethods: {},
    underscored: true,
  },
};

Server Side

Parse model to valid sequelize models

    .readdirSync(path.resolve(process.cwd(), 'src', 'shared', 'model'))// folder where the models are storaged
    .filter(file => file.endsWith('.js')) // only parse javascript files 
    .map((file) => {
      const modelDefinition = require(`model/${file}`).default;
      const { name, options } = modelDefinition;
      const model = Object.keys(modelDefinition.model).reduce((newModel, key) => {
        const modelField = modelDefinition.model[key].type;
        const field = Object.assign({}, modelDefinition.model[key], {
          type: Sequelize[modelField.type], //override the type from the custom type to the sequelize type
        });
        newModel[key] = field;
        return newModel;
      }, {});
      const Model = this._sequelize.define(name, model, options);
      // create the Model Constructor ex. you  can later use this.User.create({...}) NOTE the Capitalize first letter
      this[capitalizeFirstLetter(name)] = Model; 
      return modelDefinition;
    }).forEach(modelDefinition => {
      const { name, relations } = modelDefinition;
      Object.keys(relations || {}).forEach(relation => {
        this[capitalizeFirstLetter(name)][relation](this[capitalizeFirstLetter(relations[relation])]);
      });
    });
    return this._sequelize.sync({ force: true }).then(() => Promise.resolve(this));

Client Side (Validation)

Create a Validation function that use the Model and the object to be validated as parameter (this probably will change to a class or something)

class CustomValidator {
  static notEmpty(str) {
    return !str.match(/^[\s\t\r\n]*$/);
  }
  static len(str, min, max) {
    console.log('??', str, min, max);
    return Validator.isLength(str, min, max);
  }
  static isUrl(str) {
    return Validator.isURL(str);
  }
  static isIPv6(str) {
    return Validator.isIP(str, 6);
  }
  static isIPv4(str) {
    return Validator.isIP(str, 4);
  }
  static notIn(str, values) {
    return !Validator.isIn(str, values);
  }
  static regex(str, pattern, modifiers) {
    str += '';
    if (Object.prototype.toString.call(pattern).slice(8, -1) !== 'RegExp') {
      pattern = new RegExp(pattern, modifiers);
    }
    return str.match(pattern);
  }
  static notRegex(str, pattern, modifiers) {
    return !Validator.regex(str, pattern, modifiers);
  }
  static isDecimal(str) {
    return str !== '' && str.match(/^(?:-?(?:[0-9]+))?(?:\.[0-9]*)?(?:[eE][\+\-]?(?:[0-9]+))?$/);
  }
  static min(str, val) {
    const number = parseFloat(str);
    return isNaN(number) || number >= val;
  }
  static max(str, val) {
    const number = parseFloat(str);
    return isNaN(number) || number <= val;
  }
  static not(str, pattern, modifiers) {
    return Validator.notRegex(str, pattern, modifiers);
  }
  static contains(str, elem) {
    return str.indexOf(elem) >= 0 && !!elem;
  }
  static notContains(str, elem) {
    return !Validator.contains(str, elem);
  }
  static is(str, pattern, modifiers) {
    return Validator.regex(str, pattern, modifiers);
  }
}

const defaultOptions = {
  exact: true,
  onlyIntanciated: false,
};

export default function validator(Model, instance, opts = defaultOptions) {
  const options = Object.assign({}, defaultOptions, opts);
  const { model } = Model;
  const errors = [];
  try {
    Object.keys(instance).forEach(key => {
      if(!model[key]) {
        throw new ValidationError({
          message: `${key} is not a field of this model`,
        });
      }
    });
  } catch(e) {
    errors.push(e);
  }
  Object.keys(model)
  .forEach(key => {
    const m = model[key];
    const value = instance[key];
    try {
      if(options.exact &&
        !value &&
        !m.autoIncrement &&
        !m.defaultValue &&
        m.allowNull === false
      ) {
        throw new ValidationError({
          field: key,
          message: `${key} is required`,
        });
      }
      if(value) {
        m.type.validate(value);
      }
      if(m.validate) {
        Object.keys(m.validate).forEach(rule => {
          const validationRule = m.validate[rule];
          if(validationRule === true ||
            (typeof validationRule === 'object' &&
            !Array.isArray(validationRule))
          ) {
            const values = [value].concat(validationRule.args || []);
            if((Validator[rule] && !Validator[rule].apply(null, values)) ||
              (CustomValidator[rule] && !CustomValidator[rule].apply(null, values))) {
              throw new ValidationError({
                field: key,
                message: validationRule.msg || `Validate ${rule} failed (${value})`,
              });
            }
          }
        });
      }
    }catch(e) {
      errors.push(e);
    }
  });
  return errors;
}

I throw a custom exception ValidationError that extends from Error

export default class ValidationError extends Error {
  constructor(props = {}) {
    super(props);
    this.name = 'Validation Error';
    this.title = 'Validation Error';
    this.field = props.field;
    this.message = props.message || 'default error message';
  }
}

Now we can use on the server side as usual and on the client side this

// this will return an array with 2 errors: [Invalid email, test is not on the model]
Validator(UserModel, { username: 'email', test: 'test' });

this is only a raw implementation that can be improved, but I think at some point can be putted on the core of the library.

Thanks TL;DR;

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Pomax picture Pomax  路  3Comments

GuilhermeReda picture GuilhermeReda  路  3Comments

mujz picture mujz  路  3Comments

kasparsklavins picture kasparsklavins  路  3Comments

lfcgomes picture lfcgomes  路  3Comments