From 820d737f1527101bb92034806d73809537448c04 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 9 Feb 2018 01:00:53 +0100 Subject: [PATCH] Field inferring changes and cleanup (#3688) * remove not needed edge case for infering array of objects to improve code readability * don't lose field args and resolve function when infering array (fixes date format options not available in arrays of dates) * move infering date field to seperate file to improve code readability * don't recreate date field definition for every date field - we can reuse same object * create dedicated Date graphql scalar (based on string scalar) for date fields * pass fieldName to resolver when filtering on types with custom resolvers (date type fields now rely on fieldname info being passed) * use already constructed nextselector instead of rebuilding it again * move infering file field to separate file to improve code readability * create file type field object just once and reuse it * move trying to infer Files from string paths to inferGraphQLType function where rest of value based inferring is done * add tests for inferring File from string paths * add tests for inferring date type from string, array of strings and filtering date fields, move date related test to seperate date test suite --- .../infer-graphql-type-test.js.snap | 30 +- .../infer-graphql-input-type-test.js | 23 +- .../__tests__/infer-graphql-type-test.js | 173 +++++++-- .../gatsby/src/schema/build-node-types.js | 9 + .../gatsby/src/schema/infer-graphql-type.js | 349 +++--------------- packages/gatsby/src/schema/run-sift.js | 2 +- packages/gatsby/src/schema/types/type-date.js | 114 ++++++ packages/gatsby/src/schema/types/type-file.js | 211 +++++++++++ 8 files changed, 566 insertions(+), 345 deletions(-) create mode 100644 packages/gatsby/src/schema/types/type-date.js create mode 100644 packages/gatsby/src/schema/types/type-file.js diff --git a/packages/gatsby/src/schema/__tests__/__snapshots__/infer-graphql-type-test.js.snap b/packages/gatsby/src/schema/__tests__/__snapshots__/infer-graphql-type-test.js.snap index 9fb28ffd45b81..f3fb4e067e9cc 100644 --- a/packages/gatsby/src/schema/__tests__/__snapshots__/infer-graphql-type-test.js.snap +++ b/packages/gatsby/src/schema/__tests__/__snapshots__/infer-graphql-type-test.js.snap @@ -1,5 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`GraphQL type inferance Handles dates Date type inference 1`] = ` +Object { + "data": Object { + "listNode": Array [ + Object { + "dateObject": "2012-11-05T00:00:00.000Z", + }, + Object { + "dateObject": "2012-11-05T00:00:00.000Z", + }, + ], + }, +} +`; + exports[`GraphQL type inferance Infers graphql type from array of nodes 1`] = ` Object { "data": Object { @@ -115,18 +130,3 @@ Object { }, } `; - -exports[`GraphQL type inferance handles date objects 1`] = ` -Object { - "data": Object { - "listNode": Array [ - Object { - "dateObject": "2012-11-05T00:00:00.000Z", - }, - Object { - "dateObject": "2012-11-05T00:00:00.000Z", - }, - ], - }, -} -`; diff --git a/packages/gatsby/src/schema/__tests__/infer-graphql-input-type-test.js b/packages/gatsby/src/schema/__tests__/infer-graphql-input-type-test.js index 4485e13a34508..9072c3b599ae6 100644 --- a/packages/gatsby/src/schema/__tests__/infer-graphql-input-type-test.js +++ b/packages/gatsby/src/schema/__tests__/infer-graphql-input-type-test.js @@ -78,6 +78,7 @@ function queryResult(nodes, query, { types = [] } = {}) { describe(`GraphQL Input args`, () => { const nodes = [ { + index: 0, name: `The Mad Max`, hair: 1, date: `2006-07-22T22:39:53.000Z`, @@ -103,9 +104,9 @@ describe(`GraphQL Input args`, () => { boolean: true, }, { + index: 1, name: `The Mad Wax`, hair: 2, - date: `2006-07-22T22:39:53.000Z`, anArray: [1, 2, 5, 4], frontmatter: { date: `2006-07-22T22:39:53.000Z`, @@ -116,9 +117,10 @@ describe(`GraphQL Input args`, () => { boolean: false, }, { + index: 2, name: `The Mad Wax`, hair: 0, - date: `2006-07-22T22:39:53.000Z`, + date: `2006-07-29T22:39:53.000Z`, frontmatter: { date: `2006-07-22T22:39:53.000Z`, title: `The world of shave and adventure`, @@ -375,6 +377,23 @@ describe(`GraphQL Input args`, () => { expect(result.data.allNode.edges[0].node.name).toEqual(`The Mad Wax`) }) + it(`filters date fields`, async () => { + let result = await queryResult( + nodes, + ` + { + allNode(filter: {date: { ne: null }}) { + edges { node { index }} + } + } + ` + ) + expect(result.errors).not.toBeDefined() + expect(result.data.allNode.edges.length).toEqual(2) + expect(result.data.allNode.edges[0].node.index).toEqual(0) + expect(result.data.allNode.edges[1].node.index).toEqual(2) + }) + it(`sorts results`, async () => { let result = await queryResult( nodes, diff --git a/packages/gatsby/src/schema/__tests__/infer-graphql-type-test.js b/packages/gatsby/src/schema/__tests__/infer-graphql-type-test.js index 5952717c8b847..b6c3f7002bb1d 100644 --- a/packages/gatsby/src/schema/__tests__/infer-graphql-type-test.js +++ b/packages/gatsby/src/schema/__tests__/infer-graphql-type-test.js @@ -4,6 +4,9 @@ const { GraphQLList, GraphQLSchema, } = require(`graphql`) +const path = require(`path`) +const normalizePath = require(`normalize-path`) + const { inferObjectStructureFromNodes } = require(`../infer-graphql-type`) function queryResult(nodes, fragment, { types = [] } = {}) { @@ -39,7 +42,9 @@ function queryResult(nodes, fragment, { types = [] } = {}) { ${fragment} } } - ` + `, + null, + { path: `/` } ) } @@ -118,29 +123,6 @@ describe(`GraphQL type inferance`, () => { expect(result.data.listNode[0].number).toEqual(1.1) }) - it(`handles integer with valid date format`, async () => { - let result = await queryResult( - [{ number: 2018 }, { number: 1987 }], - ` - number - ` - ) - expect(result.data.listNode[0].number).toEqual(2018) - }) - - it(`handles date objects`, async () => { - let result = await queryResult( - [ - { dateObject: new Date(Date.UTC(2012, 10, 5)) }, - { dateObject: new Date(Date.UTC(2012, 10, 5)) }, - ], - ` - dateObject - ` - ) - expect(result).toMatchSnapshot() - }) - it(`filters out empty objects`, async () => { let result = await queryResult( [{ foo: {}, bar: `baz` }], @@ -206,8 +188,149 @@ describe(`GraphQL type inferance`, () => { expect(Object.keys(fields.foo.type.getFields())).toHaveLength(4) }) + describe(`Handles dates`, () => { + it(`Handles integer with valid date format`, async () => { + let result = await queryResult( + [{ number: 2018 }, { number: 1987 }], + ` + number + ` + ) + expect(result.data.listNode[0].number).toEqual(2018) + }) + + it(`Date type inference`, async () => { + let result = await queryResult( + [ + { dateObject: new Date(Date.UTC(2012, 10, 5)) }, + { dateObject: new Date(Date.UTC(2012, 10, 5)) }, + ], + ` + dateObject + ` + ) + expect(result).toMatchSnapshot() + }) + + it(`Infers from date strings`, async () => { + let result = await queryResult( + [{ date: `1012-11-01` }], + ` + date(formatString:"DD.MM.YYYY") + ` + ) + + expect(result.errors).not.toBeDefined() + expect(result.data.listNode[0].date).toEqual(`01.11.1012`) + }) + + it(`Infers from arrays of date strings`, async () => { + let result = await queryResult( + [{ date: [`1012-11-01`, `10390203`] }], + ` + date(formatString:"DD.MM.YYYY") + ` + ) + + expect(result.errors).not.toBeDefined() + expect(result.data.listNode[0].date.length).toEqual(2) + expect(result.data.listNode[0].date[0]).toEqual(`01.11.1012`) + expect(result.data.listNode[0].date[1]).toEqual(`03.02.1039`) + }) + }) + xdescribe(`Linked inference from config mappings`) - xdescribe(`Linked inference from file URIs`) + + describe(`Linked inference from file URIs`, () => { + let store, types, dir + + beforeEach(() => { + ;({ store } = require(`../../redux`)) + + const { setFileNodeRootType } = require(`../types/type-file`) + const fileType = { + name: `File`, + nodeObjectType: new GraphQLObjectType({ + name: `File`, + fields: inferObjectStructureFromNodes({ + nodes: [{ id: `file_1`, absolutePath: `path`, dir: `path` }], + types: [{ name: `File` }], + }), + }), + } + + types = [fileType] + setFileNodeRootType(fileType.nodeObjectType) + + dir = normalizePath(path.resolve(`/path/`)) + + store.dispatch({ + type: `CREATE_NODE`, + payload: { + id: `parent`, + internal: { type: `File` }, + absolutePath: normalizePath(path.resolve(dir, `index.md`)), + dir: dir, + }, + }) + store.dispatch({ + type: `CREATE_NODE`, + payload: { + id: `file_1`, + internal: { type: `File` }, + absolutePath: normalizePath(path.resolve(dir, `file_1.jpg`)), + dir, + }, + }) + store.dispatch({ + type: `CREATE_NODE`, + payload: { + id: `file_2`, + internal: { type: `File` }, + absolutePath: normalizePath(path.resolve(dir, `file_2.txt`)), + dir, + }, + }) + }) + + it(`Links to file node`, async () => { + let result = await queryResult( + [{ file: `./file_1.jpg`, parent: `parent` }], + ` + file { + absolutePath + } + `, + { types } + ) + + expect(result.errors).not.toBeDefined() + expect(result.data.listNode[0].file.absolutePath).toEqual( + normalizePath(path.resolve(dir, `file_1.jpg`)) + ) + }) + + it(`Links to array of file nodes`, async () => { + let result = await queryResult( + [{ files: [`./file_1.jpg`, `./file_2.txt`], parent: `parent` }], + ` + files { + absolutePath + } + `, + { types } + ) + + expect(result.errors).not.toBeDefined() + expect(result.data.listNode[0].files.length).toEqual(2) + expect(result.data.listNode[0].files[0].absolutePath).toEqual( + normalizePath(path.resolve(dir, `file_1.jpg`)) + ) + expect(result.data.listNode[0].files[1].absolutePath).toEqual( + normalizePath(path.resolve(dir, `file_2.txt`)) + ) + }) + }) describe(`Linked inference by __NODE convention`, () => { let store, types diff --git a/packages/gatsby/src/schema/build-node-types.js b/packages/gatsby/src/schema/build-node-types.js index b5af54e61db89..54852ec4f564e 100644 --- a/packages/gatsby/src/schema/build-node-types.js +++ b/packages/gatsby/src/schema/build-node-types.js @@ -18,6 +18,7 @@ const { const { nodeInterface } = require(`./node-interface`) const { getNodes, getNode, getNodeAndSavePathDependency } = require(`../redux`) const { createPageDependency } = require(`../redux/actions/add-page-dependency`) +const { setFileNodeRootType } = require(`./types/type-file`) import type { ProcessedNodeType } from "./infer-graphql-type" @@ -27,6 +28,9 @@ module.exports = async () => { const types = _.groupBy(getNodes(), node => node.internal.type) const processedTypes: TypeMap = {} + // Reset stored File type to not point to outdated type definition + setFileNodeRootType(null) + function createNodeFields(type: ProcessedNodeType) { const defaultNodeFields = { id: { @@ -183,6 +187,11 @@ module.exports = async () => { } processedTypes[_.camelCase(typeName)] = proccesedType + + // Special case to construct linked file type used by type inferring + if (typeName === `File`) { + setFileNodeRootType(gqlType) + } } // Create node types and node fields for nodes that have a resolve function. diff --git a/packages/gatsby/src/schema/infer-graphql-type.js b/packages/gatsby/src/schema/infer-graphql-type.js index 34b5bf75db102..fcdccdaa53b2b 100644 --- a/packages/gatsby/src/schema/infer-graphql-type.js +++ b/packages/gatsby/src/schema/infer-graphql-type.js @@ -10,16 +10,9 @@ const { } = require(`graphql`) const _ = require(`lodash`) const invariant = require(`invariant`) -const moment = require(`moment`) -const mime = require(`mime`) -const isRelative = require(`is-relative`) -const isRelativeUrl = require(`is-relative-url`) -const normalize = require(`normalize-path`) -const systemPath = require(`path`) const { oneLine } = require(`common-tags`) -const { store, getNode, getNodes, getRootNodeId } = require(`../redux`) -const { joinPath } = require(`../utils/path`) +const { store, getNode, getNodes } = require(`../redux`) const { createPageDependency } = require(`../redux/actions/add-page-dependency`) const createTypeName = require(`./create-type-name`) const createKey = require(`./create-key`) @@ -27,6 +20,8 @@ const { extractFieldExamples, isEmptyObjectOrArray, } = require(`./data-tree-utils`) +const DateType = require(`./types/type-date`) +const FileType = require(`./types/type-file`) import type { GraphQLOutputType } from "graphql" import type { @@ -42,133 +37,66 @@ export type ProcessedNodeType = { nodeObjectType: GraphQLOutputType, } -const ISO_8601_FORMAT = [ - `YYYY`, - `YYYY-MM`, - `YYYY-MM-DD`, - `YYYYMMDD`, - `YYYY-MM-DDTHHZ`, - `YYYY-MM-DDTHH:mmZ`, - `YYYY-MM-DDTHHmmZ`, - `YYYY-MM-DDTHH:mm:ssZ`, - `YYYY-MM-DDTHHmmssZ`, - `YYYY-MM-DDTHH:mm:ss.SSSZ`, - `YYYY-MM-DDTHHmmss.SSSZ`, - `YYYY-[W]WW`, - `YYYY[W]WW`, - `YYYY-[W]WW-E`, - `YYYY[W]WWE`, - `YYYY-DDDD`, - `YYYYDDDD`, -] - function inferGraphQLType({ exampleValue, selector, + nodes, + types, ...otherArgs }): ?GraphQLFieldConfig<*, *> { if (exampleValue == null || isEmptyObjectOrArray(exampleValue)) return null let fieldName = selector.split(`.`).pop() + // Check this before checking for array as FileType has + // builtin support for inferring array of files and inferred + // array type will have faster resolver than resolving array + // of files separately. + if (FileType.shouldInfer(nodes, selector, exampleValue)) { + return _.isArray(exampleValue) ? FileType.getListType() : FileType.getType() + } + if (Array.isArray(exampleValue)) { exampleValue = exampleValue[0] if (exampleValue == null) return null - let headType - // If the array contains non-array objects, than treat them as "nodes" - // and create an object type. - if (_.isObject(exampleValue) && !_.isArray(exampleValue)) { - headType = new GraphQLObjectType({ - name: createTypeName(fieldName), - fields: inferObjectStructureFromNodes({ - ...otherArgs, - exampleValue, - selector, - }), - }) - // Else if the values are simple values or arrays, just infer their type. - } else { - let inferredType = inferGraphQLType({ - ...otherArgs, - exampleValue, - selector, - }) - invariant( - inferredType, - `Could not infer graphQL type for value: ${exampleValue}` - ) + let inferredType = inferGraphQLType({ + ...otherArgs, + exampleValue, + selector, + nodes, + types, + }) + invariant( + inferredType, + `Could not infer graphQL type for value: ${exampleValue}` + ) - headType = inferredType.type - } - return { type: new GraphQLList(headType) } - } + const { type, args = null, resolve = null } = inferredType - // Check if this is a date. - // All the allowed ISO 8601 date-time formats used. - const momentDate = moment.utc(exampleValue, ISO_8601_FORMAT, true) - if (momentDate.isValid() && typeof exampleValue !== `number`) { - return { - type: GraphQLString, - args: { - formatString: { - type: GraphQLString, - description: oneLine` - Format the date using Moment.js' date tokens e.g. - "date(formatString: "YYYY MMMM DD)" - See https://momentjs.com/docs/#/displaying/format/ - for documentation for different tokens`, - }, - fromNow: { - type: GraphQLBoolean, - description: oneLine` - Returns a string generated with Moment.js' fromNow function`, - }, - difference: { - type: GraphQLString, - description: oneLine` - Returns the difference between this date and the current time. - Defaults to miliseconds but you can also pass in as the - measurement years, months, weeks, days, hours, minutes, - and seconds.`, - }, - locale: { - type: GraphQLString, - description: oneLine` - Configures the locale Moment.js will use to format the date. - `, - }, - }, - resolve(object, args) { - let date - if (object[fieldName]) { - date = JSON.parse(JSON.stringify(object[fieldName])) - } else { + const listType = { type: new GraphQLList(type), args } + + if (resolve) { + // If inferredType has resolve function wrap it with Array.map + listType.resolve = (object, args, context, resolveInfo) => { + const fieldValue = object[fieldName] + if (!fieldValue) { return null } - if (_.isPlainObject(args)) { - const { fromNow, difference, formatString, locale = `en` } = args - if (formatString) { - return moment - .utc(date, ISO_8601_FORMAT, true) - .locale(locale) - .format(formatString) - } else if (fromNow) { - return moment - .utc(date, ISO_8601_FORMAT, true) - .locale(locale) - .fromNow() - } else if (difference) { - return moment().diff( - moment.utc(date, ISO_8601_FORMAT, true).locale(locale), - difference - ) - } - } - return date - }, + // Field resolver expects first parameter to be plain object + // containing key with name of field we want to resolve. + return fieldValue.map(value => + resolve({ [fieldName]: value }, args, context, resolveInfo) + ) + } } + + return listType + } + + if (DateType.shouldInfer(exampleValue)) { + return DateType.getType() } switch (typeof exampleValue) { @@ -184,6 +112,8 @@ function inferGraphQLType({ ...otherArgs, exampleValue, selector, + nodes, + types, }), }), } @@ -361,177 +291,6 @@ function inferFromFieldName(value, selector, types): GraphQLFieldConfig<*, *> { } } -function findRootNode(node) { - // Find the root node. - let rootNode = node - let whileCount = 0 - let rootNodeId - while ( - (rootNodeId = getRootNodeId(rootNode) || rootNode.parent) && - (getNode(rootNode.parent) !== undefined || getNode(rootNodeId)) && - whileCount < 101 - ) { - if (rootNodeId) { - rootNode = getNode(rootNodeId) - } else { - rootNode = getNode(rootNode.parent) - } - whileCount += 1 - if (whileCount > 100) { - console.log( - `It looks like you have a node that's set its parent as itself`, - rootNode - ) - } - } - - return rootNode -} - -function shouldInferFile(nodes, key, value) { - const looksLikeFile = - _.isString(value) && - mime.lookup(value) !== `application/octet-stream` && - // domains ending with .com - mime.lookup(value) !== `application/x-msdownload` && - isRelative(value) && - isRelativeUrl(value) - - if (!looksLikeFile) { - return false - } - - // Find the node used for this example. - let node = nodes.find(n => _.get(n, key) === value) - - if (!node) { - // Try another search as our "key" isn't always correct e.g. - // it doesn't support arrays so the right key could be "a.b[0].c" but - // this function will get "a.b.c". - // - // We loop through every value of nodes until we find - // a match. - const visit = (current, selector = [], fn) => { - for (let i = 0, keys = Object.keys(current); i < keys.length; i++) { - const key = keys[i] - const value = current[key] - - if (value === undefined || value === null) continue - - if (typeof value === `object` || typeof value === `function`) { - visit(current[key], selector.concat([key]), fn) - continue - } - - let proceed = fn(current[key], key, selector, current) - - if (proceed === false) { - break - } - } - } - - const isNormalInteger = str => /^\+?(0|[1-9]\d*)$/.test(str) - - node = nodes.find(n => { - let isMatch = false - visit(n, [], (v, k, selector, parent) => { - if (v === value) { - // Remove integers as they're for arrays, which our passed - // in object path doesn't have. - const normalizedSelector = selector - .map(s => (isNormalInteger(s) ? `` : s)) - .filter(s => s !== ``) - const fullSelector = `${normalizedSelector.join(`.`)}.${k}` - if (fullSelector === key) { - isMatch = true - return false - } - } - - // Not a match so we continue - return true - }) - - return isMatch - }) - - // Still no node. - if (!node) { - return false - } - } - - const rootNode = findRootNode(node) - - // Only nodes transformed (ultimately) from a File - // can link to another File. - if (rootNode.internal.type !== `File`) { - return false - } - - const pathToOtherNode = normalize(joinPath(rootNode.dir, value)) - const otherFileExists = getNodes().some( - n => n.absolutePath === pathToOtherNode - ) - return otherFileExists -} - -// Look for fields that are pointing at a file — if the field has a known -// extension then assume it should be a file field. -function inferFromUri(key, types, isArray) { - const fileField = types.find(type => type.name === `File`) - - if (!fileField) return null - - return { - type: isArray - ? new GraphQLList(fileField.nodeObjectType) - : fileField.nodeObjectType, - resolve: (node, a, { path }) => { - const fieldValue = node[key] - - if (!fieldValue) { - return null - } - - const findLinkedFileNode = relativePath => { - // Use the parent File node to create the absolute path to - // the linked file. - const fileLinkPath = normalize( - systemPath.resolve(parentFileNode.dir, relativePath) - ) - - // Use that path to find the linked File node. - const linkedFileNode = _.find( - getNodes(), - n => n.internal.type === `File` && n.absolutePath === fileLinkPath - ) - if (linkedFileNode) { - createPageDependency({ - path, - nodeId: linkedFileNode.id, - }) - return linkedFileNode - } else { - return null - } - } - - // Find the File node for this node (we assume the node is something - // like markdown which would be a child node of a File node). - const parentFileNode = findRootNode(node) - - // Find the linked File node(s) - if (isArray) { - return fieldValue.map(relativePath => findLinkedFileNode(relativePath)) - } else { - return findLinkedFileNode(fieldValue) - } - }, - } -} - type inferTypeOptions = { nodes: Object[], types: ProcessedNodeType[], @@ -584,20 +343,6 @@ export function inferObjectStructureFromNodes({ } else if (_.includes(key, `___NODE`)) { ;[fieldName] = key.split(`___`) inferredField = inferFromFieldName(value, nextSelector, types) - - // Third if the field (whether a string or array of string(s)) is - // pointing to a file (from another file). - } else if ( - nodes[0].internal.type !== `File` && - ((_.isString(value) && - !_.isEmpty(value) && - shouldInferFile(nodes, nextSelector, value)) || - (_.isArray(value) && - _.isString(value[0]) && - !_.isEmpty(value[0]) && - shouldInferFile(nodes, `${nextSelector}[0]`, value[0]))) - ) { - inferredField = inferFromUri(key, types, _.isArray(value)) } // Finally our automatic inference of field value type. @@ -606,7 +351,7 @@ export function inferObjectStructureFromNodes({ nodes, types, exampleValue: value, - selector: selector ? `${selector}.${key}` : key, + selector: nextSelector, }) } diff --git a/packages/gatsby/src/schema/run-sift.js b/packages/gatsby/src/schema/run-sift.js index d77e849f50be4..9b3119630975a 100644 --- a/packages/gatsby/src/schema/run-sift.js +++ b/packages/gatsby/src/schema/run-sift.js @@ -9,7 +9,7 @@ const Promise = require(`bluebird`) function awaitSiftField(fields, node, k) { const field = fields[k] if (field.resolve) { - return field.resolve(node) + return field.resolve(node, {}, {}, { fieldName: k }) } else if (node[k] !== undefined) { return node[k] } diff --git a/packages/gatsby/src/schema/types/type-date.js b/packages/gatsby/src/schema/types/type-date.js new file mode 100644 index 0000000000000..a38d30182fa37 --- /dev/null +++ b/packages/gatsby/src/schema/types/type-date.js @@ -0,0 +1,114 @@ +const moment = require(`moment`) +const { + GraphQLString, + GraphQLBoolean, + GraphQLScalarType, + Kind, +} = require(`graphql`) +const _ = require(`lodash`) +const { oneLine } = require(`common-tags`) + +const ISO_8601_FORMAT = [ + `YYYY`, + `YYYY-MM`, + `YYYY-MM-DD`, + `YYYYMMDD`, + `YYYY-MM-DDTHHZ`, + `YYYY-MM-DDTHH:mmZ`, + `YYYY-MM-DDTHHmmZ`, + `YYYY-MM-DDTHH:mm:ssZ`, + `YYYY-MM-DDTHHmmssZ`, + `YYYY-MM-DDTHH:mm:ss.SSSZ`, + `YYYY-MM-DDTHHmmss.SSSZ`, + `YYYY-[W]WW`, + `YYYY[W]WW`, + `YYYY-[W]WW-E`, + `YYYY[W]WWE`, + `YYYY-DDDD`, + `YYYYDDDD`, +] + +// Check if this is a date. +// All the allowed ISO 8601 date-time formats used. +export function shouldInfer(value) { + const momentDate = moment.utc(value, ISO_8601_FORMAT, true) + return momentDate.isValid() && typeof value !== `number` +} + +export const GraphQLDate = new GraphQLScalarType({ + name: `Date`, + description: oneLine` + A date string, such as 2007-12-03, compliant with the ISO 8601 standard + for representation of dates and times using the Gregorian calendar.`, + serialize: String, + parseValue: String, + parseLiteral(ast) { + return ast.kind === Kind.STRING ? ast.value : undefined + }, +}) + +const type = Object.freeze({ + type: GraphQLDate, + args: { + formatString: { + type: GraphQLString, + description: oneLine` + Format the date using Moment.js' date tokens e.g. + "date(formatString: "YYYY MMMM DD)" + See https://momentjs.com/docs/#/displaying/format/ + for documentation for different tokens`, + }, + fromNow: { + type: GraphQLBoolean, + description: oneLine` + Returns a string generated with Moment.js' fromNow function`, + }, + difference: { + type: GraphQLString, + description: oneLine` + Returns the difference between this date and the current time. + Defaults to miliseconds but you can also pass in as the + measurement years, months, weeks, days, hours, minutes, + and seconds.`, + }, + locale: { + type: GraphQLString, + description: oneLine` + Configures the locale Moment.js will use to format the date.`, + }, + }, + resolve(source, args, context, { fieldName }) { + let date + if (source[fieldName]) { + date = JSON.parse(JSON.stringify(source[fieldName])) + } else { + return null + } + + if (_.isPlainObject(args)) { + const { fromNow, difference, formatString, locale = `en` } = args + if (formatString) { + return moment + .utc(date, ISO_8601_FORMAT, true) + .locale(locale) + .format(formatString) + } else if (fromNow) { + return moment + .utc(date, ISO_8601_FORMAT, true) + .locale(locale) + .fromNow() + } else if (difference) { + return moment().diff( + moment.utc(date, ISO_8601_FORMAT, true).locale(locale), + difference + ) + } + } + + return date + }, +}) + +export function getType() { + return type +} diff --git a/packages/gatsby/src/schema/types/type-file.js b/packages/gatsby/src/schema/types/type-file.js new file mode 100644 index 0000000000000..0f84c3e288f4f --- /dev/null +++ b/packages/gatsby/src/schema/types/type-file.js @@ -0,0 +1,211 @@ +const { GraphQLList } = require(`graphql`) +const _ = require(`lodash`) +const mime = require(`mime`) +const isRelative = require(`is-relative`) +const isRelativeUrl = require(`is-relative-url`) +const normalize = require(`normalize-path`) +const systemPath = require(`path`) + +const { getNode, getNodes, getRootNodeId } = require(`../../redux`) +const { + createPageDependency, +} = require(`../../redux/actions/add-page-dependency`) +const { joinPath } = require(`../../utils/path`) + +let type, listType + +export function setFileNodeRootType(fileNodeRootType) { + if (fileNodeRootType) { + type = createType(fileNodeRootType, false) + listType = createType(fileNodeRootType, true) + } else { + type = null + listType = null + } +} + +function findRootNode(node) { + // Find the root node. + let rootNode = node + let whileCount = 0 + let rootNodeId + while ( + (rootNodeId = getRootNodeId(rootNode) || rootNode.parent) && + (getNode(rootNode.parent) !== undefined || getNode(rootNodeId)) && + whileCount < 101 + ) { + if (rootNodeId) { + rootNode = getNode(rootNodeId) + } else { + rootNode = getNode(rootNode.parent) + } + whileCount += 1 + if (whileCount > 100) { + console.log( + `It looks like you have a node that's set its parent as itself`, + rootNode + ) + } + } + + return rootNode +} + +function pointsToFile(nodes, key, value) { + const looksLikeFile = + _.isString(value) && + mime.lookup(value) !== `application/octet-stream` && + // domains ending with .com + mime.lookup(value) !== `application/x-msdownload` && + isRelative(value) && + isRelativeUrl(value) + + if (!looksLikeFile) { + return false + } + + // Find the node used for this example. + let node = nodes.find(n => _.get(n, key) === value) + + if (!node) { + // Try another search as our "key" isn't always correct e.g. + // it doesn't support arrays so the right key could be "a.b[0].c" but + // this function will get "a.b.c". + // + // We loop through every value of nodes until we find + // a match. + const visit = (current, selector = [], fn) => { + for (let i = 0, keys = Object.keys(current); i < keys.length; i++) { + const key = keys[i] + const value = current[key] + + if (value === undefined || value === null) continue + + if (typeof value === `object` || typeof value === `function`) { + visit(current[key], selector.concat([key]), fn) + continue + } + + let proceed = fn(current[key], key, selector, current) + + if (proceed === false) { + break + } + } + } + + const isNormalInteger = str => /^\+?(0|[1-9]\d*)$/.test(str) + + node = nodes.find(n => { + let isMatch = false + visit(n, [], (v, k, selector, parent) => { + if (v === value) { + // Remove integers as they're for arrays, which our passed + // in object path doesn't have. + const normalizedSelector = selector + .map(s => (isNormalInteger(s) ? `` : s)) + .filter(s => s !== ``) + const fullSelector = `${normalizedSelector.join(`.`)}.${k}` + if (fullSelector === key) { + isMatch = true + return false + } + } + + // Not a match so we continue + return true + }) + + return isMatch + }) + + // Still no node. + if (!node) { + return false + } + } + + const rootNode = findRootNode(node) + + // Only nodes transformed (ultimately) from a File + // can link to another File. + if (rootNode.internal.type !== `File`) { + return false + } + + const pathToOtherNode = normalize(joinPath(rootNode.dir, value)) + const otherFileExists = getNodes().some( + n => n.absolutePath === pathToOtherNode + ) + return otherFileExists +} + +export function shouldInfer(nodes, selector, value) { + return ( + nodes[0].internal.type !== `File` && + ((_.isString(value) && + !_.isEmpty(value) && + pointsToFile(nodes, selector, value)) || + (_.isArray(value) && + _.isString(value[0]) && + !_.isEmpty(value[0]) && + pointsToFile(nodes, `${selector}[0]`, value[0]))) + ) +} + +function createType(fileNodeRootType, isArray) { + if (!fileNodeRootType) return null + + return { + type: isArray ? new GraphQLList(fileNodeRootType) : fileNodeRootType, + resolve: (node, args, { path }, { fieldName }) => { + let fieldValue = node[fieldName] + + if (!fieldValue) { + return null + } + + const findLinkedFileNode = relativePath => { + // Use the parent File node to create the absolute path to + // the linked file. + const fileLinkPath = normalize( + systemPath.resolve(parentFileNode.dir, relativePath) + ) + + // Use that path to find the linked File node. + const linkedFileNode = _.find( + getNodes(), + n => n.internal.type === `File` && n.absolutePath === fileLinkPath + ) + if (linkedFileNode) { + createPageDependency({ + path, + nodeId: linkedFileNode.id, + }) + return linkedFileNode + } else { + return null + } + } + + // Find the File node for this node (we assume the node is something + // like markdown which would be a child node of a File node). + const parentFileNode = findRootNode(node) + + // Find the linked File node(s) + if (isArray) { + return fieldValue.map(relativePath => findLinkedFileNode(relativePath)) + } else { + return findLinkedFileNode(fieldValue) + } + }, + } +} + +export function getType() { + return type +} + +export function getListType() { + return listType +}