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 Dec 9, 2022
1 parent 9f31c95 commit 4bdc2f7
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 65 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.js';
import {
serializeDateTime,
serializeDateTimeString,
serializeUnixTimestamp,
serializeTimestamp,
parseDateTime,
} from './formatter.js';
import { createGraphQLError } from '../../error.js';
Expand All @@ -38,9 +38,9 @@ export const GraphQLDateTimeConfig: GraphQLScalarTypeConfig<Date, string> = /*#_
throw createGraphQLError(`DateTime cannot represent an invalid date-time-string ${value}.`);
} else if (typeof value === 'number') {
try {
return serializeUnixTimestamp(value);
return serializeTimestamp(value);
} catch (e) {
throw createGraphQLError('DateTime cannot represent an invalid Unix timestamp ' + value);
throw createGraphQLError('DateTime cannot represent an invalid timestamp ' + value);
}
} else {
throw createGraphQLError(
Expand Down Expand Up @@ -93,7 +93,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 = /*#__PURE__*/ new GraphQLScalarType(GraphQLDateTimeConfig);
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();
};
23 changes: 14 additions & 9 deletions src/scalars/iso-date/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,17 +140,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
22 changes: 11 additions & 11 deletions tests/iso-date/DateTime.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,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 @@ -38,13 +38,13 @@ const schema = new GraphQLSchema({
type: GraphQLDateTime,
resolve: () => new Date('wrong'),
},
invalidType: {
invalidTimestamp: {
type: GraphQLDateTime,
resolve: () => [],
resolve: () => Number.POSITIVE_INFINITY,
},
invalidUnixTimestamp: {
invalidType: {
type: GraphQLDateTime,
resolve: () => Number.POSITIVE_INFINITY,
resolve: () => [],
},
input: {
type: GraphQLDateTime,
Expand All @@ -65,7 +65,7 @@ it('executes a query that includes a DateTime', async () => {
validDate
validUTCDateString
validDateString
validUnixTimestamp
validTimestamp
input(date: $date)
inputNull: input
}
Expand All @@ -81,7 +81,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 @@ -145,8 +145,8 @@ it('errors if an invalid date-time is returned from the resolver', async () => {
{
invalidDateString
invalidDate
invalidTimestamp
invalidType
invalidUnixTimestamp
}
`;

Expand All @@ -157,14 +157,14 @@ it('errors if an invalid date-time is returned from the resolver', async () => {
"data": {
"invalidDate": null,
"invalidDateString": null,
"invalidTimestamp": null,
"invalidType": null,
"invalidUnixTimestamp": null,
},
"errors": [
[GraphQLError: DateTime cannot represent an invalid date-time-string 2017-01-001T00:00:00Z.],
[GraphQLError: DateTime cannot represent an invalid Date instance],
[GraphQLError: DateTime cannot represent an invalid timestamp Infinity],
[GraphQLError: DateTime cannot be serialized from a non string, non numeric or non Date type []],
[GraphQLError: DateTime cannot represent an invalid Unix timestamp Infinity],
],
}
`);
Expand Down
20 changes: 10 additions & 10 deletions tests/iso-date/DateTime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,24 +88,24 @@ 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(value)} into date-string ${expected}`, () => {
it(`serializes timestamp ${stringify(value)} 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(value)}`, () => {
it(`throws an error serializing the invalid timestamp ${stringify(value)}`, () => {
expect(() => GraphQLDateTime.serialize(value)).toThrowErrorMatchingSnapshot();
});
});
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 @@ -85,20 +85,20 @@ 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(timestamp)} into date-time-string ${dateTimeString}`, () => {
expect(serializeUnixTimestamp(timestamp)).toEqual(dateTimeString);
it(`serializes timestamp ${stringify(timestamp)} into date-time-string ${dateTimeString}`, () => {
expect(serializeTimestamp(timestamp)).toEqual(dateTimeString);
});
});

Expand Down
32 changes: 19 additions & 13 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.js';

Expand Down Expand Up @@ -117,22 +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,
].forEach(timestamp => {
it(`identifies ${timestamp} as a valid Unix timestamp`, () => {
expect(validateUnixTimestamp(timestamp)).toEqual(true);
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 timestamp`, () => {
expect(validateTimestamp(timestamp)).toEqual(true);
});
});

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

0 comments on commit 4bdc2f7

Please sign in to comment.