Skip to content

Commit

Permalink
fix(gatsby): added looksLikeADate to check date on schema creation (#…
Browse files Browse the repository at this point in the history
…12722)

* fix(gatsby): add some quickchecks to isDate

* update tests
  • Loading branch information
wardpeet authored and freiksenet committed Mar 25, 2019
1 parent c860c2a commit aff2c5d
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 9 deletions.
4 changes: 2 additions & 2 deletions packages/gatsby/src/schema/infer/example-value.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const _ = require(`lodash`)
const is32BitInteger = require(`./is-32-bit-integer`)
const { isDate } = require(`../types/date`)
const { looksLikeADate } = require(`../types/date`)

const getExampleValue = ({
nodes,
Expand Down Expand Up @@ -170,7 +170,7 @@ const getType = value => {
case `number`:
return `number`
case `string`:
return isDate(value) ? `date` : `string`
return looksLikeADate(value) ? `date` : `string`
case `boolean`:
return `boolean`
case `object`:
Expand Down
128 changes: 127 additions & 1 deletion packages/gatsby/src/schema/types/__tests__/date.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { store } = require(`../../../redux`)
const { build } = require(`../..`)
const { isDate } = require(`../date`)
const { isDate, looksLikeADate } = require(`../date`)
require(`../../../db/__tests__/fixtures/ensure-loki`)()

// Timestamps grabbed from https://github.com/moment/moment/blob/2e2a5b35439665d4b0200143d808a7c26d6cd30f/src/test/moment/is_valid.js
Expand Down Expand Up @@ -152,6 +152,132 @@ describe(`isDate`, () => {
})
})

describe(`looksLikeADate`, () => {
it.each([
`1970`,
`2019`,
`1970-01`,
`2019-01`,
`1970-01-01`,
`2010-01-01`,
`2010-01-30`,
`19700101`,
`20100101`,
`20100130`,
`2010-01-30T23+00:00`,
`2010-01-30T23:59+00:00`,
`2010-01-30T23:59:59+00:00`,
`2010-01-30T23:59:59.999+00:00`,
`2010-01-30T23:59:59.999-07:00`,
`2010-01-30T00:00:00.000+07:00`,
`2010-01-30T23:59:59.999-07`,
`2010-01-30T00:00:00.000+07`,
`2010-01-30T23Z`,
`2010-01-30T23:59Z`,
`2010-01-30T23:59:59Z`,
`2010-01-30T23:59:59.999Z`,
`2010-01-30T00:00:00.000Z`,
`1970-01-01T00:00:00.000001Z`,
`2012-04-01T00:00:00-05:00`,
`2012-11-12T00:00:00+01:00`,
])(`should return true for valid ISO 8601: %s`, dateString => {
expect(looksLikeADate(dateString)).toBeTruthy()
})

it.each([
`2010-01-30 23+00:00`,
`2010-01-30 23:59+00:00`,
`2010-01-30 23:59:59+00:00`,
`2010-01-30 23:59:59.999+00:00`,
`2010-01-30 23:59:59.999-07:00`,
`2010-01-30 00:00:00.000+07:00`,
`2010-01-30 23:59:59.999-07`,
`2010-01-30 00:00:00.000+07`,
`1970-01-01 00:00:00.000Z`,
`2012-04-01 00:00:00-05:00`,
`2012-11-12 00:00:00+01:00`,
`1970-01-01 00:00:00.0000001 Z`,
`1970-01-01 00:00:00.000 Z`,
`1970-01-01 00:00:00 Z`,
`1970-01-01 000000 Z`,
`1970-01-01 00:00 Z`,
`1970-01-01 00 Z`,
])(`should return true for ISO 8601 (no T, extra space): %s`, dateString => {
expect(looksLikeADate(dateString)).toBeTruthy()
})

it.each([`1970-W31`, `2006-W01`, `1970W31`, `2009-W53-7`, `2009W537`])(
`should return true for ISO 8601 week dates: %s`,
dateString => {
expect(looksLikeADate(dateString)).toBeTruthy()
}
)

it.each([`1970-334`, `1970334`, `2090-001`, `2090001`])(
`should return true for ISO 8601 ordinal dates: %s`,
dateString => {
expect(looksLikeADate(dateString)).toBeTruthy()
}
)

it.skip.each([
`2018-08-31T23:25:16.019345+02:00`,
`2018-08-31T23:25:16.019345Z`,
])(`should return true for microsecond precision: %s`, dateString => {
expect(looksLikeADate(dateString)).toBeTruthy()
})

it.skip.each([
`2018-08-31T23:25:16.019345123+02:00`,
`2018-08-31T23:25:16.019345123Z`,
])(`should return true for nanosecond precision: %s`, dateString => {
expect(looksLikeADate(dateString)).toBeTruthy()
})

it.skip.each([`2018-08-31T23:25:16.012345678901+02:00`])(
`should return false for precision beyond 9 digits: %s`,
dateString => {
expect(looksLikeADate(dateString)).toBeFalsy()
}
)

it.each([
`2010-00-00`,
`2010-01-00`,
`2010-01-40`,
`2010-01-01T24:01`, // 24:00:00 is actually valid
`2010-01-40T24:01+00:00`,
`2010-01-01T23:60`,
`2010-01-01T23:59:60`,
`2010-01-40T23:60+00:00`,
`2010-01-40T23:59:60+00:00`,
])(`should return true for some valid ISO 8601: %s`, dateString => {
expect(looksLikeADate(dateString)).toBeTruthy()
})

it.each([
`2010-01-40T23:59:59.9999`,
`2010-01-40T23:59:59.9999+00:00`,
`2010-01-40T23:59:59,9999+00:00`,
`2010-00-00T+00:00`,
`2010-01-00T+00:00`,
`2010-01-40T+00:00`,
`2012-04-01T00:00:00-5:00`, // should be -05:00
`2012-04-01T00:00:00+1:00`, // should be +01:00
undefined,
`undefined`,
null,
`null`,
[],
{},
``,
` `,
`2012-04-01T00:basketball`,
])(`should return false for invalid ISO 8601: %s`, dateString => {
expect(looksLikeADate(dateString)).toBeFalsy()
})
})

const nodes = [
{
id: `id1`,
Expand Down
92 changes: 86 additions & 6 deletions packages/gatsby/src/schema/types/date.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,96 @@ const GraphQLDate = new GraphQLScalarType({
},
})

// Check if this is a date.
// All the allowed ISO 8601 date-time formats used.
function isDate(value) {
const momentFormattingTokens = /(\[[^[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g
const momentFormattingRegexes = {
YYYY: `\\d{4}`,
MM: `\\d{2}`,
DD: `\\d{2}`,
DDDD: `\\d{4}`,
HH: `\\d{2}`,
mm: `\\d{2}`,
ss: `\\d{2}`,
SSS: `\\d{3}`,
SSSSSS: `\\d{6}`,
E: `\\d`,
W: `\\d`,
WW: `\\d{2}`,
"[W]": `W`,
".": `\\.`,
Z: `(Z|[+-]\\d\\d(?::?\\d\\d)?)`,
}
const ISO_8601_FORMAT_AS_REGEX = ISO_8601_FORMAT.map(format =>
// convert ISO string to a map of momentTokens ([YYYY, MM, DD])
[...format.match(momentFormattingTokens)]
.map(token =>
// see if the token (YYYY or ss) is found, else we just return the value
momentFormattingRegexes[token] ? momentFormattingRegexes[token] : token
)
.join(``)
).join(`|`)

// calculate all lengths of the formats, if a string is longer or smaller it can't be valid
const ISO_8601_FORMAT_LENGTHS = [
...new Set(
ISO_8601_FORMAT.reduce((acc, val) => {
if (!val.endsWith(`Z`)) {
return acc.concat(val.length)
}

// we add count of +01 & +01:00
return acc.concat([val.length, val.length + 3, val.length + 5])
}, [])
),
]

// lets imagine these formats: YYYY-MM-DDTHH & YYYY-MM-DD HHmmss.SSSSSS Z
// this regex looks like (/^(\d{4}-\d{2}-\d{2}T\d{2}|\d{4}-\d{2}-\d{2} \d{2}\d{2}\d{2}.\d{6} Z)$)
const quickDateValidateRegex = new RegExp(`^(${ISO_8601_FORMAT_AS_REGEX})$`)

const looksLikeDateStartRegex = /^\d{4}/
// this regex makes sure the last characters are a number or the letter Z
const looksLikeDateEndRegex = /(\d|Z)$/

/**
* looksLikeADate isn't a 100% valid check if it is a real date but at least it's something that looks like a date.
* It won't catch values like 2010-02-30
* 1) is it a number?
* 2) does the length of the value comply with any of our formats
* 3) does the str starts with 4 digites (YYYY)
* 4) does the str ends with something that looks like a date
* 5) Small regex to see if it matches any of the formats
* 6) check momentjs
*
* @param {*} value
* @return {boolean}
*/
function looksLikeADate(value) {
// quick check if value does not look like a date
if (typeof value === `number` || !/^\d{4}/.test(value)) {
if (
!value ||
(value.length && !ISO_8601_FORMAT_LENGTHS.includes(value.length)) ||
!looksLikeDateStartRegex.test(value) ||
!looksLikeDateEndRegex.test(value)
) {
return false
}

// If it looks like a date we parse the date with a regex to see if we can handle it.
// momentjs just does regex validation itself if you don't do any operations on it.
if (typeof value === `string` && quickDateValidateRegex.test(value)) {
return true
}

return isDate(value)
}

/**
* @param {*} value
* @return {boolean}
*/
function isDate(value) {
const momentDate = moment.utc(value, ISO_8601_FORMAT, true)
return momentDate.isValid()
return typeof value !== `number` && momentDate.isValid()
}

const formatDate = ({
Expand Down Expand Up @@ -175,4 +255,4 @@ const dateResolver = {
},
}

module.exports = { GraphQLDate, dateResolver, isDate }
module.exports = { GraphQLDate, dateResolver, isDate, looksLikeADate }

0 comments on commit aff2c5d

Please sign in to comment.