From 5905a23badb85e4b1aed29a9ec026d1642e24755 Mon Sep 17 00:00:00 2001 From: Justin Moroz Date: Sun, 6 Nov 2022 20:47:43 -0500 Subject: [PATCH 1/4] fix: implement temporary schema definition to support datetime scalars --- packages/graphql-scalars/src/datetime.ts | 100 +++++++++++++++++++++++ packages/graphql-scalars/src/index.ts | 8 +- 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 packages/graphql-scalars/src/datetime.ts diff --git a/packages/graphql-scalars/src/datetime.ts b/packages/graphql-scalars/src/datetime.ts new file mode 100644 index 000000000..7ec60d619 --- /dev/null +++ b/packages/graphql-scalars/src/datetime.ts @@ -0,0 +1,100 @@ +//@TODO Remove this file when https://github.com/Urigo/graphql-scalars/pull/1641 is merged + +import { GraphQLScalarType, Kind } from 'graphql' + +function 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]))$/ + return TIME_REGEX.test(time) +} + +function validateDate(date: string): boolean { + const RFC_3339_REGEX = /^(\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))$/ + if (!RFC_3339_REGEX.test(date)) { + return false + } + // Verify the correct number of days for + // the month contained in the date-string. + const year = Number(date.substr(0, 4)) + const month = Number(date.substr(5, 2)) + const day = Number(date.substr(8, 2)) + switch (month) { + case 2: // February + if (((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) && day > 29) { + return false + } else if (((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) && day > 28) { + return false + } + return true + case 4: // April + case 6: // June + case 9: // September + case 11: // November + if (day > 30) { + return false + } + break + } + return true +} + +function validateDateTime(input: string): boolean { + input = input?.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]))$/ + + // Validate the structure of the date-string + if (!RFC_3339_REGEX.test(input)) { + return false + } + + // Check if it is a correct date using the javascript Date parse() method. + const time = Date.parse(input) + if (time !== time) { + // eslint-disable-line + return false + } + // Split the date-time-string up into the string-date and time-string part. + // and check whether these parts are RFC 3339 compliant. + const index = input.indexOf('T') + const dateString = input.substr(0, index) + const timeString = input.substr(index + 1) + return validateDate(dateString) && validateTime(timeString) +} + +export const DateTime = new GraphQLScalarType({ + name: 'DateTime', + description: + '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.', + serialize: (dateString) => { + if (typeof dateString === 'string' && validateDateTime(dateString)) { + if (dateString.indexOf('Z') !== -1) { + return dateString + } else + throw new TypeError(`DateTime string must be formatted to UTC time ${String(dateString)}.`) + } else + throw new TypeError( + `DateTime cannot represent an invalid date-time-string ${String(dateString)}.` + ) + }, + parseValue: (value) => { + if (typeof value === 'string' && validateDateTime(value)) { + return value + } else + throw new TypeError(`DateTime cannot represent an invalid date-time-string ${String(value)}.`) + }, + parseLiteral: (ast) => { + if (ast.kind !== Kind.STRING) { + throw new TypeError('DateTime cannot represent non string or Date type') + } + const { value } = ast + if (validateDateTime(value)) { + return value + } + throw new TypeError(`DateTime cannot represent an invalid date-time-string ${String(value)}.`) + }, +}) diff --git a/packages/graphql-scalars/src/index.ts b/packages/graphql-scalars/src/index.ts index a66bc0fc6..b6ac6739a 100644 --- a/packages/graphql-scalars/src/index.ts +++ b/packages/graphql-scalars/src/index.ts @@ -10,17 +10,21 @@ import { import { GraphQLCountryCode, GraphQLDate, - GraphQLDateTime, + //GraphQLDateTime, GraphQLDID, GraphQLTime, } from 'graphql-scalars' import { CeramicCommitID, CeramicStreamID } from './ceramic.js' +//@TODO Remove these imports when https://github.com/Urigo/graphql-scalars/pull/1641 is merged +import { DateTime as GraphQLDateTime } from './datetime.js' +export { DateTime as GraphQLDateTime } from './datetime.js' + export { GraphQLCountryCode, GraphQLDate, - GraphQLDateTime, + //GraphQLDateTime, GraphQLDID, GraphQLTime, } from 'graphql-scalars' From 2111f262edfb306d6b6e045154236575670c5e37 Mon Sep 17 00:00:00 2001 From: Justin Moroz Date: Mon, 7 Nov 2022 19:53:00 -0500 Subject: [PATCH 2/4] fix: address review comments --- packages/graphql-scalars/src/datetime.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/graphql-scalars/src/datetime.ts b/packages/graphql-scalars/src/datetime.ts index 7ec60d619..d6aab13b4 100644 --- a/packages/graphql-scalars/src/datetime.ts +++ b/packages/graphql-scalars/src/datetime.ts @@ -51,8 +51,7 @@ function validateDateTime(input: string): boolean { // Check if it is a correct date using the javascript Date parse() method. const time = Date.parse(input) - if (time !== time) { - // eslint-disable-line + if (Number.isNaN(time)) { return false } // Split the date-time-string up into the string-date and time-string part. @@ -75,10 +74,10 @@ export const DateTime = new GraphQLScalarType({ if (dateString.indexOf('Z') !== -1) { return dateString } else - throw new TypeError(`DateTime string must be formatted to UTC time ${String(dateString)}.`) + throw new TypeError(`DateTime string must be formatted to UTC time ${String(dateString)}.`) } else throw new TypeError( - `DateTime cannot represent an invalid date-time-string ${String(dateString)}.` + `DateTime cannot represent an invalid date-time-string ${String(dateString)}.` ) }, parseValue: (value) => { @@ -97,4 +96,11 @@ export const DateTime = new GraphQLScalarType({ } throw new TypeError(`DateTime cannot represent an invalid date-time-string ${String(value)}.`) }, + extensions: { + codegenScalarType: 'Date | string', + jsonSchema: { + type: 'string', + format: 'date-time', + }, + }, }) From 5520b7d0c8e7f189c97e3905868967cb075b5f79 Mon Sep 17 00:00:00 2001 From: Justin Moroz Date: Mon, 7 Nov 2022 20:06:01 -0500 Subject: [PATCH 3/4] fix: update extensions object --- packages/graphql-scalars/src/datetime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql-scalars/src/datetime.ts b/packages/graphql-scalars/src/datetime.ts index d6aab13b4..f2f1c88ca 100644 --- a/packages/graphql-scalars/src/datetime.ts +++ b/packages/graphql-scalars/src/datetime.ts @@ -97,7 +97,7 @@ export const DateTime = new GraphQLScalarType({ throw new TypeError(`DateTime cannot represent an invalid date-time-string ${String(value)}.`) }, extensions: { - codegenScalarType: 'Date | string', + codegenScalarType: 'string', jsonSchema: { type: 'string', format: 'date-time', From 9ab208717d8d3248a7305952b47a64b2cca723ad Mon Sep 17 00:00:00 2001 From: Justin Moroz Date: Thu, 10 Nov 2022 17:54:21 -0500 Subject: [PATCH 4/4] fix: datetime scalar description --- packages/graphql-scalars/src/datetime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql-scalars/src/datetime.ts b/packages/graphql-scalars/src/datetime.ts index f2f1c88ca..3b22fa5fc 100644 --- a/packages/graphql-scalars/src/datetime.ts +++ b/packages/graphql-scalars/src/datetime.ts @@ -66,7 +66,7 @@ export const DateTime = new GraphQLScalarType({ name: 'DateTime', description: '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 ' + + '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.', serialize: (dateString) => {