Skip to content

Commit e01cc2f

Browse files
mkaufmanersushantdhiman
authored andcommitted
fix(postgres/date): support for infinity timestamp (#8357)
1 parent 45457b0 commit e01cc2f

File tree

6 files changed

+516
-43
lines changed

6 files changed

+516
-43
lines changed

lib/data-types.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,27 @@ BOOLEAN.prototype.validate = function validate(value) {
283283
return true;
284284
};
285285

286+
BOOLEAN.prototype._sanitize = function _sanitize(value) {
287+
if (value !== null && value !== undefined) {
288+
if (Buffer.isBuffer(value) && value.length === 1) {
289+
// Bit fields are returned as buffers
290+
value = value[0];
291+
}
292+
293+
if (_.isString(value)) {
294+
// Only take action on valid boolean strings.
295+
value = value === 'true' ? true : value === 'false' ? false : value;
296+
297+
} else if (_.isNumber(value)) {
298+
// Only take action on valid boolean integers.
299+
value = value === 1 ? true : value === 0 ? false : value;
300+
}
301+
}
302+
303+
return value;
304+
};
305+
BOOLEAN.parse = BOOLEAN.prototype._sanitize;
306+
286307
function TIME() {
287308
if (!(this instanceof TIME)) return new TIME();
288309
}
@@ -315,6 +336,28 @@ DATE.prototype.validate = function validate(value) {
315336
return true;
316337
};
317338

339+
DATE.prototype._sanitize = function _sanitize(value, options) {
340+
if ((!options || options && !options.raw) && !(value instanceof Date) && !!value) {
341+
return new Date(value);
342+
}
343+
344+
return value;
345+
};
346+
347+
DATE.prototype._isChanged = function _isChanged(value, originalValue) {
348+
if (
349+
originalValue && !!value &&
350+
(
351+
value === originalValue ||
352+
value instanceof Date && originalValue instanceof Date && value.getTime() === originalValue.getTime()
353+
)
354+
) {
355+
return false;
356+
}
357+
358+
return true;
359+
};
360+
318361
DATE.prototype._applyTimezone = function _applyTimezone(date, options) {
319362
if (options.timezone) {
320363
if (momentTz.tz.zone(options.timezone)) {
@@ -350,6 +393,22 @@ DATEONLY.prototype._stringify = function _stringify(date) {
350393
return moment(date).format('YYYY-MM-DD');
351394
};
352395

396+
DATEONLY.prototype._sanitize = function _sanitize(value, options) {
397+
if (!options || options && !options.raw) {
398+
return moment(value).format('YYYY-MM-DD');
399+
}
400+
401+
return value;
402+
};
403+
404+
DATEONLY.prototype._isChanged = function _isChanged(value, originalValue) {
405+
if (originalValue && !!value && originalValue === value) {
406+
return false;
407+
}
408+
409+
return true;
410+
};
411+
353412
function HSTORE() {
354413
if (!(this instanceof HSTORE)) return new HSTORE();
355414
}

lib/dialects/postgres/data-types.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,38 @@ module.exports = BaseTypes => {
3434
inherits(DATEONLY, BaseTypes.DATEONLY);
3535

3636
DATEONLY.parse = function parse(value) {
37+
if (value === 'infinity') {
38+
value = Infinity;
39+
} else if (value === '-infinity') {
40+
value = -Infinity;
41+
}
42+
43+
return value;
44+
};
45+
46+
DATEONLY.prototype._stringify = function _stringify(value, options) {
47+
if (value === Infinity) {
48+
return 'Infinity';
49+
} else if (value === -Infinity) {
50+
return '-Infinity';
51+
}
52+
53+
return BaseTypes.DATEONLY.prototype._stringify.call(this, value, options);
54+
};
55+
56+
DATEONLY.prototype._sanitize = function _sanitize(value, options) {
57+
if ((!options || options && !options.raw) && value !== Infinity && value !== -Infinity) {
58+
if (_.isString(value)) {
59+
if (_.toLower(value) === 'infinity') {
60+
return Infinity;
61+
} else if (_.toLower(value) === '-infinity') {
62+
return -Infinity;
63+
}
64+
}
65+
66+
return BaseTypes.DATEONLY.prototype._sanitize.call(this, value);
67+
}
68+
3769
return value;
3870
};
3971

@@ -123,6 +155,27 @@ module.exports = BaseTypes => {
123155
return 'BOOLEAN';
124156
};
125157

158+
BOOLEAN.prototype._sanitize = function _sanitize(value) {
159+
if (value !== null && value !== undefined) {
160+
if (Buffer.isBuffer(value) && value.length === 1) {
161+
// Bit fields are returned as buffers
162+
value = value[0];
163+
}
164+
165+
if (_.isString(value)) {
166+
// Only take action on valid boolean strings.
167+
value = value === 'true' || value === 't' ? true : value === 'false' || value === 'f' ? false : value;
168+
169+
} else if (_.isNumber(value)) {
170+
// Only take action on valid boolean integers.
171+
value = value === 1 ? true : value === 0 ? false : value;
172+
}
173+
}
174+
175+
return value;
176+
};
177+
BOOLEAN.parse = BOOLEAN.prototype._sanitize;
178+
126179
BaseTypes.BOOLEAN.types.postgres = {
127180
oids: [16],
128181
array_oids: [1000]
@@ -138,6 +191,40 @@ module.exports = BaseTypes => {
138191
return 'TIMESTAMP WITH TIME ZONE';
139192
};
140193

194+
DATE.prototype.validate = function validate(value) {
195+
if (value !== Infinity && value !== -Infinity) {
196+
return BaseTypes.DATE.prototype.validate.call(this, value);
197+
}
198+
199+
return true;
200+
};
201+
202+
DATE.prototype._stringify = function _stringify(value, options) {
203+
if (value === Infinity) {
204+
return 'Infinity';
205+
} else if (value === -Infinity) {
206+
return '-Infinity';
207+
}
208+
209+
return BaseTypes.DATE.prototype._stringify.call(this, value, options);
210+
};
211+
212+
DATE.prototype._sanitize = function _sanitize(value, options) {
213+
if ((!options || options && !options.raw) && !(value instanceof Date) && !!value && value !== Infinity && value !== -Infinity) {
214+
if (_.isString(value)) {
215+
if (_.toLower(value) === 'infinity') {
216+
return Infinity;
217+
} else if (_.toLower(value) === '-infinity') {
218+
return -Infinity;
219+
}
220+
}
221+
222+
return new Date(value);
223+
}
224+
225+
return value;
226+
};
227+
141228
BaseTypes.DATE.types.postgres = {
142229
oids: [1184],
143230
array_oids: [1185]

lib/model.js

Lines changed: 29 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,9 @@ class Model {
920920
});
921921
});
922922

923+
this._dataTypeChanges = {};
924+
this._dataTypeSanitizers = {};
925+
923926
this._booleanAttributes = [];
924927
this._dateAttributes = [];
925928
this._hstoreAttributes = [];
@@ -952,6 +955,15 @@ class Model {
952955

953956
this.fieldRawAttributesMap[definition.field] = definition;
954957

958+
959+
if (definition.type._sanitize) {
960+
this._dataTypeSanitizers[name] = definition.type._sanitize;
961+
}
962+
963+
if (definition.type._isChanged) {
964+
this._dataTypeChanges[name] = definition.type._isChanged;
965+
}
966+
955967
if (definition.type instanceof DataTypes.BOOLEAN) {
956968
this._booleanAttributes.push(name);
957969
} else if (definition.type instanceof DataTypes.DATE || definition.type instanceof DataTypes.DATEONLY) {
@@ -1055,7 +1067,6 @@ class Model {
10551067
Object.defineProperty(this.prototype, key, attributeManipulation[key]);
10561068
}
10571069

1058-
10591070
this.prototype.rawAttributes = this.rawAttributes;
10601071
this.prototype.attributes = Object.keys(this.prototype.rawAttributes);
10611072
this.prototype._isAttribute = _.memoize(key => this.prototype.attributes.indexOf(key) !== -1);
@@ -3200,7 +3211,6 @@ class Model {
32003211
this._previousDataValues = _.clone(this.dataValues);
32013212
} else {
32023213
// Loop and call set
3203-
32043214
if (options.attributes) {
32053215
let keys = options.attributes;
32063216
if (this.constructor._hasVirtualAttributes) {
@@ -3243,7 +3253,6 @@ class Model {
32433253
}
32443254
} else {
32453255
// Check if we have included models, and if this key matches the include model names/aliases
3246-
32473256
if (this._options && this._options.include && this._options.includeNames.indexOf(key) !== -1) {
32483257
// Pass it on to the include handler
32493258
this._setInclude(key, value, options);
@@ -3272,53 +3281,30 @@ class Model {
32723281
if (!this.isNewRecord && this.constructor._hasReadOnlyAttributes && this.constructor._isReadOnlyAttribute(key)) {
32733282
return this;
32743283
}
3275-
3276-
// Convert date fields to real date objects
3277-
if (this.constructor._hasDateAttributes && this.constructor._isDateAttribute(key) && !!value && !(value instanceof Utils.SequelizeMethod)) {
3278-
// Dont parse DATEONLY to new Date, keep them as string
3279-
if (this.rawAttributes[key].type instanceof DataTypes.DATEONLY) {
3280-
if (originalValue && originalValue === value) {
3281-
return this;
3282-
} else {
3283-
value = moment(value).format('YYYY-MM-DD');
3284-
}
3285-
} else { // go ahread and parse as Date if required
3286-
if (!(value instanceof Date)) {
3287-
value = new Date(value);
3288-
}
3289-
if (originalValue) {
3290-
if (!(originalValue instanceof Date)) {
3291-
originalValue = new Date(originalValue);
3292-
}
3293-
if (value.getTime() === originalValue.getTime()) {
3294-
return this;
3295-
}
3296-
}
3297-
}
3298-
}
32993284
}
33003285

3301-
// Convert boolean-ish values to booleans
3302-
if (this.constructor._hasBooleanAttributes && this.constructor._isBooleanAttribute(key) && value !== null && value !== undefined && !(value instanceof Utils.SequelizeMethod)) {
3303-
if (Buffer.isBuffer(value) && value.length === 1) {
3304-
// Bit fields are returned as buffers
3305-
value = value[0];
3306-
}
3307-
3308-
if (_.isString(value)) {
3309-
// Only take action on valid boolean strings.
3310-
value = value === 'true' ? true : value === 'false' ? false : value;
3311-
3312-
} else if (_.isNumber(value)) {
3313-
// Only take action on valid boolean integers.
3314-
value = value === 1 ? true : value === 0 ? false : value;
3315-
}
3286+
// If there's a data type sanitizer
3287+
if (!(value instanceof Utils.SequelizeMethod) && this.constructor._dataTypeSanitizers[key]) {
3288+
value = this.constructor._dataTypeSanitizers[key].call(this, value, options);
33163289
}
33173290

3318-
if (!options.raw && (!Utils.isPrimitive(value) && value !== null || value !== originalValue)) {
3291+
// Set when the value has changed and not raw
3292+
if (
3293+
!options.raw &&
3294+
(
3295+
// True when sequelize method
3296+
value instanceof Utils.SequelizeMethod ||
3297+
// Check for data type type comparators
3298+
!(value instanceof Utils.SequelizeMethod) && this.constructor._dataTypeChanges[key] && this.constructor._dataTypeChanges[key].call(this, value, originalValue, options) ||
3299+
// Check default
3300+
!this.constructor._dataTypeChanges[key] && (!Utils.isPrimitive(value) && value !== null || value !== originalValue)
3301+
)
3302+
) {
33193303
this._previousDataValues[key] = originalValue;
33203304
this.changed(key, true);
33213305
}
3306+
3307+
// set data value
33223308
this.dataValues[key] = value;
33233309
}
33243310
}

test/integration/data-types.test.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,17 +455,64 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => {
455455
stamp: Sequelize.DATEONLY
456456
});
457457
const testDate = moment().format('YYYY-MM-DD');
458+
const newDate = new Date();
458459

459460
return Model.sync({ force: true})
460461
.then(() => Model.create({ stamp: testDate }))
461462
.then(record => {
462463
expect(typeof record.stamp).to.be.eql('string');
463464
expect(record.stamp).to.be.eql(testDate);
465+
464466
return Model.findById(record.id);
465467
}).then(record => {
466468
expect(typeof record.stamp).to.be.eql('string');
467469
expect(record.stamp).to.be.eql(testDate);
470+
471+
return record.update({
472+
stamp: testDate
473+
});
474+
}).then(record => {
475+
return record.reload();
476+
}).then(record => {
477+
expect(typeof record.stamp).to.be.eql('string');
478+
expect(record.stamp).to.be.eql(testDate);
479+
480+
return record.update({
481+
stamp: newDate
482+
});
483+
}).then(record => {
484+
return record.reload();
485+
}).then(record => {
486+
expect(typeof record.stamp).to.be.eql('string');
487+
expect(new Date(record.stamp)).to.equalDate(newDate);
468488
});
469489
});
470490

491+
it('should be able to cast buffer as boolean', function() {
492+
const ByteModel = this.sequelize.define('Model', {
493+
byteToBool: this.sequelize.Sequelize.BLOB
494+
}, {
495+
timestamps: false
496+
});
497+
498+
const BoolModel = this.sequelize.define('Model', {
499+
byteToBool: this.sequelize.Sequelize.BOOLEAN
500+
}, {
501+
timestamps: false
502+
});
503+
504+
return ByteModel.sync({
505+
force: true
506+
}).then(() => {
507+
return ByteModel.create({
508+
byteToBool: new Buffer([true])
509+
});
510+
}).then(byte => {
511+
expect(byte.byteToBool).to.be.ok;
512+
513+
return BoolModel.findById(byte.id);
514+
}).then(bool => {
515+
expect(bool.byteToBool).to.be.true;
516+
});
517+
});
471518
});

0 commit comments

Comments
 (0)