diff --git a/src/data-type.ts b/src/data-type.ts index 77d81eed9..ae0e4447b 100644 --- a/src/data-type.ts +++ b/src/data-type.ts @@ -80,7 +80,7 @@ export interface DataType { generateTypeInfo(parameter: ParameterData, options: InternalConnectionOptions): Buffer; generateParameterLength(parameter: ParameterData, options: InternalConnectionOptions): Buffer; generateParameterData(parameter: ParameterData, options: InternalConnectionOptions): Generator; - validate(value: any, collation: Collation | undefined): any; // TODO: Refactor 'any' and replace with more specific type. + validate(value: any, collation: Collation | undefined, options?: InternalConnectionOptions): any; // TODO: Refactor 'any' and replace with more specific type. hasTableName?: boolean; diff --git a/src/data-types/date.ts b/src/data-types/date.ts index 8e8e1b760..9bec9d5ef 100644 --- a/src/data-types/date.ts +++ b/src/data-types/date.ts @@ -7,6 +7,9 @@ const EPOCH_DATE = LocalDate.ofYearDay(1, 1); const NULL_LENGTH = Buffer.from([0x00]); const DATA_LENGTH = Buffer.from([0x03]); +const MIN_DATE = new globalDate('January 1, 0001'); +const MAX_DATE = new globalDate('December 31, 9999'); + const Date: DataType = { id: 0x28, type: 'DATEN', @@ -35,7 +38,7 @@ const Date: DataType = { const value = parameter.value as any; // Temporary solution. Remove 'any' later. - let date; + let date: LocalDate; if (options.useUTC) { date = LocalDate.of(value.getUTCFullYear(), value.getUTCMonth() + 1, value.getUTCDate()); } else { @@ -49,7 +52,7 @@ const Date: DataType = { }, // TODO: value is technically of type 'unknown'. - validate: function(value): null | Date { + validate: function(value, collation, options): null | Date { if (value == null) { return null; } @@ -58,6 +61,18 @@ const Date: DataType = { value = new globalDate(globalDate.parse(value)); } + value = value as Date; + + // TODO: check date range: January 1, 0001, through December 31, 9999 + // : time range: 00:00:00 through 23:59:59.997 + if (options && options.useUTC) { + value = new globalDate(value.toUTCString()); + } + + if (value < MIN_DATE || value > MAX_DATE) { + throw new TypeError('Out of range.'); + } + if (isNaN(value)) { throw new TypeError('Invalid date.'); } diff --git a/src/data-types/datetime.ts b/src/data-types/datetime.ts index 02f62ef15..6c253c756 100644 --- a/src/data-types/datetime.ts +++ b/src/data-types/datetime.ts @@ -6,6 +6,9 @@ const EPOCH_DATE = LocalDate.ofYearDay(1900, 1); const NULL_LENGTH = Buffer.from([0x00]); const DATA_LENGTH = Buffer.from([0x08]); +const MIN_DATE = new Date('January 1, 1753'); +const MAX_DATE = new Date('December 31, 9999'); + const DateTime: DataType = { id: 0x3D, type: 'DATETIME', @@ -34,7 +37,7 @@ const DateTime: DataType = { const value = parameter.value as any; // Temporary solution. Remove 'any' later. - let date; + let date: LocalDate; if (options.useUTC) { date = LocalDate.of(value.getUTCFullYear(), value.getUTCMonth() + 1, value.getUTCDate()); } else { @@ -72,7 +75,7 @@ const DateTime: DataType = { }, // TODO: type 'any' needs to be revisited. - validate: function(value): null | number { + validate: function(value: any, collation, options): null | number { if (value == null) { return null; } @@ -81,6 +84,18 @@ const DateTime: DataType = { value = new Date(Date.parse(value)); } + value = value as Date; + + // TODO: check date range: January 1, 1753, through December 31, 9999 + // : time range: 00:00:00 through 23:59:59.997 + if (options && options.useUTC) { + value = new Date(value.toUTCString()); + } + + if (value < MIN_DATE || value > MAX_DATE) { + throw new TypeError('Out of range.'); + } + if (isNaN(value)) { throw new TypeError('Invalid date.'); } diff --git a/src/data-types/datetime2.ts b/src/data-types/datetime2.ts index a31151107..5299c3b9d 100644 --- a/src/data-types/datetime2.ts +++ b/src/data-types/datetime2.ts @@ -5,6 +5,9 @@ import WritableTrackingBuffer from '../tracking-buffer/writable-tracking-buffer' const EPOCH_DATE = LocalDate.ofYearDay(1, 1); const NULL_LENGTH = Buffer.from([0x00]); +const MIN_DATE = new Date('January 1, 0001'); +const MAX_DATE = new Date('December 31, 9999'); + const DateTime2: DataType & { resolveScale: NonNullable } = { id: 0x2A, type: 'DATETIME2N', @@ -64,7 +67,7 @@ const DateTime2: DataType & { resolveScale: NonNullable MAX_DATE) { + throw new TypeError('Out of range.'); + } + if (isNaN(value)) { throw new TypeError('Invalid date.'); } diff --git a/src/data-types/datetimeoffset.ts b/src/data-types/datetimeoffset.ts index b936fcc18..3dec978df 100644 --- a/src/data-types/datetimeoffset.ts +++ b/src/data-types/datetimeoffset.ts @@ -5,6 +5,9 @@ import WritableTrackingBuffer from '../tracking-buffer/writable-tracking-buffer' const EPOCH_DATE = LocalDate.ofYearDay(1, 1); const NULL_LENGTH = Buffer.from([0x00]); +const MIN_DATE = new Date('January 1, 0001'); +const MAX_DATE = new Date('December 31, 9999'); + const DateTimeOffset: DataType & { resolveScale: NonNullable } = { id: 0x2B, type: 'DATETIMEOFFSETN', @@ -62,7 +65,7 @@ const DateTimeOffset: DataType & { resolveScale: NonNullable MAX_DATE) { + throw new TypeError('Out of range.'); + } + if (isNaN(value)) { throw new TypeError('Invalid date.'); } diff --git a/src/data-types/money.ts b/src/data-types/money.ts index 7a614606e..5c1271484 100644 --- a/src/data-types/money.ts +++ b/src/data-types/money.ts @@ -49,6 +49,14 @@ const Money: DataType = { if (isNaN(value)) { throw new TypeError('Invalid number.'); } + // money: -922337203685477.5808 to 922337203685477.5807 + // in javascript -922337203685477.5808 === -922337203685477.6 + // 922337203685477.5807 === 922337203685477.6 + // javascript number doesn't have enough precision. + if (value < -922337203685477.6 || value > 922337203685477.6) { + throw new TypeError('Value must be between -922337203685477.5808 and 922337203685477.5807, inclusive.'); + } + return value; } }; diff --git a/src/data-types/smalldatetime.ts b/src/data-types/smalldatetime.ts index b17d0d834..e31d811bf 100644 --- a/src/data-types/smalldatetime.ts +++ b/src/data-types/smalldatetime.ts @@ -4,6 +4,9 @@ import DateTimeN from './datetimen'; const EPOCH_DATE = new Date(1900, 0, 1); const UTC_EPOCH_DATE = new Date(Date.UTC(1900, 0, 1)); +const MIN_DATE = new Date(1900, 1, 1); +const MAX_DATE = new Date(2079, 5, 6, 23, 59, 59, 0); + const DATA_LENGTH = Buffer.from([0x04]); const NULL_LENGTH = Buffer.from([0x00]); @@ -35,7 +38,7 @@ const SmallDateTime: DataType = { const buffer = Buffer.alloc(4); - let days, dstDiff, minutes; + let days: number, dstDiff: number, minutes: number; if (options.useUTC) { days = Math.floor((parameter.value.getTime() - UTC_EPOCH_DATE.getTime()) / (1000 * 60 * 60 * 24)); minutes = (parameter.value.getUTCHours() * 60) + parameter.value.getUTCMinutes(); @@ -51,7 +54,7 @@ const SmallDateTime: DataType = { yield buffer; }, - validate: function(value): null | Date { + validate: function(value, collation, options): null | Date { if (value == null) { return null; } @@ -60,6 +63,16 @@ const SmallDateTime: DataType = { value = new Date(Date.parse(value)); } + value = value as Date; + + if (options && options.useUTC) { + value = new Date(value.toUTCString()); + } + + if (value < MIN_DATE || value > MAX_DATE) { + throw new TypeError('Out of range.'); + } + if (isNaN(value)) { throw new TypeError('Invalid date.'); } diff --git a/test/unit/data-type.js b/test/unit/data-type.js index 6f51391bd..362c2ae15 100644 --- a/test/unit/data-type.js +++ b/test/unit/data-type.js @@ -211,6 +211,18 @@ describe('Date', function() { assert.deepEqual(result, expected); }); }); + + describe('.validate', function() { + it('returns a TypeError for dates that are out of range', function() { + assert.throws(() => { + TYPES.Date.validate(new Date('Dec 31 2000')); + }, TypeError, 'Out of range.'); + + assert.throws(() => { + TYPES.Date.validate(new Date('Jan 1, 10000')); + }, TypeError, 'Out of range.'); + }); + }); }); describe('DateTime', function() { @@ -246,6 +258,18 @@ describe('DateTime', function() { assert.deepEqual(result, expected); }); }); + + describe('.validate', function() { + it('returns a TypeError for dates that are out of range', function() { + assert.throws(() => { + TYPES.DateTime.validate(new Date('Dec 1, 1752')); + }, TypeError, 'Out of range.'); + + assert.throws(() => { + TYPES.DateTime.validate('Jan 1, 10000'); + }, TypeError, 'Out of range.'); + }); + }); }); describe('DateTime2', function() { @@ -293,6 +317,17 @@ describe('DateTime2', function() { assert.deepEqual(buffer, expected); }); }); + describe('.validate', function() { + it('returns a TypeError for dates that are out of range', function() { + assert.throws(() => { + TYPES.DateTime2.validate(new Date('Dec 31, 2000')); + }, TypeError, 'Out of range.'); + + assert.throws(() => { + TYPES.DateTime2.validate(new Date('Jan 1, 10000')); + }, TypeError, 'Out of range.'); + }); + }); }); describe('DateTimeOffset', function() { @@ -346,6 +381,18 @@ describe('DateTimeOffset', function() { assert.deepEqual(buffer, expected); }); }); + + describe('.validate', function() { + it('returns a TypeError for dates that are out of range', function() { + assert.throws(() => { + TYPES.DateTimeOffset.validate(new Date('Dec 31, 2000')); + }, TypeError, 'Out of range.'); + + assert.throws(() => { + TYPES.DateTimeOffset.validate(new Date('Jan 1, 10000')); + }, TypeError, 'Out of range.'); + }); + }); }); describe('Decimal', function() { @@ -449,6 +496,34 @@ describe('Decimal', function() { assert.deepEqual(result4, expected4); }); }); + + describe('.validate', function() { + it('returns a TypeError for decimals if the passed in value is unacceptable', function() { + assert.throws(() => { + TYPES.Decimal.validate('ABC'); + }, TypeError, 'Invalid number.'); + assert.throws(() => { + TYPES.Decimal.validate('e123'); + }, TypeError, 'Invalid number.'); + }); + + it('returns a the "Infinity" literal the decimals is outside the double-precision 64-bit IEEE 754-2019 format range', function() { + assert.equal(TYPES.Decimal.validate(1.7976931348623159e+308), Infinity); + assert.equal(TYPES.Decimal.validate(-1.7976931348623159e+308), -Infinity); + assert.equal(TYPES.Decimal.validate('Infinity'), Infinity); + assert.equal(TYPES.Decimal.validate('-Infinity'), -Infinity); + }); + + it('Corect pasing the decimals with special cases', function() { + assert.equal(TYPES.Decimal.validate('123.3.3'), 123.3); + assert.equal(TYPES.Decimal.validate('1-23'), 1); + assert.equal(TYPES.Decimal.validate('1+23'), 1); + assert.equal(TYPES.Decimal.validate('1e23e4'), 1e23); + assert.equal(TYPES.Decimal.validate(' 123'), 123); + assert.equal(TYPES.Decimal.validate('1-e5'), 1); + assert.equal(TYPES.Decimal.validate('1e2e3'), 100); + }); + }); }); describe('Float', function() { @@ -492,6 +567,34 @@ describe('Float', function() { assert.deepEqual(result, expected); }); }); + + describe('.validate', function() { + it('returns a TypeError for decimals if the passed in value is unacceptable', function() { + assert.throws(() => { + TYPES.Float.validate('ABC'); + }, TypeError, 'Invalid number.'); + assert.throws(() => { + TYPES.Float.validate('e123'); + }, TypeError, 'Invalid number.'); + }); + + it('returns a the "Infinity" literal the decimals is outside the double-precision 64-bit IEEE 754-2019 format range', function() { + assert.equal(TYPES.Float.validate(1.7976931348623159e+308), Infinity); + assert.equal(TYPES.Float.validate(-1.7976931348623159e+308), -Infinity); + assert.equal(TYPES.Float.validate('Infinity'), Infinity); + assert.equal(TYPES.Float.validate('-Infinity'), -Infinity); + }); + + it('Corect pasing the decimals with special cases', function() { + assert.equal(TYPES.Float.validate('123.3.3'), 123.3); + assert.equal(TYPES.Float.validate('1-23'), 1); + assert.equal(TYPES.Float.validate('1+23'), 1); + assert.equal(TYPES.Float.validate('1e23e4'), 1e23); + assert.equal(TYPES.Float.validate(' 123'), 123); + assert.equal(TYPES.Float.validate('1-e5'), 1); + assert.equal(TYPES.Float.validate('1e2e3'), 100); + }); + }); }); describe('Image', function() { @@ -577,6 +680,24 @@ describe('Int', function() { assert.deepEqual(result, expected); }); }); + + describe('.validate', function() { + it('throws Invalid number error for NaN input', function() { + assert.throws(() => { + TYPES.Int.validate('string'); + }, TypeError, 'Invalid number.'); + }); + + it('throws Out of Range error for numbers out of range', function() { + assert.throws(() => { + TYPES.Int.validate(-2147483648 - 1); + }, TypeError, 'Value must be between -2147483648 and 2147483647, inclusive.'); + + assert.throws(() => { + TYPES.Int.validate(2147483647 + 1); + }, TypeError, 'Value must be between -2147483648 and 2147483647, inclusive.'); + }); + }); }); describe('Money', function() { @@ -620,6 +741,25 @@ describe('Money', function() { assert.deepEqual(result, expected); }); }); + + describe.only('.validate', function() { + it('throws Invalid number error for NaN input', function() { + assert.throws(() => { + TYPES.TinyInt.validate('string'); + }, TypeError, 'Invalid number.'); + }); + + it('throws Out of Range error for numbers out of range', function() { + assert.throws(() => { + + TYPES.Money.validate(-922337203685477.5808 - 0.1); + }, TypeError, 'Value must be between -922337203685477.5808 and 922337203685477.5807, inclusive.'); + + assert.throws(() => { + TYPES.Money.validate(922337203685477.5807 + 0.1); + }, TypeError, 'Value must be between -922337203685477.5808 and 922337203685477.5807, inclusive.'); + }); + }); }); describe('NChar', function() { @@ -919,6 +1059,18 @@ describe('SmallDateTime', function() { assert.deepEqual(result, expected); }); }); + + describe('.validate', function() { + it('returns a TypeError for dates that are out of range', function() { + assert.throws(() => { + TYPES.SmallDateTime.validate(new Date('Dec 31, 1889')); + }, TypeError, 'Out of range.'); + + assert.throws(() => { + TYPES.SmallDateTime.validate(new Date('June 7, 2079')); + }, TypeError, 'Out of range.'); + }); + }); }); describe('SmallInt', function() { @@ -962,6 +1114,24 @@ describe('SmallInt', function() { assert.deepEqual(result, expected); }); }); + + describe('.validate', function() { + it('throws Invalid number error for NaN input', function() { + assert.throws(() => { + TYPES.SmallInt.validate('string'); + }, TypeError, 'Invalid number.'); + }); + + it('throws Out of Range error for numbers out of range', function() { + assert.throws(() => { + TYPES.SmallInt.validate(-32768 - 1); + }, TypeError, 'Value must be between -32768 and 32767, inclusive.'); + + assert.throws(() => { + TYPES.SmallInt.validate(32767 + 1); + }, TypeError, 'Value must be between -32768 and 32767, inclusive.'); + }); + }); }); describe('SmallMoney', function() { @@ -994,6 +1164,24 @@ describe('SmallMoney', function() { const buffer = Buffer.concat([...type.generateParameterData(parameterValue, { useUTC: false })]); assert.deepEqual(buffer, expected); }); + + describe('.validate', function() { + it('throws Invalid number error for NaN input', function() { + assert.throws(() => { + TYPES.SmallMoney.validate('string'); + }, TypeError, 'Invalid number.'); + }); + + it('throws Out of Range error for numbers out of range', function() { + assert.throws(() => { + TYPES.SmallMoney.validate(-214748.3648 - 0.0001); + }, TypeError, 'Value must be between -214748.3648 and 214748.3647.'); + + assert.throws(() => { + TYPES.SmallMoney.validate(214748.3647 + 0.0001); + }, TypeError, 'Value must be between -214748.3648 and 214748.3647.'); + }); + }); }); describe('.generateTypeInfo', function() { @@ -1142,6 +1330,24 @@ describe('TinyInt', function() { assert.deepEqual(result, expected); }); }); + + describe('.validate', function() { + it('throws Invalid number error for NaN input', function() { + assert.throws(() => { + TYPES.TinyInt.validate('string'); + }, TypeError, 'Invalid number.'); + }); + + it('throws Out of Range error for numbers out of range', function() { + assert.throws(() => { + TYPES.TinyInt.validate(-1); + }, TypeError, 'Value must be between 0 and 255, inclusive.'); + + assert.throws(() => { + TYPES.TinyInt.validate(256); + }, TypeError, 'Value must be between 0 and 255, inclusive.'); + }); + }); }); describe('TVP', function() {