Skip to content

Commit aff2c5d

Browse files
wardpeetfreiksenet
authored andcommitted
fix(gatsby): added looksLikeADate to check date on schema creation (#12722)
* fix(gatsby): add some quickchecks to isDate * update tests
1 parent c860c2a commit aff2c5d

File tree

3 files changed

+215
-9
lines changed

3 files changed

+215
-9
lines changed

packages/gatsby/src/schema/infer/example-value.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const _ = require(`lodash`)
22
const is32BitInteger = require(`./is-32-bit-integer`)
3-
const { isDate } = require(`../types/date`)
3+
const { looksLikeADate } = require(`../types/date`)
44

55
const getExampleValue = ({
66
nodes,
@@ -170,7 +170,7 @@ const getType = value => {
170170
case `number`:
171171
return `number`
172172
case `string`:
173-
return isDate(value) ? `date` : `string`
173+
return looksLikeADate(value) ? `date` : `string`
174174
case `boolean`:
175175
return `boolean`
176176
case `object`:

packages/gatsby/src/schema/types/__tests__/date.js

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const { store } = require(`../../../redux`)
22
const { build } = require(`../..`)
3-
const { isDate } = require(`../date`)
3+
const { isDate, looksLikeADate } = require(`../date`)
44
require(`../../../db/__tests__/fixtures/ensure-loki`)()
55

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

155+
describe(`looksLikeADate`, () => {
156+
it.each([
157+
`1970`,
158+
`2019`,
159+
`1970-01`,
160+
`2019-01`,
161+
`1970-01-01`,
162+
`2010-01-01`,
163+
`2010-01-30`,
164+
`19700101`,
165+
`20100101`,
166+
`20100130`,
167+
`2010-01-30T23+00:00`,
168+
`2010-01-30T23:59+00:00`,
169+
`2010-01-30T23:59:59+00:00`,
170+
`2010-01-30T23:59:59.999+00:00`,
171+
`2010-01-30T23:59:59.999-07:00`,
172+
`2010-01-30T00:00:00.000+07:00`,
173+
`2010-01-30T23:59:59.999-07`,
174+
`2010-01-30T00:00:00.000+07`,
175+
`2010-01-30T23Z`,
176+
`2010-01-30T23:59Z`,
177+
`2010-01-30T23:59:59Z`,
178+
`2010-01-30T23:59:59.999Z`,
179+
`2010-01-30T00:00:00.000Z`,
180+
`1970-01-01T00:00:00.000001Z`,
181+
`2012-04-01T00:00:00-05:00`,
182+
`2012-11-12T00:00:00+01:00`,
183+
])(`should return true for valid ISO 8601: %s`, dateString => {
184+
expect(looksLikeADate(dateString)).toBeTruthy()
185+
})
186+
187+
it.each([
188+
`2010-01-30 23+00:00`,
189+
`2010-01-30 23:59+00:00`,
190+
`2010-01-30 23:59:59+00:00`,
191+
`2010-01-30 23:59:59.999+00:00`,
192+
`2010-01-30 23:59:59.999-07:00`,
193+
`2010-01-30 00:00:00.000+07:00`,
194+
`2010-01-30 23:59:59.999-07`,
195+
`2010-01-30 00:00:00.000+07`,
196+
`1970-01-01 00:00:00.000Z`,
197+
`2012-04-01 00:00:00-05:00`,
198+
`2012-11-12 00:00:00+01:00`,
199+
`1970-01-01 00:00:00.0000001 Z`,
200+
`1970-01-01 00:00:00.000 Z`,
201+
`1970-01-01 00:00:00 Z`,
202+
`1970-01-01 000000 Z`,
203+
`1970-01-01 00:00 Z`,
204+
`1970-01-01 00 Z`,
205+
])(`should return true for ISO 8601 (no T, extra space): %s`, dateString => {
206+
expect(looksLikeADate(dateString)).toBeTruthy()
207+
})
208+
209+
it.each([`1970-W31`, `2006-W01`, `1970W31`, `2009-W53-7`, `2009W537`])(
210+
`should return true for ISO 8601 week dates: %s`,
211+
dateString => {
212+
expect(looksLikeADate(dateString)).toBeTruthy()
213+
}
214+
)
215+
216+
it.each([`1970-334`, `1970334`, `2090-001`, `2090001`])(
217+
`should return true for ISO 8601 ordinal dates: %s`,
218+
dateString => {
219+
expect(looksLikeADate(dateString)).toBeTruthy()
220+
}
221+
)
222+
223+
it.skip.each([
224+
`2018-08-31T23:25:16.019345+02:00`,
225+
`2018-08-31T23:25:16.019345Z`,
226+
])(`should return true for microsecond precision: %s`, dateString => {
227+
expect(looksLikeADate(dateString)).toBeTruthy()
228+
})
229+
230+
it.skip.each([
231+
`2018-08-31T23:25:16.019345123+02:00`,
232+
`2018-08-31T23:25:16.019345123Z`,
233+
])(`should return true for nanosecond precision: %s`, dateString => {
234+
expect(looksLikeADate(dateString)).toBeTruthy()
235+
})
236+
237+
it.skip.each([`2018-08-31T23:25:16.012345678901+02:00`])(
238+
`should return false for precision beyond 9 digits: %s`,
239+
dateString => {
240+
expect(looksLikeADate(dateString)).toBeFalsy()
241+
}
242+
)
243+
244+
it.each([
245+
`2010-00-00`,
246+
`2010-01-00`,
247+
`2010-01-40`,
248+
`2010-01-01T24:01`, // 24:00:00 is actually valid
249+
`2010-01-40T24:01+00:00`,
250+
`2010-01-01T23:60`,
251+
`2010-01-01T23:59:60`,
252+
`2010-01-40T23:60+00:00`,
253+
`2010-01-40T23:59:60+00:00`,
254+
])(`should return true for some valid ISO 8601: %s`, dateString => {
255+
expect(looksLikeADate(dateString)).toBeTruthy()
256+
})
257+
258+
it.each([
259+
`2010-01-40T23:59:59.9999`,
260+
`2010-01-40T23:59:59.9999+00:00`,
261+
`2010-01-40T23:59:59,9999+00:00`,
262+
`2010-00-00T+00:00`,
263+
`2010-01-00T+00:00`,
264+
`2010-01-40T+00:00`,
265+
`2012-04-01T00:00:00-5:00`, // should be -05:00
266+
`2012-04-01T00:00:00+1:00`, // should be +01:00
267+
undefined,
268+
`undefined`,
269+
null,
270+
`null`,
271+
[],
272+
{},
273+
``,
274+
` `,
275+
`2012-04-01T00:basketball`,
276+
])(`should return false for invalid ISO 8601: %s`, dateString => {
277+
expect(looksLikeADate(dateString)).toBeFalsy()
278+
})
279+
})
280+
155281
const nodes = [
156282
{
157283
id: `id1`,

packages/gatsby/src/schema/types/date.js

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,16 +96,96 @@ const GraphQLDate = new GraphQLScalarType({
9696
},
9797
})
9898

99-
// Check if this is a date.
100-
// All the allowed ISO 8601 date-time formats used.
101-
function isDate(value) {
99+
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
100+
const momentFormattingRegexes = {
101+
YYYY: `\\d{4}`,
102+
MM: `\\d{2}`,
103+
DD: `\\d{2}`,
104+
DDDD: `\\d{4}`,
105+
HH: `\\d{2}`,
106+
mm: `\\d{2}`,
107+
ss: `\\d{2}`,
108+
SSS: `\\d{3}`,
109+
SSSSSS: `\\d{6}`,
110+
E: `\\d`,
111+
W: `\\d`,
112+
WW: `\\d{2}`,
113+
"[W]": `W`,
114+
".": `\\.`,
115+
Z: `(Z|[+-]\\d\\d(?::?\\d\\d)?)`,
116+
}
117+
const ISO_8601_FORMAT_AS_REGEX = ISO_8601_FORMAT.map(format =>
118+
// convert ISO string to a map of momentTokens ([YYYY, MM, DD])
119+
[...format.match(momentFormattingTokens)]
120+
.map(token =>
121+
// see if the token (YYYY or ss) is found, else we just return the value
122+
momentFormattingRegexes[token] ? momentFormattingRegexes[token] : token
123+
)
124+
.join(``)
125+
).join(`|`)
126+
127+
// calculate all lengths of the formats, if a string is longer or smaller it can't be valid
128+
const ISO_8601_FORMAT_LENGTHS = [
129+
...new Set(
130+
ISO_8601_FORMAT.reduce((acc, val) => {
131+
if (!val.endsWith(`Z`)) {
132+
return acc.concat(val.length)
133+
}
134+
135+
// we add count of +01 & +01:00
136+
return acc.concat([val.length, val.length + 3, val.length + 5])
137+
}, [])
138+
),
139+
]
140+
141+
// lets imagine these formats: YYYY-MM-DDTHH & YYYY-MM-DD HHmmss.SSSSSS Z
142+
// 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)$)
143+
const quickDateValidateRegex = new RegExp(`^(${ISO_8601_FORMAT_AS_REGEX})$`)
144+
145+
const looksLikeDateStartRegex = /^\d{4}/
146+
// this regex makes sure the last characters are a number or the letter Z
147+
const looksLikeDateEndRegex = /(\d|Z)$/
148+
149+
/**
150+
* looksLikeADate isn't a 100% valid check if it is a real date but at least it's something that looks like a date.
151+
* It won't catch values like 2010-02-30
152+
* 1) is it a number?
153+
* 2) does the length of the value comply with any of our formats
154+
* 3) does the str starts with 4 digites (YYYY)
155+
* 4) does the str ends with something that looks like a date
156+
* 5) Small regex to see if it matches any of the formats
157+
* 6) check momentjs
158+
*
159+
* @param {*} value
160+
* @return {boolean}
161+
*/
162+
function looksLikeADate(value) {
102163
// quick check if value does not look like a date
103-
if (typeof value === `number` || !/^\d{4}/.test(value)) {
164+
if (
165+
!value ||
166+
(value.length && !ISO_8601_FORMAT_LENGTHS.includes(value.length)) ||
167+
!looksLikeDateStartRegex.test(value) ||
168+
!looksLikeDateEndRegex.test(value)
169+
) {
104170
return false
105171
}
106172

173+
// If it looks like a date we parse the date with a regex to see if we can handle it.
174+
// momentjs just does regex validation itself if you don't do any operations on it.
175+
if (typeof value === `string` && quickDateValidateRegex.test(value)) {
176+
return true
177+
}
178+
179+
return isDate(value)
180+
}
181+
182+
/**
183+
* @param {*} value
184+
* @return {boolean}
185+
*/
186+
function isDate(value) {
107187
const momentDate = moment.utc(value, ISO_8601_FORMAT, true)
108-
return momentDate.isValid()
188+
return typeof value !== `number` && momentDate.isValid()
109189
}
110190

111191
const formatDate = ({
@@ -175,4 +255,4 @@ const dateResolver = {
175255
},
176256
}
177257

178-
module.exports = { GraphQLDate, dateResolver, isDate }
258+
module.exports = { GraphQLDate, dateResolver, isDate, looksLikeADate }

0 commit comments

Comments
 (0)