From 9f31c95b41b4aa471e2829c372f81ef05efc05cf Mon Sep 17 00:00:00 2001 From: Maciej Holyszko <14310995+falkenhawk@users.noreply.github.com> Date: Fri, 9 Dec 2022 10:39:42 +0100 Subject: [PATCH 1/3] fix: restore DateTime to usable state pre-1.2.0 A value cannot be serialized into something other than js primitives (boolean string, number) or array, object, null otherwise there is no guarantee for it to be transported properly over graphql protocol. Here `DateTime` scalar was being serialized to an instance of `Date` - in contrary to `Date` or `Time` scalars, which have been left untouched in v1.2.0 and still serialize to string - and that broke the expected outputs, mangling data for fractional seconds and failing to comply with graphql spec which does not allow such values. Moreover, `astFromValue('2021-10-02T00:00:00.000Z', DateTime)` (a function from `graphql/utilities`) fails on such "serialized" value (here: `Date`) with `TypeError: Cannot convert value to AST: 2021-10-02T00:00:00.000Z.` - that was the initial reason for me to submit the PR - as it broke our toolset. The affecting change happened in https://github.com/Urigo/graphql-scalars/commit/3b1352c0f6e6f67ec1b6c10fbb9be0737832c61f#diff-5bff20d592f8d56ae20cad088bf374d5ce38af414afd5631ab82f42481bb8473 with no message explaining why, no linked PR, moreover there is no release notes for v1.2.0 (https://github.com/Urigo/graphql-scalars/releases/tag/v1.2.0) only v1.2.1 but there is nothing mentioning the change in any of future releases. Additionally, fixed inconsistencies in handling unix timestamps (maybe that was initial motivation to go with those changes in v1.2.0?) # Conflicts: # src/scalars/iso-date/DateTime.ts # tests/iso-date/DateTime.integration.test.ts # tests/iso-date/DateTime.test.ts # tests/iso-date/formatter.test.ts --- src/scalars/iso-date/DateTime.ts | 15 +- src/scalars/iso-date/formatter.ts | 20 ++- tests/iso-date/DateTime.integration.test.ts | 21 ++- tests/iso-date/DateTime.test.ts | 29 ++-- .../__snapshots__/DateTime.test.ts.snap | 136 +++++++++--------- tests/iso-date/formatter.test.ts | 55 ++++++- 6 files changed, 180 insertions(+), 96 deletions(-) diff --git a/src/scalars/iso-date/DateTime.ts b/src/scalars/iso-date/DateTime.ts index 83ae0c5cc..fa033ca94 100644 --- a/src/scalars/iso-date/DateTime.ts +++ b/src/scalars/iso-date/DateTime.ts @@ -10,10 +10,15 @@ import { GraphQLScalarType, Kind } from 'graphql'; import type { GraphQLScalarTypeConfig } from 'graphql'; // eslint-disable-line import { validateJSDate, validateDateTime } from './validator.js'; -import { parseDateTime } from './formatter.js'; +import { + serializeDateTime, + serializeDateTimeString, + serializeUnixTimestamp, + parseDateTime, +} from './formatter.js'; import { createGraphQLError } from '../../error.js'; -export const GraphQLDateTimeConfig: GraphQLScalarTypeConfig = /*#__PURE__*/ { +export const GraphQLDateTimeConfig: GraphQLScalarTypeConfig = /*#__PURE__*/ { name: 'DateTime', description: 'A date-time string at UTC, such as 2007-12-03T10:15:30Z, ' + @@ -23,17 +28,17 @@ export const GraphQLDateTimeConfig: GraphQLScalarTypeConfig = /*#__P serialize(value) { if (value instanceof Date) { if (validateJSDate(value)) { - return value; + return serializeDateTime(value); } throw createGraphQLError('DateTime cannot represent an invalid Date instance'); } else if (typeof value === 'string') { if (validateDateTime(value)) { - return parseDateTime(value); + return serializeDateTimeString(value); } throw createGraphQLError(`DateTime cannot represent an invalid date-time-string ${value}.`); } else if (typeof value === 'number') { try { - return new Date(value); + return serializeUnixTimestamp(value); } catch (e) { throw createGraphQLError('DateTime cannot represent an invalid Unix timestamp ' + value); } diff --git a/src/scalars/iso-date/formatter.ts b/src/scalars/iso-date/formatter.ts index 03fd144b1..e88820745 100644 --- a/src/scalars/iso-date/formatter.ts +++ b/src/scalars/iso-date/formatter.ts @@ -88,12 +88,18 @@ export const parseDateTime = (dateTime: string): Date => { return new Date(dateTime); }; +// Serializes a Date into an RFC 3339 compliant date-time-string +// in the format YYYY-MM-DDThh:mm:ss.sssZ. +export const serializeDateTime = (dateTime: Date): string => { + return dateTime.toISOString(); +}; + // Serializes an RFC 3339 compliant date-time-string by shifting // it to UTC. -export const serializeDateTimeString = (dateTime: string): Date => { +export const serializeDateTimeString = (dateTime: string): string => { // If already formatted to UTC then return the time string if (dateTime.indexOf('Z') !== -1) { - return new Date(dateTime); + return dateTime; } else { // These are time-strings with timezone information, // these need to be shifted to UTC. @@ -112,7 +118,7 @@ export const serializeDateTimeString = (dateTime: string): Date => { // The date-time-string has no fractional part, // so we remove it from the dateTimeUTC variable. dateTimeUTC = dateTimeUTC.replace(regexFracSec, ''); - return new Date(dateTimeUTC); + return dateTimeUTC; } else { // These are datetime-string with fractional seconds. // Make sure that we inject the fractional @@ -120,7 +126,13 @@ export const serializeDateTimeString = (dateTime: string): Date => { // has millisecond precision, we may want more or less // depending on the string that was passed. dateTimeUTC = dateTimeUTC.replace(regexFracSec, fractionalPart[0]); - return new Date(dateTimeUTC); + return dateTimeUTC; } } }; + +// Serializes a Unix timestamp 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(); +}; diff --git a/tests/iso-date/DateTime.integration.test.ts b/tests/iso-date/DateTime.integration.test.ts index 6e73208d0..e58db390a 100644 --- a/tests/iso-date/DateTime.integration.test.ts +++ b/tests/iso-date/DateTime.integration.test.ts @@ -28,7 +28,7 @@ const schema = new GraphQLSchema({ }, validUnixTimestamp: { type: GraphQLDateTime, - resolve: () => 854325678000, + resolve: () => 854325678, }, invalidDateString: { type: GraphQLDateTime, @@ -42,6 +42,10 @@ const schema = new GraphQLSchema({ type: GraphQLDateTime, resolve: () => [], }, + invalidUnixTimestamp: { + type: GraphQLDateTime, + resolve: () => Number.POSITIVE_INFINITY, + }, input: { type: GraphQLDateTime, args: { @@ -73,11 +77,11 @@ it('executes a query that includes a DateTime', async () => { expect(response).toEqual({ data: { - validDate: new Date('2016-05-02T10:31:42.200Z'), - validUTCDateString: new Date('1991-12-24T00:00:00Z'), - validDateString: new Date('2016-02-01T11:00:00Z'), - input: new Date('2017-10-01T00:00:00.000Z'), - validUnixTimestamp: new Date('1997-01-27T00:41:18.000Z'), + validDate: '2016-05-02T10:31:42.200Z', + 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', inputNull: null, }, }); @@ -96,7 +100,7 @@ it('shifts an input date-time to UTC', async () => { expect(response).toEqual({ data: { - input: new Date('2016-02-01T11:00:00.000Z'), + input: '2016-02-01T11:00:00.000Z', }, }); }); @@ -142,6 +146,7 @@ it('errors if an invalid date-time is returned from the resolver', async () => { invalidDateString invalidDate invalidType + invalidUnixTimestamp } `; @@ -153,11 +158,13 @@ it('errors if an invalid date-time is returned from the resolver', async () => { "invalidDate": null, "invalidDateString": 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 be serialized from a non string, non numeric or non Date type []], + [GraphQLError: DateTime cannot represent an invalid Unix timestamp Infinity], ], } `); diff --git a/tests/iso-date/DateTime.test.ts b/tests/iso-date/DateTime.test.ts index 8157e56c9..c87a68718 100644 --- a/tests/iso-date/DateTime.test.ts +++ b/tests/iso-date/DateTime.test.ts @@ -59,7 +59,7 @@ describe('GraphQLDateTime', () => { [new Date(Date.UTC(2016, 0, 1, 14, 48, 10, 30)), '2016-01-01T14:48:10.030Z'], ].forEach(([value, expected]) => { it(`serializes javascript Date ${stringify(value)} into ${stringify(expected)}`, () => { - expect(GraphQLDateTime.serialize(value).toJSON()).toEqual(expected); + expect(GraphQLDateTime.serialize(value)).toEqual(expected); }); }); @@ -68,13 +68,17 @@ describe('GraphQLDateTime', () => { }); [ + ['2016-02-01T00:00:15Z', '2016-02-01T00:00:15Z'], ['2016-02-01T00:00:15.000Z', '2016-02-01T00:00:15.000Z'], ['2016-02-01T00:00:00.234Z', '2016-02-01T00:00:00.234Z'], - ['2016-02-01T00:00:00-11:00', '2016-02-01T11:00:00.000Z'], - ['2017-01-07T00:00:00.1+01:20', '2017-01-06T22:40:00.100Z'], + ['2016-02-01T00:00:00.23498Z', '2016-02-01T00:00:00.23498Z'], + ['2016-02-01T00:00:00-11:00', '2016-02-01T11:00:00Z'], + ['2016-02-01T00:00:00+11:00', '2016-01-31T13:00:00Z'], + ['2016-02-02T00:00:00.4567+01:30', '2016-02-01T22:30:00.4567Z'], + ['2017-01-07T00:00:00.1+01:20', '2017-01-06T22:40:00.1Z'], ].forEach(([input, output]) => { it(`serializes date-time-string ${input} into UTC date-time-string ${output}`, () => { - expect(GraphQLDateTime.serialize(input).toJSON()).toEqual(output); + expect(GraphQLDateTime.serialize(input)).toEqual(output); }); }); @@ -86,19 +90,26 @@ describe('GraphQLDateTime', () => { // Serializes Unix timestamp [ - [854325678000, '1997-01-27T00:41:18.000Z'], - [876535000, '1970-01-11T03:28:55.000Z'], + [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 - [2147483647000, '2038-01-19T03:14:07.000Z'], + [2147483647, '2038-01-19T03:14:07.000Z'], // The minimum representable unit timestamp - [-2147483648000, '1901-12-13T20:45:52.000Z'], + [-2147483648, '1901-12-13T20:45:52.000Z'], ].forEach(([value, expected]) => { it(`serializes unix timestamp ${stringify(value)} into date-string ${expected}`, () => { - expect(GraphQLDateTime.serialize(value).toJSON()).toEqual(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)}`, () => { + expect(() => GraphQLDateTime.serialize(value)).toThrowErrorMatchingSnapshot(); + }); + }); + describe('value parsing', () => { validDates.forEach(([value, expected]) => { it(`parses date-string ${stringify(value)} into javascript Date ${stringify(expected)}`, () => { diff --git a/tests/iso-date/__snapshots__/DateTime.test.ts.snap b/tests/iso-date/__snapshots__/DateTime.test.ts.snap index a75c80670..fd0202c59 100644 --- a/tests/iso-date/__snapshots__/DateTime.test.ts.snap +++ b/tests/iso-date/__snapshots__/DateTime.test.ts.snap @@ -1,65 +1,71 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`GraphQLDateTime has a description 1`] = `"A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the \`date-time\` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar."`; - -exports[`GraphQLDateTime literal parsing errors when parsing invalid literal {"kind": "Document"} 1`] = `"DateTime cannot represent non string or Date type false"`; - -exports[`GraphQLDateTime literal parsing errors when parsing invalid literal {"kind": "FloatValue", "value": "5"} 1`] = `"DateTime cannot represent non string or Date type 5"`; - -exports[`GraphQLDateTime literal parsing errors when parsing invalid literal {"kind": "StringValue", "value": "2015-02-24T00:00:00.000+0100"} 1`] = `"DateTime cannot represent an invalid date-time-string 2015-02-24T00:00:00.000+0100."`; - -exports[`GraphQLDateTime literal parsing errors when parsing invalid literal {"kind": "StringValue", "value": "2016-02-01T00:00:00.Z"} 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00:00:00.Z."`; - -exports[`GraphQLDateTime literal parsing errors when parsing invalid literal {"kind": "StringValue", "value": "2016-02-01T00:00Z"} 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00:00Z."`; - -exports[`GraphQLDateTime literal parsing errors when parsing invalid literal {"kind": "StringValue", "value": "2016-02-01T000059Z"} 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T000059Z."`; - -exports[`GraphQLDateTime literal parsing errors when parsing invalid literal {"kind": "StringValue", "value": "2016-02-01T00Z"} 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00Z."`; - -exports[`GraphQLDateTime literal parsing errors when parsing invalid literal {"kind": "StringValue", "value": "Invalid date"} 1`] = `"DateTime cannot represent an invalid date-time-string Invalid date."`; - -exports[`GraphQLDateTime serialization throws an error when serializing 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."`; - -exports[`GraphQLDateTime serialization throws an error when serializing an invalid date-string "2016-02-01T00:00:00.Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00:00:00.Z."`; - -exports[`GraphQLDateTime serialization throws an error when serializing an invalid date-string "2016-02-01T00:00Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00:00Z."`; - -exports[`GraphQLDateTime serialization throws an error when serializing an invalid date-string "2016-02-01T000059Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T000059Z."`; - -exports[`GraphQLDateTime serialization throws an error when serializing an invalid date-string "2016-02-01T00Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00Z."`; - -exports[`GraphQLDateTime serialization throws an error when serializing an invalid date-string "Invalid date" 1`] = `"DateTime cannot represent an invalid date-time-string Invalid date."`; - -exports[`GraphQLDateTime serialization throws error when serializing [] 1`] = `"DateTime cannot be serialized from a non string, non numeric or non Date type []"`; - -exports[`GraphQLDateTime serialization throws error when serializing {} 1`] = `"DateTime cannot be serialized from a non string, non numeric or non Date type {}"`; - -exports[`GraphQLDateTime serialization throws error when serializing invalid date 1`] = `"DateTime cannot represent an invalid Date instance"`; - -exports[`GraphQLDateTime serialization throws error when serializing null 1`] = `"DateTime cannot be serialized from a non string, non numeric or non Date type null"`; - -exports[`GraphQLDateTime serialization throws error when serializing true 1`] = `"DateTime cannot be serialized from a non string, non numeric or non Date type true"`; - -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 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."`; - -exports[`GraphQLDateTime value parsing throws an error parsing an invalid date-string "2016-02-01T00:00:00.Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00:00:00.Z."`; - -exports[`GraphQLDateTime value parsing throws an error parsing an invalid date-string "2016-02-01T00:00Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00:00Z."`; - -exports[`GraphQLDateTime value parsing throws an error parsing an invalid date-string "2016-02-01T000059Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T000059Z."`; - -exports[`GraphQLDateTime value parsing throws an error parsing an invalid date-string "2016-02-01T00Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00Z."`; - -exports[`GraphQLDateTime value parsing throws an error parsing an invalid date-string "Invalid date" 1`] = `"DateTime cannot represent an invalid date-time-string Invalid date."`; - -exports[`GraphQLDateTime value parsing throws an error when parsing [] 1`] = `"DateTime cannot represent non string or Date type []"`; - -exports[`GraphQLDateTime value parsing throws an error when parsing {} 1`] = `"DateTime cannot represent non string or Date type {}"`; - -exports[`GraphQLDateTime value parsing throws an error when parsing 4566 1`] = `"DateTime cannot represent non string or Date type 4566"`; - -exports[`GraphQLDateTime value parsing throws an error when parsing null 1`] = `"DateTime cannot represent non string or Date type null"`; - -exports[`GraphQLDateTime value parsing throws an error when parsing true 1`] = `"DateTime cannot represent non string or Date type true"`; +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GraphQLDateTime has a description 1`] = `"A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the \`date-time\` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar."`; + +exports[`GraphQLDateTime literal parsing errors when parsing invalid literal {"kind": "Document"} 1`] = `"DateTime cannot represent non string or Date type false"`; + +exports[`GraphQLDateTime literal parsing errors when parsing invalid literal {"kind": "FloatValue", "value": "5"} 1`] = `"DateTime cannot represent non string or Date type 5"`; + +exports[`GraphQLDateTime literal parsing errors when parsing invalid literal {"kind": "StringValue", "value": "2015-02-24T00:00:00.000+0100"} 1`] = `"DateTime cannot represent an invalid date-time-string 2015-02-24T00:00:00.000+0100."`; + +exports[`GraphQLDateTime literal parsing errors when parsing invalid literal {"kind": "StringValue", "value": "2016-02-01T00:00:00.Z"} 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00:00:00.Z."`; + +exports[`GraphQLDateTime literal parsing errors when parsing invalid literal {"kind": "StringValue", "value": "2016-02-01T00:00Z"} 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00:00Z."`; + +exports[`GraphQLDateTime literal parsing errors when parsing invalid literal {"kind": "StringValue", "value": "2016-02-01T000059Z"} 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T000059Z."`; + +exports[`GraphQLDateTime literal parsing errors when parsing invalid literal {"kind": "StringValue", "value": "2016-02-01T00Z"} 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00Z."`; + +exports[`GraphQLDateTime literal parsing errors when parsing invalid literal {"kind": "StringValue", "value": "Invalid date"} 1`] = `"DateTime cannot represent an invalid date-time-string Invalid date."`; + +exports[`GraphQLDateTime serialization throws an error when serializing 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."`; + +exports[`GraphQLDateTime serialization throws an error when serializing an invalid date-string "2016-02-01T00:00:00.Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00:00:00.Z."`; + +exports[`GraphQLDateTime serialization throws an error when serializing an invalid date-string "2016-02-01T00:00Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00:00Z."`; + +exports[`GraphQLDateTime serialization throws an error when serializing an invalid date-string "2016-02-01T000059Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T000059Z."`; + +exports[`GraphQLDateTime serialization throws an error when serializing an invalid date-string "2016-02-01T00Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00Z."`; + +exports[`GraphQLDateTime serialization throws an error when serializing an invalid date-string "Invalid date" 1`] = `"DateTime cannot represent an invalid date-time-string Invalid date."`; + +exports[`GraphQLDateTime serialization throws error when serializing [] 1`] = `"DateTime cannot be serialized from a non string, non numeric or non Date type []"`; + +exports[`GraphQLDateTime serialization throws error when serializing {} 1`] = `"DateTime cannot be serialized from a non string, non numeric or non Date type {}"`; + +exports[`GraphQLDateTime serialization throws error when serializing invalid date 1`] = `"DateTime cannot represent an invalid Date instance"`; + +exports[`GraphQLDateTime serialization throws error when serializing null 1`] = `"DateTime cannot be serialized from a non string, non numeric or non Date type null"`; + +exports[`GraphQLDateTime serialization throws error when serializing true 1`] = `"DateTime cannot be serialized from a non string, non numeric or non Date type true"`; + +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 unix timestamp Infinity 2`] = `"DateTime cannot represent an invalid Unix timestamp Infinity"`; + +exports[`GraphQLDateTime throws an error serializing the invalid unix timestamp NaN 1`] = `"DateTime cannot represent an invalid Unix 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."`; + +exports[`GraphQLDateTime value parsing throws an error parsing an invalid date-string "2016-02-01T00:00:00.Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00:00:00.Z."`; + +exports[`GraphQLDateTime value parsing throws an error parsing an invalid date-string "2016-02-01T00:00Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00:00Z."`; + +exports[`GraphQLDateTime value parsing throws an error parsing an invalid date-string "2016-02-01T000059Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T000059Z."`; + +exports[`GraphQLDateTime value parsing throws an error parsing an invalid date-string "2016-02-01T00Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00Z."`; + +exports[`GraphQLDateTime value parsing throws an error parsing an invalid date-string "Invalid date" 1`] = `"DateTime cannot represent an invalid date-time-string Invalid date."`; + +exports[`GraphQLDateTime value parsing throws an error when parsing [] 1`] = `"DateTime cannot represent non string or Date type []"`; + +exports[`GraphQLDateTime value parsing throws an error when parsing {} 1`] = `"DateTime cannot represent non string or Date type {}"`; + +exports[`GraphQLDateTime value parsing throws an error when parsing 4566 1`] = `"DateTime cannot represent non string or Date type 4566"`; + +exports[`GraphQLDateTime value parsing throws an error when parsing null 1`] = `"DateTime cannot represent non string or Date type null"`; + +exports[`GraphQLDateTime value parsing throws an error when parsing true 1`] = `"DateTime cannot represent non string or Date type true"`; diff --git a/tests/iso-date/formatter.test.ts b/tests/iso-date/formatter.test.ts index 6f8c0ba5e..26be7cee1 100644 --- a/tests/iso-date/formatter.test.ts +++ b/tests/iso-date/formatter.test.ts @@ -11,7 +11,9 @@ import { serializeTime, serializeTimeString, serializeDate, + serializeDateTime, serializeDateTimeString, + serializeUnixTimestamp, parseTime, parseDate, parseDateTime, @@ -59,6 +61,47 @@ describe('formatting', () => { }); }); + ( + [ + [new Date(Date.UTC(2016, 1, 1)), '2016-02-01T00:00:00.000Z'], + [new Date(Date.UTC(2016, 3, 5, 10, 1, 4, 555)), '2016-04-05T10:01:04.555Z'], + ] as [Date, string][] + ).forEach(([date, dateTimeString]) => { + it(`serializes ${stringify(date)} into date-time-string ${dateTimeString}`, () => { + expect(serializeDateTime(date)).toEqual(dateTimeString); + }); + }); + + ( + [ + [new Date(Date.UTC(2016, 1, 1)), '2016-02-01T00:00:00.000Z'], + [new Date(Date.UTC(2016, 3, 5, 10, 1, 4, 555)), '2016-04-05T10:01:04.555Z'], + ] as [Date, string][] + ).forEach(([date, dateTimeString]) => { + it(`serializes ${stringify(date)} into date-time-string ${dateTimeString}`, () => { + expect(serializeDateTime(date)).toEqual(dateTimeString); + }); + }); + + ( + [ + [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'], + [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'], + ] as [number, string][] + ).forEach(([timestamp, dateTimeString]) => { + it(`serializes Unix timestamp ${stringify(timestamp)} into date-time-string ${dateTimeString}`, () => { + expect(serializeUnixTimestamp(timestamp)).toEqual(dateTimeString); + }); + }); + ( [ ['00:00:59Z', new Date(Date.UTC(2017, 0, 1, 0, 0, 59))], @@ -114,14 +157,14 @@ describe('formatting', () => { }); [ - ['2016-02-01T00:00:00Z', '2016-02-01T00:00:00.000Z'], - ['2016-02-01T12:23:44Z', '2016-02-01T12:23:44.000Z'], - ['2016-02-01T14:38:12-01:00', '2016-02-01T15:38:12.000Z'], - ['2016-02-02T00:00:00.456+01:30', '2016-02-01T22:30:00.456Z'], - ['2016-02-01T14:38:12.1+01:00', '2016-02-01T13:38:12.100Z'], + ['2016-02-01T00:00:00Z', '2016-02-01T00:00:00Z'], + ['2016-02-01T12:23:44Z', '2016-02-01T12:23:44Z'], + ['2016-02-01T14:38:12-01:00', '2016-02-01T15:38:12Z'], + ['2016-02-02T00:00:00.4567+01:30', '2016-02-01T22:30:00.4567Z'], + ['2016-02-01T14:38:12.1+01:00', '2016-02-01T13:38:12.1Z'], ].forEach(([input, output]) => { it(`serializes date-time-string ${input} into UTC date-time-string ${output}`, () => { - expect(serializeDateTimeString(input).toJSON()).toEqual(output); + expect(serializeDateTimeString(input)).toEqual(output); }); }); }); From 4bdc2f7786f54d6c33bbe86830bb65752691f46b Mon Sep 17 00:00:00 2001 From: Maciej Holyszko <14310995+falkenhawk@users.noreply.github.com> Date: Fri, 9 Dec 2022 10:50:55 +0100 Subject: [PATCH 2/3] fix: DateTime: timestamps are expected to be number of milliseconds 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 https://github.com/Urigo/graphql-scalars/issues/387#issuecomment-648202200 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 --- src/scalars/iso-date/DateTime.ts | 8 ++--- src/scalars/iso-date/formatter.ts | 6 ++-- src/scalars/iso-date/validator.ts | 23 +++++++------ tests/iso-date/DateTime.integration.test.ts | 22 ++++++------- tests/iso-date/DateTime.test.ts | 20 ++++++------ .../__snapshots__/DateTime.test.ts.snap | 6 ++-- tests/iso-date/formatter.test.ts | 24 +++++++------- tests/iso-date/validator.test.ts | 32 +++++++++++-------- 8 files changed, 76 insertions(+), 65 deletions(-) diff --git a/src/scalars/iso-date/DateTime.ts b/src/scalars/iso-date/DateTime.ts index fa033ca94..78e8edf9e 100644 --- a/src/scalars/iso-date/DateTime.ts +++ b/src/scalars/iso-date/DateTime.ts @@ -13,7 +13,7 @@ import { validateJSDate, validateDateTime } from './validator.js'; import { serializeDateTime, serializeDateTimeString, - serializeUnixTimestamp, + serializeTimestamp, parseDateTime, } from './formatter.js'; import { createGraphQLError } from '../../error.js'; @@ -38,9 +38,9 @@ export const GraphQLDateTimeConfig: GraphQLScalarTypeConfig = /*#_ 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( @@ -93,7 +93,7 @@ export const GraphQLDateTimeConfig: GraphQLScalarTypeConfig = /*#_ * * 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); diff --git a/src/scalars/iso-date/formatter.ts b/src/scalars/iso-date/formatter.ts index e88820745..0180c93a2 100644 --- a/src/scalars/iso-date/formatter.ts +++ b/src/scalars/iso-date/formatter.ts @@ -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(); }; diff --git a/src/scalars/iso-date/validator.ts b/src/scalars/iso-date/validator.ts index bb4b66c74..763a8129a 100644 --- a/src/scalars/iso-date/validator.ts +++ b/src/scalars/iso-date/validator.ts @@ -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 diff --git a/tests/iso-date/DateTime.integration.test.ts b/tests/iso-date/DateTime.integration.test.ts index e58db390a..f92951b73 100644 --- a/tests/iso-date/DateTime.integration.test.ts +++ b/tests/iso-date/DateTime.integration.test.ts @@ -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, @@ -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, @@ -65,7 +65,7 @@ it('executes a query that includes a DateTime', async () => { validDate validUTCDateString validDateString - validUnixTimestamp + validTimestamp input(date: $date) inputNull: input } @@ -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, }, }); @@ -145,8 +145,8 @@ it('errors if an invalid date-time is returned from the resolver', async () => { { invalidDateString invalidDate + invalidTimestamp invalidType - invalidUnixTimestamp } `; @@ -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], ], } `); diff --git a/tests/iso-date/DateTime.test.ts b/tests/iso-date/DateTime.test.ts index c87a68718..5d247d11a 100644 --- a/tests/iso-date/DateTime.test.ts +++ b/tests/iso-date/DateTime.test.ts @@ -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(); }); }); diff --git a/tests/iso-date/__snapshots__/DateTime.test.ts.snap b/tests/iso-date/__snapshots__/DateTime.test.ts.snap index fd0202c59..1edb1fa95 100644 --- a/tests/iso-date/__snapshots__/DateTime.test.ts.snap +++ b/tests/iso-date/__snapshots__/DateTime.test.ts.snap @@ -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."`; diff --git a/tests/iso-date/formatter.test.ts b/tests/iso-date/formatter.test.ts index 26be7cee1..837b0eb79 100644 --- a/tests/iso-date/formatter.test.ts +++ b/tests/iso-date/formatter.test.ts @@ -13,7 +13,7 @@ import { serializeDate, serializeDateTime, serializeDateTimeString, - serializeUnixTimestamp, + serializeTimestamp, parseTime, parseDate, parseDateTime, @@ -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); }); }); diff --git a/tests/iso-date/validator.test.ts b/tests/iso-date/validator.test.ts index 71f29c322..f0e87b65d 100644 --- a/tests/iso-date/validator.test.ts +++ b/tests/iso-date/validator.test.ts @@ -11,7 +11,7 @@ import { validateTime, validateDate, validateDateTime, - validateUnixTimestamp, + validateTimestamp, validateJSDate, } from '../../src/scalars/iso-date/validator.js'; @@ -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); }); }); }); From 796aae36dc8acf35c7bf46a62332e037f0e595b1 Mon Sep 17 00:00:00 2001 From: Maciej Holyszko <14310995+falkenhawk@users.noreply.github.com> Date: Wed, 26 Apr 2023 12:50:52 +0200 Subject: [PATCH 3/3] docs: include milliseconds to the example value of ISO string representation of ECMAScript timestamp Co-authored-by: Sumegh Ghutke --- src/scalars/iso-date/DateTime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scalars/iso-date/DateTime.ts b/src/scalars/iso-date/DateTime.ts index 78e8edf9e..9e1dcc812 100644 --- a/src/scalars/iso-date/DateTime.ts +++ b/src/scalars/iso-date/DateTime.ts @@ -21,7 +21,7 @@ import { createGraphQLError } from '../../error.js'; export const GraphQLDateTimeConfig: GraphQLScalarTypeConfig = /*#__PURE__*/ { name: 'DateTime', description: - 'A date-time string at UTC, such as 2007-12-03T10:15:30Z, ' + + 'A date-time string at UTC, such as 2007-12-03T10:15:30.123Z, ' + 'compliant with the `date-time` format outlined in section 5.6 of ' + 'the RFC 3339 profile of the ISO 8601 standard for representation ' + 'of dates and times using the Gregorian calendar.',