Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(gatsby): added looksLikeADate to check date on schema creation #12722

Merged
merged 3 commits into from
Mar 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -168,7 +168,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([
wardpeet marked this conversation as resolved.
Show resolved Hide resolved
`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 }