Skip to content

Commit

Permalink
fix: DateTime: timestamps are expected to be number of milliseconds
Browse files Browse the repository at this point in the history
graphql-iso-date operated on Unix timestamp values, but graphql-scalars operates on ECMAScript timestamps (number of milliseconds since January 1, 1970, UTC)
as decided in #387 (comment)

It has to be clear which is used. Certainly values are not Unix timestamps and all references must be removed.
Docs are updated.

ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#the_ecmascript_epoch_and_timestamps
  • Loading branch information
falkenhawk committed Jul 6, 2022
1 parent 48402fe commit 95e86c0
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 69 deletions.
8 changes: 4 additions & 4 deletions src/scalars/iso-date/DateTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { validateJSDate, validateDateTime } from './validator';
import {
serializeDateTime,
serializeDateTimeString,
serializeUnixTimestamp,
serializeTimestamp,
parseDateTime,
} from './formatter';

Expand Down Expand Up @@ -42,10 +42,10 @@ export const GraphQLDateTimeConfig: GraphQLScalarTypeConfig<Date, string> =
);
} else if (typeof value === 'number') {
try {
return serializeUnixTimestamp(value);
return serializeTimestamp(value);
} catch (e) {
throw new TypeError(
'DateTime cannot represent an invalid Unix timestamp ' + value,
'DateTime cannot represent an invalid timestamp ' + value,
);
}
} else {
Expand Down Expand Up @@ -111,7 +111,7 @@ export const GraphQLDateTimeConfig: GraphQLScalarTypeConfig<Date, string> =
*
* Output:
* This scalar serializes javascript Dates,
* RFC 3339 date-time strings and unix timestamps
* RFC 3339 date-time strings and ECMAScript timestamps (number of milliseconds)
* to RFC 3339 UTC date-time strings.
*/
export const GraphQLDateTime: GraphQLScalarType =
Expand Down
6 changes: 3 additions & 3 deletions src/scalars/iso-date/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ export const serializeDateTimeString = (dateTime: string): string => {
}
};

