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

Commit e01cc2f8 by Michael Kaufman Committed by Sushant

fix(postgres/date): support for infinity timestamp (#8357)

1 parent 45457b00
......@@ -283,6 +283,27 @@ BOOLEAN.prototype.validate = function validate(value) {
return true;
};
BOOLEAN.prototype._sanitize = function _sanitize(value) {
if (value !== null && value !== undefined) {
if (Buffer.isBuffer(value) && value.length === 1) {
// Bit fields are returned as buffers
value = value[0];
}
if (_.isString(value)) {
// Only take action on valid boolean strings.
value = value === 'true' ? true : value === 'false' ? false : value;
} else if (_.isNumber(value)) {
// Only take action on valid boolean integers.
value = value === 1 ? true : value === 0 ? false : value;
}
}
return value;
};
BOOLEAN.parse = BOOLEAN.prototype._sanitize;
function TIME() {
if (!(this instanceof TIME)) return new TIME();
}
......@@ -315,6 +336,28 @@ DATE.prototype.validate = function validate(value) {
return true;
};
DATE.prototype._sanitize = function _sanitize(value, options) {
if ((!options || options && !options.raw) && !(value instanceof Date) && !!value) {
return new Date(value);
}
return value;
};
DATE.prototype._isChanged = function _isChanged(value, originalValue) {
if (
originalValue && !!value &&
(
value === originalValue ||
value instanceof Date && originalValue instanceof Date && value.getTime() === originalValue.getTime()
)
) {
return false;
}
return true;
};
DATE.prototype._applyTimezone = function _applyTimezone(date, options) {
if (options.timezone) {
if (momentTz.tz.zone(options.timezone)) {
......@@ -350,6 +393,22 @@ DATEONLY.prototype._stringify = function _stringify(date) {
return moment(date).format('YYYY-MM-DD');
};
DATEONLY.prototype._sanitize = function _sanitize(value, options) {
if (!options || options && !options.raw) {
return moment(value).format('YYYY-MM-DD');
}
return value;
};
DATEONLY.prototype._isChanged = function _isChanged(value, originalValue) {
if (originalValue && !!value && originalValue === value) {
return false;
}
return true;
};
function HSTORE() {
if (!(this instanceof HSTORE)) return new HSTORE();
}
......
......@@ -34,6 +34,38 @@ module.exports = BaseTypes => {
inherits(DATEONLY, BaseTypes.DATEONLY);
DATEONLY.parse = function parse(value) {
if (value === 'infinity') {
value = Infinity;
} else if (value === '-infinity') {
value = -Infinity;
}
return value;
};
DATEONLY.prototype._stringify = function _stringify(value, options) {
if (value === Infinity) {
return 'Infinity';
} else if (value === -Infinity) {
return '-Infinity';
}
return BaseTypes.DATEONLY.prototype._stringify.call(this, value, options);
};
DATEONLY.prototype._sanitize = function _sanitize(value, options) {
if ((!options || options && !options.raw) && value !== Infinity && value !== -Infinity) {
if (_.isString(value)) {
if (_.toLower(value) === 'infinity') {
return Infinity;
} else if (_.toLower(value) === '-infinity') {
return -Infinity;
}
}
return BaseTypes.DATEONLY.prototype._sanitize.call(this, value);
}
return value;
};
......@@ -123,6 +155,27 @@ module.exports = BaseTypes => {
return 'BOOLEAN';
};
BOOLEAN.prototype._sanitize = function _sanitize(value) {
if (value !== null && value !== undefined) {
if (Buffer.isBuffer(value) && value.length === 1) {
// Bit fields are returned as buffers
value = value[0];
}
if (_.isString(value)) {
// Only take action on valid boolean strings.
value = value === 'true' || value === 't' ? true : value === 'false' || value === 'f' ? false : value;
} else if (_.isNumber(value)) {
// Only take action on valid boolean integers.
value = value === 1 ? true : value === 0 ? false : value;
}
}
return value;
};
BOOLEAN.parse = BOOLEAN.prototype._sanitize;
BaseTypes.BOOLEAN.types.postgres = {
oids: [16],
array_oids: [1000]
......@@ -138,6 +191,40 @@ module.exports = BaseTypes => {
return 'TIMESTAMP WITH TIME ZONE';
};
DATE.prototype.validate = function validate(value) {
if (value !== Infinity && value !== -Infinity) {
return BaseTypes.DATE.prototype.validate.call(this, value);
}
return true;
};
DATE.prototype._stringify = function _stringify(value, options) {
if (value === Infinity) {
return 'Infinity';
} else if (value === -Infinity) {
return '-Infinity';
}
return BaseTypes.DATE.prototype._stringify.call(this, value, options);
};
DATE.prototype._sanitize = function _sanitize(value, options) {
if ((!options || options && !options.raw) && !(value instanceof Date) && !!value && value !== Infinity && value !== -Infinity) {
if (_.isString(value)) {
if (_.toLower(value) === 'infinity') {
return Infinity;
} else if (_.toLower(value) === '-infinity') {
return -Infinity;
}
}
return new Date(value);
}
return value;
};
BaseTypes.DATE.types.postgres = {
oids: [1184],
array_oids: [1185]
......
......@@ -920,6 +920,9 @@ class Model {
});
});
this._dataTypeChanges = {};
this._dataTypeSanitizers = {};
this._booleanAttributes = [];
this._dateAttributes = [];
this._hstoreAttributes = [];
......@@ -952,6 +955,15 @@ class Model {
this.fieldRawAttributesMap[definition.field] = definition;
if (definition.type._sanitize) {
this._dataTypeSanitizers[name] = definition.type._sanitize;
}
if (definition.type._isChanged) {
this._dataTypeChanges[name] = definition.type._isChanged;
}
if (definition.type instanceof DataTypes.BOOLEAN) {
this._booleanAttributes.push(name);
} else if (definition.type instanceof DataTypes.DATE || definition.type instanceof DataTypes.DATEONLY) {
......@@ -1055,7 +1067,6 @@ class Model {
Object.defineProperty(this.prototype, key, attributeManipulation[key]);
}
this.prototype.rawAttributes = this.rawAttributes;
this.prototype.attributes = Object.keys(this.prototype.rawAttributes);
this.prototype._isAttribute = _.memoize(key => this.prototype.attributes.indexOf(key) !== -1);
......@@ -3200,7 +3211,6 @@ class Model {
this._previousDataValues = _.clone(this.dataValues);
} else {
// Loop and call set
if (options.attributes) {
let keys = options.attributes;
if (this.constructor._hasVirtualAttributes) {
......@@ -3243,7 +3253,6 @@ class Model {
}
} else {
// Check if we have included models, and if this key matches the include model names/aliases
if (this._options && this._options.include && this._options.includeNames.indexOf(key) !== -1) {
// Pass it on to the include handler
this._setInclude(key, value, options);
......@@ -3272,53 +3281,30 @@ class Model {
if (!this.isNewRecord && this.constructor._hasReadOnlyAttributes && this.constructor._isReadOnlyAttribute(key)) {
return this;
}
// Convert date fields to real date objects
if (this.constructor._hasDateAttributes && this.constructor._isDateAttribute(key) && !!value && !(value instanceof Utils.SequelizeMethod)) {
// Dont parse DATEONLY to new Date, keep them as string
if (this.rawAttributes[key].type instanceof DataTypes.DATEONLY) {
if (originalValue && originalValue === value) {
return this;
} else {
value = moment(value).format('YYYY-MM-DD');
}
} else { // go ahread and parse as Date if required
if (!(value instanceof Date)) {
value = new Date(value);
}
if (originalValue) {
if (!(originalValue instanceof Date)) {
originalValue = new Date(originalValue);
}
if (value.getTime() === originalValue.getTime()) {
return this;
}
}
}
}
}
// Convert boolean-ish values to booleans
if (this.constructor._hasBooleanAttributes && this.constructor._isBooleanAttribute(key) && value !== null && value !== undefined && !(value instanceof Utils.SequelizeMethod)) {
if (Buffer.isBuffer(value) && value.length === 1) {
// Bit fields are returned as buffers
value = value[0];
}
if (_.isString(value)) {
// Only take action on valid boolean strings.
value = value === 'true' ? true : value === 'false' ? false : value;
} else if (_.isNumber(value)) {
// Only take action on valid boolean integers.
value = value === 1 ? true : value === 0 ? false : value;
}
// If there's a data type sanitizer
if (!(value instanceof Utils.SequelizeMethod) && this.constructor._dataTypeSanitizers[key]) {
value = this.constructor._dataTypeSanitizers[key].call(this, value, options);
}
if (!options.raw && (!Utils.isPrimitive(value) && value !== null || value !== originalValue)) {
// Set when the value has changed and not raw
if (
!options.raw &&
(
// True when sequelize method
value instanceof Utils.SequelizeMethod ||
// Check for data type type comparators
!(value instanceof Utils.SequelizeMethod) && this.constructor._dataTypeChanges[key] && this.constructor._dataTypeChanges[key].call(this, value, originalValue, options) ||
// Check default
!this.constructor._dataTypeChanges[key] && (!Utils.isPrimitive(value) && value !== null || value !== originalValue)
)
) {
this._previousDataValues[key] = originalValue;
this.changed(key, true);
}
// set data value
this.dataValues[key] = value;
}
}
......
......@@ -455,17 +455,64 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => {
stamp: Sequelize.DATEONLY
});
const testDate = moment().format('YYYY-MM-DD');
const newDate = new Date();
return Model.sync({ force: true})
.then(() => Model.create({ stamp: testDate }))
.then(record => {
expect(typeof record.stamp).to.be.eql('string');
expect(record.stamp).to.be.eql(testDate);
return Model.findById(record.id);
}).then(record => {
expect(typeof record.stamp).to.be.eql('string');
expect(record.stamp).to.be.eql(testDate);
return record.update({
stamp: testDate
});
}).then(record => {
return record.reload();
}).then(record => {
expect(typeof record.stamp).to.be.eql('string');
expect(record.stamp).to.be.eql(testDate);
return record.update({
stamp: newDate
});
}).then(record => {
return record.reload();
}).then(record => {
expect(typeof record.stamp).to.be.eql('string');
expect(new Date(record.stamp)).to.equalDate(newDate);
});
});
it('should be able to cast buffer as boolean', function() {
const ByteModel = this.sequelize.define('Model', {
byteToBool: this.sequelize.Sequelize.BLOB
}, {
timestamps: false
});
const BoolModel = this.sequelize.define('Model', {
byteToBool: this.sequelize.Sequelize.BOOLEAN
}, {
timestamps: false
});
return ByteModel.sync({
force: true
}).then(() => {
return ByteModel.create({
byteToBool: new Buffer([true])
});
}).then(byte => {
expect(byte.byteToBool).to.be.ok;
return BoolModel.findById(byte.id);
}).then(bool => {
expect(bool.byteToBool).to.be.true;
});
});
});
'use strict';
const chai = require('chai');
const expect = chai.expect;
const Support = require(__dirname + '/../../support');
const dialect = Support.getTestDialect();
const DataTypes = require(__dirname + '/../../../../lib/data-types');
if (dialect === 'postgres') {
describe('[POSTGRES Specific] Data Types', () => {
describe('DATE/DATEONLY Validate and Stringify', () => {
const now = new Date();
const nowString = now.toISOString();
it('DATE should validate a Date as normal', () => {
expect(DataTypes[dialect].DATE().validate(now)).to.equal(true);
expect(DataTypes[dialect].DATE().validate(nowString)).to.equal(true);
});
it('DATE should validate Infinity/-Infinity as true', () => {
expect(DataTypes[dialect].DATE().validate(Infinity)).to.equal(true);
expect(DataTypes[dialect].DATE().validate(-Infinity)).to.equal(true);
});
it('DATE should stringify Infinity/-Infinity to infinity/-infinity', () => {
expect(DataTypes[dialect].DATE().stringify(Infinity)).to.equal('Infinity');
expect(DataTypes[dialect].DATE().stringify(-Infinity)).to.equal('-Infinity');
});
it('DATEONLY should stringify Infinity/-Infinity to infinity/-infinity', () => {
expect(DataTypes[dialect].DATEONLY().stringify(Infinity)).to.equal('Infinity');
expect(DataTypes[dialect].DATEONLY().stringify(-Infinity)).to.equal('-Infinity');
});
});
describe('DATE/DATEONLY Sanitize', () => {
const now = new Date();
const nowString = now.toISOString();
const nowDateOnly = nowString.substr(0, 10);
it('DATE should sanitize a Date as normal', () => {
expect(DataTypes[dialect].DATE()._sanitize(now)).to.equalTime(now);
expect(DataTypes[dialect].DATE()._sanitize(nowString)).to.equalTime(now);
});
it('DATE should sanitize Infinity/-Infinity as Infinity/-Infinity', () => {
expect(DataTypes[dialect].DATE()._sanitize(Infinity)).to.equal(Infinity);
expect(DataTypes[dialect].DATE()._sanitize(-Infinity)).to.equal(-Infinity);
});
it('DATE should sanitize "Infinity"/"-Infinity" as Infinity/-Infinity', () => {
expect(DataTypes[dialect].DATE()._sanitize('Infinity')).to.equal(Infinity);
expect(DataTypes[dialect].DATE()._sanitize('-Infinity')).to.equal(-Infinity);
});
it('DATEONLY should sanitize a Date as normal', () => {
expect(DataTypes[dialect].DATEONLY()._sanitize(now)).to.equal(nowDateOnly);
expect(DataTypes[dialect].DATEONLY()._sanitize(nowString)).to.equal(nowDateOnly);
});
it('DATEONLY should sanitize Infinity/-Infinity as Infinity/-Infinity', () => {
expect(DataTypes[dialect].DATEONLY()._sanitize(Infinity)).to.equal(Infinity);
expect(DataTypes[dialect].DATEONLY()._sanitize(-Infinity)).to.equal(-Infinity);
});
it('DATEONLY should sanitize "Infinity"/"-Infinity" as Infinity/-Infinity', () => {
expect(DataTypes[dialect].DATEONLY()._sanitize('Infinity')).to.equal(Infinity);
expect(DataTypes[dialect].DATEONLY()._sanitize('-Infinity')).to.equal(-Infinity);
});
});
describe('DATE SQL', () => {
// create dummy user
it('should be able to create and update records with Infinity/-Infinity', function() {
this.sequelize.options.typeValidation = true;
const date = new Date();
const User = this.sequelize.define('User', {
username: this.sequelize.Sequelize.STRING,
beforeTime: {
type: this.sequelize.Sequelize.DATE,
defaultValue: -Infinity
},
sometime: {
type: this.sequelize.Sequelize.DATE,
defaultValue: this.sequelize.fn('NOW')
},
anotherTime: {
type: this.sequelize.Sequelize.DATE
},
afterTime: {
type: this.sequelize.Sequelize.DATE,
defaultValue: Infinity
}
}, {
timestamps: true
});
return User.sync({
force: true
}).then(() => {
return User.create({
username: 'bob',
anotherTime: Infinity
}, {
validate: true
});
}).then(user => {
expect(user.username).to.equal('bob');
expect(user.beforeTime).to.equal(-Infinity);
expect(user.sometime).to.be.withinTime(date, new Date());
expect(user.anotherTime).to.equal(Infinity);
expect(user.afterTime).to.equal(Infinity);
return user.update({
sometime: Infinity
}, {
returning: true
});
}).then(user => {
expect(user.sometime).to.equal(Infinity);
return user.update({
sometime: Infinity
});
}).then(user => {
expect(user.sometime).to.equal(Infinity);
return user.update({
sometime: this.sequelize.fn('NOW')
}, {
returning: true
});
}).then(user => {
expect(user.sometime).to.be.withinTime(date, new Date());
// find
return User.findAll();
}).then(users => {
expect(users[0].beforeTime).to.equal(-Infinity);
expect(users[0].sometime).to.not.equal(Infinity);
expect(users[0].afterTime).to.equal(Infinity);
return users[0].update({
sometime: date
});
}).then(user => {
expect(user.sometime).to.equalTime(date);
return user.update({
sometime: date
});
}).then(user => {
expect(user.sometime).to.equalTime(date);
});
});
});
describe('DATEONLY SQL', () => {
// create dummy user
it('should be able to create and update records with Infinity/-Infinity', function() {
this.sequelize.options.typeValidation = true;
const date = new Date();
const User = this.sequelize.define('User', {
username: this.sequelize.Sequelize.STRING,
beforeTime: {
type: this.sequelize.Sequelize.DATEONLY,
defaultValue: -Infinity
},
sometime: {
type: this.sequelize.Sequelize.DATEONLY,
defaultValue: this.sequelize.fn('NOW')
},
anotherTime: {
type: this.sequelize.Sequelize.DATEONLY
},
afterTime: {
type: this.sequelize.Sequelize.DATEONLY,
defaultValue: Infinity
}
}, {
timestamps: true
});
return User.sync({
force: true
}).then(() => {
return User.create({
username: 'bob',
anotherTime: Infinity
}, {
validate: true
});
}).then(user => {
expect(user.username).to.equal('bob');
expect(user.beforeTime).to.equal(-Infinity);
expect(new Date(user.sometime)).to.be.withinDate(date, new Date());
expect(user.anotherTime).to.equal(Infinity);
expect(user.afterTime).to.equal(Infinity);
return user.update({
sometime: Infinity
}, {
returning: true
});
}).then(user => {
expect(user.sometime).to.equal(Infinity);
return user.update({
sometime: Infinity
});
}).then(user => {
expect(user.sometime).to.equal(Infinity);
return user.update({
sometime: this.sequelize.fn('NOW')
}, {
returning: true
});
}).then(user => {
expect(user.sometime).to.not.equal(Infinity);
expect(new Date(user.sometime)).to.be.withinDate(date, new Date());
// find
return User.findAll();
}).then(users => {
expect(users[0].beforeTime).to.equal(-Infinity);
expect(users[0].sometime).to.not.equal(Infinity);
expect(users[0].afterTime).to.equal(Infinity);
return users[0].update({
sometime: '1969-07-20'
});
}).then(user => {
expect(user.sometime).to.equal('1969-07-20');
return user.update({
sometime: '1969-07-20'
});
}).then(user => {
expect(user.sometime).to.equal('1969-07-20');
});
});
});
});
}
......@@ -175,6 +175,52 @@ describe(Support.getTestDialectTeaser('Model'), () => {
});
});
if (current.dialect.name === 'postgres') {
describe('GEOGRAPHY(POLYGON, SRID)', () => {
beforeEach(function() {
this.User = this.sequelize.define('User', {
username: DataTypes.STRING,
geography: DataTypes.GEOGRAPHY('POLYGON', 4326)
});
return this.User.sync({ force: true });
});
it('should create a geography object', function() {
const User = this.User;
const point = { type: 'Polygon', coordinates: [
[ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],
[100.0, 1.0], [100.0, 0.0] ]
]};
return User.create({username: 'username', geography: point }).then(newUser => {
expect(newUser).not.to.be.null;
expect(newUser.geography).to.be.deep.eql(point);
});
});
it('should update a geography object', function() {
const User = this.User;
const polygon1 = { type: 'Polygon', coordinates: [
[ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ]
]},
polygon2 = { type: 'Polygon', coordinates: [
[ [100.0, 0.0], [102.0, 0.0], [102.0, 1.0],
[100.0, 1.0], [100.0, 0.0] ]
]};
const props = {username: 'username', geography: polygon1};
return User.create(props).then(() => {
return User.update({geography: polygon2}, {where: {username: props.username}});
}).then(() => {
return User.findOne({where: {username: props.username}});
}).then(user => {
expect(user.geography).to.be.deep.eql(polygon2);
});
});
});
}
describe('sql injection attacks', () => {
beforeEach(function() {
this.Model = this.sequelize.define('Model', {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!