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!
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)
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;
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,
},
};
.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));
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';
}
}
// 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;
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
Later Create the Model using object like this
Server Side
Parse model to valid sequelize models
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)
I throw a custom exception
ValidationError
that extends from ErrorNow we can use on the server side as usual and on the client side this
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;