不要怂,就是干,撸起袖子干!

Commit a243a82c by Tiago Ribeiro

Add default validation based on attribute type

1 parent a56a1882
......@@ -3,7 +3,9 @@
var util = require('util')
, _ = require('lodash')
, Wkt = require('wellknown')
, warnings = {};
, sequelizeErrors = require('./errors')
, warnings = {}
, Validator = require('validator');
/**
* A convenience class holding commonly used data types. The datatypes are used when definining a new model using `Sequelize.define`, like this:
......@@ -80,6 +82,13 @@ STRING.prototype.key = STRING.key = 'STRING';
STRING.prototype.toSql = function() {
return 'VARCHAR(' + this._length + ')' + ((this._binary) ? ' BINARY' : '');
};
STRING.prototype.validate = function(value) {
if (typeof value !== 'string') {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid string', value));
}
return true;
};
Object.defineProperty(STRING.prototype, 'BINARY', {
get: function() {
this._binary = true;
......@@ -137,6 +146,13 @@ TEXT.prototype.toSql = function() {
return this.key;
}
};
TEXT.prototype.validate = function(value) {
if (typeof value !== 'string') {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid string', value));
}
return true;
};
var NUMBER = function(options) {
this.options = options;
......@@ -167,6 +183,13 @@ NUMBER.prototype.toSql = function() {
}
return result;
};
NUMBER.prototype.validate = function(value) {
if (!_.isNumber(value)) {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid number', value));
}
return true;
};
Object.defineProperty(NUMBER.prototype, 'UNSIGNED', {
get: function() {
......@@ -200,6 +223,13 @@ var INTEGER = function(length) {
util.inherits(INTEGER, NUMBER);
INTEGER.prototype.key = INTEGER.key = 'INTEGER';
INTEGER.prototype.validate = function(value) {
if (!Validator.isInt(value)) {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid integer', value));
}
return true;
};
/**
* A 64 bit integer.
......@@ -219,6 +249,13 @@ var BIGINT = function(length) {
util.inherits(BIGINT, NUMBER);
BIGINT.prototype.key = BIGINT.key = 'BIGINT';
BIGINT.prototype.validate = function(value) {
if (!Validator.isInt(value)) {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid bigint', value));
}
return true;
};
/**
* Floating point number (4-byte precision). Accepts one or two arguments for precision
......@@ -238,6 +275,13 @@ var FLOAT = function(length, decimals) {
util.inherits(FLOAT, NUMBER);
FLOAT.prototype.key = FLOAT.key = 'FLOAT';
FLOAT.prototype.validate = function(value) {
if (!Validator.isFloat(value)) {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid float', value));
}
return true;
};
/**
* Floating point number (4-byte precision). Accepts one or two arguments for precision
......@@ -302,6 +346,13 @@ DECIMAL.prototype.toSql = function() {
return 'DECIMAL';
};
DECIMAL.prototype.validate = function(value) {
if (!_.isNumber(value)) {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid decimal', value));
}
return true;
};
/**
* A boolean / tinyint column, depending on dialect
......@@ -317,6 +368,13 @@ BOOLEAN.prototype.key = BOOLEAN.key = 'BOOLEAN';
BOOLEAN.prototype.toSql = function() {
return 'TINYINT(1)';
};
BOOLEAN.prototype.validate = function(value) {
if (!_.isBoolean(value)) {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid boolean', value));
}
return true;
};
/**
* A time column
......@@ -348,7 +406,13 @@ DATE.prototype.key = DATE.key = 'DATE';
DATE.prototype.toSql = function() {
return 'DATETIME';
};
DATE.prototype.validate = function(value) {
if (!Validator.isDate(value)) {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid date', value));
}
return true;
};
/**
* A date only column
* @property DATEONLY
......@@ -377,6 +441,13 @@ var HSTORE = function() {
util.inherits(HSTORE, ABSTRACT);
HSTORE.prototype.key = HSTORE.key = 'HSTORE';
HSTORE.prototype.validate = function(value) {
if (!_.isPlainObject(value)) {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid hstore', value));
}
return true;
};
/**
* A JSON string column. Only available in postgres.
......@@ -389,6 +460,9 @@ var JSONTYPE = function() {
util.inherits(JSONTYPE, ABSTRACT);
JSONTYPE.prototype.key = JSONTYPE.key = 'JSON';
JSONTYPE.prototype.validate = function(value) {
return true;
};
/**
* A pre-processed JSON data column. Only available in postgres.
......@@ -442,6 +516,13 @@ BLOB.prototype.toSql = function() {
return this.key;
}
};
BLOB.prototype.validate = function(value) {
if (!_.isString(value) && !Buffer.isBuffer(value)) {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid blob', value));
}
return true;
};
/**
* Range types are data types representing a range of values of some element type (called the range's subtype).
......@@ -474,6 +555,21 @@ RANGE.prototype.key = RANGE.key = 'RANGE';
RANGE.prototype.toSql = function() {
return pgRangeSubtypes[this._subtype.toLowerCase()];
};
RANGE.prototype.validate = function(value) {
if (_.isPlainObject(value) && value.inclusive) {
value = value.inclusive;
}
if (!_.isArray(value)) {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid range', value));
}
if (value.length !== 2) {
throw new sequelizeErrors.ValidationError('A range must be an array with two elements');
}
return true;
};
/**
* A column storing a unique univeral identifier. Use with `UUIDV1` or `UUIDV4` for default values.
......@@ -486,6 +582,13 @@ var UUID = function() {
util.inherits(UUID, ABSTRACT);
UUID.prototype.key = UUID.key = 'UUID';
UUID.prototype.validate = function(value) {
if (!Validator.isUUID(value)) {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid uuid', value));
}
return true;
};
/**
* A default unique universal identifier generated following the UUID v1 standard
......@@ -499,6 +602,13 @@ var UUIDV1 = function() {
util.inherits(UUIDV1, ABSTRACT);
UUIDV1.prototype.key = UUIDV1.key = 'UUIDV1';
UUIDV1.prototype.validate = function(value) {
if (!Validator.isUUID(value)) {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid uuid', value));
}
return true;
};
/**
* A default unique universal identifier generated following the UUID v2 standard
......@@ -512,6 +622,13 @@ var UUIDV4 = function() {
util.inherits(UUIDV4, ABSTRACT);
UUIDV4.prototype.key = UUIDV4.key = 'UUIDV4';
UUIDV4.prototype.validate = function(value) {
if (!Validator.isUUID(value, 4)) {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid uuidv4', value));
}
return true;
};
/**
* A virtual value that is not stored in the DB. This could for example be useful if you want to provide a default value in your model that is returned to the user but not stored in the DB.
......@@ -565,6 +682,13 @@ var ENUM = function(value) {
util.inherits(ENUM, ABSTRACT);
ENUM.prototype.key = ENUM.key = 'ENUM';
ENUM.prototype.validate = function(value) {
if (!_.contains(this.values, value)) {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid choice in %j', value, this.values));
}
return true;
};
/**
* An array of `type`, e.g. `DataTypes.ARRAY(DataTypes.DECIMAL)`. Only available in postgres.
......@@ -583,6 +707,13 @@ ARRAY.prototype.key = ARRAY.key = 'ARRAY';
ARRAY.prototype.toSql = function() {
return this.type.toSql() + '[]';
};
ARRAY.prototype.validate = function(value) {
if (!Array.isArray(value)) {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid array', value));
}
return true;
};
ARRAY.is = function(obj, type) {
return obj instanceof ARRAY && obj.type instanceof type;
};
......
......@@ -877,6 +877,12 @@ var QueryGenerator = {
return this.handleSequelizeMethod(value);
}
if (field && field.type && value) {
if (field.type.validate) {
field.type.validate(value);
}
}
if (Utils._.isObject(value) && field && (field.type instanceof DataTypes.HSTORE || DataTypes.ARRAY.is(field.type, DataTypes.HSTORE))) {
if (field.type instanceof DataTypes.HSTORE){
return "'" + hstore.stringify(value) + "'";
......
......@@ -91,26 +91,8 @@ var Model = function(name, attributes, options) {
throw new Error('Values for ENUM have not been defined.');
}
attribute.validate = attribute.validate || {
_checkEnum: function(value, next) {
var hasValue = value !== undefined
, isMySQL = ['mysql', 'mariadb'].indexOf(options.sequelize.options.dialect) !== -1
, ciCollation = !!options.collate && options.collate.match(/_ci$/i) !== null
, valueOutOfScope;
if (isMySQL && ciCollation && hasValue) {
var scopeIndex = (attributes[name].values || []).map(function(d) { return d.toLowerCase(); }).indexOf(value.toLowerCase());
valueOutOfScope = scopeIndex === -1;
} else {
valueOutOfScope = ((attributes[name].values || []).indexOf(value) === -1);
}
if (hasValue && valueOutOfScope && attributes[name].allowNull !== true) {
return next('Value "' + value + '" for ENUM ' + name + ' is out of allowed scope. Allowed values: ' + attributes[name].values.join(', '));
}
next();
}
};
// BC compatible.
attribute.type.values = attribute.values;
}
return attribute;
......
......@@ -406,7 +406,7 @@ QueryInterface.prototype.renameColumn = function(tableName, attrNameBefore, attr
_options[attrNameAfter] = {
attribute: attrNameAfter,
type: data.type,
type: DataTypes[data.type],
allowNull: data.allowNull,
defaultValue: data.defaultValue
};
......
......@@ -16,9 +16,9 @@ if (dialect.match(/^postgres/)) {
beforeEach(function() {
this.User = this.sequelize.define('User', {
username: DataTypes.STRING,
email: {type: DataTypes.ARRAY(DataTypes.TEXT)},
numbers: {type: DataTypes.ARRAY(DataTypes.FLOAT)},
document: {type: DataTypes.HSTORE, defaultValue: '"default"=>"value"'}
email: { type: DataTypes.ARRAY(DataTypes.TEXT) },
numbers: { type: DataTypes.ARRAY(DataTypes.FLOAT) },
document: { type: DataTypes.HSTORE, defaultValue: { default: '"value"' } }
});
return this.User.sync({ force: true });
});
......
......@@ -718,4 +718,4 @@ describe(Support.getTestDialectTeaser('InstanceValidator'), function() {
expect(errors.get('name')[0].message).to.equal('Validation isExactly7Characters failed');
});
});
});
\ No newline at end of file
});
......@@ -927,7 +927,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() {
it("doesn't save an instance if value is not in the range of enums", function() {
return this.Review.create({status: 'fnord'}).catch(function(err) {
expect(err).to.be.instanceOf(Error);
expect(err.get('status')[0].message).to.equal('Value "fnord" for ENUM status is out of allowed scope. Allowed values: scheduled, active, finished');
expect(err.message).to.equal('"fnord" is not a valid choice in ["scheduled","active","finished"]');
});
});
});
......
......@@ -2,8 +2,13 @@
var Support = require(__dirname + '/../support')
, DataTypes = require(__dirname + '/../../../lib/data-types')
, Sequelize = Support.Sequelize
, chai = require('chai')
, util = require('util')
, uuid = require('node-uuid')
, expectsql = Support.expectsql
, current = Support.sequelize;
, current = Support.sequelize
, expect = chai.expect;
// Notice: [] will be replaced by dialect specific tick/quote character when there is not dialect specific expectation but only a default expectation
......@@ -44,6 +49,22 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
mssql: 'BINARY(255)',
postgres: 'BYTEA'
});
suite('validate', function () {
test('should throw an error if `value` is invalid', function() {
var type = DataTypes.STRING();
expect(function () {
type.validate(12345);
}).to.throw(Sequelize.ValidationError, '12345 is not a valid string');
});
test('should return `true` if `value` is a string', function() {
var type = DataTypes.STRING();
expect(type.validate('foobar')).to.equal(true);
});
});
});
suite('TEXT', function () {
......@@ -79,6 +100,22 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
mysql: 'LONGTEXT',
mariadb: 'LONGTEXT'
});
suite('validate', function () {
test('should throw an error if `value` is invalid', function() {
var type = DataTypes.TEXT();
expect(function () {
type.validate(12345);
}).to.throw(Sequelize.ValidationError, '12345 is not a valid string');
});
test('should return `true` if `value` is a string', function() {
var type = DataTypes.TEXT();
expect(type.validate('foobar')).to.equal(true);
});
});
});
suite('CHAR', function () {
......@@ -114,6 +151,23 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
mysql: 'TINYINT(1)',
sqlite: 'TINYINT(1)'
});
suite('validate', function () {
test('should throw an error if `value` is invalid', function() {
var type = DataTypes.BOOLEAN();
expect(function () {
type.validate(12345);
}).to.throw(Sequelize.ValidationError, '12345 is not a valid boolean');
});
test('should return `true` if `value` is a boolean', function() {
var type = DataTypes.BOOLEAN();
expect(type.validate(true)).to.equal(true);
expect(type.validate(false)).to.equal(true);
});
});
});
suite('DATE', function () {
......@@ -123,8 +177,44 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
mysql: 'DATETIME',
sqlite: 'DATETIME'
});
suite('validate', function () {
test('should throw an error if `value` is invalid', function() {
var type = DataTypes.DATE();
expect(function () {
type.validate('foobar');
}).to.throw(Sequelize.ValidationError, '"foobar" is not a valid date');
});
test('should return `true` if `value` is a date', function() {
var type = DataTypes.DATE();
expect(type.validate(new Date())).to.equal(true);
});
});
});
if (current.dialect.supports.HSTORE) {
suite('HSTORE', function () {
suite('validate', function () {
test('should throw an error if `value` is invalid', function() {
var type = DataTypes.HSTORE();
expect(function () {
type.validate('foobar');
}).to.throw(Sequelize.ValidationError, 'foobar is not a valid hstore');
});
test('should return `true` if `value` is an hstore', function() {
var type = DataTypes.HSTORE();
expect(type.validate({ foo: 'bar' })).to.equal(true);
});
});
});
}
suite('UUID', function () {
testsql('UUID', DataTypes.UUID, {
postgres: 'UUID',
......@@ -133,13 +223,66 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
sqlite: 'UUID'
});
suite('validate', function () {
test('should throw an error if `value` is invalid', function() {
var type = DataTypes.UUID();
expect(function () {
type.validate('foobar');
}).to.throw(Sequelize.ValidationError, '"foobar" is not a valid uuid');
});
test('should return `true` if `value` is an uuid', function() {
var type = DataTypes.UUID();
expect(type.validate(uuid.v4())).to.equal(true);
});
});
});
suite('UUIDV1', function () {
testsql('UUIDV1', DataTypes.UUIDV1, {
default: 'UUIDV1'
});
suite('validate', function () {
test('should throw an error if `value` is invalid', function() {
var type = DataTypes.UUIDV1();
expect(function () {
type.validate('foobar');
}).to.throw(Sequelize.ValidationError, '"foobar" is not a valid uuid');
});
test('should return `true` if `value` is an uuid', function() {
var type = DataTypes.UUIDV1();
expect(type.validate(uuid.v1())).to.equal(true);
});
});
});
suite('UUIDV4', function () {
testsql('UUIDV4', DataTypes.UUIDV4, {
default: 'UUIDV4'
});
suite('validate', function () {
test('should throw an error if `value` is invalid', function() {
var type = DataTypes.UUIDV4();
var value = uuid.v1();
expect(function () {
type.validate(value);
}).to.throw(Sequelize.ValidationError, util.format('%j is not a valid uuidv4', value));
});
test('should return `true` if `value` is an uuid', function() {
var type = DataTypes.UUIDV4();
expect(type.validate(uuid.v4())).to.equal(true);
});
});
});
suite('NOW', function () {
......@@ -205,6 +348,22 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
postgres: 'INTEGER',
mssql: 'INTEGER'
});
suite('validate', function () {
test('should throw an error if `value` is invalid', function() {
var type = DataTypes.INTEGER();
expect(function () {
type.validate('foobar');
}).to.throw(Sequelize.ValidationError, '"foobar" is not a valid integer');
});
test('should return `true` if `value` is a valid integer', function() {
var type = DataTypes.INTEGER();
expect(type.validate(12345)).to.equal(true);
});
});
});
suite('BIGINT', function () {
......@@ -263,6 +422,22 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
postgres: 'BIGINT',
mssql: 'BIGINT'
});
suite('validate', function () {
test('should throw an error if `value` is invalid', function() {
var type = DataTypes.BIGINT();
expect(function () {
type.validate('foobar');
}).to.throw(Sequelize.ValidationError, '"foobar" is not a valid bigint');
});
test('should return `true` if `value` is an integer', function() {
var type = DataTypes.BIGINT();
expect(type.validate(12345)).to.equal(true);
});
});
});
suite('REAL', function () {
......@@ -526,6 +701,22 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
postgres: 'FLOAT',
mssql: 'FLOAT'
});
suite('validate', function () {
test('should throw an error if `value` is invalid', function() {
var type = DataTypes.FLOAT();
expect(function () {
type.validate('foobar');
}).to.throw(Sequelize.ValidationError, '"foobar" is not a valid float');
});
test('should return `true` if `value` is a float', function() {
var type = DataTypes.FLOAT();
expect(type.validate(1.2)).to.equal(true);
});
});
});
if (current.dialect.supports.NUMERIC) {
......@@ -560,12 +751,29 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
});
});
// TODO: Fix Enums and add more tests
// suite('ENUM', function () {
// testsql('ENUM("value 1", "value 2")', DataTypes.ENUM('value 1', 'value 2'), {
// default: 'ENUM'
// });
// });
suite('ENUM', function () {
// TODO: Fix Enums and add more tests
// testsql('ENUM("value 1", "value 2")', DataTypes.ENUM('value 1', 'value 2'), {
// default: 'ENUM'
// });
suite('validate', function () {
test('should throw an error if `value` is invalid', function() {
var type = DataTypes.ENUM('foo');
expect(function () {
type.validate('foobar');
}).to.throw(Sequelize.ValidationError, '"foobar" is not a valid choice in ["foo"]');
});
test('should return `true` if `value` is a valid choice', function() {
var type = DataTypes.ENUM('foobar', 'foobiz');
expect(type.validate('foobar')).to.equal(true);
expect(type.validate('foobiz')).to.equal(true);
});
});
});
suite('BLOB', function () {
testsql('BLOB', DataTypes.BLOB, {
......@@ -597,6 +805,71 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
mssql: 'VARBINARY(MAX)',
postgres: 'BYTEA'
});
suite('validate', function () {
test('should throw an error if `value` is invalid', function() {
var type = DataTypes.BLOB();
expect(function () {
type.validate(12345);
}).to.throw(Sequelize.ValidationError, '12345 is not a valid blob');
});
test('should return `true` if `value` is a blob', function() {
var type = DataTypes.BLOB();
expect(type.validate('foobar')).to.equal(true);
expect(type.validate(new Buffer('foobar'))).to.equal(true);
});
});
});
suite('RANGE', function () {
suite('validate', function () {
test('should throw an error if `value` is invalid', function() {
var type = DataTypes.RANGE();
expect(function () {
type.validate('foobar');
}).to.throw(Sequelize.ValidationError, '"foobar" is not a valid range');
});
test('should throw an error if `value` is not an array with two elements', function() {
var type = DataTypes.RANGE();
expect(function () {
type.validate([1]);
}).to.throw(Sequelize.ValidationError, 'A range must be an array with two elements');
});
test('should throw an error if `value.inclusive` is invalid', function() {
var type = DataTypes.RANGE();
expect(function () {
type.validate({ inclusive: 'foobar' });
}).to.throw(Sequelize.ValidationError, '"foobar" is not a valid range');
});
test('should throw an error if `value.inclusive` is not an array with two elements', function() {
var type = DataTypes.RANGE();
expect(function () {
type.validate({ inclusive: [1] });
}).to.throw(Sequelize.ValidationError, 'A range must be an array with two elements');
});
test('should return `true` if `value` is a range', function() {
var type = DataTypes.RANGE();
expect(type.validate([1, 2])).to.equal(true);
});
test('should return `true` if `value.inclusive` is a range', function() {
var type = DataTypes.RANGE();
expect(type.validate({ inclusive: [1, 2] })).to.equal(true);
});
});
});
if (current.dialect.supports.ARRAY) {
......@@ -664,6 +937,22 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
postgres: 'JSONB[]'
});
}
suite('validate', function () {
test('should throw an error if `value` is invalid', function() {
var type = DataTypes.ARRAY();
expect(function () {
type.validate('foobar');
}).to.throw(Sequelize.ValidationError, '"foobar" is not a valid array');
});
test('should return `true` if `value` is an array', function() {
var type = DataTypes.ARRAY();
expect(type.validate(['foo', 'bar'])).to.equal(true);
});
});
});
}
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!