// Serializes a Unix timestamp to an RFC 3339 compliant date-time-string
// Serializes ECMAScript timestamp (number of milliseconds) to an RFC 3339 compliant date-time-string
// in the format YYYY-MM-DDThh:mm:ss.sssZ
export const serializeUnixTimestamp = (timestamp: number): string => {
return new Date(timestamp * 1000).toISOString();
export const serializeTimestamp = (timestamp: number): string => {
return new Date(timestamp).toISOString();
};
29 changes: 18 additions & 11 deletions src/scalars/iso-date/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ const leapYear = (year: number): boolean => {
//
export const validateTime = (time: string): boolean => {
time = time?.toUpperCase();
const TIME_REGEX = /^([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(\.\d{1,})?(([Z])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))$/;
const TIME_REGEX =
/^([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(\.\d{1,})?(([Z])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))$/;
return TIME_REGEX.test(time);
};

Expand Down Expand Up @@ -119,7 +120,8 @@ export const validateDate = (datestring: string): boolean => {
//
export const validateDateTime = (dateTimeString: string): boolean => {
dateTimeString = dateTimeString?.toUpperCase();
const RFC_3339_REGEX = /^(\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60))(\.\d{1,})?(([Z])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))$/;
const RFC_3339_REGEX =
/^(\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60))(\.\d{1,})?(([Z])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))$/;

// Validate the structure of the date-string
if (!RFC_3339_REGEX.test(dateTimeString)) {
Expand All @@ -140,17 +142,22 @@ export const validateDateTime = (dateTimeString: string): boolean => {
};

// Function that checks whether a given number is a valid
// Unix timestamp.
// ECMAScript timestamp.
//
// Unix timestamps are signed 32-bit integers. They are interpreted
// as the number of seconds since 00:00:00 UTC on 1 January 1970.
// ECMAScript are interpreted as the number of milliseconds
// since 00:00:00 UTC on 1 January 1970.
//
export const validateUnixTimestamp = (timestamp: number): boolean => {
const MAX_INT = 2147483647;
const MIN_INT = -2147483648;
return (
timestamp === timestamp && timestamp <= MAX_INT && timestamp >= MIN_INT
); // eslint-disable-line
// It is defined in ECMA-262 that a maximum of ±100,000,000 days relative to
// January 1, 1970 UTC (that is, April 20, 271821 BCE ~ September 13, 275760 CE)
// can be represented by the standard Date object
// (equivalent to ±8,640,000,000,000,000 milliseconds).
//
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#the_ecmascript_epoch_and_timestamps
//
export const validateTimestamp = (timestamp: number): boolean => {
const MAX = 8640000000000000;
const MIN = -8640000000000000;
return timestamp === timestamp && timestamp <= MAX && timestamp >= MIN; // eslint-disable-line
};

// Function that checks whether a javascript Date instance
Expand Down
16 changes: 8 additions & 8 deletions tests/iso-date/DateTime.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ const schema = new GraphQLSchema({
type: GraphQLDateTime,
resolve: () => '2016-02-01T00:00:00-11:00',
},
validUnixTimestamp: {
validTimestamp: {
type: GraphQLDateTime,
resolve: () => 854325678,
resolve: () => 854325678000,
},
invalidDateString: {
type: GraphQLDateTime,
Expand All @@ -43,7 +43,7 @@ const schema = new GraphQLSchema({
type: GraphQLDateTime,
resolve: () => new Date('wrong'),
},
invalidUnixTimestamp: {
invalidTimestamp: {
type: GraphQLDateTime,
resolve: () => Number.POSITIVE_INFINITY,
},
Expand All @@ -70,7 +70,7 @@ it('executes a query that includes a DateTime', async () => {
validDate
validUTCDateString
validDateString
validUnixTimestamp
validTimestamp
input(date: $date)
inputNull: input
}
Expand All @@ -86,7 +86,7 @@ it('executes a query that includes a DateTime', async () => {
validUTCDateString: '1991-12-24T00:00:00Z',
validDateString: '2016-02-01T11:00:00Z',
input: '2017-10-01T00:00:00.000Z',
validUnixTimestamp: '1997-01-27T00:41:18.000Z',
validTimestamp: '1997-01-27T00:41:18.000Z',
inputNull: null,
},
});
Expand Down Expand Up @@ -152,7 +152,7 @@ it('errors if an invalid date-time is returned from the resolver', async () => {
{
invalidDateString
invalidDate
invalidUnixTimestamp
invalidTimestamp
invalidType
}
`;
Expand All @@ -163,7 +163,7 @@ it('errors if an invalid date-time is returned from the resolver', async () => {
data: {
invalidDateString: null,
invalidDate: null,
invalidUnixTimestamp: null,
invalidTimestamp: null,
invalidType: null,
},
errors: [
Expand All @@ -172,7 +172,7 @@ it('errors if an invalid date-time is returned from the resolver', async () => {
),
new GraphQLError('DateTime cannot represent an invalid Date instance'),
new GraphQLError(
'DateTime cannot represent an invalid Unix timestamp Infinity',
'DateTime cannot represent an invalid timestamp Infinity',
),
new GraphQLError(
'DateTime cannot be serialized from a non string, non numeric or non Date type []',
Expand Down
22 changes: 11 additions & 11 deletions tests/iso-date/DateTime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,27 +104,27 @@ describe('GraphQLDateTime', () => {
});
});

// Serializes Unix timestamp
// Serializes ECMAScript timestamp
[
[854325678, '1997-01-27T00:41:18.000Z'],
[854325678.123, '1997-01-27T00:41:18.123Z'],
[876535, '1970-01-11T03:28:55.000Z'],
// The maximum representable unix timestamp
[2147483647, '2038-01-19T03:14:07.000Z'],
// The minimum representable unit timestamp
[-2147483648, '1901-12-13T20:45:52.000Z'],
[854325678000, '1997-01-27T00:41:18.000Z'],
[854325678123, '1997-01-27T00:41:18.123Z'],
[876535000, '1970-01-11T03:28:55.000Z'],
// The maximum representable ECMAScript timestamp
[8640000000000000, '+275760-09-13T00:00:00.000Z'],
// The minimum representable ECMAScript timestamp
[-8640000000000000, '-271821-04-20T00:00:00.000Z'],
].forEach(([value, expected]) => {
it(`serializes unix timestamp ${stringify(
it(`serializes timestamp ${stringify(
value,
)} into date-string ${expected}`, () => {
)} into date-time-string ${expected}`, () => {
expect(GraphQLDateTime.serialize(value)).toEqual(expected);
});
});
});

[Number.NaN, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY].forEach(
(value) => {
it(`throws an error serializing the invalid unix timestamp ${stringify(
it(`throws an error serializing the invalid timestamp ${stringify(
value,
)}`, () => {
expect(() =>
Expand Down
6 changes: 3 additions & 3 deletions tests/iso-date/__snapshots__/DateTime.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ exports[`GraphQLDateTime serialization throws error when serializing true 1`] =

exports[`GraphQLDateTime serialization throws error when serializing undefined 1`] = `"DateTime cannot be serialized from a non string, non numeric or non Date type undefined"`;

exports[`GraphQLDateTime throws an error serializing the invalid unix timestamp Infinity 1`] = `"DateTime cannot represent an invalid Unix timestamp Infinity"`;
exports[`GraphQLDateTime throws an error serializing the invalid timestamp Infinity 1`] = `"DateTime cannot represent an invalid timestamp Infinity"`;

exports[`GraphQLDateTime throws an error serializing the invalid unix timestamp Infinity 2`] = `"DateTime cannot represent an invalid Unix timestamp Infinity"`;
exports[`GraphQLDateTime throws an error serializing the invalid timestamp Infinity 2`] = `"DateTime cannot represent an invalid timestamp Infinity"`;

exports[`GraphQLDateTime throws an error serializing the invalid unix timestamp NaN 1`] = `"DateTime cannot represent an invalid Unix timestamp NaN"`;
exports[`GraphQLDateTime throws an error serializing the invalid timestamp NaN 1`] = `"DateTime cannot represent an invalid timestamp NaN"`;

exports[`GraphQLDateTime value parsing throws an error parsing an invalid date-string "2015-02-24T00:00:00.000+0100" 1`] = `"DateTime cannot represent an invalid date-time-string 2015-02-24T00:00:00.000+0100."`;

Expand Down
24 changes: 12 additions & 12 deletions tests/iso-date/formatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
serializeDate,
serializeDateTime,
serializeDateTimeString,
serializeUnixTimestamp,
serializeTimestamp,
parseTime,
parseDate,
parseDateTime,
Expand Down Expand Up @@ -95,22 +95,22 @@ describe('formatting', () => {

(
[
[854325678, '1997-01-27T00:41:18.000Z'],
[876535, '1970-01-11T03:28:55.000Z'],
[876535.8, '1970-01-11T03:28:55.800Z'],
[876535.8321, '1970-01-11T03:28:55.832Z'],
[-876535.8, '1969-12-21T20:31:04.200Z'],
[854325678000, '1997-01-27T00:41:18.000Z'],
[876535000, '1970-01-11T03:28:55.000Z'],
[876535800, '1970-01-11T03:28:55.800Z'],
[876535832.1, '1970-01-11T03:28:55.832Z'],
[-876535800, '1969-12-21T20:31:04.200Z'],
[0, '1970-01-01T00:00:00.000Z'],
// The maximum representable unix timestamp
[2147483647, '2038-01-19T03:14:07.000Z'],
// The minimum representable unit timestamp
[-2147483648, '1901-12-13T20:45:52.000Z'],
// The maximum representable ECMAScript timestamp
[8640000000000000, '+275760-09-13T00:00:00.000Z'],
// The minimum representable ECMAScript timestamp
[-8640000000000000, '-271821-04-20T00:00:00.000Z'],
] as [number, string][]
).forEach(([timestamp, dateTimeString]) => {
it(`serializes Unix timestamp ${stringify(
it(`serializes timestamp ${stringify(
timestamp,
)} into date-time-string ${dateTimeString}`, () => {
expect(serializeUnixTimestamp(timestamp)).toEqual(dateTimeString);
expect(serializeTimestamp(timestamp)).toEqual(dateTimeString);
});
});

Expand Down
30 changes: 13 additions & 17 deletions tests/iso-date/validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
validateTime,
validateDate,
validateDateTime,
validateUnixTimestamp,
validateTimestamp,
validateJSDate,
} from '../../src/scalars/iso-date/validator';

Expand Down Expand Up @@ -117,32 +117,28 @@ describe('validator', () => {
});
});

describe('validateUnixTimestamp', () => {
describe('validateTimestamp', () => {
[
854325678,
876535,
876535.8,
876535.8321,
-876535.8,
// The maximum representable unix timestamp
2147483647,
// The minimum representable unit timestamp
-2147483648,
854325678000, 876535000, 876535800, 876535832.1, -876535800,
// The maximum representable ECMAScript timestamp
8640000000000000,
// The minimum representable ECMAScript timestamp
-8640000000000000,
].forEach((timestamp) => {
it(`identifies ${timestamp} as a valid Unix timestamp`, () => {
expect(validateUnixTimestamp(timestamp)).toEqual(true);
it(`identifies ${timestamp} as a valid timestamp`, () => {
expect(validateTimestamp(timestamp)).toEqual(true);
});
});

[
Number.NaN,
Number.POSITIVE_INFINITY,
Number.POSITIVE_INFINITY,
2147483648,
-2147483649,
8640000000000001,
-8640000000000001,
].forEach((timestamp) => {
it(`identifies ${timestamp} as an invalid Unix timestamp`, () => {
expect(validateUnixTimestamp(timestamp)).toEqual(false);
it(`identifies ${timestamp} as an invalid ECMAScript timestamp`, () => {
expect(validateTimestamp(timestamp)).toEqual(false);
});
});
});
Expand Down

0 comments on commit 95e86c0

Please sign in to comment.