From ff596b972ad52705420fc5bff9721d3c59bcbbad Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Fri, 29 Mar 2019 17:01:23 +0200 Subject: [PATCH 01/32] WIP inference changes --- packages/gatsby/package.json | 2 +- .../gatsby/src/schema/__tests__/queries.js | 6 +- .../gatsby/src/schema/add-field-resolvers.js | 81 ++++++++++ .../src/schema/infer/add-inferred-fields.js | 118 ++++----------- packages/gatsby/src/schema/infer/index.js | 138 +++++++++++------- .../schema/infer/merge-inferred-composer.js | 131 +++++++++++++++++ packages/gatsby/src/schema/resolvers.js | 4 +- packages/gatsby/src/schema/schema.js | 46 ++++-- yarn.lock | 17 ++- 9 files changed, 376 insertions(+), 167 deletions(-) create mode 100644 packages/gatsby/src/schema/add-field-resolvers.js create mode 100644 packages/gatsby/src/schema/infer/merge-inferred-composer.js diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 6d8749bdb02b1..4cb22cf48f152 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -71,7 +71,7 @@ "gatsby-telemetry": "^1.0.4", "glob": "^7.1.1", "graphql": "^14.1.1", - "graphql-compose": "^6.0.3", + "graphql-compose": "^6.1.0", "graphql-playground-middleware-express": "^1.7.10", "graphql-relay": "^0.6.0", "graphql-tools": "^3.0.4", diff --git a/packages/gatsby/src/schema/__tests__/queries.js b/packages/gatsby/src/schema/__tests__/queries.js index 8ac99f452d215..3b70b8dffddf9 100644 --- a/packages/gatsby/src/schema/__tests__/queries.js +++ b/packages/gatsby/src/schema/__tests__/queries.js @@ -45,7 +45,7 @@ describe(`Query schema`, () => { } else if (api === `createResolvers`) { return [ args[0].createResolvers({ - Frontmatter: { + MarkdownFrontmatter: { authors: { resolve(source, args, context, info) { // NOTE: When using the first field resolver argument (here called @@ -122,8 +122,8 @@ describe(`Query schema`, () => { ) const typeDefs = [ - `type Markdown implements Node { frontmatter: Frontmatter! }`, - `type Frontmatter { authors: [Author] }`, + `type Markdown implements Node { frontmatter: MarkdownFrontmatter! }`, + `type MarkdownFrontmatter { authors: [Author] }`, `type Author implements Node { posts: [Markdown] }`, ] typeDefs.forEach(def => diff --git a/packages/gatsby/src/schema/add-field-resolvers.js b/packages/gatsby/src/schema/add-field-resolvers.js new file mode 100644 index 0000000000000..94739787568eb --- /dev/null +++ b/packages/gatsby/src/schema/add-field-resolvers.js @@ -0,0 +1,81 @@ +const _ = require(`lodash`) +const { defaultFieldResolver } = require(`graphql`) +const { dateResolver } = require(`./types/date`) +const { link, fileByPath } = require(`./resolvers`) + +export const addFieldResolvers = ({ + schemaComposer, + typeComposer, + parentSpan, +}) => { + typeComposer.getFieldNames().forEach(fieldName => { + let field = typeComposer.getField(fieldName) + + const extensions = typeComposer.getFieldExtensions(fieldName) + + if (!field.resolve && extensions.resolver) { + const options = extensions.resolverOptions || {} + switch (extensions.resolver) { + case `date`: { + addDateResolver({ + typeComposer, + fieldName, + options: extensions.resolverOptions || {}, + }) + break + } + case `link`: { + typeComposer.extendField(fieldName, { + resolve: link({ from: options.from, by: options.by }), + }) + break + } + case `relativeFile`: { + typeComposer.extendField(fieldName, { + resolve: fileByPath({ from: options.from }), + }) + break + } + } + } + + if (extensions.proxyFrom) { + // XXX(freiksenet): get field again cause it will be changed because of above + field = typeComposer.getField(fieldName) + const resolver = field.resolve || defaultFieldResolver + typeComposer.extendField(fieldName, { + resolve: (source, args, context, info) => + resolver(source, args, context, { + ...info, + fieldName: extensions.proxyFrom, + }), + }) + } + }) + return typeComposer +} + +const addDateResolver = ({ + typeComposer, + fieldName, + options: { defaultFormat, defaultLocale }, +}) => { + const field = typeComposer.getField(fieldName) + + let options = { + resolve: dateResolver.resolve, + } + if (!field.args || _.isEmpty(field.args)) { + options.args = { + ...dateResolver.args, + } + if (defaultFormat) { + options.args.formatString.defaultValue = defaultFormat + } + if (defaultLocale) { + options.args.formatString.defaultLocale = defaultLocale + } + } + + typeComposer.extendField(fieldName, options) +} diff --git a/packages/gatsby/src/schema/infer/add-inferred-fields.js b/packages/gatsby/src/schema/infer/add-inferred-fields.js index b27c53aacbb77..5be25ed1d7d8d 100644 --- a/packages/gatsby/src/schema/infer/add-inferred-fields.js +++ b/packages/gatsby/src/schema/infer/add-inferred-fields.js @@ -1,17 +1,12 @@ const _ = require(`lodash`) -const { - defaultFieldResolver, - getNamedType, - GraphQLObjectType, - GraphQLList, -} = require(`graphql`) -const { ObjectTypeComposer } = require(`graphql-compose`) +const { getNamedType, GraphQLObjectType } = require(`graphql`) +const { ObjectTypeComposer, UnionTypeComposer } = require(`graphql-compose`) const invariant = require(`invariant`) const report = require(`gatsby-cli/lib/reporter`) const { isFile } = require(`./is-file`) -const { link, fileByPath } = require(`../resolvers`) -const { isDate, dateResolver } = require(`../types/date`) +const { link } = require(`../resolvers`) +const { isDate } = require(`../types/date`) const is32BitInteger = require(`./is-32-bit-integer`) const addInferredFields = ({ @@ -19,7 +14,6 @@ const addInferredFields = ({ typeComposer, exampleValue, nodeStore, - inferConfig, typeMapping, parentSpan, }) => { @@ -30,8 +24,6 @@ const addInferredFields = ({ exampleObject: exampleValue, prefix: typeComposer.getTypeName(), typeMapping, - addNewFields: inferConfig ? inferConfig.infer : true, - addDefaultResolvers: inferConfig ? inferConfig.addDefaultResolvers : true, }) } @@ -46,8 +38,6 @@ const addInferredFieldsImpl = ({ exampleObject, typeMapping, prefix, - addNewFields, - addDefaultResolvers, }) => { const fields = [] Object.keys(exampleObject).forEach(unsanitizedKey => { @@ -61,8 +51,6 @@ const addInferredFieldsImpl = ({ exampleValue, unsanitizedKey, typeMapping, - addNewFields, - addDefaultResolvers, }) ) }) @@ -89,61 +77,13 @@ const addInferredFieldsImpl = ({ fieldConfig = possibleFields[0].fieldConfig } - let arrays = 0 let namedInferredType = fieldConfig.type while (Array.isArray(namedInferredType)) { namedInferredType = namedInferredType[0] - arrays++ } - if (typeComposer.hasField(key)) { - const fieldType = typeComposer.getFieldType(key) - - let lists = 0 - let namedFieldType = fieldType - while (namedFieldType.ofType) { - if (namedFieldType instanceof GraphQLList) { - lists++ - } - namedFieldType = namedFieldType.ofType - } - - const namedInferredTypeName = - typeof namedInferredType === `string` - ? namedInferredType - : namedInferredType.getTypeName() - - if (arrays === lists && namedFieldType.name === namedInferredTypeName) { - if ( - namedFieldType instanceof GraphQLObjectType && - namedInferredType instanceof ObjectTypeComposer - ) { - const fieldTypeComposer = typeComposer.getFieldTC(key) - const inferredFields = namedInferredType.getFields() - fieldTypeComposer.addFields(inferredFields) - } - if (addDefaultResolvers) { - let field = typeComposer.getField(key) - if (!field.type) { - field = { - type: field, - } - } - if (_.isEmpty(field.args) && fieldConfig.args) { - field.args = fieldConfig.args - } - if (!field.resolve && fieldConfig.resolve) { - field.resolve = fieldConfig.resolve - } - typeComposer.setField(key, field) - } - } - } else if (addNewFields) { - if (namedInferredType instanceof ObjectTypeComposer) { - schemaComposer.add(namedInferredType) - } - typeComposer.setField(key, fieldConfig) - } + typeComposer.addFields({ [key]: fieldConfig }) + typeComposer.setFieldExtension(key, `createdFrom`, `infer`) }) return typeComposer @@ -157,7 +97,6 @@ const getFieldConfig = ({ exampleValue, unsanitizedKey, typeMapping, - addNewFields, addDefaultResolvers, }) => { let key = createFieldName(unsanitizedKey) @@ -192,21 +131,18 @@ const getFieldConfig = ({ value, selector, typeMapping, - addNewFields, addDefaultResolvers, }) } // Proxy resolver to unsanitized fieldName in case it contained invalid characters if (key !== unsanitizedKey) { - const resolver = fieldConfig.resolve || defaultFieldResolver fieldConfig = { ...fieldConfig, - resolve: (source, args, context, info) => - resolver(source, args, context, { - ...info, - fieldName: unsanitizedKey, - }), + extensions: { + ...(fieldConfig.extensions || {}), + proxyFrom: unsanitizedKey, + }, } } @@ -293,18 +229,24 @@ const getFieldConfigFromFieldNameConvention = ({ // (ii) hinders reusing types. if (linkedTypes.length > 1) { const typeName = linkedTypes.sort().join(``) + `Union` - type = schemaComposer.getOrCreateUTC(typeName, utc => { - const types = linkedTypes.map(typeName => - schemaComposer.getOrCreateOTC(typeName) - ) - utc.setTypes(types) - utc.setResolveType(node => node.internal.type) + type = UnionTypeComposer.createTemp({ + name: typeName, + types: () => linkedTypes.map(typeName => schemaComposer.getOTC(typeName)), }) + type.setResolveType(node => node.internal.type) } else { type = linkedTypes[0] } - return { type, resolve: link({ by: foreignKey || `id` }) } + return { + type, + extensions: { + resolver: `link`, + resolverOptions: { + by: foreignKey || `id`, + }, + }, + } } const getSimpleFieldConfig = ({ @@ -315,8 +257,6 @@ const getSimpleFieldConfig = ({ value, selector, typeMapping, - addNewFields, - addDefaultResolvers, }) => { switch (typeof value) { case `boolean`: @@ -325,21 +265,19 @@ const getSimpleFieldConfig = ({ return { type: is32BitInteger(value) ? `Int` : `Float` } case `string`: if (isDate(value)) { - return dateResolver + return { type: `Date`, extensions: { resolver: `date` } } } - // FIXME: The weird thing is that we are trying to infer a File, - // but cannot assume that a source plugin for File nodes is actually present. - if (schemaComposer.has(`File`) && isFile(nodeStore, selector, value)) { + if (isFile(nodeStore, selector, value)) { // NOTE: For arrays of files, where not every path references // a File node in the db, it is semi-random if the field is // inferred as File or String, since the exampleValue only has // the first entry (which could point to an existing file or not). - return { type: `File`, resolve: fileByPath } + return { type: `File`, extensions: { resolver: `relativeFile` } } } return { type: `String` } case `object`: if (value instanceof Date) { - return dateResolver + return { type: `Date`, extensions: { resolver: `date` } } } if (value instanceof String) { return { type: `String` } @@ -375,8 +313,6 @@ const getSimpleFieldConfig = ({ exampleObject: value, typeMapping, prefix: selector, - addNewFields, - addDefaultResolvers, }), } } diff --git a/packages/gatsby/src/schema/infer/index.js b/packages/gatsby/src/schema/infer/index.js index 85c5249b77e4d..20ca0664e5f1a 100644 --- a/packages/gatsby/src/schema/infer/index.js +++ b/packages/gatsby/src/schema/infer/index.js @@ -1,50 +1,14 @@ const report = require(`gatsby-cli/lib/reporter`) +const { ObjectTypeComposer } = require(`graphql-compose`) const { getExampleValue } = require(`./example-value`) const { addNodeInterface, getNodeInterface, } = require(`../types/node-interface`) const { addInferredFields } = require(`./add-inferred-fields`) +const { mergeInferredComposer } = require(`./merge-inferred-composer`) const getInferConfig = require(`./get-infer-config`) -const addInferredType = ({ - schemaComposer, - typeComposer, - nodeStore, - typeConflictReporter, - typeMapping, - parentSpan, -}) => { - const typeName = typeComposer.getTypeName() - const nodes = nodeStore.getNodesByType(typeName) - if ( - !typeComposer.hasExtension(`plugin`) && - typeComposer.getExtension(`createdFrom`) === `infer` - ) { - typeComposer.setExtension(`plugin`, nodes[0].internal.owner) - } - const exampleValue = getExampleValue({ - nodes, - typeName, - typeConflictReporter, - ignoreFields: [ - ...getNodeInterface({ schemaComposer }).getFieldNames(), - `$loki`, - ], - }) - - addInferredFields({ - schemaComposer, - typeComposer, - nodeStore, - exampleValue, - inferConfig: getInferConfig(typeComposer), - typeMapping, - parentSpan, - }) - return typeComposer -} - const addInferredTypes = ({ schemaComposer, nodeStore, @@ -57,6 +21,8 @@ const addInferredTypes = ({ const typeNames = putFileFirst(nodeStore.getTypes()) const noNodeInterfaceTypes = [] + const nodeTypesToInfer = [] + typeNames.forEach(typeName => { let typeComposer let inferConfig @@ -67,26 +33,13 @@ const addInferredTypes = ({ if (!typeComposer.hasInterface(`Node`)) { noNodeInterfaceTypes.push(typeComposer.getType()) } + nodeTypesToInfer.push(typeName) } } else { - typeComposer = schemaComposer.createObjectTC(typeName) - typeComposer.setExtension(`createdFrom`, `infer`) - addNodeInterface({ schemaComposer, typeComposer }) + nodeTypesToInfer.push(typeName) } }) - // XXX(freiksenet): We iterate twice to pre-create all types - const typeComposers = typeNames.map(typeName => - addInferredType({ - schemaComposer, - nodeStore, - typeConflictReporter, - typeComposer: schemaComposer.getOTC(typeName), - typeMapping, - parentSpan, - }) - ) - if (noNodeInterfaceTypes.length > 0) { noNodeInterfaceTypes.forEach(type => { report.warn( @@ -103,9 +56,88 @@ const addInferredTypes = ({ report.panic(`Building schema failed`) } + nodeTypesToInfer.forEach(typeName => { + schemaComposer.getOrCreateOTC(typeName) + }) + + const typeComposers = nodeTypesToInfer.map(typeName => + addInferredType({ + schemaComposer, + typeName, + nodeStore, + typeConflictReporter, + typeMapping, + parentSpan, + }) + ) + return typeComposers } +const addInferredType = ({ + schemaComposer, + typeName, + nodeStore, + typeConflictReporter, + typeMapping, + parentSpan, +}) => { + const inferredComposer = ObjectTypeComposer.createTemp( + typeName, + schemaComposer + ) + inferType({ + schemaComposer, + nodeStore, + typeConflictReporter, + typeComposer: inferredComposer, + typeMapping, + parentSpan, + }) + const definedComposer = schemaComposer.getOrCreateOTC(typeName) + addNodeInterface({ schemaComposer, typeComposer: definedComposer }) + mergeInferredComposer({ + schemaComposer, + definedComposer, + inferredComposer, + }) + return definedComposer +} + +const inferType = ({ + schemaComposer, + typeComposer, + nodeStore, + typeConflictReporter, + typeMapping, + parentSpan, +}) => { + const typeName = typeComposer.getTypeName() + const nodes = nodeStore.getNodesByType(typeName) + typeComposer.setExtension(`createdFrom`, `infer`) + typeComposer.setExtension(`plugin`, nodes[0].internal.owner) + + const exampleValue = getExampleValue({ + nodes, + typeName, + typeConflictReporter, + ignoreFields: [ + ...getNodeInterface({ schemaComposer }).getFieldNames(), + `$loki`, + ], + }) + + addInferredFields({ + schemaComposer, + typeComposer, + nodeStore, + exampleValue, + typeMapping, + parentSpan, + }) + return typeComposer +} + const putFileFirst = typeNames => { const index = typeNames.indexOf(`File`) if (index !== -1) { diff --git a/packages/gatsby/src/schema/infer/merge-inferred-composer.js b/packages/gatsby/src/schema/infer/merge-inferred-composer.js new file mode 100644 index 0000000000000..9235808b9bbcd --- /dev/null +++ b/packages/gatsby/src/schema/infer/merge-inferred-composer.js @@ -0,0 +1,131 @@ +const _ = require(`lodash`) +const { GraphQLList, GraphQLNonNull } = require(`graphql`) +const { UnionTypeComposer, ObjectTypeComposer } = require(`graphql-compose`) + +export const mergeInferredComposer = ({ + schemaComposer, + definedComposer, + inferredComposer, +}) => { + const addDefaultResolvers = + definedComposer.getExtension(`addDefaultResolvers`) || true + + inferredComposer.getFieldNames().forEach(fieldName => { + const inferredField = inferredComposer.getField(fieldName) + if (definedComposer.hasField(fieldName)) { + maybeExtendDefinedField({ + schemaComposer, + definedComposer, + inferredComposer, + fieldName, + addDefaultResolvers, + }) + } else { + definedComposer.addFields({ [fieldName]: inferredField }) + } + }) + + if (!definedComposer.hasExtension(`createdFrom`)) { + definedComposer.setExtension( + `createdFrom`, + inferredComposer.getExtension(`createFrom`) + ) + definedComposer.setExtension( + `plugin`, + inferredComposer.getExtension(`plugin`) + ) + } + + return definedComposer +} + +const maybeExtendDefinedField = ({ + schemaComposer, + definedComposer, + inferredComposer, + fieldName, + addDefaultResolvers, +}) => { + const inferredField = inferredComposer.getField(fieldName) + + let inferredFieldComposer + try { + inferredFieldComposer = inferredComposer.getFieldTC(fieldName) + } catch (e) { + inferredFieldComposer = null + } + const definedField = definedComposer.getField(fieldName) + if ( + typesLooselyEqual( + inferredComposer.getFieldType(fieldName), + definedComposer.getFieldType(fieldName) + ) + ) { + const extensions = definedComposer.getFieldExtensions(fieldName) + if (addDefaultResolvers && !definedField.resolver && !extensions.resolver) { + const config = {} + if ( + (!definedField.args || _.isEmpty(definedField.args)) && + inferredField.args + ) { + config.args = inferredField.args + } + + if (inferredField.resolve) { + config.resolve = inferredField.resolve + } + + definedComposer.extendField(fieldName, config) + if (inferredField.extensions.resolver) { + definedComposer.setFieldExtension( + fieldName, + `resolver`, + inferredField.extensions.resolver + ) + definedComposer.setFieldExtension( + fieldName, + `resolverOptions`, + inferredField.extensions.resolverOptions + ) + } + } + + if (inferredFieldComposer instanceof UnionTypeComposer) { + if (!schemaComposer.has(inferredFieldComposer.getTypeName())) { + schemaComposer.addAsComposer(inferredFieldComposer) + } + } else if ( + inferredFieldComposer instanceof ObjectTypeComposer && + !inferredFieldComposer.hasInterface(`Node`) + ) { + const definedFieldComposer = schemaComposer.getOrCreateOTC( + inferredFieldComposer.getTypeName() + ) + if ( + !definedFieldComposer.hasExtension(`infer`) || + definedFieldComposer.getExtension(`infer`) + ) { + schemaComposer.set( + definedFieldComposer.getTypeName(), + mergeInferredComposer({ + schemaComposer, + definedComposer: definedFieldComposer, + inferredComposer: inferredFieldComposer, + }) + ) + } + } + } +} + +const typesLooselyEqual = (left, right) => { + if (left instanceof GraphQLList && right instanceof GraphQLList) { + return typesLooselyEqual(left.ofType, right.ofType) + } else if (left instanceof GraphQLNonNull) { + return typesLooselyEqual(left.ofType, right) + } else if (right instanceof GraphQLNonNull) { + return typesLooselyEqual(left, right.ofType) + } else { + return left.name === right.name + } +} diff --git a/packages/gatsby/src/schema/resolvers.js b/packages/gatsby/src/schema/resolvers.js index 75c440f7a1af2..04fc4e7b83a0e 100644 --- a/packages/gatsby/src/schema/resolvers.js +++ b/packages/gatsby/src/schema/resolvers.js @@ -140,8 +140,8 @@ const link = ({ by, from }) => async (source, args, context, info) => { ) } -const fileByPath = (source, args, context, info) => { - const fieldValue = source && source[info.fieldName] +const fileByPath = ({ from }) => (source, args, context, info) => { + const fieldValue = source && source[from || info.fieldName] if (fieldValue == null || _.isPlainObject(fieldValue)) return fieldValue if ( diff --git a/packages/gatsby/src/schema/schema.js b/packages/gatsby/src/schema/schema.js index a482a422d0908..f01542d2542b0 100644 --- a/packages/gatsby/src/schema/schema.js +++ b/packages/gatsby/src/schema/schema.js @@ -19,6 +19,7 @@ const report = require(`gatsby-cli/lib/reporter`) const { addNodeInterfaceFields } = require(`./types/node-interface`) const { addInferredType, addInferredTypes } = require(`./infer`) const { findOne, findManyPaginated } = require(`./resolvers`) +const { addFieldResolvers } = require(`./add-field-resolvers`) const { getPagination } = require(`./types/pagination`) const { getSortInput } = require(`./types/sort`) const { getFilterInput } = require(`./types/filter`) @@ -57,7 +58,7 @@ const rebuildSchemaWithSitePage = async ({ }) => { const typeComposer = addInferredType({ schemaComposer, - typeComposer: schemaComposer.getOTC(`SitePage`), + typeName: `SitePage`, nodeStore, typeConflictReporter, typeMapping, @@ -123,6 +124,7 @@ const processTypeComposer = async ({ typeComposer instanceof ObjectTypeComposer && typeComposer.hasInterface(`Node`) ) { + await addFieldResolvers({ schemaComposer, typeComposer, parentSpan }) await addNodeInterfaceFields({ schemaComposer, typeComposer, parentSpan }) await addResolvers({ schemaComposer, typeComposer, parentSpan }) await addConvenienceChildrenFields({ @@ -132,6 +134,8 @@ const processTypeComposer = async ({ parentSpan, }) await addTypeToRootQuery({ schemaComposer, typeComposer, parentSpan }) + } else if (typeComposer instanceof ObjectTypeComposer) { + await addFieldResolvers({ schemaComposer, typeComposer, parentSpan }) } } @@ -209,21 +213,45 @@ const processAddedType = ({ type.astNode.directives.forEach(directive => { if (directive.name.value === `infer`) { typeComposer.setExtension(`infer`, true) - typeComposer.setExtension( - `addDefaultResolvers`, - getNoDefaultResolvers(directive) - ) + const addDefaultResolvers = getNoDefaultResolvers(directive) + if (addDefaultResolvers) { + typeComposer.setExtension( + `addDefaultResolvers`, + addDefaultResolvers + ) + } } else if (directive.name.value === `dontInfer`) { typeComposer.setExtension(`infer`, false) - typeComposer.setExtension( - `addDefaultResolvers`, - getNoDefaultResolvers(directive) - ) + const addDefaultResolvers = getNoDefaultResolvers(directive) + if (addDefaultResolvers) { + typeComposer.setExtension( + `addDefaultResolvers`, + addDefaultResolvers + ) + } } }) } } + // XXX(freiksenet): This currently forces types too early + // if (typeComposer.getFieldNames) { + // typeComposer.getFieldNames().forEach(fieldName => { + // typeComposer.setFieldExtension(fieldName, `createdFrom`, createdFrom) + // typeComposer.setFieldExtension( + // fieldName, + // `plugin`, + // plugin ? plugin.name : null + // ) + // }) + // } + + if (typeComposer.hasExtension(`addDefaultResolvers`)) { + report.warn( + `Default resolve behaviour is deprecated. In future, only fields with explicit resolver directives/extensions (date, link) will get arguments and resolvers. "noDefaultResolvers" argument will be removed from the directive.` + ) + } + return typeComposer } diff --git a/yarn.lock b/yarn.lock index c6d46cebb6fe4..ff3a8d71d9053 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9002,12 +9002,12 @@ graceful-fs@^4.1.15, graceful-fs@^4.1.9: version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" -graphql-compose@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/graphql-compose/-/graphql-compose-6.0.3.tgz#fa5668a30694abef4166703aa03af07a741039a8" - integrity sha512-QpywEtNvlEQS0a5VIseMA/tk67QmEN9NNUx1B1tzGR/p7MePyus9wvci2cIP/mwdDrvLRRbwpmidSKQXFD3SEA== +graphql-compose@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/graphql-compose/-/graphql-compose-6.1.0.tgz#f9140a7d5430d763a3743de2ef3f5763f7cdafbb" + integrity sha512-SHyRuamg3BD6E9Gg4uemWrywJuzEsgggNcFFAVM6N7Y7dLiqtkS5s6rEf37hqYM0lpGikc8XyzMt6cHscXAQgA== dependencies: - graphql-type-json "^0.2.1" + graphql-type-json "^0.2.2" object-path "^0.11.4" graphql-config@^2.0.1: @@ -9059,9 +9059,10 @@ graphql-tools@^3.0.4: iterall "^1.1.3" uuid "^3.1.0" -graphql-type-json@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/graphql-type-json/-/graphql-type-json-0.2.1.tgz#d2c177e2f1b17d87f81072cd05311c0754baa420" +graphql-type-json@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/graphql-type-json/-/graphql-type-json-0.2.2.tgz#d4d3808fbf2ead9b6184fd338fe23794cd9715be" + integrity sha512-srKbRJWxvZ8J6b7P3F0PrOtKgWg3pxlUPb1xbSIB+aMdK+UPKpp4aDzPV1A+IUTlea6lk9FWwI08UXQApC03lw== graphql@^14.1.1: version "14.1.1" From a88165cec2ffa906a26bf08530f0f581aef7db8c Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Tue, 2 Apr 2019 14:51:27 +0300 Subject: [PATCH 02/32] Fixed all inferrence issues --- packages/gatsby/package.json | 2 +- .../src/schema/infer/get-infer-config.js | 27 ------ packages/gatsby/src/schema/infer/index.js | 10 +- .../schema/infer/merge-inferred-composer.js | 96 ++++++++++++------- packages/gatsby/src/schema/schema.js | 4 +- yarn.lock | 8 +- 6 files changed, 76 insertions(+), 71 deletions(-) delete mode 100644 packages/gatsby/src/schema/infer/get-infer-config.js diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 4cb22cf48f152..9514a9b0f65be 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -71,7 +71,7 @@ "gatsby-telemetry": "^1.0.4", "glob": "^7.1.1", "graphql": "^14.1.1", - "graphql-compose": "^6.1.0", + "graphql-compose": "^6.1.1", "graphql-playground-middleware-express": "^1.7.10", "graphql-relay": "^0.6.0", "graphql-tools": "^3.0.4", diff --git a/packages/gatsby/src/schema/infer/get-infer-config.js b/packages/gatsby/src/schema/infer/get-infer-config.js deleted file mode 100644 index 1591be4461044..0000000000000 --- a/packages/gatsby/src/schema/infer/get-infer-config.js +++ /dev/null @@ -1,27 +0,0 @@ -// @flow - -export interface InferConfig { - infer: boolean; - addDefaultResolvers: boolean; -} - -const DEFAULT_INFER_CONFIG: InferConfig = { - infer: true, - addDefaultResolvers: true, -} - -// Get inferance config from type directives -const getInferConfig: ( - typeComposer: TypeComposer -) => InferConfig = typeComposer => { - return { - infer: typeComposer.hasExtension(`infer`) - ? typeComposer.getExtension(`infer`) - : DEFAULT_INFER_CONFIG.infer, - addDefaultResolvers: typeComposer.hasExtension(`addDefaultResolvers`) - ? typeComposer.getExtension(`addDefaultResolvers`) - : DEFAULT_INFER_CONFIG.addDefaultResolvers, - } -} - -module.exports = getInferConfig diff --git a/packages/gatsby/src/schema/infer/index.js b/packages/gatsby/src/schema/infer/index.js index 20ca0664e5f1a..3dd52ca071a4e 100644 --- a/packages/gatsby/src/schema/infer/index.js +++ b/packages/gatsby/src/schema/infer/index.js @@ -7,7 +7,6 @@ const { } = require(`../types/node-interface`) const { addInferredFields } = require(`./add-inferred-fields`) const { mergeInferredComposer } = require(`./merge-inferred-composer`) -const getInferConfig = require(`./get-infer-config`) const addInferredTypes = ({ schemaComposer, @@ -25,11 +24,14 @@ const addInferredTypes = ({ typeNames.forEach(typeName => { let typeComposer - let inferConfig if (schemaComposer.has(typeName)) { typeComposer = schemaComposer.getOTC(typeName) - inferConfig = getInferConfig(typeComposer) - if (inferConfig.infer) { + // Infer we have enable infer or if it's "@dontInfer" but we have "addDefaultResolvers: false" + const runInfer = typeComposer.hasExtension(`infer`) + ? typeComposer.getExtension(`infer`) || + typeComposer.getExtension(`addDefaultResolvers`) + : true + if (runInfer) { if (!typeComposer.hasInterface(`Node`)) { noNodeInterfaceTypes.push(typeComposer.getType()) } diff --git a/packages/gatsby/src/schema/infer/merge-inferred-composer.js b/packages/gatsby/src/schema/infer/merge-inferred-composer.js index 9235808b9bbcd..db3c7373deab6 100644 --- a/packages/gatsby/src/schema/infer/merge-inferred-composer.js +++ b/packages/gatsby/src/schema/infer/merge-inferred-composer.js @@ -1,14 +1,27 @@ const _ = require(`lodash`) -const { GraphQLList, GraphQLNonNull } = require(`graphql`) -const { UnionTypeComposer, ObjectTypeComposer } = require(`graphql-compose`) +const { GraphQLList, GraphQLNonNull, GraphQLObjectType } = require(`graphql`) +const { ObjectTypeComposer } = require(`graphql-compose`) export const mergeInferredComposer = ({ schemaComposer, definedComposer, inferredComposer, + dontAddFields, + addDefaultResolvers, }) => { - const addDefaultResolvers = - definedComposer.getExtension(`addDefaultResolvers`) || true + if (dontAddFields == null && definedComposer.hasExtension(`infer`)) { + const infer = definedComposer.getExtension(`infer`) + dontAddFields = !infer ? true : dontAddFields + } + + if ( + addDefaultResolvers == null && + definedComposer.hasExtension(`addDefaultResolvers`) + ) { + addDefaultResolvers = definedComposer.getExtension(`addDefaultResolvers`) + } else { + addDefaultResolvers = true + } inferredComposer.getFieldNames().forEach(fieldName => { const inferredField = inferredComposer.getField(fieldName) @@ -18,9 +31,21 @@ export const mergeInferredComposer = ({ definedComposer, inferredComposer, fieldName, + dontAddFields, addDefaultResolvers, }) - } else { + } else if (!dontAddFields) { + const inferredFieldComposer = inferredComposer.getFieldTC(fieldName) + const typeName = inferredFieldComposer.getTypeName() + if (schemaComposer.has(typeName)) { + const wrappedType = mapType( + inferredComposer.getFieldType(fieldName), + () => schemaComposer.get(typeName).getType() + ) + inferredField.type = wrappedType + } else { + schemaComposer.addAsComposer(inferredFieldComposer) + } definedComposer.addFields({ [fieldName]: inferredField }) } }) @@ -44,16 +69,14 @@ const maybeExtendDefinedField = ({ definedComposer, inferredComposer, fieldName, + dontAddFields, addDefaultResolvers, }) => { const inferredField = inferredComposer.getField(fieldName) - let inferredFieldComposer - try { - inferredFieldComposer = inferredComposer.getFieldTC(fieldName) - } catch (e) { - inferredFieldComposer = null - } + const definedFieldComposer = definedComposer.getFieldTC(fieldName) + const inferredFieldComposer = inferredComposer.getFieldTC(fieldName) + const definedField = definedComposer.getField(fieldName) if ( typesLooselyEqual( @@ -90,30 +113,24 @@ const maybeExtendDefinedField = ({ } } - if (inferredFieldComposer instanceof UnionTypeComposer) { - if (!schemaComposer.has(inferredFieldComposer.getTypeName())) { - schemaComposer.addAsComposer(inferredFieldComposer) - } - } else if ( + if ( inferredFieldComposer instanceof ObjectTypeComposer && - !inferredFieldComposer.hasInterface(`Node`) + !inferredFieldComposer.hasInterface(`Node`) && + definedFieldComposer instanceof ObjectTypeComposer && + !definedFieldComposer.hasInterface(`Node`) && + (!definedFieldComposer.hasExtension(`infer`) || + definedFieldComposer.getExtension(`infer`)) ) { - const definedFieldComposer = schemaComposer.getOrCreateOTC( - inferredFieldComposer.getTypeName() + schemaComposer.set( + definedFieldComposer.getTypeName(), + mergeInferredComposer({ + schemaComposer, + definedComposer: definedFieldComposer, + inferredComposer: inferredFieldComposer, + dontAddFields, + addDefaultResolvers, + }) ) - if ( - !definedFieldComposer.hasExtension(`infer`) || - definedFieldComposer.getExtension(`infer`) - ) { - schemaComposer.set( - definedFieldComposer.getTypeName(), - mergeInferredComposer({ - schemaComposer, - definedComposer: definedFieldComposer, - inferredComposer: inferredFieldComposer, - }) - ) - } } } } @@ -126,6 +143,19 @@ const typesLooselyEqual = (left, right) => { } else if (right instanceof GraphQLNonNull) { return typesLooselyEqual(left, right.ofType) } else { - return left.name === right.name + return ( + left.name === right.name || + (left instanceof GraphQLObjectType && right instanceof GraphQLObjectType) + ) + } +} + +const mapType = (type, fn) => { + if (type instanceof GraphQLList) { + return new GraphQLList(mapType(type.ofType, fn)) + } else if (type instanceof GraphQLNonNull) { + return new GraphQLNonNull(mapType(type.ofType, fn)) + } else { + return fn(type) } } diff --git a/packages/gatsby/src/schema/schema.js b/packages/gatsby/src/schema/schema.js index f01542d2542b0..8d18814024d9c 100644 --- a/packages/gatsby/src/schema/schema.js +++ b/packages/gatsby/src/schema/schema.js @@ -214,7 +214,7 @@ const processAddedType = ({ if (directive.name.value === `infer`) { typeComposer.setExtension(`infer`, true) const addDefaultResolvers = getNoDefaultResolvers(directive) - if (addDefaultResolvers) { + if (addDefaultResolvers != null) { typeComposer.setExtension( `addDefaultResolvers`, addDefaultResolvers @@ -223,7 +223,7 @@ const processAddedType = ({ } else if (directive.name.value === `dontInfer`) { typeComposer.setExtension(`infer`, false) const addDefaultResolvers = getNoDefaultResolvers(directive) - if (addDefaultResolvers) { + if (addDefaultResolvers != null) { typeComposer.setExtension( `addDefaultResolvers`, addDefaultResolvers diff --git a/yarn.lock b/yarn.lock index ff3a8d71d9053..3a9ed5bc5c65e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9002,10 +9002,10 @@ graceful-fs@^4.1.15, graceful-fs@^4.1.9: version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" -graphql-compose@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/graphql-compose/-/graphql-compose-6.1.0.tgz#f9140a7d5430d763a3743de2ef3f5763f7cdafbb" - integrity sha512-SHyRuamg3BD6E9Gg4uemWrywJuzEsgggNcFFAVM6N7Y7dLiqtkS5s6rEf37hqYM0lpGikc8XyzMt6cHscXAQgA== +graphql-compose@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/graphql-compose/-/graphql-compose-6.1.1.tgz#3764de5b2c48fbbd83d7b43809b76deac53734c2" + integrity sha512-54ZRGrbuCsCZw9NCe4k1IScVV0p1c7lkDXXBca0e1KVo80e8LT5+T4fUHaG6R0uXEcMlSijKZmRrbcj+eITNkg== dependencies: graphql-type-json "^0.2.2" object-path "^0.11.4" From 494f2e5e426da9d34c3bab6d6a3313cc8ecafd81 Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Tue, 2 Apr 2019 15:28:48 +0300 Subject: [PATCH 03/32] Reorder adding fields so field name resolving happens first --- .../src/schema/infer/add-inferred-fields.js | 38 ++++++++++--------- .../gatsby/src/schema/types/directives.js | 2 +- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/gatsby/src/schema/infer/add-inferred-fields.js b/packages/gatsby/src/schema/infer/add-inferred-fields.js index 5be25ed1d7d8d..7319c5d296d4d 100644 --- a/packages/gatsby/src/schema/infer/add-inferred-fields.js +++ b/packages/gatsby/src/schema/infer/add-inferred-fields.js @@ -41,25 +41,19 @@ const addInferredFieldsImpl = ({ }) => { const fields = [] Object.keys(exampleObject).forEach(unsanitizedKey => { - const exampleValue = exampleObject[unsanitizedKey] - fields.push( - getFieldConfig({ - schemaComposer, - typeComposer, - nodeStore, - prefix, - exampleValue, - unsanitizedKey, - typeMapping, - }) - ) + let key = createFieldName(unsanitizedKey) + fields.push({ + key, + unsanitizedKey, + exampleValue: exampleObject[unsanitizedKey], + }) }) const fieldsByKey = _.groupBy(fields, field => field.key) Object.keys(fieldsByKey).forEach(key => { const possibleFields = fieldsByKey[key] - let fieldConfig + let selectedField if (possibleFields.length > 1) { const field = resolveMultipleFields(possibleFields) const possibleFieldsNames = possibleFields @@ -72,11 +66,21 @@ const addInferredFieldsImpl = ({ field.unsanitizedKey }\`.` ) - fieldConfig = field.fieldConfig + selectedField = field } else { - fieldConfig = possibleFields[0].fieldConfig + selectedField = possibleFields[0] } + let fieldConfig + ;({ key, fieldConfig } = getFieldConfig({ + ...selectedField, + schemaComposer, + typeComposer, + nodeStore, + prefix, + typeMapping, + })) + let namedInferredType = fieldConfig.type while (Array.isArray(namedInferredType)) { namedInferredType = namedInferredType[0] @@ -95,11 +99,10 @@ const getFieldConfig = ({ nodeStore, prefix, exampleValue, + key, unsanitizedKey, typeMapping, - addDefaultResolvers, }) => { - let key = createFieldName(unsanitizedKey) const selector = `${prefix}.${key}` let arrays = 0 @@ -131,7 +134,6 @@ const getFieldConfig = ({ value, selector, typeMapping, - addDefaultResolvers, }) } diff --git a/packages/gatsby/src/schema/types/directives.js b/packages/gatsby/src/schema/types/directives.js index 55894a2996677..a15f04464f26f 100644 --- a/packages/gatsby/src/schema/types/directives.js +++ b/packages/gatsby/src/schema/types/directives.js @@ -26,7 +26,7 @@ const DontInferDirective = new GraphQLDirective({ args: { noDefaultResolvers: { type: new GraphQLNonNull(GraphQLBoolean), - default: false, + default: true, description: `Don't add default resolvers to defined fields.`, deprecationReason: `noDefaultResolvers is deprecated, annotate individual fields.`, }, From b1fff33aea8b211c66b40ac8ecafc43d3bfa44b2 Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Tue, 2 Apr 2019 16:34:02 +0300 Subject: [PATCH 04/32] Warnings and addResolver directives --- .../gatsby/src/schema/add-field-resolvers.js | 23 +++++++++-------- .../src/schema/infer/add-inferred-fields.js | 17 ++++++++----- .../schema/infer/merge-inferred-composer.js | 15 ++++++----- packages/gatsby/src/schema/schema-composer.js | 10 ++++++-- packages/gatsby/src/schema/schema.js | 14 +---------- .../gatsby/src/schema/types/directives.js | 25 ++++++++++++++++--- 6 files changed, 61 insertions(+), 43 deletions(-) diff --git a/packages/gatsby/src/schema/add-field-resolvers.js b/packages/gatsby/src/schema/add-field-resolvers.js index 94739787568eb..0b04dedb44da1 100644 --- a/packages/gatsby/src/schema/add-field-resolvers.js +++ b/packages/gatsby/src/schema/add-field-resolvers.js @@ -12,15 +12,18 @@ export const addFieldResolvers = ({ let field = typeComposer.getField(fieldName) const extensions = typeComposer.getFieldExtensions(fieldName) - - if (!field.resolve && extensions.resolver) { - const options = extensions.resolverOptions || {} - switch (extensions.resolver) { + if ( + !field.resolve && + extensions.addResolver && + _.isObject(extensions.addResolver) + ) { + const options = extensions.addResolver.options || {} + switch (extensions.addResolver.type) { case `date`: { addDateResolver({ typeComposer, fieldName, - options: extensions.resolverOptions || {}, + options, }) break } @@ -62,20 +65,20 @@ const addDateResolver = ({ }) => { const field = typeComposer.getField(fieldName) - let options = { + let fieldConfig = { resolve: dateResolver.resolve, } if (!field.args || _.isEmpty(field.args)) { - options.args = { + fieldConfig.args = { ...dateResolver.args, } if (defaultFormat) { - options.args.formatString.defaultValue = defaultFormat + fieldConfig.args.formatString.defaultValue = defaultFormat } if (defaultLocale) { - options.args.formatString.defaultLocale = defaultLocale + fieldConfig.args.formatString.defaultLocale = defaultLocale } } - typeComposer.extendField(fieldName, options) + typeComposer.extendField(fieldName, fieldConfig) } diff --git a/packages/gatsby/src/schema/infer/add-inferred-fields.js b/packages/gatsby/src/schema/infer/add-inferred-fields.js index 7319c5d296d4d..0860f606e817a 100644 --- a/packages/gatsby/src/schema/infer/add-inferred-fields.js +++ b/packages/gatsby/src/schema/infer/add-inferred-fields.js @@ -243,9 +243,11 @@ const getFieldConfigFromFieldNameConvention = ({ return { type, extensions: { - resolver: `link`, - resolverOptions: { - by: foreignKey || `id`, + addResolver: { + type: `link`, + options: { + by: foreignKey || `id`, + }, }, }, } @@ -267,19 +269,22 @@ const getSimpleFieldConfig = ({ return { type: is32BitInteger(value) ? `Int` : `Float` } case `string`: if (isDate(value)) { - return { type: `Date`, extensions: { resolver: `date` } } + return { type: `Date`, extensions: { addResolver: { type: `date` } } } } if (isFile(nodeStore, selector, value)) { // NOTE: For arrays of files, where not every path references // a File node in the db, it is semi-random if the field is // inferred as File or String, since the exampleValue only has // the first entry (which could point to an existing file or not). - return { type: `File`, extensions: { resolver: `relativeFile` } } + return { + type: `File`, + extensions: { addResolver: { type: `relativeFile` } }, + } } return { type: `String` } case `object`: if (value instanceof Date) { - return { type: `Date`, extensions: { resolver: `date` } } + return { type: `Date`, extensions: { addResolver: { type: `date` } } } } if (value instanceof String) { return { type: `String` } diff --git a/packages/gatsby/src/schema/infer/merge-inferred-composer.js b/packages/gatsby/src/schema/infer/merge-inferred-composer.js index db3c7373deab6..4d83537b3444e 100644 --- a/packages/gatsby/src/schema/infer/merge-inferred-composer.js +++ b/packages/gatsby/src/schema/infer/merge-inferred-composer.js @@ -1,6 +1,7 @@ const _ = require(`lodash`) const { GraphQLList, GraphQLNonNull, GraphQLObjectType } = require(`graphql`) const { ObjectTypeComposer } = require(`graphql-compose`) +const report = require(`gatsby-cli/lib/reporter`) export const mergeInferredComposer = ({ schemaComposer, @@ -85,7 +86,7 @@ const maybeExtendDefinedField = ({ ) ) { const extensions = definedComposer.getFieldExtensions(fieldName) - if (addDefaultResolvers && !definedField.resolver && !extensions.resolver) { + if (addDefaultResolvers && !extensions.addResolver) { const config = {} if ( (!definedField.args || _.isEmpty(definedField.args)) && @@ -99,16 +100,14 @@ const maybeExtendDefinedField = ({ } definedComposer.extendField(fieldName, config) - if (inferredField.extensions.resolver) { - definedComposer.setFieldExtension( - fieldName, - `resolver`, - inferredField.extensions.resolver + if (inferredField.extensions.addResolver) { + report.warn( + `Deprecation warning - adding inferred resolver for field ${definedComposer.getTypeName()}.${fieldName}. In Gatsby 3, only fields with a "addResolver" directive/extension will get a resolver.` ) definedComposer.setFieldExtension( fieldName, - `resolverOptions`, - inferredField.extensions.resolverOptions + `addResolver`, + inferredField.extensions.addResolver ) } } diff --git a/packages/gatsby/src/schema/schema-composer.js b/packages/gatsby/src/schema/schema-composer.js index 8b12a0f1d6a52..21e391faf1c8f 100644 --- a/packages/gatsby/src/schema/schema-composer.js +++ b/packages/gatsby/src/schema/schema-composer.js @@ -1,14 +1,20 @@ -const { SchemaComposer } = require(`graphql-compose`) +const { SchemaComposer, GraphQLJSON } = require(`graphql-compose`) const { getNodeInterface } = require(`./types/node-interface`) const { GraphQLDate } = require(`./types/date`) -const { InferDirective, DontInferDirective } = require(`./types/directives`) +const { + InferDirective, + DontInferDirective, + AddResolver, +} = require(`./types/directives`) const createSchemaComposer = () => { const schemaComposer = new SchemaComposer() getNodeInterface({ schemaComposer }) schemaComposer.addAsComposer(GraphQLDate) + schemaComposer.addAsComposer(GraphQLJSON) schemaComposer.addDirective(InferDirective) schemaComposer.addDirective(DontInferDirective) + schemaComposer.addDirective(AddResolver) return schemaComposer } diff --git a/packages/gatsby/src/schema/schema.js b/packages/gatsby/src/schema/schema.js index 8d18814024d9c..c5d05707fa23a 100644 --- a/packages/gatsby/src/schema/schema.js +++ b/packages/gatsby/src/schema/schema.js @@ -234,21 +234,9 @@ const processAddedType = ({ } } - // XXX(freiksenet): This currently forces types too early - // if (typeComposer.getFieldNames) { - // typeComposer.getFieldNames().forEach(fieldName => { - // typeComposer.setFieldExtension(fieldName, `createdFrom`, createdFrom) - // typeComposer.setFieldExtension( - // fieldName, - // `plugin`, - // plugin ? plugin.name : null - // ) - // }) - // } - if (typeComposer.hasExtension(`addDefaultResolvers`)) { report.warn( - `Default resolve behaviour is deprecated. In future, only fields with explicit resolver directives/extensions (date, link) will get arguments and resolvers. "noDefaultResolvers" argument will be removed from the directive.` + `Deprecation warning - "noDefaultResolvers" is deprecated. In Gatsby 3, defined fields won't get resolvers, unless "addResolver" directive/extension is used.` ) } diff --git a/packages/gatsby/src/schema/types/directives.js b/packages/gatsby/src/schema/types/directives.js index a15f04464f26f..7fd7e929fecbe 100644 --- a/packages/gatsby/src/schema/types/directives.js +++ b/packages/gatsby/src/schema/types/directives.js @@ -2,8 +2,10 @@ const { GraphQLBoolean, GraphQLNonNull, GraphQLDirective, + GraphQLString, DirectiveLocation, } = require(`graphql`) +const { GraphQLJSON } = require(`graphql-compose`) const InferDirective = new GraphQLDirective({ name: `infer`, @@ -11,8 +13,7 @@ const InferDirective = new GraphQLDirective({ locations: [DirectiveLocation.OBJECT], args: { noDefaultResolvers: { - type: new GraphQLNonNull(GraphQLBoolean), - default: false, + type: GraphQLBoolean, description: `Don't add default resolvers to defined fields.`, deprecationReason: `noDefaultResolvers is deprecated, annotate individual fields.`, }, @@ -25,15 +26,31 @@ const DontInferDirective = new GraphQLDirective({ locations: [DirectiveLocation.OBJECT], args: { noDefaultResolvers: { - type: new GraphQLNonNull(GraphQLBoolean), - default: true, + type: GraphQLBoolean, description: `Don't add default resolvers to defined fields.`, deprecationReason: `noDefaultResolvers is deprecated, annotate individual fields.`, }, }, }) +const AddResolver = new GraphQLDirective({ + name: `addResolver`, + description: `Add a resolver specified by "type" to field`, + locations: [DirectiveLocation.FIELD_DEFINITION], + args: { + type: { + type: new GraphQLNonNull(GraphQLString), + description: `Type of the resolver. Types available by default are: "date", "link" and "relativeFile".`, + }, + options: { + type: GraphQLJSON, + description: `Resolver options. Vary based on resolver type.`, + }, + }, +}) + module.exports = { InferDirective, DontInferDirective, + AddResolver, } From 0ded10ddab6efdb060d200799a9604489e1c1609 Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Wed, 3 Apr 2019 17:04:21 +0300 Subject: [PATCH 05/32] Kitchen sink tests --- .../__snapshots__/kitchen-sink.js.snap | 6 + .../src/schema/__tests__/kitchen-sink.js | 8 +- .../gatsby/src/schema/add-field-resolvers.js | 2 +- .../src/schema/infer/__tests__/merge-types.js | 228 ++++++++++++++++-- packages/gatsby/src/schema/infer/index.js | 17 +- .../schema/infer/merge-inferred-composer.js | 5 + packages/gatsby/src/schema/schema.js | 41 +++- .../gatsby/src/schema/types/__tests__/date.js | 37 +-- 8 files changed, 277 insertions(+), 67 deletions(-) diff --git a/packages/gatsby/src/schema/__tests__/__snapshots__/kitchen-sink.js.snap b/packages/gatsby/src/schema/__tests__/__snapshots__/kitchen-sink.js.snap index b59190f7bae47..4f989db5d450a 100644 --- a/packages/gatsby/src/schema/__tests__/__snapshots__/kitchen-sink.js.snap +++ b/packages/gatsby/src/schema/__tests__/__snapshots__/kitchen-sink.js.snap @@ -43,6 +43,7 @@ Object { "_3invalidKey": null, "code": "BShF_8qhtEv", "comment": 0, + "defaultTime": "05 huhtikuu", "id": "1486495736706552111", "idWithDecoration": "decoration-1486495736706552111", "image": Object { @@ -51,6 +52,8 @@ Object { }, }, "likes": 8, + "localeFormat": "05 huhtikuu 2017", + "localeString": "05 апреля", "time": "05.04.2017", }, }, @@ -59,6 +62,7 @@ Object { "_3invalidKey": null, "code": "BY6B8z5lR1F", "comment": 0, + "defaultTime": "11 syyskuu", "id": "1601601194425654597", "idWithDecoration": "decoration-1601601194425654597", "image": Object { @@ -67,6 +71,8 @@ Object { }, }, "likes": 9, + "localeFormat": "11 syyskuu 2017", + "localeString": "11 сентября", "time": "11.09.2017", }, }, diff --git a/packages/gatsby/src/schema/__tests__/kitchen-sink.js b/packages/gatsby/src/schema/__tests__/kitchen-sink.js index 4b76b33ff20f8..5ce178f9d60be 100644 --- a/packages/gatsby/src/schema/__tests__/kitchen-sink.js +++ b/packages/gatsby/src/schema/__tests__/kitchen-sink.js @@ -47,10 +47,11 @@ describe(`Kichen sink schema test`, () => { store.dispatch({ type: `CREATE_TYPES`, payload: ` - type PostsJson implements Node { + type PostsJson implements Node @infer { id: String! - time: Date + time: Date @addResolver(type: "date", options: { defaultLocale: "fi", defaultFormat: "DD MMMM"}) code: String + image: File @addResolver(type: "relativeFile") } `, }) @@ -72,6 +73,9 @@ describe(`Kichen sink schema test`, () => { id idWithDecoration time(formatString: "DD.MM.YYYY") + localeString: time(locale: "ru") + localeFormat: time(formatString: "DD MMMM YYYY") + defaultTime: time code likes comment diff --git a/packages/gatsby/src/schema/add-field-resolvers.js b/packages/gatsby/src/schema/add-field-resolvers.js index 0b04dedb44da1..1838b47997067 100644 --- a/packages/gatsby/src/schema/add-field-resolvers.js +++ b/packages/gatsby/src/schema/add-field-resolvers.js @@ -76,7 +76,7 @@ const addDateResolver = ({ fieldConfig.args.formatString.defaultValue = defaultFormat } if (defaultLocale) { - fieldConfig.args.formatString.defaultLocale = defaultLocale + fieldConfig.args.locale.defaultValue = defaultLocale } } diff --git a/packages/gatsby/src/schema/infer/__tests__/merge-types.js b/packages/gatsby/src/schema/infer/__tests__/merge-types.js index 4327e225929d3..5465238b6a4b2 100644 --- a/packages/gatsby/src/schema/infer/__tests__/merge-types.js +++ b/packages/gatsby/src/schema/infer/__tests__/merge-types.js @@ -57,12 +57,17 @@ describe(`merges explicit and inferred type definitions`, () => { ) }) - const buildTestSchemaWithSdl = async ({ - infer = true, - addDefaultResolvers = true, - }) => { - const inferDirective = infer ? `@infer` : `@dontInfer` - const noDefaultResolvers = addDefaultResolvers ? `false` : `true` + const buildTestSchemaWithSdl = async ({ infer, addDefaultResolvers }) => { + let directive = `` + if (infer != null) { + directive = infer ? `@infer` : `@dontInfer` + if (addDefaultResolvers != null) { + directive += `(noDefaultResolvers: ${ + addDefaultResolvers ? `false` : `true` + })` + } + } + const typeDefs = [ ` type NestedNested { @@ -77,7 +82,7 @@ describe(`merges explicit and inferred type definitions`, () => { nested: NestedNested } - type Test implements Node ${inferDirective}(noDefaultResolvers: ${noDefaultResolvers}) { + type Test implements Node ${directive} { explicitDate: Date bar: Boolean! nested: Nested! @@ -102,9 +107,16 @@ describe(`merges explicit and inferred type definitions`, () => { } const buildTestSchemaWithTypeBuilders = async ({ - infer = true, - addDefaultResolvers = true, + infer, + addDefaultResolvers, }) => { + let extensions = {} + if (infer != null) { + extensions.infer = infer + if (addDefaultResolvers != null) { + extensions.addDefaultResolvers = addDefaultResolvers + } + } const typeDefs = [ buildObjectType({ name: `NestedNested`, @@ -125,10 +137,7 @@ describe(`merges explicit and inferred type definitions`, () => { buildObjectType({ name: `Test`, interfaces: [`Node`], - extensions: { - infer, - addDefaultResolvers, - }, + extensions, fields: { explicitDate: `Date`, bar: `Boolean!`, @@ -159,7 +168,7 @@ describe(`merges explicit and inferred type definitions`, () => { [`typeBuilders`, buildTestSchemaWithTypeBuilders], ].forEach(([name, buildTestSchema]) => { describe(`with ${name}`, () => { - it(`with default strategy`, async () => { + it(`with default strategy (implicit "@infer(noDefaultResolvers: false)")`, async () => { const schema = await buildTestSchema({}) const fields = schema.getType(`Test`).getFields() const nestedFields = schema.getType(`Nested`).getFields() @@ -207,7 +216,58 @@ describe(`merges explicit and inferred type definitions`, () => { expect(fields.inferDate.resolve).toBeDefined() }) - it(`with @dontInfer directive`, async () => { + it(`with @infer (implicit "noDefaultResolvers: true")`, async () => { + const schema = await buildTestSchema({ + infer: true, + }) + + const fields = schema.getType(`Test`).getFields() + const nestedFields = schema.getType(`Nested`).getFields() + const nestedNestedFields = schema.getType(`NestedNested`).getFields() + + // Non-conflicting top-level fields added + expect(fields.foo.type.toString()).toBe(`Boolean`) + expect(fields.bar.type.toString()).toBe(`Boolean!`) + + // Non-conflicting fields added on nested type + expect(fields.nested.type.toString()).toBe(`Nested!`) + expect(fields.nestedArray.type.toString()).toBe(`[Nested!]!`) + expect(nestedFields.foo.type.toString()).toBe(`Boolean`) + expect(nestedFields.bar.type.toString()).toBe(`Boolean!`) + expect(nestedNestedFields.foo.type.toString()).toBe(`Boolean`) + expect(nestedNestedFields.bar.type.toString()).toBe(`Boolean!`) + + // When type is referenced more than once on typeDefs, all non-conflicting + // fields are added + expect(nestedFields.extra.type.toString()).toBe(`Boolean`) + expect(nestedNestedFields.notExtra.type.toString()).toBe(`Boolean`) + expect(nestedNestedFields.extraExtra.type.toString()).toBe(`Boolean`) + expect(nestedNestedFields.extraExtraExtra.type.toString()).toBe( + `Boolean` + ) + + // Explicit typeDefs have proprity in case of type conflict + expect(fields.conflictType.type.toString()).toBe(`String!`) + expect(fields.conflictArray.type.toString()).toBe(`Int!`) + expect(fields.conflictArrayReverse.type.toString()).toBe(`[Int!]!`) + expect(fields.conflictArrayType.type.toString()).toBe(`[String!]!`) + expect(fields.conflictScalar.type.toString()).toBe(`Int!`) + expect(fields.conflictScalarReverse.type.toString()).toBe(`Nested!`) + expect(fields.conflictScalarArray.type.toString()).toBe(`[Int!]!`) + expect(fields.conflcitScalarArrayReverse.type.toString()).toBe( + `[Nested!]!` + ) + + // Explicit typeDefs have priority on nested types as well + expect(nestedFields.conflict.type.toString()).toBe(`String!`) + expect(nestedNestedFields.conflict.type.toString()).toBe(`String!`) + + // Date resolvers + expect(fields.explicitDate.resolve).toBeUndefined() + expect(fields.inferDate.resolve).toBeDefined() + }) + + it(`with @dontInfer directive (implicit "noDefaultResolvers: true")`, async () => { const schema = await buildTestSchema({ infer: false, }) @@ -253,12 +313,64 @@ describe(`merges explicit and inferred type definitions`, () => { expect(nestedFields.conflict.type.toString()).toBe(`String!`) expect(nestedNestedFields.conflict.type.toString()).toBe(`String!`) + // Date resolvers + expect(fields.explicitDate.resolve).toBeUndefined() + }) + + it(`with "infer(noDefaultResolvers: false)"`, async () => { + const schema = await buildTestSchema({ + infer: true, + addDefaultResolvers: true, + }) + const fields = schema.getType(`Test`).getFields() + const nestedFields = schema.getType(`Nested`).getFields() + const nestedNestedFields = schema.getType(`NestedNested`).getFields() + + // Non-conflicting top-level fields added + expect(fields.foo.type.toString()).toBe(`Boolean`) + expect(fields.bar.type.toString()).toBe(`Boolean!`) + + // Non-conflicting fields added on nested type + expect(fields.nested.type.toString()).toBe(`Nested!`) + expect(fields.nestedArray.type.toString()).toBe(`[Nested!]!`) + expect(nestedFields.foo.type.toString()).toBe(`Boolean`) + expect(nestedFields.bar.type.toString()).toBe(`Boolean!`) + expect(nestedNestedFields.foo.type.toString()).toBe(`Boolean`) + expect(nestedNestedFields.bar.type.toString()).toBe(`Boolean!`) + + // When type is referenced more than once on typeDefs, all non-conflicting + // fields are added + expect(nestedFields.extra.type.toString()).toBe(`Boolean`) + expect(nestedNestedFields.notExtra.type.toString()).toBe(`Boolean`) + expect(nestedNestedFields.extraExtra.type.toString()).toBe(`Boolean`) + expect(nestedNestedFields.extraExtraExtra.type.toString()).toBe( + `Boolean` + ) + + // Explicit typeDefs have proprity in case of type conflict + expect(fields.conflictType.type.toString()).toBe(`String!`) + expect(fields.conflictArray.type.toString()).toBe(`Int!`) + expect(fields.conflictArrayReverse.type.toString()).toBe(`[Int!]!`) + expect(fields.conflictArrayType.type.toString()).toBe(`[String!]!`) + expect(fields.conflictScalar.type.toString()).toBe(`Int!`) + expect(fields.conflictScalarReverse.type.toString()).toBe(`Nested!`) + expect(fields.conflictScalarArray.type.toString()).toBe(`[Int!]!`) + expect(fields.conflcitScalarArrayReverse.type.toString()).toBe( + `[Nested!]!` + ) + + // Explicit typeDefs have priority on nested types as well + expect(nestedFields.conflict.type.toString()).toBe(`String!`) + expect(nestedNestedFields.conflict.type.toString()).toBe(`String!`) + // Date resolvers expect(fields.explicitDate.resolve).toBeDefined() + expect(fields.inferDate.resolve).toBeDefined() }) - it(`with noDefaultResolvers: true`, async () => { + it(`with "infer(noDefaultResolvers: true)"`, async () => { const schema = await buildTestSchema({ + infer: true, addDefaultResolvers: false, }) const fields = schema.getType(`Test`).getFields() @@ -307,10 +419,62 @@ describe(`merges explicit and inferred type definitions`, () => { expect(fields.inferDate.resolve).toBeDefined() }) - it(`with both @dontInfer and noDefaultResolvers: true`, async () => { + it(`with "@dontInfer(noDefaultResolvers: false)"`, async () => { + const schema = await buildTestSchema({ + infer: false, + addDefaultResolvers: true, + }) + + const fields = schema.getType(`Test`).getFields() + const nestedFields = schema.getType(`Nested`).getFields() + const nestedNestedFields = schema.getType(`NestedNested`).getFields() + + // Non-conflicting top-level fields added + expect(fields.bar.type.toString()).toBe(`Boolean!`) + + // Not adding inferred fields + expect(fields.foo).toBeUndefined() + expect(nestedFields.foo).toBeUndefined() + expect(nestedNestedFields.foo).toBeUndefined() + expect(nestedFields.extra).toBeUndefined() + expect(nestedNestedFields.extraExtra).toBeUndefined() + expect(nestedNestedFields.extraExtraExtra).toBeUndefined() + expect(fields.inferDate).toBeUndefined() + + // Non-conflicting fields added on nested type + expect(fields.nested.type.toString()).toBe(`Nested!`) + expect(fields.nestedArray.type.toString()).toBe(`[Nested!]!`) + expect(nestedFields.bar.type.toString()).toBe(`Boolean!`) + expect(nestedNestedFields.bar.type.toString()).toBe(`Boolean!`) + + // When type is referenced more than once on typeDefs, all non-conflicting + // fields are added + expect(nestedNestedFields.notExtra.type.toString()).toBe(`Boolean`) + + // Explicit typeDefs have proprity in case of type conflict + expect(fields.conflictType.type.toString()).toBe(`String!`) + expect(fields.conflictArray.type.toString()).toBe(`Int!`) + expect(fields.conflictArrayReverse.type.toString()).toBe(`[Int!]!`) + expect(fields.conflictArrayType.type.toString()).toBe(`[String!]!`) + expect(fields.conflictScalar.type.toString()).toBe(`Int!`) + expect(fields.conflictScalarReverse.type.toString()).toBe(`Nested!`) + expect(fields.conflictScalarArray.type.toString()).toBe(`[Int!]!`) + expect(fields.conflcitScalarArrayReverse.type.toString()).toBe( + `[Nested!]!` + ) + + // Explicit typeDefs have priority on nested types as well + expect(nestedFields.conflict.type.toString()).toBe(`String!`) + expect(nestedNestedFields.conflict.type.toString()).toBe(`String!`) + + // Date resolvers + expect(fields.explicitDate.resolve).toBeDefined() + }) + + it(`with "@dontInfer(noDefaultResolvers: true)"`, async () => { const schema = await buildTestSchema({ - addDefaultResolvers: false, infer: false, + addDefaultResolvers: false, }) const fields = schema.getType(`Test`).getFields() @@ -361,6 +525,34 @@ describe(`merges explicit and inferred type definitions`, () => { }) }) + it(`adds explicit resolvers through directives`, async () => { + const typeDefs = ` + type Test implements Node @infer { + explicitDate: Date @addResolver(type: "date") + } + + type LinkTest implements Node @infer { + link: Test! @addResolver(type: "link") + links: [Test!]! @addResolver(type: "link") + } + ` + store.dispatch({ type: `CREATE_TYPES`, payload: typeDefs }) + await build({}) + const { schema } = store.getState() + + const { link, links } = schema.getType(`LinkTest`).getFields() + expect(link.type.toString()).toBe(`Test!`) + expect(links.type.toString()).toBe(`[Test!]!`) + expect(link.resolve).toBeDefined() + expect(links.resolve).toBeDefined() + + const { explicitDate, inferDate } = schema.getType(`Test`).getFields() + expect(explicitDate.resolve).toBeDefined() + expect(inferDate.resolve).toBeDefined() + }) + + it(`adds explicit resolvers through extensions`, async () => {}) + it(`honors array depth when merging types`, async () => { const typeDefs = ` type FooBar { diff --git a/packages/gatsby/src/schema/infer/index.js b/packages/gatsby/src/schema/infer/index.js index 3dd52ca071a4e..cb87f8fb98d1e 100644 --- a/packages/gatsby/src/schema/infer/index.js +++ b/packages/gatsby/src/schema/infer/index.js @@ -58,22 +58,9 @@ const addInferredTypes = ({ report.panic(`Building schema failed`) } - nodeTypesToInfer.forEach(typeName => { + return nodeTypesToInfer.map(typeName => schemaComposer.getOrCreateOTC(typeName) - }) - - const typeComposers = nodeTypesToInfer.map(typeName => - addInferredType({ - schemaComposer, - typeName, - nodeStore, - typeConflictReporter, - typeMapping, - parentSpan, - }) ) - - return typeComposers } const addInferredType = ({ @@ -98,11 +85,13 @@ const addInferredType = ({ }) const definedComposer = schemaComposer.getOrCreateOTC(typeName) addNodeInterface({ schemaComposer, typeComposer: definedComposer }) + mergeInferredComposer({ schemaComposer, definedComposer, inferredComposer, }) + return definedComposer } diff --git a/packages/gatsby/src/schema/infer/merge-inferred-composer.js b/packages/gatsby/src/schema/infer/merge-inferred-composer.js index 4d83537b3444e..9675990c6be02 100644 --- a/packages/gatsby/src/schema/infer/merge-inferred-composer.js +++ b/packages/gatsby/src/schema/infer/merge-inferred-composer.js @@ -20,6 +20,11 @@ export const mergeInferredComposer = ({ definedComposer.hasExtension(`addDefaultResolvers`) ) { addDefaultResolvers = definedComposer.getExtension(`addDefaultResolvers`) + } else if ( + definedComposer.hasExtension(`infer`) && + !definedComposer.hasExtension(`addDefaultResolvers`) + ) { + addDefaultResolvers = false } else { addDefaultResolvers = true } diff --git a/packages/gatsby/src/schema/schema.js b/packages/gatsby/src/schema/schema.js index c5d05707fa23a..e5862a44bbfc3 100644 --- a/packages/gatsby/src/schema/schema.js +++ b/packages/gatsby/src/schema/schema.js @@ -13,6 +13,7 @@ const { InterfaceTypeComposer, UnionTypeComposer, InputTypeComposer, + GraphQLJSON, } = require(`graphql-compose`) const apiRunner = require(`../utils/api-runner-node`) const report = require(`gatsby-cli/lib/reporter`) @@ -88,13 +89,24 @@ const updateSchemaComposer = async ({ parentSpan, }) => { await addTypes({ schemaComposer, parentSpan, types }) - await addInferredTypes({ + const inferredTypes = await addInferredTypes({ schemaComposer, nodeStore, typeConflictReporter, typeMapping, parentSpan, }) + await processFieldExtensions({ schemaComposer }) + inferredTypes.map(inferredType => + addInferredType({ + schemaComposer, + typeName: inferredType.getTypeName(), + nodeStore, + typeConflictReporter, + typeMapping, + parentSpan, + }) + ) await addSetFieldsOnGraphQLNodeTypeFields({ schemaComposer, nodeStore, @@ -256,6 +268,33 @@ const getNoDefaultResolvers = directive => { return null } +const processFieldExtensions = ({ schemaComposer }) => { + schemaComposer.forEach(typeComposer => { + if ( + typeComposer.getExtension(`createdFrom`) === `sdl` && + (typeComposer instanceof ObjectTypeComposer || + typeComposer instanceof InterfaceTypeComposer) + ) { + typeComposer.getFieldNames().forEach(fieldName => { + const field = typeComposer.getField(fieldName) + if (field.astNode && field.astNode.directives) { + field.astNode.directives.forEach(directive => { + if (directive.name.value === `addResolver`) { + const options = {} + directive.arguments.forEach(argument => { + options[argument.name.value] = GraphQLJSON.parseLiteral( + argument.value + ) + }) + typeComposer.setFieldExtension(fieldName, `addResolver`, options) + } + }) + } + }) + } + }) +} + const checkIsAllowedTypeName = name => { invariant( name !== `Node`, diff --git a/packages/gatsby/src/schema/types/__tests__/date.js b/packages/gatsby/src/schema/types/__tests__/date.js index 936e8056dc2e7..7bbf40ad6f47a 100644 --- a/packages/gatsby/src/schema/types/__tests__/date.js +++ b/packages/gatsby/src/schema/types/__tests__/date.js @@ -335,6 +335,7 @@ const nodes = [ invalidDate14: ``, invalidDate15: ` `, invalidDate16: `2012-04-01T00:basketball`, + defaultFormatDate: `2010-01-30T23:59:59.999-07:00`, }, ] @@ -346,33 +347,7 @@ describe(`dateResolver`, () => { ) }) - const buildTestSchema = async ({ - infer = true, - addDefaultResolvers = true, - }) => { - const inferDirective = infer ? `@infer` : `@dontInfer` - const noDefaultResolvers = addDefaultResolvers ? `false` : `true` - const typeDefs = [ - ` - type Test implements Node ${inferDirective}(noDefaultResolvers: ${noDefaultResolvers}) { - testDate: Date - explicitValidDate: Date - invalidHighPrecision: Date - invalidDate8: Date - invalidDate9: Date - invalidDate10: Date - invalidDate11: Date - invalidDate12: Date - invalidDate13: Date - invalidDate14: Date - invalidDate15: Date - invalidDate16: Date - }`, - ] - typeDefs.forEach(def => - store.dispatch({ type: `CREATE_TYPES`, payload: def }) - ) - + const buildTestSchema = async () => { await build({}) return store.getState().schema } @@ -423,12 +398,12 @@ describe(`dateResolver`, () => { expect(fields.invalidDate5.resolve).toBeUndefined() expect(fields.invalidDate6.resolve).toBeUndefined() expect(fields.invalidDate7.resolve).toBeUndefined() - expect(fields.invalidDate8.resolve).toBeUndefined() + expect(fields.invalidDate8).toBeUndefined() expect(fields.invalidDate9.resolve).toBeUndefined() - expect(fields.invalidDate10.resolve).toBeUndefined() + expect(fields.invalidDate10).toBeUndefined() expect(fields.invalidDate11.resolve).toBeUndefined() - expect(fields.invalidDate12.resolve).toBeUndefined() - expect(fields.invalidDate13.resolve).toBeUndefined() + expect(fields.invalidDate12).toBeUndefined() + expect(fields.invalidDate13).toBeUndefined() expect(fields.invalidDate14.resolve).toBeUndefined() expect(fields.invalidDate15.resolve).toBeUndefined() expect(fields.invalidDate16.resolve).toBeUndefined() From 354f11e73e06d998475e147ba8e56bdc707cb6e6 Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Thu, 4 Apr 2019 09:33:12 +0300 Subject: [PATCH 06/32] More fixes --- packages/gatsby/src/schema/__tests__/queries.js | 6 +++--- packages/gatsby/src/schema/schema.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/gatsby/src/schema/__tests__/queries.js b/packages/gatsby/src/schema/__tests__/queries.js index 3b70b8dffddf9..8ac99f452d215 100644 --- a/packages/gatsby/src/schema/__tests__/queries.js +++ b/packages/gatsby/src/schema/__tests__/queries.js @@ -45,7 +45,7 @@ describe(`Query schema`, () => { } else if (api === `createResolvers`) { return [ args[0].createResolvers({ - MarkdownFrontmatter: { + Frontmatter: { authors: { resolve(source, args, context, info) { // NOTE: When using the first field resolver argument (here called @@ -122,8 +122,8 @@ describe(`Query schema`, () => { ) const typeDefs = [ - `type Markdown implements Node { frontmatter: MarkdownFrontmatter! }`, - `type MarkdownFrontmatter { authors: [Author] }`, + `type Markdown implements Node { frontmatter: Frontmatter! }`, + `type Frontmatter { authors: [Author] }`, `type Author implements Node { posts: [Markdown] }`, ] typeDefs.forEach(def => diff --git a/packages/gatsby/src/schema/schema.js b/packages/gatsby/src/schema/schema.js index e5862a44bbfc3..010303cdad74f 100644 --- a/packages/gatsby/src/schema/schema.js +++ b/packages/gatsby/src/schema/schema.js @@ -438,7 +438,7 @@ const processThirdPartyType = ({ typeComposer, schemaQueryType, }) => { - typeComposer.getType().isThirdPartyType = true + typeComposer.setExtension(`createdFrom`, `thirdPartySchema`) // Fix for types that refer to Query. Thanks Relay Classic! if ( typeComposer instanceof ObjectTypeComposer || @@ -475,7 +475,7 @@ const addCustomResolveFunctions = async ({ schemaComposer, parentSpan }) => { !fieldTypeName || fieldTypeName.replace(/!/g, ``) === originalTypeName.replace(/!/g, ``) || - tc.getType().isThirdPartyType + tc.getExtension(`createdFrom`) === `thirdPartySchema` ) { const newConfig = {} if (fieldConfig.type) { From 774ada430f6dc67378facc6e2493976740f84f4f Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Thu, 4 Apr 2019 09:37:54 +0300 Subject: [PATCH 07/32] Mapping via extensions --- packages/gatsby/src/schema/infer/add-inferred-fields.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/gatsby/src/schema/infer/add-inferred-fields.js b/packages/gatsby/src/schema/infer/add-inferred-fields.js index 0860f606e817a..c7ccb3b98afdf 100644 --- a/packages/gatsby/src/schema/infer/add-inferred-fields.js +++ b/packages/gatsby/src/schema/infer/add-inferred-fields.js @@ -5,7 +5,6 @@ const invariant = require(`invariant`) const report = require(`gatsby-cli/lib/reporter`) const { isFile } = require(`./is-file`) -const { link } = require(`../resolvers`) const { isDate } = require(`../types/date`) const is32BitInteger = require(`./is-32-bit-integer`) @@ -187,7 +186,12 @@ const hasMapping = (mapping, selector) => const getFieldConfigFromMapping = ({ typeMapping, selector }) => { const [type, ...path] = typeMapping[selector].split(`.`) - return { type, resolve: link({ by: path.join(`.`) || `id` }) } + return { + type, + extensions: { + addResolver: { type: `link`, options: { by: path.join(`.`) || `id` } }, + }, + } } // probably should be in example value From ffa396065339c59684a0e056b35ae2b06f0cb0f1 Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Thu, 4 Apr 2019 09:39:38 +0300 Subject: [PATCH 08/32] Remove unused code --- packages/gatsby/src/schema/infer/add-inferred-fields.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/gatsby/src/schema/infer/add-inferred-fields.js b/packages/gatsby/src/schema/infer/add-inferred-fields.js index c7ccb3b98afdf..fe86b76e710aa 100644 --- a/packages/gatsby/src/schema/infer/add-inferred-fields.js +++ b/packages/gatsby/src/schema/infer/add-inferred-fields.js @@ -80,11 +80,6 @@ const addInferredFieldsImpl = ({ typeMapping, })) - let namedInferredType = fieldConfig.type - while (Array.isArray(namedInferredType)) { - namedInferredType = namedInferredType[0] - } - typeComposer.addFields({ [key]: fieldConfig }) typeComposer.setFieldExtension(key, `createdFrom`, `infer`) }) From fd57bb5349717ed2b2b8071de1a40bd6c3ce19e6 Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Thu, 4 Apr 2019 11:24:41 +0300 Subject: [PATCH 09/32] Fix comment --- packages/gatsby/src/schema/infer/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gatsby/src/schema/infer/index.js b/packages/gatsby/src/schema/infer/index.js index cb87f8fb98d1e..9e03a34afd7f7 100644 --- a/packages/gatsby/src/schema/infer/index.js +++ b/packages/gatsby/src/schema/infer/index.js @@ -26,7 +26,7 @@ const addInferredTypes = ({ let typeComposer if (schemaComposer.has(typeName)) { typeComposer = schemaComposer.getOTC(typeName) - // Infer we have enable infer or if it's "@dontInfer" but we have "addDefaultResolvers: false" + // Infer if we have enabled infer or if it's "@dontInfer" but we have "addDefaultResolvers: true" const runInfer = typeComposer.hasExtension(`infer`) ? typeComposer.getExtension(`infer`) || typeComposer.getExtension(`addDefaultResolvers`) From 5a6320f18f57be93fb50193335dc26e5cf46ab34 Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Fri, 5 Apr 2019 10:48:27 +0300 Subject: [PATCH 10/32] Simplify some code, make some other code very complicated --- packages/gatsby/package.json | 2 +- packages/gatsby/src/schema/infer/index.js | 13 +- packages/gatsby/src/schema/schema.js | 179 +++++++++++++--------- yarn.lock | 8 +- 4 files changed, 127 insertions(+), 75 deletions(-) diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 9514a9b0f65be..5b9394091eb2c 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -71,7 +71,7 @@ "gatsby-telemetry": "^1.0.4", "glob": "^7.1.1", "graphql": "^14.1.1", - "graphql-compose": "^6.1.1", + "graphql-compose": "^6.1.4", "graphql-playground-middleware-express": "^1.7.10", "graphql-relay": "^0.6.0", "graphql-tools": "^3.0.4", diff --git a/packages/gatsby/src/schema/infer/index.js b/packages/gatsby/src/schema/infer/index.js index 9e03a34afd7f7..120837a4bd069 100644 --- a/packages/gatsby/src/schema/infer/index.js +++ b/packages/gatsby/src/schema/infer/index.js @@ -58,8 +58,19 @@ const addInferredTypes = ({ report.panic(`Building schema failed`) } - return nodeTypesToInfer.map(typeName => + nodeTypesToInfer.forEach(typeName => { schemaComposer.getOrCreateOTC(typeName) + }) + + return nodeTypesToInfer.map(typeName => + addInferredType({ + schemaComposer, + typeName, + nodeStore, + typeConflictReporter, + typeMapping, + parentSpan, + }) ) } diff --git a/packages/gatsby/src/schema/schema.js b/packages/gatsby/src/schema/schema.js index 010303cdad74f..143e45667f19a 100644 --- a/packages/gatsby/src/schema/schema.js +++ b/packages/gatsby/src/schema/schema.js @@ -7,6 +7,9 @@ const { assertValidName, getNamedType, Kind, + GraphQLObjectType, + GraphQLInterfaceType, + GraphQLUnionType, } = require(`graphql`) const { ObjectTypeComposer, @@ -15,6 +18,10 @@ const { InputTypeComposer, GraphQLJSON, } = require(`graphql-compose`) +const { + defineFieldMapToConfig, +} = require(`graphql-compose/lib/utils/configToDefine`) + const apiRunner = require(`../utils/api-runner-node`) const report = require(`gatsby-cli/lib/reporter`) const { addNodeInterfaceFields } = require(`./types/node-interface`) @@ -89,24 +96,13 @@ const updateSchemaComposer = async ({ parentSpan, }) => { await addTypes({ schemaComposer, parentSpan, types }) - const inferredTypes = await addInferredTypes({ + await addInferredTypes({ schemaComposer, nodeStore, typeConflictReporter, typeMapping, parentSpan, }) - await processFieldExtensions({ schemaComposer }) - inferredTypes.map(inferredType => - addInferredType({ - schemaComposer, - typeName: inferredType.getTypeName(), - nodeStore, - typeConflictReporter, - typeMapping, - parentSpan, - }) - ) await addSetFieldsOnGraphQLNodeTypeFields({ schemaComposer, nodeStore, @@ -221,8 +217,9 @@ const processAddedType = ({ typeComposer.setExtension(`plugin`, plugin ? plugin.name : null) if (createdFrom === `sdl`) { - if (type.astNode && type.astNode.directives) { - type.astNode.directives.forEach(directive => { + const ast = typeComposer.getType().astNode + if (ast && ast.directives) { + ast.directives.forEach(directive => { if (directive.name.value === `infer`) { typeComposer.setExtension(`infer`, true) const addDefaultResolvers = getNoDefaultResolvers(directive) @@ -246,6 +243,37 @@ const processAddedType = ({ } } + if ( + typeComposer instanceof ObjectTypeComposer || + typeComposer instanceof InterfaceTypeComposer + ) { + typeComposer.getFieldNames().forEach(fieldName => { + typeComposer.setFieldExtension(fieldName, `createdFrom`, createdFrom) + typeComposer.setFieldExtension( + fieldName, + `plugin`, + plugin ? plugin.name : null + ) + + if (createdFrom === `sdl`) { + const field = typeComposer.getField(fieldName) + if (field.astNode && field.astNode.directives) { + field.astNode.directives.forEach(directive => { + if (directive.name.value === `addResolver`) { + const options = {} + directive.arguments.forEach(argument => { + options[argument.name.value] = GraphQLJSON.parseLiteral( + argument.value + ) + }) + typeComposer.setFieldExtension(fieldName, `addResolver`, options) + } + }) + } + } + }) + } + if (typeComposer.hasExtension(`addDefaultResolvers`)) { report.warn( `Deprecation warning - "noDefaultResolvers" is deprecated. In Gatsby 3, defined fields won't get resolvers, unless "addResolver" directive/extension is used.` @@ -268,33 +296,6 @@ const getNoDefaultResolvers = directive => { return null } -const processFieldExtensions = ({ schemaComposer }) => { - schemaComposer.forEach(typeComposer => { - if ( - typeComposer.getExtension(`createdFrom`) === `sdl` && - (typeComposer instanceof ObjectTypeComposer || - typeComposer instanceof InterfaceTypeComposer) - ) { - typeComposer.getFieldNames().forEach(fieldName => { - const field = typeComposer.getField(fieldName) - if (field.astNode && field.astNode.directives) { - field.astNode.directives.forEach(directive => { - if (directive.name.value === `addResolver`) { - const options = {} - directive.arguments.forEach(argument => { - options[argument.name.value] = GraphQLJSON.parseLiteral( - argument.value - ) - }) - typeComposer.setFieldExtension(fieldName, `addResolver`, options) - } - }) - } - }) - } - }) -} - const checkIsAllowedTypeName = name => { invariant( name !== `Node`, @@ -405,14 +406,6 @@ const addThirdPartySchemas = ({ }) => { thirdPartySchemas.forEach(schema => { const schemaQueryType = schema.getQueryType() - const queryTC = ObjectTypeComposer.createTemp(schemaQueryType) - processThirdPartyType({ - schemaComposer, - typeComposer: queryTC, - schemaQueryType, - }) - const fields = queryTC.getFields() - schemaComposer.Query.addFields(fields) // Explicitly add the third-party schema's types, so they can be targeted // in `createResolvers` API. @@ -424,38 +417,86 @@ const addThirdPartySchemas = ({ !isSpecifiedScalarType(type) && !isIntrospectionType(type) ) { - schemaComposer.addAsComposer(type) - const typeComposer = schemaComposer.getAnyTC(type.name) - processThirdPartyType({ schemaComposer, typeComposer, schemaQueryType }) - schemaComposer.addSchemaMustHaveType(typeComposer) + processThirdPartyType({ schemaComposer, type, schemaQueryType }) } }) + + const queryTC = ObjectTypeComposer.createTemp( + { + name: `TempQuery`, + fields: processThirdPartyTypeFields({ + type: schemaQueryType, + schemaQueryType, + }), + }, + schemaComposer + ) + schemaComposer.Query.addFields(queryTC.getFields()) }) } -const processThirdPartyType = ({ - schemaComposer, - typeComposer, - schemaQueryType, -}) => { - typeComposer.setExtension(`createdFrom`, `thirdPartySchema`) +const processThirdPartyType = ({ schemaComposer, type, schemaQueryType }) => { + let typeComposer // Fix for types that refer to Query. Thanks Relay Classic! if ( - typeComposer instanceof ObjectTypeComposer || - typeComposer instanceof InterfaceTypeComposer + type instanceof GraphQLObjectType || + type instanceof GraphQLInterfaceType ) { - typeComposer.getFieldNames().forEach(fieldName => { - const fieldType = typeComposer.getFieldType(fieldName) - if (getNamedType(fieldType) === schemaQueryType) { - typeComposer.extendField(fieldName, { - type: fieldType.toString().replace(schemaQueryType.name, `Query`), - }) - } - }) + const fields = processThirdPartyTypeFields({ type, schemaQueryType }) + if (type instanceof GraphQLObjectType) { + typeComposer = ObjectTypeComposer.create( + { + name: type.name, + fields, + interfaces: () => + type + .getInterfaces() + .map(iface => schemaComposer.getIFTC(iface.toString()).getType()), + }, + schemaComposer + ) + } else { + typeComposer = schemaComposer.getOrCreateIFTC(type.name) + typeComposer.setResolveType(type.resolveType) + } + typeComposer.setFields(fields) + } else if (type instanceof GraphQLUnionType) { + const types = type.getTypes() + typeComposer = schemaComposer.getOrCreateUTC(type.name) + typeComposer.setResolveType(type.resolveType) + typeComposer.setTypes(types.map(type => type.toString())) + schemaComposer.add(typeComposer) + } else { + schemaComposer.addAsComposer(type) + typeComposer = schemaComposer.get(type.name) } + typeComposer.setExtension(`createdFrom`, `thirdPartySchema`) + schemaComposer.addSchemaMustHaveType(typeComposer) + return typeComposer } +const processThirdPartyTypeFields = ({ type, schemaQueryType }) => { + const fields = {} + const typeFields = defineFieldMapToConfig(type.getFields()) + Object.keys(typeFields).forEach(fieldName => { + const field = typeFields[fieldName] + const fieldType = field.type + if (getNamedType(fieldType) === schemaQueryType) { + fields[fieldName] = { + ...field, + type: fieldType.toString().replace(schemaQueryType.name, `Query`), + } + } else { + fields[fieldName] = { + ...field, + type: fieldType.toString(), + } + } + }) + return fields +} + const addCustomResolveFunctions = async ({ schemaComposer, parentSpan }) => { const intermediateSchema = schemaComposer.buildSchema() const createResolvers = resolvers => { diff --git a/yarn.lock b/yarn.lock index 3a9ed5bc5c65e..fcee12ac0cefc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9002,10 +9002,10 @@ graceful-fs@^4.1.15, graceful-fs@^4.1.9: version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" -graphql-compose@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/graphql-compose/-/graphql-compose-6.1.1.tgz#3764de5b2c48fbbd83d7b43809b76deac53734c2" - integrity sha512-54ZRGrbuCsCZw9NCe4k1IScVV0p1c7lkDXXBca0e1KVo80e8LT5+T4fUHaG6R0uXEcMlSijKZmRrbcj+eITNkg== +graphql-compose@^6.1.4: + version "6.1.4" + resolved "https://registry.yarnpkg.com/graphql-compose/-/graphql-compose-6.1.4.tgz#3c9b9688c622a5c82fdf65744f9af93e0eb4dc00" + integrity sha512-xMfLkUMRRIbVvsp8KhiB6VR7ZYiaerzubL07yWmqWa8GB64kvlkRHMBZfuDeka+U5NncF+if5fcJJusvsc1zMg== dependencies: graphql-type-json "^0.2.2" object-path "^0.11.4" From d065cb6934eccbd2a54e5cfa964736e3ffb08093 Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Fri, 5 Apr 2019 11:40:22 +0300 Subject: [PATCH 11/32] v2.4.0-alpha.1 --- packages/gatsby/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 5b9394091eb2c..5da58afefd261 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -1,7 +1,7 @@ { "name": "gatsby", "description": "Blazing fast modern site generator for React", - "version": "2.3.5", + "version": "2.4.0-alpha.1", "author": "Kyle Mathews ", "bin": { "gatsby": "./dist/bin/gatsby.js" From 978e22e6a5f2c81b40503811c9be54e4989a65e5 Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Fri, 5 Apr 2019 14:47:21 +0300 Subject: [PATCH 12/32] Fix review comments --- .../src/schema/infer/add-inferred-fields.js | 9 ++++--- .../schema/infer/merge-inferred-composer.js | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/gatsby/src/schema/infer/add-inferred-fields.js b/packages/gatsby/src/schema/infer/add-inferred-fields.js index fe86b76e710aa..8de0c89beed92 100644 --- a/packages/gatsby/src/schema/infer/add-inferred-fields.js +++ b/packages/gatsby/src/schema/infer/add-inferred-fields.js @@ -252,6 +252,9 @@ const getFieldConfigFromFieldNameConvention = ({ } } +const DATE_EXTENSION = { addResolver: { type: `date` } } +const FILE_EXTENSION = { addResolver: { type: `relativeFile` } } + const getSimpleFieldConfig = ({ schemaComposer, typeComposer, @@ -268,7 +271,7 @@ const getSimpleFieldConfig = ({ return { type: is32BitInteger(value) ? `Int` : `Float` } case `string`: if (isDate(value)) { - return { type: `Date`, extensions: { addResolver: { type: `date` } } } + return { type: `Date`, extensions: DATE_EXTENSION } } if (isFile(nodeStore, selector, value)) { // NOTE: For arrays of files, where not every path references @@ -277,13 +280,13 @@ const getSimpleFieldConfig = ({ // the first entry (which could point to an existing file or not). return { type: `File`, - extensions: { addResolver: { type: `relativeFile` } }, + extensions: FILE_EXTENSION, } } return { type: `String` } case `object`: if (value instanceof Date) { - return { type: `Date`, extensions: { addResolver: { type: `date` } } } + return { type: `Date`, extensions: DATE_EXTENSION } } if (value instanceof String) { return { type: `String` } diff --git a/packages/gatsby/src/schema/infer/merge-inferred-composer.js b/packages/gatsby/src/schema/infer/merge-inferred-composer.js index 9675990c6be02..43c25b052492b 100644 --- a/packages/gatsby/src/schema/infer/merge-inferred-composer.js +++ b/packages/gatsby/src/schema/infer/merge-inferred-composer.js @@ -3,6 +3,31 @@ const { GraphQLList, GraphQLNonNull, GraphQLObjectType } = require(`graphql`) const { ObjectTypeComposer } = require(`graphql-compose`) const report = require(`gatsby-cli/lib/reporter`) +/** + Combined types defined in `createTypes` and types inferred from the data. + + This is called assuming we already inferred the type, so when we don't infer, + this doesn't need to be called. Because we have to accomodate legacy logic, + stuff is a bit more complex. With 3.0 we can remove all the extra code except + the one that adds the fields. + + Algorithm in brief: + + * for every field not present in definedComposer, but present in + inferredComposer, add it. If the type of field is unknown, add it to + composer. + + * legacy - won't add new fields if "dontAddFields" are true all (for + `@dontInfer(noDefaultResolvers: false)`) case + + * for fields that are present in definedComposer, we merge them together. + This is needed so that if we have inline object type, we can recurse into it + add add inferred fields to it too. When recursing, we inherit infer config, + unless inline type has those defined + + * legacy - add extensions, args and resolvers for fields that match the type, + but don't have those. For `noDefaultResolvers: false` cases + */ export const mergeInferredComposer = ({ schemaComposer, definedComposer, From 1c2a50119fadfd4f23f95b0eb3dbebe989596f83 Mon Sep 17 00:00:00 2001 From: stefanprobst Date: Tue, 23 Apr 2019 08:28:54 +0200 Subject: [PATCH 13/32] [schema] Simplify type merging (#13557) * Add another test * Add thirdparty types and fix relay classic weirdness * Remove unused deps * Simplify inference --- packages/gatsby/package.json | 2 +- .../__snapshots__/kitchen-sink.js.snap | 2 +- .../src/schema/__tests__/kitchen-sink.js | 61 +++++- .../src/schema/infer/add-inferred-fields.js | 157 +++++++++++---- packages/gatsby/src/schema/infer/index.js | 62 ++---- .../schema/infer/merge-inferred-composer.js | 190 ------------------ packages/gatsby/src/schema/schema.js | 103 +++------- yarn.lock | 18 +- 8 files changed, 218 insertions(+), 377 deletions(-) delete mode 100644 packages/gatsby/src/schema/infer/merge-inferred-composer.js diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 5da58afefd261..4e6490ad03152 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -71,7 +71,7 @@ "gatsby-telemetry": "^1.0.4", "glob": "^7.1.1", "graphql": "^14.1.1", - "graphql-compose": "^6.1.4", + "graphql-compose": "^6.3.2", "graphql-playground-middleware-express": "^1.7.10", "graphql-relay": "^0.6.0", "graphql-tools": "^3.0.4", diff --git a/packages/gatsby/src/schema/__tests__/__snapshots__/kitchen-sink.js.snap b/packages/gatsby/src/schema/__tests__/__snapshots__/kitchen-sink.js.snap index 4f989db5d450a..c08c144dd6613 100644 --- a/packages/gatsby/src/schema/__tests__/__snapshots__/kitchen-sink.js.snap +++ b/packages/gatsby/src/schema/__tests__/__snapshots__/kitchen-sink.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Kichen sink schema test passes kitchen sink query 1`] = ` +exports[`Kitchen sink schema test passes kitchen sink query 1`] = ` Object { "data": Object { "addResolvers": Array [ diff --git a/packages/gatsby/src/schema/__tests__/kitchen-sink.js b/packages/gatsby/src/schema/__tests__/kitchen-sink.js index 5ce178f9d60be..c9a582349869d 100644 --- a/packages/gatsby/src/schema/__tests__/kitchen-sink.js +++ b/packages/gatsby/src/schema/__tests__/kitchen-sink.js @@ -1,7 +1,14 @@ // @flow const { SchemaComposer } = require(`graphql-compose`) -const { graphql } = require(`graphql`) +const { + graphql, + GraphQLSchema, + GraphQLNonNull, + GraphQLList, + GraphQLObjectType, + getNamedType, +} = require(`graphql`) const { store } = require(`../../redux`) const { build } = require(`../index`) const fs = require(`fs-extra`) @@ -14,7 +21,7 @@ jest.mock(`../../utils/api-runner-node`) const apiRunnerNode = require(`../../utils/api-runner-node`) // XXX(freiksenet): Expand -describe(`Kichen sink schema test`, () => { +describe(`Kitchen sink schema test`, () => { let schema const runQuery = query => @@ -55,10 +62,12 @@ describe(`Kichen sink schema test`, () => { } `, }) - store.dispatch({ - type: `ADD_THIRD_PARTY_SCHEMA`, - payload: buildThirdPartySchema(), - }) + buildThirdPartySchemas().forEach(schema => + store.dispatch({ + type: `ADD_THIRD_PARTY_SCHEMA`, + payload: schema, + }) + ) await build({}) schema = store.getState().schema }) @@ -134,9 +143,21 @@ describe(`Kichen sink schema test`, () => { `) ).toMatchSnapshot() }) + + it(`correctly resolves nested Query types from third-party types`, () => { + const queryFields = schema.getQueryType().getFields() + ;[`relay`, `relay2`, `query`, `manyQueries`].forEach(fieldName => + expect(getNamedType(queryFields[fieldName].type)).toBe( + schema.getQueryType() + ) + ) + expect(schema.getType(`Nested`).getFields().query.type).toBe( + schema.getQueryType() + ) + }) }) -const buildThirdPartySchema = () => { +const buildThirdPartySchemas = () => { const schemaComposer = new SchemaComposer() schemaComposer.addTypeDefs(` type ThirdPartyStuff { @@ -209,7 +230,31 @@ const buildThirdPartySchema = () => { schemaComposer.addSchemaMustHaveType( schemaComposer.getOTC(`ThirdPartyStuff3`) ) - return schemaComposer.buildSchema() + + // Query type with non-default name + const RootQueryType = new GraphQLObjectType({ + name: `RootQueryType`, + fields: () => { + return { + query: { type: RootQueryType }, + manyQueries: { + type: new GraphQLNonNull(new GraphQLList(RootQueryType)), + }, + nested: { type: Nested }, + } + }, + }) + const Nested = new GraphQLObjectType({ + name: `Nested`, + fields: () => { + return { + query: { type: RootQueryType }, + } + }, + }) + const schema = new GraphQLSchema({ query: RootQueryType }) + + return [schemaComposer.buildSchema(), schema] } const mockSetFieldsOnGraphQLNodeType = async ({ type: { name } }) => { diff --git a/packages/gatsby/src/schema/infer/add-inferred-fields.js b/packages/gatsby/src/schema/infer/add-inferred-fields.js index 8de0c89beed92..8ea7742b13116 100644 --- a/packages/gatsby/src/schema/infer/add-inferred-fields.js +++ b/packages/gatsby/src/schema/infer/add-inferred-fields.js @@ -1,6 +1,6 @@ const _ = require(`lodash`) -const { getNamedType, GraphQLObjectType } = require(`graphql`) -const { ObjectTypeComposer, UnionTypeComposer } = require(`graphql-compose`) +const { ObjectTypeComposer } = require(`graphql-compose`) +const { GraphQLList } = require(`graphql`) const invariant = require(`invariant`) const report = require(`gatsby-cli/lib/reporter`) @@ -16,6 +16,17 @@ const addInferredFields = ({ typeMapping, parentSpan, }) => { + const config = getInferenceConfig({ + typeComposer, + defaults: { + shouldAddFields: true, + // FIXME: This is a behavioral change + shouldAddDefaultResolvers: typeComposer.hasExtension(`infer`) + ? false + : true, + // shouldAddDefaultResolvers: true, + }, + }) addInferredFieldsImpl({ schemaComposer, typeComposer, @@ -23,6 +34,7 @@ const addInferredFields = ({ exampleObject: exampleValue, prefix: typeComposer.getTypeName(), typeMapping, + config, }) } @@ -37,10 +49,11 @@ const addInferredFieldsImpl = ({ exampleObject, typeMapping, prefix, + config, }) => { const fields = [] Object.keys(exampleObject).forEach(unsanitizedKey => { - let key = createFieldName(unsanitizedKey) + const key = createFieldName(unsanitizedKey) fields.push({ key, unsanitizedKey, @@ -70,18 +83,49 @@ const addInferredFieldsImpl = ({ selectedField = possibleFields[0] } - let fieldConfig - ;({ key, fieldConfig } = getFieldConfig({ + const fieldConfig = getFieldConfig({ ...selectedField, schemaComposer, typeComposer, nodeStore, prefix, typeMapping, - })) + config, + }) - typeComposer.addFields({ [key]: fieldConfig }) - typeComposer.setFieldExtension(key, `createdFrom`, `infer`) + if (!fieldConfig) return + + if (!typeComposer.hasField(key)) { + if (config.shouldAddFields) { + typeComposer.addFields({ [key]: fieldConfig }) + typeComposer.setFieldExtension(key, `createdFrom`, `inference`) + } + } else { + // Deprecated, remove in v3 + if (config.shouldAddDefaultResolvers) { + // Add default resolvers to existing fields if the type matches + // and the field has neither args nor resolver explicitly defined. + const field = typeComposer.getField(key) + if ( + !typeComposer.hasFieldExtension(key, `addResolver`) && + field.type.toString().replace(/[[\]!]/g, ``) === + fieldConfig.type.toString() && + _.isEmpty(field.args) && + !field.resolve + ) { + const extension = + fieldConfig.extensions && fieldConfig.extensions.addResolver + if (extension) { + typeComposer.setFieldExtension(key, `addResolver`, extension) + report.warn( + `Deprecation warning - adding inferred resolver for field ` + + `${typeComposer}.${key}. In Gatsby v3, only fields with a ` + + `\`addResolver\` extension/directive will get a resolver.` + ) + } + } + } + } }) return typeComposer @@ -96,6 +140,7 @@ const getFieldConfig = ({ key, unsanitizedKey, typeMapping, + config, }) => { const selector = `${prefix}.${key}` @@ -111,14 +156,13 @@ const getFieldConfig = ({ // TODO: Use `prefix` instead of `selector` in hasMapping and getFromMapping? // i.e. does the config contain sanitized field names? fieldConfig = getFieldConfigFromMapping({ typeMapping, selector }) - } else if (key.includes(`___NODE`)) { + } else if (unsanitizedKey.includes(`___NODE`)) { fieldConfig = getFieldConfigFromFieldNameConvention({ schemaComposer, nodeStore, value: exampleValue, key: unsanitizedKey, }) - key = key.split(`___NODE`)[0] } else { fieldConfig = getSimpleFieldConfig({ schemaComposer, @@ -128,11 +172,15 @@ const getFieldConfig = ({ value, selector, typeMapping, + config, + arrays, }) } + if (!fieldConfig) return null + // Proxy resolver to unsanitized fieldName in case it contained invalid characters - if (key !== unsanitizedKey) { + if (key !== unsanitizedKey.split(`___NODE`)[0]) { fieldConfig = { ...fieldConfig, extensions: { @@ -147,11 +195,7 @@ const getFieldConfig = ({ arrays-- } - return { - key, - unsanitizedKey, - fieldConfig, - } + return fieldConfig } const resolveMultipleFields = possibleFields => { @@ -225,16 +269,12 @@ const getFieldConfigFromFieldNameConvention = ({ // scalar fields link to different types. Similarly, an array of objects // with foreign-key fields will produce union types if those foreign-key // fields are arrays, but not if they are scalars. See the tests for an example. - // FIXME: The naming of union types is a breaking change. In current master, - // the type name includes the key, which is (i) potentially not unique, and - // (ii) hinders reusing types. if (linkedTypes.length > 1) { const typeName = linkedTypes.sort().join(``) + `Union` - type = UnionTypeComposer.createTemp({ - name: typeName, - types: () => linkedTypes.map(typeName => schemaComposer.getOTC(typeName)), + type = schemaComposer.getOrCreateUTC(typeName, utc => { + utc.setTypes(linkedTypes.map(typeName => schemaComposer.getOTC(typeName))) + utc.setResolveType(node => node.internal.type) }) - type.setResolveType(node => node.internal.type) } else { type = linkedTypes[0] } @@ -246,6 +286,7 @@ const getFieldConfigFromFieldNameConvention = ({ type: `link`, options: { by: foreignKey || `id`, + from: key, }, }, }, @@ -263,6 +304,8 @@ const getSimpleFieldConfig = ({ value, selector, typeMapping, + config, + arrays, }) => { switch (typeof value) { case `boolean`: @@ -278,10 +321,7 @@ const getSimpleFieldConfig = ({ // a File node in the db, it is semi-random if the field is // inferred as File or String, since the exampleValue only has // the first entry (which could point to an existing file or not). - return { - type: `File`, - extensions: FILE_EXTENSION, - } + return { type: `File`, extensions: FILE_EXTENSION } } return { type: `String` } case `object`: @@ -292,28 +332,46 @@ const getSimpleFieldConfig = ({ return { type: `String` } } if (value /* && depth < MAX_DEPTH*/) { - // We only create a temporary TypeComposer on nested fields - // (either a clone of an existing field type, or a temporary new one), - // because we don't yet know if this type should end up in the schema. - // It might be for a possibleField that will be disregarded later, - // so we cannot mutate the original. let fieldTypeComposer - if ( - typeComposer.hasField(key) && - getNamedType(typeComposer.getFieldType(key)) instanceof - GraphQLObjectType - ) { - const originalFieldTypeComposer = typeComposer.getFieldTC(key) - fieldTypeComposer = originalFieldTypeComposer.clone( - originalFieldTypeComposer.getTypeName() - ) + if (typeComposer.hasField(key)) { + fieldTypeComposer = typeComposer.getFieldTC(key) + // If we have an object as a field value, but the field type is + // explicitly defined as something other than an ObjectType + // we can bail early. + if (!(fieldTypeComposer instanceof ObjectTypeComposer)) return null + // If the array depth of the field value and of the explicitly + // defined field type don't match we can also bail early. + let lists = 0 + let fieldType = typeComposer.getFieldType(key) + while (fieldType.ofType) { + if (fieldType instanceof GraphQLList) lists++ + fieldType = fieldType.ofType + } + if (lists !== arrays) return null } else { - fieldTypeComposer = ObjectTypeComposer.createTemp( + // When the field type has not been explicitly defined, we + // don't need to continue in case of @dontInfer, because + // "addDefaultResolvers: true" only makes sense for + // pre-existing types. + if (!config.shouldAddFields) return null + fieldTypeComposer = ObjectTypeComposer.create( createTypeName(selector), schemaComposer ) + fieldTypeComposer.setExtension(`createdFrom`, `inference`) + fieldTypeComposer.setExtension( + `plugin`, + typeComposer.getExtension(`plugin`) + ) } + // Inference config options are either explicitly defined on a type + // with directive/extension, or inherited from the parent type. + const inferenceConfig = getInferenceConfig({ + typeComposer: fieldTypeComposer, + defaults: config, + }) + return { type: addInferredFieldsImpl({ schemaComposer, @@ -322,6 +380,7 @@ const getSimpleFieldConfig = ({ exampleObject: value, typeMapping, prefix: selector, + config: inferenceConfig, }), } } @@ -352,7 +411,8 @@ const createFieldName = key => { `GraphQL field name (key) is not a string: \`${key}\`.` ) - const replaced = key.replace(NON_ALPHA_NUMERIC_EXPR, `_`) + const fieldName = key.split(`___NODE`)[0] + const replaced = fieldName.replace(NON_ALPHA_NUMERIC_EXPR, `_`) // key is invalid; normalize with leading underscore and rest with x if (replaced.match(/^__/)) { @@ -366,3 +426,14 @@ const createFieldName = key => { return replaced } + +const getInferenceConfig = ({ typeComposer, defaults }) => { + return { + shouldAddFields: typeComposer.hasExtension(`infer`) + ? typeComposer.getExtension(`infer`) + : defaults.shouldAddFields, + shouldAddDefaultResolvers: typeComposer.hasExtension(`addDefaultResolvers`) + ? typeComposer.getExtension(`addDefaultResolvers`) + : defaults.shouldAddDefaultResolvers, + } +} diff --git a/packages/gatsby/src/schema/infer/index.js b/packages/gatsby/src/schema/infer/index.js index 120837a4bd069..031141d138624 100644 --- a/packages/gatsby/src/schema/infer/index.js +++ b/packages/gatsby/src/schema/infer/index.js @@ -6,7 +6,6 @@ const { getNodeInterface, } = require(`../types/node-interface`) const { addInferredFields } = require(`./add-inferred-fields`) -const { mergeInferredComposer } = require(`./merge-inferred-composer`) const addInferredTypes = ({ schemaComposer, @@ -20,25 +19,29 @@ const addInferredTypes = ({ const typeNames = putFileFirst(nodeStore.getTypes()) const noNodeInterfaceTypes = [] - const nodeTypesToInfer = [] + const typesToInfer = [] typeNames.forEach(typeName => { let typeComposer if (schemaComposer.has(typeName)) { typeComposer = schemaComposer.getOTC(typeName) - // Infer if we have enabled infer or if it's "@dontInfer" but we have "addDefaultResolvers: true" + // Infer if we have enabled "@infer" or if it's "@dontInfer" but we + // have "addDefaultResolvers: true" const runInfer = typeComposer.hasExtension(`infer`) ? typeComposer.getExtension(`infer`) || typeComposer.getExtension(`addDefaultResolvers`) : true if (runInfer) { if (!typeComposer.hasInterface(`Node`)) { - noNodeInterfaceTypes.push(typeComposer.getType()) + noNodeInterfaceTypes.push(typeComposer) } - nodeTypesToInfer.push(typeName) + typesToInfer.push(typeComposer) } } else { - nodeTypesToInfer.push(typeName) + typeComposer = ObjectTypeComposer.create(typeName, schemaComposer) + addNodeInterface({ schemaComposer, typeComposer }) + typeComposer.setExtension(`createdFrom`, `inference`) + typesToInfer.push(typeComposer) } }) @@ -58,14 +61,10 @@ const addInferredTypes = ({ report.panic(`Building schema failed`) } - nodeTypesToInfer.forEach(typeName => { - schemaComposer.getOrCreateOTC(typeName) - }) - - return nodeTypesToInfer.map(typeName => + return typesToInfer.map(typeComposer => addInferredType({ schemaComposer, - typeName, + typeComposer, nodeStore, typeConflictReporter, typeMapping, @@ -75,38 +74,6 @@ const addInferredTypes = ({ } const addInferredType = ({ - schemaComposer, - typeName, - nodeStore, - typeConflictReporter, - typeMapping, - parentSpan, -}) => { - const inferredComposer = ObjectTypeComposer.createTemp( - typeName, - schemaComposer - ) - inferType({ - schemaComposer, - nodeStore, - typeConflictReporter, - typeComposer: inferredComposer, - typeMapping, - parentSpan, - }) - const definedComposer = schemaComposer.getOrCreateOTC(typeName) - addNodeInterface({ schemaComposer, typeComposer: definedComposer }) - - mergeInferredComposer({ - schemaComposer, - definedComposer, - inferredComposer, - }) - - return definedComposer -} - -const inferType = ({ schemaComposer, typeComposer, nodeStore, @@ -116,8 +83,11 @@ const inferType = ({ }) => { const typeName = typeComposer.getTypeName() const nodes = nodeStore.getNodesByType(typeName) - typeComposer.setExtension(`createdFrom`, `infer`) - typeComposer.setExtension(`plugin`, nodes[0].internal.owner) + // TODO: Move this to where the type is created once we can get + // node type owner information directly from store + if (typeComposer.getExtension(`createdFrom`) === `inference`) { + typeComposer.setExtension(`plugin`, nodes[0].internal.owner) + } const exampleValue = getExampleValue({ nodes, diff --git a/packages/gatsby/src/schema/infer/merge-inferred-composer.js b/packages/gatsby/src/schema/infer/merge-inferred-composer.js deleted file mode 100644 index 43c25b052492b..0000000000000 --- a/packages/gatsby/src/schema/infer/merge-inferred-composer.js +++ /dev/null @@ -1,190 +0,0 @@ -const _ = require(`lodash`) -const { GraphQLList, GraphQLNonNull, GraphQLObjectType } = require(`graphql`) -const { ObjectTypeComposer } = require(`graphql-compose`) -const report = require(`gatsby-cli/lib/reporter`) - -/** - Combined types defined in `createTypes` and types inferred from the data. - - This is called assuming we already inferred the type, so when we don't infer, - this doesn't need to be called. Because we have to accomodate legacy logic, - stuff is a bit more complex. With 3.0 we can remove all the extra code except - the one that adds the fields. - - Algorithm in brief: - - * for every field not present in definedComposer, but present in - inferredComposer, add it. If the type of field is unknown, add it to - composer. - - * legacy - won't add new fields if "dontAddFields" are true all (for - `@dontInfer(noDefaultResolvers: false)`) case - - * for fields that are present in definedComposer, we merge them together. - This is needed so that if we have inline object type, we can recurse into it - add add inferred fields to it too. When recursing, we inherit infer config, - unless inline type has those defined - - * legacy - add extensions, args and resolvers for fields that match the type, - but don't have those. For `noDefaultResolvers: false` cases - */ -export const mergeInferredComposer = ({ - schemaComposer, - definedComposer, - inferredComposer, - dontAddFields, - addDefaultResolvers, -}) => { - if (dontAddFields == null && definedComposer.hasExtension(`infer`)) { - const infer = definedComposer.getExtension(`infer`) - dontAddFields = !infer ? true : dontAddFields - } - - if ( - addDefaultResolvers == null && - definedComposer.hasExtension(`addDefaultResolvers`) - ) { - addDefaultResolvers = definedComposer.getExtension(`addDefaultResolvers`) - } else if ( - definedComposer.hasExtension(`infer`) && - !definedComposer.hasExtension(`addDefaultResolvers`) - ) { - addDefaultResolvers = false - } else { - addDefaultResolvers = true - } - - inferredComposer.getFieldNames().forEach(fieldName => { - const inferredField = inferredComposer.getField(fieldName) - if (definedComposer.hasField(fieldName)) { - maybeExtendDefinedField({ - schemaComposer, - definedComposer, - inferredComposer, - fieldName, - dontAddFields, - addDefaultResolvers, - }) - } else if (!dontAddFields) { - const inferredFieldComposer = inferredComposer.getFieldTC(fieldName) - const typeName = inferredFieldComposer.getTypeName() - if (schemaComposer.has(typeName)) { - const wrappedType = mapType( - inferredComposer.getFieldType(fieldName), - () => schemaComposer.get(typeName).getType() - ) - inferredField.type = wrappedType - } else { - schemaComposer.addAsComposer(inferredFieldComposer) - } - definedComposer.addFields({ [fieldName]: inferredField }) - } - }) - - if (!definedComposer.hasExtension(`createdFrom`)) { - definedComposer.setExtension( - `createdFrom`, - inferredComposer.getExtension(`createFrom`) - ) - definedComposer.setExtension( - `plugin`, - inferredComposer.getExtension(`plugin`) - ) - } - - return definedComposer -} - -const maybeExtendDefinedField = ({ - schemaComposer, - definedComposer, - inferredComposer, - fieldName, - dontAddFields, - addDefaultResolvers, -}) => { - const inferredField = inferredComposer.getField(fieldName) - - const definedFieldComposer = definedComposer.getFieldTC(fieldName) - const inferredFieldComposer = inferredComposer.getFieldTC(fieldName) - - const definedField = definedComposer.getField(fieldName) - if ( - typesLooselyEqual( - inferredComposer.getFieldType(fieldName), - definedComposer.getFieldType(fieldName) - ) - ) { - const extensions = definedComposer.getFieldExtensions(fieldName) - if (addDefaultResolvers && !extensions.addResolver) { - const config = {} - if ( - (!definedField.args || _.isEmpty(definedField.args)) && - inferredField.args - ) { - config.args = inferredField.args - } - - if (inferredField.resolve) { - config.resolve = inferredField.resolve - } - - definedComposer.extendField(fieldName, config) - if (inferredField.extensions.addResolver) { - report.warn( - `Deprecation warning - adding inferred resolver for field ${definedComposer.getTypeName()}.${fieldName}. In Gatsby 3, only fields with a "addResolver" directive/extension will get a resolver.` - ) - definedComposer.setFieldExtension( - fieldName, - `addResolver`, - inferredField.extensions.addResolver - ) - } - } - - if ( - inferredFieldComposer instanceof ObjectTypeComposer && - !inferredFieldComposer.hasInterface(`Node`) && - definedFieldComposer instanceof ObjectTypeComposer && - !definedFieldComposer.hasInterface(`Node`) && - (!definedFieldComposer.hasExtension(`infer`) || - definedFieldComposer.getExtension(`infer`)) - ) { - schemaComposer.set( - definedFieldComposer.getTypeName(), - mergeInferredComposer({ - schemaComposer, - definedComposer: definedFieldComposer, - inferredComposer: inferredFieldComposer, - dontAddFields, - addDefaultResolvers, - }) - ) - } - } -} - -const typesLooselyEqual = (left, right) => { - if (left instanceof GraphQLList && right instanceof GraphQLList) { - return typesLooselyEqual(left.ofType, right.ofType) - } else if (left instanceof GraphQLNonNull) { - return typesLooselyEqual(left.ofType, right) - } else if (right instanceof GraphQLNonNull) { - return typesLooselyEqual(left, right.ofType) - } else { - return ( - left.name === right.name || - (left instanceof GraphQLObjectType && right instanceof GraphQLObjectType) - ) - } -} - -const mapType = (type, fn) => { - if (type instanceof GraphQLList) { - return new GraphQLList(mapType(type.ofType, fn)) - } else if (type instanceof GraphQLNonNull) { - return new GraphQLNonNull(mapType(type.ofType, fn)) - } else { - return fn(type) - } -} diff --git a/packages/gatsby/src/schema/schema.js b/packages/gatsby/src/schema/schema.js index 143e45667f19a..f1a0dc32e58c0 100644 --- a/packages/gatsby/src/schema/schema.js +++ b/packages/gatsby/src/schema/schema.js @@ -5,11 +5,7 @@ const { isIntrospectionType, defaultFieldResolver, assertValidName, - getNamedType, Kind, - GraphQLObjectType, - GraphQLInterfaceType, - GraphQLUnionType, } = require(`graphql`) const { ObjectTypeComposer, @@ -18,9 +14,6 @@ const { InputTypeComposer, GraphQLJSON, } = require(`graphql-compose`) -const { - defineFieldMapToConfig, -} = require(`graphql-compose/lib/utils/configToDefine`) const apiRunner = require(`../utils/api-runner-node`) const report = require(`gatsby-cli/lib/reporter`) @@ -66,7 +59,7 @@ const rebuildSchemaWithSitePage = async ({ }) => { const typeComposer = addInferredType({ schemaComposer, - typeName: `SitePage`, + typeComposer: schemaComposer.getOTC(`SitePage`), nodeStore, typeConflictReporter, typeMapping, @@ -406,6 +399,9 @@ const addThirdPartySchemas = ({ }) => { thirdPartySchemas.forEach(schema => { const schemaQueryType = schema.getQueryType() + const queryTC = schemaComposer.createTempTC(schemaQueryType) + processThirdPartyTypeFields({ typeComposer: queryTC, schemaQueryType }) + schemaComposer.Query.addFields(queryTC.getFields()) // Explicitly add the third-party schema's types, so they can be targeted // in `createResolvers` API. @@ -415,86 +411,35 @@ const addThirdPartySchemas = ({ if ( type !== schemaQueryType && !isSpecifiedScalarType(type) && - !isIntrospectionType(type) + !isIntrospectionType(type) && + type.name !== `Date` && + type.name !== `JSON` ) { - processThirdPartyType({ schemaComposer, type, schemaQueryType }) + const typeComposer = schemaComposer.createTC(type) + if ( + typeComposer instanceof ObjectTypeComposer || + typeComposer instanceof InterfaceTypeComposer + ) { + processThirdPartyTypeFields({ typeComposer, schemaQueryType }) + } + typeComposer.setExtension(`createdFrom`, `thirdPartySchema`) + schemaComposer.addSchemaMustHaveType(typeComposer) } }) - - const queryTC = ObjectTypeComposer.createTemp( - { - name: `TempQuery`, - fields: processThirdPartyTypeFields({ - type: schemaQueryType, - schemaQueryType, - }), - }, - schemaComposer - ) - schemaComposer.Query.addFields(queryTC.getFields()) }) } -const processThirdPartyType = ({ schemaComposer, type, schemaQueryType }) => { - let typeComposer +const processThirdPartyTypeFields = ({ typeComposer, schemaQueryType }) => { // Fix for types that refer to Query. Thanks Relay Classic! - if ( - type instanceof GraphQLObjectType || - type instanceof GraphQLInterfaceType - ) { - const fields = processThirdPartyTypeFields({ type, schemaQueryType }) - if (type instanceof GraphQLObjectType) { - typeComposer = ObjectTypeComposer.create( - { - name: type.name, - fields, - interfaces: () => - type - .getInterfaces() - .map(iface => schemaComposer.getIFTC(iface.toString()).getType()), - }, - schemaComposer - ) - } else { - typeComposer = schemaComposer.getOrCreateIFTC(type.name) - typeComposer.setResolveType(type.resolveType) - } - typeComposer.setFields(fields) - } else if (type instanceof GraphQLUnionType) { - const types = type.getTypes() - typeComposer = schemaComposer.getOrCreateUTC(type.name) - typeComposer.setResolveType(type.resolveType) - typeComposer.setTypes(types.map(type => type.toString())) - schemaComposer.add(typeComposer) - } else { - schemaComposer.addAsComposer(type) - typeComposer = schemaComposer.get(type.name) - } - typeComposer.setExtension(`createdFrom`, `thirdPartySchema`) - schemaComposer.addSchemaMustHaveType(typeComposer) - - return typeComposer -} - -const processThirdPartyTypeFields = ({ type, schemaQueryType }) => { - const fields = {} - const typeFields = defineFieldMapToConfig(type.getFields()) - Object.keys(typeFields).forEach(fieldName => { - const field = typeFields[fieldName] - const fieldType = field.type - if (getNamedType(fieldType) === schemaQueryType) { - fields[fieldName] = { - ...field, - type: fieldType.toString().replace(schemaQueryType.name, `Query`), - } - } else { - fields[fieldName] = { - ...field, - type: fieldType.toString(), - } + typeComposer.getFieldNames().forEach(fieldName => { + const field = typeComposer.getField(fieldName) + const fieldType = field.type.toString() + if (fieldType.replace(/[[\]!]/g, ``) === schemaQueryType.name) { + typeComposer.extendField(fieldName, { + type: fieldType.replace(schemaQueryType.name, `Query`), + }) } }) - return fields } const addCustomResolveFunctions = async ({ schemaComposer, parentSpan }) => { diff --git a/yarn.lock b/yarn.lock index fcee12ac0cefc..b9f4bf0d24ce8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9002,12 +9002,12 @@ graceful-fs@^4.1.15, graceful-fs@^4.1.9: version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" -graphql-compose@^6.1.4: - version "6.1.4" - resolved "https://registry.yarnpkg.com/graphql-compose/-/graphql-compose-6.1.4.tgz#3c9b9688c622a5c82fdf65744f9af93e0eb4dc00" - integrity sha512-xMfLkUMRRIbVvsp8KhiB6VR7ZYiaerzubL07yWmqWa8GB64kvlkRHMBZfuDeka+U5NncF+if5fcJJusvsc1zMg== +graphql-compose@^6.3.2: + version "6.3.2" + resolved "https://registry.yarnpkg.com/graphql-compose/-/graphql-compose-6.3.2.tgz#0eff6e0f086c934af950db88b90a6667f879c59b" + integrity sha512-2sk4G3F/j7U4OBnPkB/HrE8Cejh8nHIJFBOGcqQvsELHXUHtx4S11zR0OU+J3cMtpE/2visBUGUhEHL9WlUK9A== dependencies: - graphql-type-json "^0.2.2" + graphql-type-json "^0.2.4" object-path "^0.11.4" graphql-config@^2.0.1: @@ -9059,10 +9059,10 @@ graphql-tools@^3.0.4: iterall "^1.1.3" uuid "^3.1.0" -graphql-type-json@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/graphql-type-json/-/graphql-type-json-0.2.2.tgz#d4d3808fbf2ead9b6184fd338fe23794cd9715be" - integrity sha512-srKbRJWxvZ8J6b7P3F0PrOtKgWg3pxlUPb1xbSIB+aMdK+UPKpp4aDzPV1A+IUTlea6lk9FWwI08UXQApC03lw== +graphql-type-json@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/graphql-type-json/-/graphql-type-json-0.2.4.tgz#545af27903e40c061edd30840a272ea0a49992f9" + integrity sha512-/tq02ayMQjrG4oDFDRLLrPk0KvJXue0nVXoItBe7uAdbNXjQUu+HYCBdAmPLQoseVzUKKMzrhq2P/sfI76ON6w== graphql@^14.1.1: version "14.1.1" From 9ca160e9345297c4c698b3b831817e111ed8f38f Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Tue, 23 Apr 2019 13:28:29 +0300 Subject: [PATCH 14/32] v2.4.0-alpha.2 --- packages/gatsby/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 7a0b397ca2d20..e98eaba1a361d 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -1,7 +1,7 @@ { "name": "gatsby", "description": "Blazing fast modern site generator for React", - "version": "2.4.0-alpha.1", + "version": "2.4.0-alpha.2", "author": "Kyle Mathews ", "bin": { "gatsby": "./dist/bin/gatsby.js" From 17d785769b249c13519489385bd3fe247a9c752f Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Wed, 24 Apr 2019 12:53:07 +0300 Subject: [PATCH 15/32] More consistent naming --- .../gatsby/src/schema/__tests__/kitchen-sink.js | 4 ++-- packages/gatsby/src/schema/add-field-resolvers.js | 14 +++++++------- .../src/schema/infer/__tests__/merge-types.js | 2 +- .../gatsby/src/schema/infer/add-inferred-fields.js | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/gatsby/src/schema/__tests__/kitchen-sink.js b/packages/gatsby/src/schema/__tests__/kitchen-sink.js index c9a582349869d..a6540bdcebf64 100644 --- a/packages/gatsby/src/schema/__tests__/kitchen-sink.js +++ b/packages/gatsby/src/schema/__tests__/kitchen-sink.js @@ -56,9 +56,9 @@ describe(`Kitchen sink schema test`, () => { payload: ` type PostsJson implements Node @infer { id: String! - time: Date @addResolver(type: "date", options: { defaultLocale: "fi", defaultFormat: "DD MMMM"}) + time: Date @addResolver(type: "dateformat", options: { locale: "fi", formatString: "DD MMMM"}) code: String - image: File @addResolver(type: "relativeFile") + image: File @addResolver(type: "fileByRelativePath") } `, }) diff --git a/packages/gatsby/src/schema/add-field-resolvers.js b/packages/gatsby/src/schema/add-field-resolvers.js index 1838b47997067..5b4959cf83f0f 100644 --- a/packages/gatsby/src/schema/add-field-resolvers.js +++ b/packages/gatsby/src/schema/add-field-resolvers.js @@ -19,7 +19,7 @@ export const addFieldResolvers = ({ ) { const options = extensions.addResolver.options || {} switch (extensions.addResolver.type) { - case `date`: { + case `dateformat`: { addDateResolver({ typeComposer, fieldName, @@ -33,7 +33,7 @@ export const addFieldResolvers = ({ }) break } - case `relativeFile`: { + case `fileByRelativePath`: { typeComposer.extendField(fieldName, { resolve: fileByPath({ from: options.from }), }) @@ -61,7 +61,7 @@ export const addFieldResolvers = ({ const addDateResolver = ({ typeComposer, fieldName, - options: { defaultFormat, defaultLocale }, + options: { formatString, locale }, }) => { const field = typeComposer.getField(fieldName) @@ -72,11 +72,11 @@ const addDateResolver = ({ fieldConfig.args = { ...dateResolver.args, } - if (defaultFormat) { - fieldConfig.args.formatString.defaultValue = defaultFormat + if (formatString) { + fieldConfig.args.formatString.defaultValue = formatString } - if (defaultLocale) { - fieldConfig.args.locale.defaultValue = defaultLocale + if (locale) { + fieldConfig.args.locale.defaultValue = locale } } diff --git a/packages/gatsby/src/schema/infer/__tests__/merge-types.js b/packages/gatsby/src/schema/infer/__tests__/merge-types.js index 5465238b6a4b2..9abc21da2b141 100644 --- a/packages/gatsby/src/schema/infer/__tests__/merge-types.js +++ b/packages/gatsby/src/schema/infer/__tests__/merge-types.js @@ -528,7 +528,7 @@ describe(`merges explicit and inferred type definitions`, () => { it(`adds explicit resolvers through directives`, async () => { const typeDefs = ` type Test implements Node @infer { - explicitDate: Date @addResolver(type: "date") + explicitDate: Date @addResolver(type: "dateformat") } type LinkTest implements Node @infer { diff --git a/packages/gatsby/src/schema/infer/add-inferred-fields.js b/packages/gatsby/src/schema/infer/add-inferred-fields.js index 8ea7742b13116..0da2257b793b8 100644 --- a/packages/gatsby/src/schema/infer/add-inferred-fields.js +++ b/packages/gatsby/src/schema/infer/add-inferred-fields.js @@ -293,8 +293,8 @@ const getFieldConfigFromFieldNameConvention = ({ } } -const DATE_EXTENSION = { addResolver: { type: `date` } } -const FILE_EXTENSION = { addResolver: { type: `relativeFile` } } +const DATE_EXTENSION = { addResolver: { type: `dateformat` } } +const FILE_EXTENSION = { addResolver: { type: `fileByRelativePath` } } const getSimpleFieldConfig = ({ schemaComposer, From c18d1e941e10ed486999fe485d4d03e69e7c48cd Mon Sep 17 00:00:00 2001 From: stefanprobst Date: Wed, 24 Apr 2019 14:03:20 +0200 Subject: [PATCH 16/32] Ensure link() default arg (#13591) --- packages/gatsby/src/schema/resolvers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gatsby/src/schema/resolvers.js b/packages/gatsby/src/schema/resolvers.js index 02534f3a978b6..4342b43d4981a 100644 --- a/packages/gatsby/src/schema/resolvers.js +++ b/packages/gatsby/src/schema/resolvers.js @@ -93,7 +93,7 @@ const paginate = (results = [], { skip = 0, limit }) => { } } -const link = ({ by, from }) => async (source, args, context, info) => { +const link = ({ by = `id`, from }) => async (source, args, context, info) => { const fieldValue = source && source[from || info.fieldName] if (fieldValue == null || _.isPlainObject(fieldValue)) return fieldValue From 9b2be714ff1fa45a299755176dc92398f9e81cc4 Mon Sep 17 00:00:00 2001 From: stefanprobst Date: Mon, 29 Apr 2019 08:26:18 +0200 Subject: [PATCH 17/32] [schema] Clarify warning message (#13693) --- packages/gatsby/src/redux/actions.js | 2 +- .../gatsby/src/schema/infer/type-conflict-reporter.js | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/gatsby/src/redux/actions.js b/packages/gatsby/src/redux/actions.js index b50bab4ed9243..12c115dc5f7fa 100644 --- a/packages/gatsby/src/redux/actions.js +++ b/packages/gatsby/src/redux/actions.js @@ -1274,7 +1274,7 @@ import type GatsbyGraphQLType from "../schema/types/type-builders" * Author information * """ * # Does not include automatically inferred fields - * type AuthorJson implements Node @dontInfer(noFieldResolvers: true) { + * type AuthorJson implements Node @dontInfer(noDefaultResolvers: true) { * name: String! * birthday: Date! # no default resolvers for Date formatting added * } diff --git a/packages/gatsby/src/schema/infer/type-conflict-reporter.js b/packages/gatsby/src/schema/infer/type-conflict-reporter.js index 9db57638f40ff..2d484b3e06969 100644 --- a/packages/gatsby/src/schema/infer/type-conflict-reporter.js +++ b/packages/gatsby/src/schema/infer/type-conflict-reporter.js @@ -134,7 +134,14 @@ class TypeConflictReporter { printConflicts() { if (this.entries.size > 0) { report.warn( - `There are conflicting field types in your data. GraphQL schema will omit those fields.` + `There are conflicting field types in your data.\n\n` + + `If you have explicitly defined a type for those fields, you can ` + + `safely ignore this warning message.\n` + + `Otherwise, Gatsby will omit those fields from the GraphQL schema.\n\n` + + `If you know all field types in advance, the best strategy is to ` + + `explicitly define them with the \`createTypes\` action, and skip ` + + `inference with the \`@dontInfer\` directive.\n` + + `See https://www.gatsbyjs.org/docs/actions/#createTypes` ) this.entries.forEach(entry => entry.printEntry()) } From e40f6d210f006cf041486136b7348faf291de81a Mon Sep 17 00:00:00 2001 From: stefanprobst Date: Tue, 30 Apr 2019 07:16:46 +0200 Subject: [PATCH 18/32] Allow registering field extensions (#13623) --- .../gatsby/src/schema/add-field-resolvers.js | 84 ------- packages/gatsby/src/schema/context.js | 2 + .../gatsby/src/schema/extensions/index.js | 210 ++++++++++++++++++ .../src/schema/infer/__tests__/infer.js | 2 +- .../src/schema/infer/__tests__/merge-types.js | 2 +- packages/gatsby/src/schema/infer/index.js | 10 +- packages/gatsby/src/schema/schema-composer.js | 10 +- packages/gatsby/src/schema/schema.js | 113 ++++------ packages/gatsby/src/schema/types/date.js | 60 ++--- .../gatsby/src/schema/types/directives.js | 56 ----- packages/gatsby/src/utils/api-node-docs.js | 32 +++ 11 files changed, 329 insertions(+), 252 deletions(-) delete mode 100644 packages/gatsby/src/schema/add-field-resolvers.js create mode 100644 packages/gatsby/src/schema/extensions/index.js delete mode 100644 packages/gatsby/src/schema/types/directives.js diff --git a/packages/gatsby/src/schema/add-field-resolvers.js b/packages/gatsby/src/schema/add-field-resolvers.js deleted file mode 100644 index 5b4959cf83f0f..0000000000000 --- a/packages/gatsby/src/schema/add-field-resolvers.js +++ /dev/null @@ -1,84 +0,0 @@ -const _ = require(`lodash`) -const { defaultFieldResolver } = require(`graphql`) -const { dateResolver } = require(`./types/date`) -const { link, fileByPath } = require(`./resolvers`) - -export const addFieldResolvers = ({ - schemaComposer, - typeComposer, - parentSpan, -}) => { - typeComposer.getFieldNames().forEach(fieldName => { - let field = typeComposer.getField(fieldName) - - const extensions = typeComposer.getFieldExtensions(fieldName) - if ( - !field.resolve && - extensions.addResolver && - _.isObject(extensions.addResolver) - ) { - const options = extensions.addResolver.options || {} - switch (extensions.addResolver.type) { - case `dateformat`: { - addDateResolver({ - typeComposer, - fieldName, - options, - }) - break - } - case `link`: { - typeComposer.extendField(fieldName, { - resolve: link({ from: options.from, by: options.by }), - }) - break - } - case `fileByRelativePath`: { - typeComposer.extendField(fieldName, { - resolve: fileByPath({ from: options.from }), - }) - break - } - } - } - - if (extensions.proxyFrom) { - // XXX(freiksenet): get field again cause it will be changed because of above - field = typeComposer.getField(fieldName) - const resolver = field.resolve || defaultFieldResolver - typeComposer.extendField(fieldName, { - resolve: (source, args, context, info) => - resolver(source, args, context, { - ...info, - fieldName: extensions.proxyFrom, - }), - }) - } - }) - return typeComposer -} - -const addDateResolver = ({ - typeComposer, - fieldName, - options: { formatString, locale }, -}) => { - const field = typeComposer.getField(fieldName) - - let fieldConfig = { - resolve: dateResolver.resolve, - } - if (!field.args || _.isEmpty(field.args)) { - fieldConfig.args = { - ...dateResolver.args, - } - if (formatString) { - fieldConfig.args.formatString.defaultValue = formatString - } - if (locale) { - fieldConfig.args.locale.defaultValue = locale - } - } - - typeComposer.extendField(fieldName, fieldConfig) -} diff --git a/packages/gatsby/src/schema/context.js b/packages/gatsby/src/schema/context.js index 9e037af253dd9..e02c7cccf0494 100644 --- a/packages/gatsby/src/schema/context.js +++ b/packages/gatsby/src/schema/context.js @@ -1,4 +1,5 @@ const { LocalNodeModel } = require(`./node-model`) +const { fieldExtensions } = require(`./extensions`) const withResolverContext = (context, schema) => { const nodeStore = require(`../db/nodes`) @@ -6,6 +7,7 @@ const withResolverContext = (context, schema) => { return { ...context, + fieldExtensions, nodeModel: new LocalNodeModel({ nodeStore, schema, diff --git a/packages/gatsby/src/schema/extensions/index.js b/packages/gatsby/src/schema/extensions/index.js new file mode 100644 index 0000000000000..6138b483316d1 --- /dev/null +++ b/packages/gatsby/src/schema/extensions/index.js @@ -0,0 +1,210 @@ +const { + GraphQLBoolean, + GraphQLNonNull, + GraphQLDirective, + GraphQLString, + DirectiveLocation, + defaultFieldResolver, +} = require(`graphql`) +const { GraphQLJSON } = require(`graphql-compose`) +const report = require(`gatsby-cli/lib/reporter`) + +const { link, fileByPath } = require(`../resolvers`) +const { getDateResolver } = require(`../types/date`) + +// Reserved for internal use +const internalExtensionNames = [ + `addDefaultResolvers`, + `createdFrom`, + `directives`, + `infer`, + `plugin`, +] + +const typeExtensions = { + infer: { + description: `Infer field types from field values.`, + args: { + noDefaultResolvers: { + type: GraphQLBoolean, + description: `Don't add default resolvers to defined fields.`, + deprecationReason: `noDefaultResolvers is deprecated, annotate individual fields.`, + }, + }, + }, + dontInfer: { + description: `Do not infer field types from field values.`, + args: { + noDefaultResolvers: { + type: GraphQLBoolean, + description: `Don't add default resolvers to defined fields.`, + deprecationReason: `noDefaultResolvers is deprecated, annotate individual fields.`, + }, + }, + }, +} + +const fieldExtensions = { + add: { + description: `Generic directive to add field extension.`, + args: { + extension: { + type: new GraphQLNonNull(GraphQLString), + }, + options: { + type: GraphQLJSON, + }, + }, + }, + + addResolver: { + description: `Add a resolver specified by "type" to field.`, + args: { + type: { + type: new GraphQLNonNull(GraphQLString), + description: + `Type of the resolver. Types available by default are: ` + + `"dateformat", "link" and "fileByRelativePath".`, + }, + options: { + type: GraphQLJSON, + description: `Resolver options. Vary based on resolver type.`, + }, + }, + process(args, fieldConfig) { + const { process } = fieldExtensions[args.type] || {} + if (typeof process === `function`) { + return process(args.options || {}, fieldConfig) + } + return {} + }, + }, + + dateformat: { + description: `Add date formating options.`, + args: { + formatString: { type: GraphQLString }, + locale: { type: GraphQLString }, + }, + process(args, fieldConfig) { + return getDateResolver(args) + }, + }, + + link: { + description: `Link to node by foreign-key relation.`, + args: { + by: { + type: new GraphQLNonNull(GraphQLString), + defaultValue: `id`, + }, + from: { + type: GraphQLString, + }, + }, + process(args, fieldConfig) { + return { + resolve: link(args), + } + }, + }, + + fileByRelativePath: { + description: `Link to File node by relative path.`, + args: { + from: { + type: GraphQLString, + }, + }, + process(args, fieldConfig) { + return { + resolve: fileByPath(args), + } + }, + }, + + // projection: { + // description: `Automatically add fields to selection set.`, + // args: {}, + // process(args, fieldConfig) {}, + // }, + + proxyFrom: { + description: `Proxy resolver from another field.`, + process(from, fieldConfig) { + const resolver = fieldConfig.resolve || defaultFieldResolver + return { + resolve(source, args, context, info) { + return resolver(source, args, context, { + ...info, + fieldName: from, + }) + }, + } + }, + }, +} + +const toDirectives = ({ extensions, locations }) => + Object.keys(extensions).map(name => { + const extension = extensions[name] + const { args, description } = extension + return new GraphQLDirective({ name, args, description, locations }) + }) + +const addDirectives = ({ schemaComposer }) => { + const fieldDirectives = toDirectives({ + extensions: fieldExtensions, + locations: [DirectiveLocation.FIELD_DEFINITION], + }) + fieldDirectives.forEach(directive => schemaComposer.addDirective(directive)) + const typeDirectives = toDirectives({ + extensions: typeExtensions, + locations: [DirectiveLocation.OBJECT], + }) + typeDirectives.forEach(directive => schemaComposer.addDirective(directive)) +} + +const processFieldExtensions = ({ + schemaComposer, + typeComposer, + parentSpan, +}) => { + typeComposer.getFieldNames().forEach(fieldName => { + const extensions = typeComposer.getFieldExtensions(fieldName) + Object.keys(extensions) + .filter(name => !internalExtensionNames.includes(name)) + .sort(a => a === `proxyFrom`) // Ensure `proxyFrom` is run last + .forEach(name => { + const { process } = fieldExtensions[name] || {} + if (process) { + // Always get fresh field config as it will have been changed + // by previous field extension + const prevFieldConfig = typeComposer.getFieldConfig(fieldName) + typeComposer.extendField( + fieldName, + process(extensions[name], prevFieldConfig) + ) + } + }) + }) +} + +const registerFieldExtension = (name, extension) => { + if (internalExtensionNames.includes(name)) { + report.error(`The extension ${name} is reserved for internal use.`) + } else if (!fieldExtensions[name]) { + fieldExtensions[name] = extension + } else { + report.error( + `A field extension with the name ${name} is already registered.` + ) + } +} + +module.exports = { + addDirectives, + fieldExtensions, + processFieldExtensions, + registerFieldExtension, +} diff --git a/packages/gatsby/src/schema/infer/__tests__/infer.js b/packages/gatsby/src/schema/infer/__tests__/infer.js index acda4be1bab69..fba26902540ca 100644 --- a/packages/gatsby/src/schema/infer/__tests__/infer.js +++ b/packages/gatsby/src/schema/infer/__tests__/infer.js @@ -320,7 +320,7 @@ describe(`GraphQL type inference`, () => { ` with_space with_hyphen - with_resolver(formatString:"DD.MM.YYYY") + with_resolver(formatString: "DD.MM.YYYY") _123 _456 { testingTypeNameCreation diff --git a/packages/gatsby/src/schema/infer/__tests__/merge-types.js b/packages/gatsby/src/schema/infer/__tests__/merge-types.js index 9abc21da2b141..fa589f7357e21 100644 --- a/packages/gatsby/src/schema/infer/__tests__/merge-types.js +++ b/packages/gatsby/src/schema/infer/__tests__/merge-types.js @@ -551,7 +551,7 @@ describe(`merges explicit and inferred type definitions`, () => { expect(inferDate.resolve).toBeDefined() }) - it(`adds explicit resolvers through extensions`, async () => {}) + it.todo(`adds explicit resolvers through extensions`) it(`honors array depth when merging types`, async () => { const typeDefs = ` diff --git a/packages/gatsby/src/schema/infer/index.js b/packages/gatsby/src/schema/infer/index.js index 031141d138624..49c2a720dd626 100644 --- a/packages/gatsby/src/schema/infer/index.js +++ b/packages/gatsby/src/schema/infer/index.js @@ -33,7 +33,7 @@ const addInferredTypes = ({ : true if (runInfer) { if (!typeComposer.hasInterface(`Node`)) { - noNodeInterfaceTypes.push(typeComposer) + noNodeInterfaceTypes.push(typeName) } typesToInfer.push(typeComposer) } @@ -46,16 +46,16 @@ const addInferredTypes = ({ }) if (noNodeInterfaceTypes.length > 0) { - noNodeInterfaceTypes.forEach(type => { + noNodeInterfaceTypes.forEach(typeName => { report.warn( - `Type \`${type}\` declared in \`createTypes\` looks like a node, ` + + `Type \`${typeName}\` declared in \`createTypes\` looks like a node, ` + `but doesn't implement a \`Node\` interface. It's likely that you should ` + `add the \`Node\` interface to your type def:\n\n` + - `\`type ${type} implements Node { ... }\`\n\n` + + `\`type ${typeName} implements Node { ... }\`\n\n` + `If you know that you don't want it to be a node (which would mean no ` + `root queries to retrieve it), you can explicitly disable inference ` + `for it:\n\n` + - `\`type ${type} @dontInfer { ... }\`` + `\`type ${typeName} @dontInfer { ... }\`` ) }) report.panic(`Building schema failed`) diff --git a/packages/gatsby/src/schema/schema-composer.js b/packages/gatsby/src/schema/schema-composer.js index 21e391faf1c8f..8d62f3ebade26 100644 --- a/packages/gatsby/src/schema/schema-composer.js +++ b/packages/gatsby/src/schema/schema-composer.js @@ -1,20 +1,14 @@ const { SchemaComposer, GraphQLJSON } = require(`graphql-compose`) const { getNodeInterface } = require(`./types/node-interface`) const { GraphQLDate } = require(`./types/date`) -const { - InferDirective, - DontInferDirective, - AddResolver, -} = require(`./types/directives`) +const { addDirectives } = require(`./extensions`) const createSchemaComposer = () => { const schemaComposer = new SchemaComposer() getNodeInterface({ schemaComposer }) schemaComposer.addAsComposer(GraphQLDate) schemaComposer.addAsComposer(GraphQLJSON) - schemaComposer.addDirective(InferDirective) - schemaComposer.addDirective(DontInferDirective) - schemaComposer.addDirective(AddResolver) + addDirectives({ schemaComposer }) return schemaComposer } diff --git a/packages/gatsby/src/schema/schema.js b/packages/gatsby/src/schema/schema.js index f1a0dc32e58c0..26b5bb0b53f10 100644 --- a/packages/gatsby/src/schema/schema.js +++ b/packages/gatsby/src/schema/schema.js @@ -5,14 +5,12 @@ const { isIntrospectionType, defaultFieldResolver, assertValidName, - Kind, } = require(`graphql`) const { ObjectTypeComposer, InterfaceTypeComposer, UnionTypeComposer, InputTypeComposer, - GraphQLJSON, } = require(`graphql-compose`) const apiRunner = require(`../utils/api-runner-node`) @@ -20,7 +18,10 @@ const report = require(`gatsby-cli/lib/reporter`) const { addNodeInterfaceFields } = require(`./types/node-interface`) const { addInferredType, addInferredTypes } = require(`./infer`) const { findOne, findManyPaginated } = require(`./resolvers`) -const { addFieldResolvers } = require(`./add-field-resolvers`) +const { + processFieldExtensions, + registerFieldExtension, +} = require(`./extensions`) const { getPagination } = require(`./types/pagination`) const { getSortInput } = require(`./types/sort`) const { getFilterInput } = require(`./types/filter`) @@ -88,6 +89,7 @@ const updateSchemaComposer = async ({ typeConflictReporter, parentSpan, }) => { + await registerExtensions({ parentSpan }) await addTypes({ schemaComposer, parentSpan, types }) await addInferredTypes({ schemaComposer, @@ -121,25 +123,29 @@ const processTypeComposer = async ({ nodeStore, parentSpan, }) => { - if ( - typeComposer instanceof ObjectTypeComposer && - typeComposer.hasInterface(`Node`) - ) { - await addFieldResolvers({ schemaComposer, typeComposer, parentSpan }) - await addNodeInterfaceFields({ schemaComposer, typeComposer, parentSpan }) - await addResolvers({ schemaComposer, typeComposer, parentSpan }) - await addConvenienceChildrenFields({ - schemaComposer, - typeComposer, - nodeStore, - parentSpan, - }) - await addTypeToRootQuery({ schemaComposer, typeComposer, parentSpan }) - } else if (typeComposer instanceof ObjectTypeComposer) { - await addFieldResolvers({ schemaComposer, typeComposer, parentSpan }) + if (typeComposer instanceof ObjectTypeComposer) { + await processFieldExtensions({ schemaComposer, typeComposer, parentSpan }) + if (typeComposer.hasInterface(`Node`)) { + await addNodeInterfaceFields({ schemaComposer, typeComposer, parentSpan }) + await addResolvers({ schemaComposer, typeComposer, parentSpan }) + await addConvenienceChildrenFields({ + schemaComposer, + typeComposer, + nodeStore, + parentSpan, + }) + await addTypeToRootQuery({ schemaComposer, typeComposer, parentSpan }) + } } } +const registerExtensions = ({ parentSpan }) => + apiRunner(`registerFieldExtension`, { + registerFieldExtension, + traceId: `initial-registerFieldExtensions`, + parentSpan: parentSpan, + }) + const addTypes = ({ schemaComposer, types, parentSpan }) => { types.forEach(({ typeOrTypeDef, plugin }) => { if (typeof typeOrTypeDef === `string`) { @@ -210,30 +216,22 @@ const processAddedType = ({ typeComposer.setExtension(`plugin`, plugin ? plugin.name : null) if (createdFrom === `sdl`) { - const ast = typeComposer.getType().astNode - if (ast && ast.directives) { - ast.directives.forEach(directive => { - if (directive.name.value === `infer`) { - typeComposer.setExtension(`infer`, true) - const addDefaultResolvers = getNoDefaultResolvers(directive) - if (addDefaultResolvers != null) { - typeComposer.setExtension( - `addDefaultResolvers`, - addDefaultResolvers - ) - } - } else if (directive.name.value === `dontInfer`) { - typeComposer.setExtension(`infer`, false) - const addDefaultResolvers = getNoDefaultResolvers(directive) - if (addDefaultResolvers != null) { + const directives = typeComposer.getDirectives() + directives.forEach(({ name, args }) => { + switch (name) { + case `infer`: + case `dontInfer`: + typeComposer.setExtension(`infer`, name === `infer`) + if (args.noDefaultResolvers != null) { typeComposer.setExtension( `addDefaultResolvers`, - addDefaultResolvers + !args.noDefaultResolvers ) } - } - }) - } + break + default: + } + }) } if ( @@ -249,46 +247,25 @@ const processAddedType = ({ ) if (createdFrom === `sdl`) { - const field = typeComposer.getField(fieldName) - if (field.astNode && field.astNode.directives) { - field.astNode.directives.forEach(directive => { - if (directive.name.value === `addResolver`) { - const options = {} - directive.arguments.forEach(argument => { - options[argument.name.value] = GraphQLJSON.parseLiteral( - argument.value - ) - }) - typeComposer.setFieldExtension(fieldName, `addResolver`, options) - } - }) - } + const directives = typeComposer.getFieldDirectives(fieldName) + directives.forEach(({ name, args }) => { + typeComposer.setFieldExtension(fieldName, name, args) + }) } }) } if (typeComposer.hasExtension(`addDefaultResolvers`)) { report.warn( - `Deprecation warning - "noDefaultResolvers" is deprecated. In Gatsby 3, defined fields won't get resolvers, unless "addResolver" directive/extension is used.` + `Deprecation warning - "noDefaultResolvers" is deprecated. In Gatsby 3, ` + + `defined fields won't get resolvers, unless \`addResolver\` ` + + `directive/extension is used.` ) } return typeComposer } -const getNoDefaultResolvers = directive => { - const noDefaultResolvers = directive.arguments.find( - ({ name }) => name.value === `noDefaultResolvers` - ) - if (noDefaultResolvers) { - if (noDefaultResolvers.value.kind === Kind.BOOLEAN) { - return !noDefaultResolvers.value.value - } - } - - return null -} - const checkIsAllowedTypeName = name => { invariant( name !== `Node`, @@ -543,8 +520,6 @@ const addResolvers = ({ schemaComposer, typeComposer }) => { sort: sortInputTC, skip: `Int`, limit: `Int`, - // page: `Int`, - // perPage: { type: `Int`, defaultValue: 20 }, }, resolve: findManyPaginated(typeName), }) diff --git a/packages/gatsby/src/schema/types/date.js b/packages/gatsby/src/schema/types/date.js index e17ec8a9be4c2..4c34d8838de3c 100644 --- a/packages/gatsby/src/schema/types/date.js +++ b/packages/gatsby/src/schema/types/date.js @@ -215,44 +215,48 @@ const formatDate = ({ return normalizedDate } -const dateResolver = { - type: `Date`, - args: { - formatString: { - type: GraphQLString, - description: oneLine` +const getDateResolver = defaults => { + const { locale, formatString } = defaults + return { + 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` + defaultValue: formatString, + }, + fromNow: { + type: GraphQLBoolean, + description: oneLine` Returns a string generated with Moment.js' \`fromNow\` function`, - }, - difference: { - type: GraphQLString, - description: oneLine` + }, + 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 + Defaults to "milliseconds" but you can also pass in as the measurement "years", "months", "weeks", "days", "hours", "minutes", and "seconds".`, - }, - locale: { - type: GraphQLString, - description: oneLine` + }, + locale: { + type: GraphQLString, + description: oneLine` Configures the locale Moment.js will use to format the date.`, + defaultValue: locale, + }, }, - }, - resolve(source, args, context, { fieldName }) { - const date = source[fieldName] - if (date == null) return null + resolve(source, args, context, { fieldName }) { + const date = source[fieldName] + if (date == null) return null - return Array.isArray(date) - ? date.map(d => formatDate({ date: d, ...args })) - : formatDate({ date, ...args }) - }, + return Array.isArray(date) + ? date.map(d => formatDate({ date: d, ...args })) + : formatDate({ date, ...args }) + }, + } } -module.exports = { GraphQLDate, dateResolver, isDate, looksLikeADate } +module.exports = { GraphQLDate, getDateResolver, isDate, looksLikeADate } diff --git a/packages/gatsby/src/schema/types/directives.js b/packages/gatsby/src/schema/types/directives.js deleted file mode 100644 index 7fd7e929fecbe..0000000000000 --- a/packages/gatsby/src/schema/types/directives.js +++ /dev/null @@ -1,56 +0,0 @@ -const { - GraphQLBoolean, - GraphQLNonNull, - GraphQLDirective, - GraphQLString, - DirectiveLocation, -} = require(`graphql`) -const { GraphQLJSON } = require(`graphql-compose`) - -const InferDirective = new GraphQLDirective({ - name: `infer`, - description: `Infer fields for this type from nodes.`, - locations: [DirectiveLocation.OBJECT], - args: { - noDefaultResolvers: { - type: GraphQLBoolean, - description: `Don't add default resolvers to defined fields.`, - deprecationReason: `noDefaultResolvers is deprecated, annotate individual fields.`, - }, - }, -}) - -const DontInferDirective = new GraphQLDirective({ - name: `dontInfer`, - description: `Do not infer additional fields for this type from nodes.`, - locations: [DirectiveLocation.OBJECT], - args: { - noDefaultResolvers: { - type: GraphQLBoolean, - description: `Don't add default resolvers to defined fields.`, - deprecationReason: `noDefaultResolvers is deprecated, annotate individual fields.`, - }, - }, -}) - -const AddResolver = new GraphQLDirective({ - name: `addResolver`, - description: `Add a resolver specified by "type" to field`, - locations: [DirectiveLocation.FIELD_DEFINITION], - args: { - type: { - type: new GraphQLNonNull(GraphQLString), - description: `Type of the resolver. Types available by default are: "date", "link" and "relativeFile".`, - }, - options: { - type: GraphQLJSON, - description: `Resolver options. Vary based on resolver type.`, - }, - }, -}) - -module.exports = { - InferDirective, - DontInferDirective, - AddResolver, -} diff --git a/packages/gatsby/src/utils/api-node-docs.js b/packages/gatsby/src/utils/api-node-docs.js index 8b778d8e1bffe..b78b7a20dda31 100644 --- a/packages/gatsby/src/utils/api-node-docs.js +++ b/packages/gatsby/src/utils/api-node-docs.js @@ -262,6 +262,38 @@ exports.setFieldsOnGraphQLNodeType = true */ exports.createResolvers = true +/** + * Register a GraphQL schema extension that can be used on field definitions. + * + * @param {object} $0 + * @param {function} $0.registerFieldExtension Register field extension + * @example + * exports.registerFieldExtension = ({ registerFieldExtension }) => { + * registerFieldExtension(`volume`, { + * description: `Adjust the volume.`, + * args: { + * loud: { type: `Boolean` } + * }, + * // Return a new partial field config to extend the previous field config with. + * // Receives extension `args` and the current field config as parameters. + * process(defaults, prevFieldConfig) { + * return { + * type: `String`, + * args: { + * loud: { type: `Boolean` } + * }, + * resolve(source, args, context, info) { + * const fieldValue = source[info.fieldName] + * const shouldUpperCase = args.loud != null ? args.loud : defaults.loud + * return shouldUpperCase ? fieldValue.toUpperCase() : fieldValue + * } + * } + * } + * }) + * } + */ +exports.registerFieldExtension = true + /** * Ask compile-to-js plugins to process source to JavaScript so the query * runner can extract out GraphQL queries for running. From 0a53e22b820069fc39fb4083fc1ad692c2b27040 Mon Sep 17 00:00:00 2001 From: stefanprobst Date: Tue, 30 Apr 2019 07:51:28 +0200 Subject: [PATCH 19/32] Revert "Allow registering field extensions (#13623)" (#13735) This reverts commit e40f6d210f006cf041486136b7348faf291de81a. --- .../gatsby/src/schema/add-field-resolvers.js | 84 +++++++ packages/gatsby/src/schema/context.js | 2 - .../gatsby/src/schema/extensions/index.js | 210 ------------------ .../src/schema/infer/__tests__/infer.js | 2 +- .../src/schema/infer/__tests__/merge-types.js | 2 +- packages/gatsby/src/schema/infer/index.js | 10 +- packages/gatsby/src/schema/schema-composer.js | 10 +- packages/gatsby/src/schema/schema.js | 113 ++++++---- packages/gatsby/src/schema/types/date.js | 60 +++-- .../gatsby/src/schema/types/directives.js | 56 +++++ packages/gatsby/src/utils/api-node-docs.js | 32 --- 11 files changed, 252 insertions(+), 329 deletions(-) create mode 100644 packages/gatsby/src/schema/add-field-resolvers.js delete mode 100644 packages/gatsby/src/schema/extensions/index.js create mode 100644 packages/gatsby/src/schema/types/directives.js diff --git a/packages/gatsby/src/schema/add-field-resolvers.js b/packages/gatsby/src/schema/add-field-resolvers.js new file mode 100644 index 0000000000000..5b4959cf83f0f --- /dev/null +++ b/packages/gatsby/src/schema/add-field-resolvers.js @@ -0,0 +1,84 @@ +const _ = require(`lodash`) +const { defaultFieldResolver } = require(`graphql`) +const { dateResolver } = require(`./types/date`) +const { link, fileByPath } = require(`./resolvers`) + +export const addFieldResolvers = ({ + schemaComposer, + typeComposer, + parentSpan, +}) => { + typeComposer.getFieldNames().forEach(fieldName => { + let field = typeComposer.getField(fieldName) + + const extensions = typeComposer.getFieldExtensions(fieldName) + if ( + !field.resolve && + extensions.addResolver && + _.isObject(extensions.addResolver) + ) { + const options = extensions.addResolver.options || {} + switch (extensions.addResolver.type) { + case `dateformat`: { + addDateResolver({ + typeComposer, + fieldName, + options, + }) + break + } + case `link`: { + typeComposer.extendField(fieldName, { + resolve: link({ from: options.from, by: options.by }), + }) + break + } + case `fileByRelativePath`: { + typeComposer.extendField(fieldName, { + resolve: fileByPath({ from: options.from }), + }) + break + } + } + } + + if (extensions.proxyFrom) { + // XXX(freiksenet): get field again cause it will be changed because of above + field = typeComposer.getField(fieldName) + const resolver = field.resolve || defaultFieldResolver + typeComposer.extendField(fieldName, { + resolve: (source, args, context, info) => + resolver(source, args, context, { + ...info, + fieldName: extensions.proxyFrom, + }), + }) + } + }) + return typeComposer +} + +const addDateResolver = ({ + typeComposer, + fieldName, + options: { formatString, locale }, +}) => { + const field = typeComposer.getField(fieldName) + + let fieldConfig = { + resolve: dateResolver.resolve, + } + if (!field.args || _.isEmpty(field.args)) { + fieldConfig.args = { + ...dateResolver.args, + } + if (formatString) { + fieldConfig.args.formatString.defaultValue = formatString + } + if (locale) { + fieldConfig.args.locale.defaultValue = locale + } + } + + typeComposer.extendField(fieldName, fieldConfig) +} diff --git a/packages/gatsby/src/schema/context.js b/packages/gatsby/src/schema/context.js index e02c7cccf0494..9e037af253dd9 100644 --- a/packages/gatsby/src/schema/context.js +++ b/packages/gatsby/src/schema/context.js @@ -1,5 +1,4 @@ const { LocalNodeModel } = require(`./node-model`) -const { fieldExtensions } = require(`./extensions`) const withResolverContext = (context, schema) => { const nodeStore = require(`../db/nodes`) @@ -7,7 +6,6 @@ const withResolverContext = (context, schema) => { return { ...context, - fieldExtensions, nodeModel: new LocalNodeModel({ nodeStore, schema, diff --git a/packages/gatsby/src/schema/extensions/index.js b/packages/gatsby/src/schema/extensions/index.js deleted file mode 100644 index 6138b483316d1..0000000000000 --- a/packages/gatsby/src/schema/extensions/index.js +++ /dev/null @@ -1,210 +0,0 @@ -const { - GraphQLBoolean, - GraphQLNonNull, - GraphQLDirective, - GraphQLString, - DirectiveLocation, - defaultFieldResolver, -} = require(`graphql`) -const { GraphQLJSON } = require(`graphql-compose`) -const report = require(`gatsby-cli/lib/reporter`) - -const { link, fileByPath } = require(`../resolvers`) -const { getDateResolver } = require(`../types/date`) - -// Reserved for internal use -const internalExtensionNames = [ - `addDefaultResolvers`, - `createdFrom`, - `directives`, - `infer`, - `plugin`, -] - -const typeExtensions = { - infer: { - description: `Infer field types from field values.`, - args: { - noDefaultResolvers: { - type: GraphQLBoolean, - description: `Don't add default resolvers to defined fields.`, - deprecationReason: `noDefaultResolvers is deprecated, annotate individual fields.`, - }, - }, - }, - dontInfer: { - description: `Do not infer field types from field values.`, - args: { - noDefaultResolvers: { - type: GraphQLBoolean, - description: `Don't add default resolvers to defined fields.`, - deprecationReason: `noDefaultResolvers is deprecated, annotate individual fields.`, - }, - }, - }, -} - -const fieldExtensions = { - add: { - description: `Generic directive to add field extension.`, - args: { - extension: { - type: new GraphQLNonNull(GraphQLString), - }, - options: { - type: GraphQLJSON, - }, - }, - }, - - addResolver: { - description: `Add a resolver specified by "type" to field.`, - args: { - type: { - type: new GraphQLNonNull(GraphQLString), - description: - `Type of the resolver. Types available by default are: ` + - `"dateformat", "link" and "fileByRelativePath".`, - }, - options: { - type: GraphQLJSON, - description: `Resolver options. Vary based on resolver type.`, - }, - }, - process(args, fieldConfig) { - const { process } = fieldExtensions[args.type] || {} - if (typeof process === `function`) { - return process(args.options || {}, fieldConfig) - } - return {} - }, - }, - - dateformat: { - description: `Add date formating options.`, - args: { - formatString: { type: GraphQLString }, - locale: { type: GraphQLString }, - }, - process(args, fieldConfig) { - return getDateResolver(args) - }, - }, - - link: { - description: `Link to node by foreign-key relation.`, - args: { - by: { - type: new GraphQLNonNull(GraphQLString), - defaultValue: `id`, - }, - from: { - type: GraphQLString, - }, - }, - process(args, fieldConfig) { - return { - resolve: link(args), - } - }, - }, - - fileByRelativePath: { - description: `Link to File node by relative path.`, - args: { - from: { - type: GraphQLString, - }, - }, - process(args, fieldConfig) { - return { - resolve: fileByPath(args), - } - }, - }, - - // projection: { - // description: `Automatically add fields to selection set.`, - // args: {}, - // process(args, fieldConfig) {}, - // }, - - proxyFrom: { - description: `Proxy resolver from another field.`, - process(from, fieldConfig) { - const resolver = fieldConfig.resolve || defaultFieldResolver - return { - resolve(source, args, context, info) { - return resolver(source, args, context, { - ...info, - fieldName: from, - }) - }, - } - }, - }, -} - -const toDirectives = ({ extensions, locations }) => - Object.keys(extensions).map(name => { - const extension = extensions[name] - const { args, description } = extension - return new GraphQLDirective({ name, args, description, locations }) - }) - -const addDirectives = ({ schemaComposer }) => { - const fieldDirectives = toDirectives({ - extensions: fieldExtensions, - locations: [DirectiveLocation.FIELD_DEFINITION], - }) - fieldDirectives.forEach(directive => schemaComposer.addDirective(directive)) - const typeDirectives = toDirectives({ - extensions: typeExtensions, - locations: [DirectiveLocation.OBJECT], - }) - typeDirectives.forEach(directive => schemaComposer.addDirective(directive)) -} - -const processFieldExtensions = ({ - schemaComposer, - typeComposer, - parentSpan, -}) => { - typeComposer.getFieldNames().forEach(fieldName => { - const extensions = typeComposer.getFieldExtensions(fieldName) - Object.keys(extensions) - .filter(name => !internalExtensionNames.includes(name)) - .sort(a => a === `proxyFrom`) // Ensure `proxyFrom` is run last - .forEach(name => { - const { process } = fieldExtensions[name] || {} - if (process) { - // Always get fresh field config as it will have been changed - // by previous field extension - const prevFieldConfig = typeComposer.getFieldConfig(fieldName) - typeComposer.extendField( - fieldName, - process(extensions[name], prevFieldConfig) - ) - } - }) - }) -} - -const registerFieldExtension = (name, extension) => { - if (internalExtensionNames.includes(name)) { - report.error(`The extension ${name} is reserved for internal use.`) - } else if (!fieldExtensions[name]) { - fieldExtensions[name] = extension - } else { - report.error( - `A field extension with the name ${name} is already registered.` - ) - } -} - -module.exports = { - addDirectives, - fieldExtensions, - processFieldExtensions, - registerFieldExtension, -} diff --git a/packages/gatsby/src/schema/infer/__tests__/infer.js b/packages/gatsby/src/schema/infer/__tests__/infer.js index fba26902540ca..acda4be1bab69 100644 --- a/packages/gatsby/src/schema/infer/__tests__/infer.js +++ b/packages/gatsby/src/schema/infer/__tests__/infer.js @@ -320,7 +320,7 @@ describe(`GraphQL type inference`, () => { ` with_space with_hyphen - with_resolver(formatString: "DD.MM.YYYY") + with_resolver(formatString:"DD.MM.YYYY") _123 _456 { testingTypeNameCreation diff --git a/packages/gatsby/src/schema/infer/__tests__/merge-types.js b/packages/gatsby/src/schema/infer/__tests__/merge-types.js index fa589f7357e21..9abc21da2b141 100644 --- a/packages/gatsby/src/schema/infer/__tests__/merge-types.js +++ b/packages/gatsby/src/schema/infer/__tests__/merge-types.js @@ -551,7 +551,7 @@ describe(`merges explicit and inferred type definitions`, () => { expect(inferDate.resolve).toBeDefined() }) - it.todo(`adds explicit resolvers through extensions`) + it(`adds explicit resolvers through extensions`, async () => {}) it(`honors array depth when merging types`, async () => { const typeDefs = ` diff --git a/packages/gatsby/src/schema/infer/index.js b/packages/gatsby/src/schema/infer/index.js index 49c2a720dd626..031141d138624 100644 --- a/packages/gatsby/src/schema/infer/index.js +++ b/packages/gatsby/src/schema/infer/index.js @@ -33,7 +33,7 @@ const addInferredTypes = ({ : true if (runInfer) { if (!typeComposer.hasInterface(`Node`)) { - noNodeInterfaceTypes.push(typeName) + noNodeInterfaceTypes.push(typeComposer) } typesToInfer.push(typeComposer) } @@ -46,16 +46,16 @@ const addInferredTypes = ({ }) if (noNodeInterfaceTypes.length > 0) { - noNodeInterfaceTypes.forEach(typeName => { + noNodeInterfaceTypes.forEach(type => { report.warn( - `Type \`${typeName}\` declared in \`createTypes\` looks like a node, ` + + `Type \`${type}\` declared in \`createTypes\` looks like a node, ` + `but doesn't implement a \`Node\` interface. It's likely that you should ` + `add the \`Node\` interface to your type def:\n\n` + - `\`type ${typeName} implements Node { ... }\`\n\n` + + `\`type ${type} implements Node { ... }\`\n\n` + `If you know that you don't want it to be a node (which would mean no ` + `root queries to retrieve it), you can explicitly disable inference ` + `for it:\n\n` + - `\`type ${typeName} @dontInfer { ... }\`` + `\`type ${type} @dontInfer { ... }\`` ) }) report.panic(`Building schema failed`) diff --git a/packages/gatsby/src/schema/schema-composer.js b/packages/gatsby/src/schema/schema-composer.js index 8d62f3ebade26..21e391faf1c8f 100644 --- a/packages/gatsby/src/schema/schema-composer.js +++ b/packages/gatsby/src/schema/schema-composer.js @@ -1,14 +1,20 @@ const { SchemaComposer, GraphQLJSON } = require(`graphql-compose`) const { getNodeInterface } = require(`./types/node-interface`) const { GraphQLDate } = require(`./types/date`) -const { addDirectives } = require(`./extensions`) +const { + InferDirective, + DontInferDirective, + AddResolver, +} = require(`./types/directives`) const createSchemaComposer = () => { const schemaComposer = new SchemaComposer() getNodeInterface({ schemaComposer }) schemaComposer.addAsComposer(GraphQLDate) schemaComposer.addAsComposer(GraphQLJSON) - addDirectives({ schemaComposer }) + schemaComposer.addDirective(InferDirective) + schemaComposer.addDirective(DontInferDirective) + schemaComposer.addDirective(AddResolver) return schemaComposer } diff --git a/packages/gatsby/src/schema/schema.js b/packages/gatsby/src/schema/schema.js index 26b5bb0b53f10..f1a0dc32e58c0 100644 --- a/packages/gatsby/src/schema/schema.js +++ b/packages/gatsby/src/schema/schema.js @@ -5,12 +5,14 @@ const { isIntrospectionType, defaultFieldResolver, assertValidName, + Kind, } = require(`graphql`) const { ObjectTypeComposer, InterfaceTypeComposer, UnionTypeComposer, InputTypeComposer, + GraphQLJSON, } = require(`graphql-compose`) const apiRunner = require(`../utils/api-runner-node`) @@ -18,10 +20,7 @@ const report = require(`gatsby-cli/lib/reporter`) const { addNodeInterfaceFields } = require(`./types/node-interface`) const { addInferredType, addInferredTypes } = require(`./infer`) const { findOne, findManyPaginated } = require(`./resolvers`) -const { - processFieldExtensions, - registerFieldExtension, -} = require(`./extensions`) +const { addFieldResolvers } = require(`./add-field-resolvers`) const { getPagination } = require(`./types/pagination`) const { getSortInput } = require(`./types/sort`) const { getFilterInput } = require(`./types/filter`) @@ -89,7 +88,6 @@ const updateSchemaComposer = async ({ typeConflictReporter, parentSpan, }) => { - await registerExtensions({ parentSpan }) await addTypes({ schemaComposer, parentSpan, types }) await addInferredTypes({ schemaComposer, @@ -123,29 +121,25 @@ const processTypeComposer = async ({ nodeStore, parentSpan, }) => { - if (typeComposer instanceof ObjectTypeComposer) { - await processFieldExtensions({ schemaComposer, typeComposer, parentSpan }) - if (typeComposer.hasInterface(`Node`)) { - await addNodeInterfaceFields({ schemaComposer, typeComposer, parentSpan }) - await addResolvers({ schemaComposer, typeComposer, parentSpan }) - await addConvenienceChildrenFields({ - schemaComposer, - typeComposer, - nodeStore, - parentSpan, - }) - await addTypeToRootQuery({ schemaComposer, typeComposer, parentSpan }) - } + if ( + typeComposer instanceof ObjectTypeComposer && + typeComposer.hasInterface(`Node`) + ) { + await addFieldResolvers({ schemaComposer, typeComposer, parentSpan }) + await addNodeInterfaceFields({ schemaComposer, typeComposer, parentSpan }) + await addResolvers({ schemaComposer, typeComposer, parentSpan }) + await addConvenienceChildrenFields({ + schemaComposer, + typeComposer, + nodeStore, + parentSpan, + }) + await addTypeToRootQuery({ schemaComposer, typeComposer, parentSpan }) + } else if (typeComposer instanceof ObjectTypeComposer) { + await addFieldResolvers({ schemaComposer, typeComposer, parentSpan }) } } -const registerExtensions = ({ parentSpan }) => - apiRunner(`registerFieldExtension`, { - registerFieldExtension, - traceId: `initial-registerFieldExtensions`, - parentSpan: parentSpan, - }) - const addTypes = ({ schemaComposer, types, parentSpan }) => { types.forEach(({ typeOrTypeDef, plugin }) => { if (typeof typeOrTypeDef === `string`) { @@ -216,22 +210,30 @@ const processAddedType = ({ typeComposer.setExtension(`plugin`, plugin ? plugin.name : null) if (createdFrom === `sdl`) { - const directives = typeComposer.getDirectives() - directives.forEach(({ name, args }) => { - switch (name) { - case `infer`: - case `dontInfer`: - typeComposer.setExtension(`infer`, name === `infer`) - if (args.noDefaultResolvers != null) { + const ast = typeComposer.getType().astNode + if (ast && ast.directives) { + ast.directives.forEach(directive => { + if (directive.name.value === `infer`) { + typeComposer.setExtension(`infer`, true) + const addDefaultResolvers = getNoDefaultResolvers(directive) + if (addDefaultResolvers != null) { typeComposer.setExtension( `addDefaultResolvers`, - !args.noDefaultResolvers + addDefaultResolvers ) } - break - default: - } - }) + } else if (directive.name.value === `dontInfer`) { + typeComposer.setExtension(`infer`, false) + const addDefaultResolvers = getNoDefaultResolvers(directive) + if (addDefaultResolvers != null) { + typeComposer.setExtension( + `addDefaultResolvers`, + addDefaultResolvers + ) + } + } + }) + } } if ( @@ -247,25 +249,46 @@ const processAddedType = ({ ) if (createdFrom === `sdl`) { - const directives = typeComposer.getFieldDirectives(fieldName) - directives.forEach(({ name, args }) => { - typeComposer.setFieldExtension(fieldName, name, args) - }) + const field = typeComposer.getField(fieldName) + if (field.astNode && field.astNode.directives) { + field.astNode.directives.forEach(directive => { + if (directive.name.value === `addResolver`) { + const options = {} + directive.arguments.forEach(argument => { + options[argument.name.value] = GraphQLJSON.parseLiteral( + argument.value + ) + }) + typeComposer.setFieldExtension(fieldName, `addResolver`, options) + } + }) + } } }) } if (typeComposer.hasExtension(`addDefaultResolvers`)) { report.warn( - `Deprecation warning - "noDefaultResolvers" is deprecated. In Gatsby 3, ` + - `defined fields won't get resolvers, unless \`addResolver\` ` + - `directive/extension is used.` + `Deprecation warning - "noDefaultResolvers" is deprecated. In Gatsby 3, defined fields won't get resolvers, unless "addResolver" directive/extension is used.` ) } return typeComposer } +const getNoDefaultResolvers = directive => { + const noDefaultResolvers = directive.arguments.find( + ({ name }) => name.value === `noDefaultResolvers` + ) + if (noDefaultResolvers) { + if (noDefaultResolvers.value.kind === Kind.BOOLEAN) { + return !noDefaultResolvers.value.value + } + } + + return null +} + const checkIsAllowedTypeName = name => { invariant( name !== `Node`, @@ -520,6 +543,8 @@ const addResolvers = ({ schemaComposer, typeComposer }) => { sort: sortInputTC, skip: `Int`, limit: `Int`, + // page: `Int`, + // perPage: { type: `Int`, defaultValue: 20 }, }, resolve: findManyPaginated(typeName), }) diff --git a/packages/gatsby/src/schema/types/date.js b/packages/gatsby/src/schema/types/date.js index 4c34d8838de3c..e17ec8a9be4c2 100644 --- a/packages/gatsby/src/schema/types/date.js +++ b/packages/gatsby/src/schema/types/date.js @@ -215,48 +215,44 @@ const formatDate = ({ return normalizedDate } -const getDateResolver = defaults => { - const { locale, formatString } = defaults - return { - args: { - formatString: { - type: GraphQLString, - description: oneLine` +const dateResolver = { + type: `Date`, + 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.`, - defaultValue: formatString, - }, - fromNow: { - type: GraphQLBoolean, - description: oneLine` + }, + fromNow: { + type: GraphQLBoolean, + description: oneLine` Returns a string generated with Moment.js' \`fromNow\` function`, - }, - difference: { - type: GraphQLString, - description: oneLine` + }, + difference: { + type: GraphQLString, + description: oneLine` Returns the difference between this date and the current time. - Defaults to "milliseconds" but you can also pass in as the + 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` + }, + locale: { + type: GraphQLString, + description: oneLine` Configures the locale Moment.js will use to format the date.`, - defaultValue: locale, - }, }, - resolve(source, args, context, { fieldName }) { - const date = source[fieldName] - if (date == null) return null + }, + resolve(source, args, context, { fieldName }) { + const date = source[fieldName] + if (date == null) return null - return Array.isArray(date) - ? date.map(d => formatDate({ date: d, ...args })) - : formatDate({ date, ...args }) - }, - } + return Array.isArray(date) + ? date.map(d => formatDate({ date: d, ...args })) + : formatDate({ date, ...args }) + }, } -module.exports = { GraphQLDate, getDateResolver, isDate, looksLikeADate } +module.exports = { GraphQLDate, dateResolver, isDate, looksLikeADate } diff --git a/packages/gatsby/src/schema/types/directives.js b/packages/gatsby/src/schema/types/directives.js new file mode 100644 index 0000000000000..7fd7e929fecbe --- /dev/null +++ b/packages/gatsby/src/schema/types/directives.js @@ -0,0 +1,56 @@ +const { + GraphQLBoolean, + GraphQLNonNull, + GraphQLDirective, + GraphQLString, + DirectiveLocation, +} = require(`graphql`) +const { GraphQLJSON } = require(`graphql-compose`) + +const InferDirective = new GraphQLDirective({ + name: `infer`, + description: `Infer fields for this type from nodes.`, + locations: [DirectiveLocation.OBJECT], + args: { + noDefaultResolvers: { + type: GraphQLBoolean, + description: `Don't add default resolvers to defined fields.`, + deprecationReason: `noDefaultResolvers is deprecated, annotate individual fields.`, + }, + }, +}) + +const DontInferDirective = new GraphQLDirective({ + name: `dontInfer`, + description: `Do not infer additional fields for this type from nodes.`, + locations: [DirectiveLocation.OBJECT], + args: { + noDefaultResolvers: { + type: GraphQLBoolean, + description: `Don't add default resolvers to defined fields.`, + deprecationReason: `noDefaultResolvers is deprecated, annotate individual fields.`, + }, + }, +}) + +const AddResolver = new GraphQLDirective({ + name: `addResolver`, + description: `Add a resolver specified by "type" to field`, + locations: [DirectiveLocation.FIELD_DEFINITION], + args: { + type: { + type: new GraphQLNonNull(GraphQLString), + description: `Type of the resolver. Types available by default are: "date", "link" and "relativeFile".`, + }, + options: { + type: GraphQLJSON, + description: `Resolver options. Vary based on resolver type.`, + }, + }, +}) + +module.exports = { + InferDirective, + DontInferDirective, + AddResolver, +} diff --git a/packages/gatsby/src/utils/api-node-docs.js b/packages/gatsby/src/utils/api-node-docs.js index b78b7a20dda31..8b778d8e1bffe 100644 --- a/packages/gatsby/src/utils/api-node-docs.js +++ b/packages/gatsby/src/utils/api-node-docs.js @@ -262,38 +262,6 @@ exports.setFieldsOnGraphQLNodeType = true */ exports.createResolvers = true -/** - * Register a GraphQL schema extension that can be used on field definitions. - * - * @param {object} $0 - * @param {function} $0.registerFieldExtension Register field extension - * @example - * exports.registerFieldExtension = ({ registerFieldExtension }) => { - * registerFieldExtension(`volume`, { - * description: `Adjust the volume.`, - * args: { - * loud: { type: `Boolean` } - * }, - * // Return a new partial field config to extend the previous field config with. - * // Receives extension `args` and the current field config as parameters. - * process(defaults, prevFieldConfig) { - * return { - * type: `String`, - * args: { - * loud: { type: `Boolean` } - * }, - * resolve(source, args, context, info) { - * const fieldValue = source[info.fieldName] - * const shouldUpperCase = args.loud != null ? args.loud : defaults.loud - * return shouldUpperCase ? fieldValue.toUpperCase() : fieldValue - * } - * } - * } - * }) - * } - */ -exports.registerFieldExtension = true - /** * Ask compile-to-js plugins to process source to JavaScript so the query * runner can extract out GraphQL queries for running. From 9bad5a44acec51b557c049f546f1a79ead8bc17a Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Mon, 29 Apr 2019 10:29:17 +0300 Subject: [PATCH 20/32] v2.4.0-alpha.3 --- packages/gatsby/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index e98eaba1a361d..ffe4d69c900ce 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -1,7 +1,7 @@ { "name": "gatsby", "description": "Blazing fast modern site generator for React", - "version": "2.4.0-alpha.2", + "version": "2.4.0-alpha.3", "author": "Kyle Mathews ", "bin": { "gatsby": "./dist/bin/gatsby.js" From 06b612ad2fa7e3b8cd5e64a258d00f75d0269f8f Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Thu, 2 May 2019 15:35:39 +0300 Subject: [PATCH 21/32] Add blog post --- .../index.md | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 docs/blog/2019-04-23-improvements-to-schema-customization/index.md diff --git a/docs/blog/2019-04-23-improvements-to-schema-customization/index.md b/docs/blog/2019-04-23-improvements-to-schema-customization/index.md new file mode 100644 index 0000000000000..a0abbc6a3ac77 --- /dev/null +++ b/docs/blog/2019-04-23-improvements-to-schema-customization/index.md @@ -0,0 +1,98 @@ +--- +title: Improvements to Schema Customization API - Available in Gatsby 2.4.0 +date: 2019-04-23 +author: Mikhail Novikov +tags: + - schema + - graphql +--- + +Today we are releasing further improvements to the schema customization that [we've released in version 2.2.0](/blog/2019-03-18-releasing-new-schema-customization). You can use them with Gatsby 2.4.0. + +It is now possible to indicate to Gatsby, that you want to add a resolver to an explicitly defined fields. Use `addResolver` to add default arguments or/and resolvers to fields. In addition, when `@dontInfer` is set, Gatsby will no longer run inference for marked type, allowing one to improve performance for large data sets. + +## Summary + +After about a month of testing schema customization both here and in pre-release we determined a couple of issues. We set up to do this because we wanted to remove uncertainty in people's schemas, when the data changes. However, the original design allowed some uncertainties anyway. In addition, we have made inferrence a more heavy process, trading performance for consistency and didn't really provide a way to opt out of it completely. To summarize: + +- Resolvers and arguments of fields like Date and File was determined by inferred data +- There was no easy way to use arguments/resolvers to override the above +- Inferrence was run even when `@dontInfer` flag was on +- There was no way to control inference outside of SDL, eg in Type Builders + +Therefore we have some changes to the way we do inferrence. In addition, we are deprecating some of the features introduced is 2.2.0 and will remove them in Gatsby 3. + +## noDefaultResolvers and inferrence modes + +First of all, we are deprecating `noDefaultResolvers`. It was an argument of `infer` and `dontInfer`. We feel it was confusing and in some cases it didn't even actually add resolvers :). We will support `noDefaultResolvers` until version 3, after which `@infer` behaviour (see below) will become a default and `noDefaultResolvers` will be removed. + +We didn't want to break things, so we keep old default behaviour, even though we think it's not optimal. Add explicit `@infer` and `@addResolver` to fields to be future proof. + +### Default (deprecated, removed in v3) + +Applies with no `@infer` and no `@dontInfer` on a type. Equals to `@infer(noDefaultResolvers: false)`. + +Type gets all inferred fields added. If type has defined fields of types `Date`, `File` and any other node, and we inferred that they should have resolver options, resolver options will be added to type with a warning. + +### Strict inference (future default in v3) + +Applies with `@infer` or `@infer(noDefaultResolvers: true)`. + +Type gets all inferred fields added. Existing fields won't automatically get resolvers (use `@addResolver` directive). + +### No inferrence + +Applies with `@dontInfer` or `@dontInfer(noDefaultResolvers: true)`. + +Inferrence won't run at all. Existing fields won't automatically get resolvers (use @addResolver directive). + +### No new fields with default resolvers (deprecated, removed in v3) + +Applies with `@dontInfer(noDefaultResolvers: false)` + +Inferrence will run, but fields won't be added. If type has defined fields of types `Date`, `File` and any other node, and we inferred that they should have resolvers/args, resolvers/args will be added to type with a warning. + +## `addResolver` + +Add resolver and resolver options (such as arguments) to the given field. + +```graphql +type MyType @infer { + date: Date @addResolver(type: "dateformat", options: { formatString: "DD MMM", locale: "fi" }) + image: File @addResolver(type: "fileByRelativePath") + authorByEmail: Author @addResolver(type: "link", { by: "email" }) +} +``` + +### Type Builders and extensions + +You can now apply configuration to type builder types through extension property on them. + +```js +schema.createObjectType({ + name: MyType, + extensions: { + infer: true, + }, + fields: { + date: { + type: "Date", + extensions: { + addResolver: { + type: "dateformat", + options: { formatString: "DD MMM", locale: "fi" }, + }, + }, + }, + }, +}) +``` + +## Conclusions + +With these improvements we hope we'll solve most of the issues that people are having with new schema customization. We are working on further improvements, like allowing users and plugins to define their own extensions (see [PR #13738](https://github.com/gatsbyjs/gatsby/pull/13738)). + +Useful links: + +- [Documentation]() +- [Umbrella issue for schema customization bug reports](https://github.com/gatsbyjs/gatsby/issues/12272) From 446c60a39d2adb8b96374e857d22b3f18dfcb6df Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Thu, 2 May 2019 15:52:43 +0300 Subject: [PATCH 22/32] Update docs --- packages/gatsby/src/redux/actions.js | 62 +++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/packages/gatsby/src/redux/actions.js b/packages/gatsby/src/redux/actions.js index 12c115dc5f7fa..6abf279235696 100644 --- a/packages/gatsby/src/redux/actions.js +++ b/packages/gatsby/src/redux/actions.js @@ -1245,8 +1245,13 @@ import type GatsbyGraphQLType from "../schema/types/type-builders" * with inferred field types, and default field resolvers for `Date` (which * adds formatting options) and `File` (which resolves the field value as * a `relativePath` foreign-key field) are added. This behavior can be - * customised with `@infer` and `@dontInfer` directives, and their - * `noDefaultResolvers` argument. + * customised with `@infer`, `@dontInfer` and `@addResolvers` directives. + * + * + * `@infer` - run inference on the type and add fields that don't exist on the + * defined type to it. + * `@dontInfer` - don't run any inference on the type + * `@addResolver` - add resolver options to a field * * @example * exports.sourceNodes = ({ actions }) => { @@ -1255,17 +1260,17 @@ import type GatsbyGraphQLType from "../schema/types/type-builders" * """ * Markdown Node * """ - * type MarkdownRemark implements Node { + * type MarkdownRemark implements Node @infer { * frontmatter: Frontmatter! * } * * """ * Markdown Frontmatter * """ - * type Frontmatter { + * type Frontmatter @infer { * title: String! - * author: AuthorJson! - * date: Date! + * author: AuthorJson! @addResolver(type: "link") + * date: Date! @addResolver(type: "dateformat") * published: Boolean! * tags: [String!]! * } @@ -1274,9 +1279,9 @@ import type GatsbyGraphQLType from "../schema/types/type-builders" * Author information * """ * # Does not include automatically inferred fields - * type AuthorJson implements Node @dontInfer(noDefaultResolvers: true) { + * type AuthorJson implements Node @dontInfer { * name: String! - * birthday: Date! # no default resolvers for Date formatting added + * birthday: Date! @addResolver(type: "dateformat") * } * ` * createTypes(typeDefs) @@ -1292,6 +1297,9 @@ import type GatsbyGraphQLType from "../schema/types/type-builders" * frontmatter: 'Frontmatter!' * }, * interfaces: ['Node'], + * extensions: { + * infer: true, + * }, * }), * schema.buildObjectType({ * name: 'Frontmatter', @@ -1302,12 +1310,44 @@ import type GatsbyGraphQLType from "../schema/types/type-builders" * return parent.title || '(Untitled)' * } * }, - * author: 'AuthorJson!', - * date: 'Date!', + * author: { + * type: 'AuthorJson' + * extensions: { + * addResolver: { + * type: 'link', + * }, + * }, + * } + * date: { + * type: 'Date!' + * extensions: { + * addResolver: { + * type: 'date', + * }, + * }, + * }, * published: 'Boolean!', * tags: '[String!]!', * } - * }) + * }), + * schema.buildObjectType({ + * name: 'AuthorJson', + * fields: { + * name: 'String!' + * birthday: { + * type: 'Date!' + * extensions: { + * addResolver: { + * type: 'date', + * }, + * }, + * }, + * }, + * interfaces: ['Node'], + * extensions: { + * infer: false, + * }, + * }), * ] * createTypes(typeDefs) * } From 2bcdc403a37fe98724e57c5ba6156422ce86dff8 Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Thu, 2 May 2019 16:04:48 +0300 Subject: [PATCH 23/32] Fix link --- .../2019-04-23-improvements-to-schema-customization/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blog/2019-04-23-improvements-to-schema-customization/index.md b/docs/blog/2019-04-23-improvements-to-schema-customization/index.md index a0abbc6a3ac77..725691a1cbb5e 100644 --- a/docs/blog/2019-04-23-improvements-to-schema-customization/index.md +++ b/docs/blog/2019-04-23-improvements-to-schema-customization/index.md @@ -94,5 +94,5 @@ With these improvements we hope we'll solve most of the issues that people are h Useful links: -- [Documentation]() +- [createTypes Documentation](https://www.gatsbyjs.org/docs/actions/#createTypes) - [Umbrella issue for schema customization bug reports](https://github.com/gatsbyjs/gatsby/issues/12272) From f2e1986e4040a5bd7e3c8de91fcfed30847e681f Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Thu, 2 May 2019 16:32:04 +0300 Subject: [PATCH 24/32] Improve docs --- packages/gatsby/src/redux/actions.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/gatsby/src/redux/actions.js b/packages/gatsby/src/redux/actions.js index 6abf279235696..f5c7dc06688bc 100644 --- a/packages/gatsby/src/redux/actions.js +++ b/packages/gatsby/src/redux/actions.js @@ -1248,10 +1248,24 @@ import type GatsbyGraphQLType from "../schema/types/type-builders" * customised with `@infer`, `@dontInfer` and `@addResolvers` directives. * * - * `@infer` - run inference on the type and add fields that don't exist on the + * Schema customization controls: + * * `@infer` - run inference on the type and add fields that don't exist on the * defined type to it. - * `@dontInfer` - don't run any inference on the type - * `@addResolver` - add resolver options to a field + * * `@dontInfer` - don't run any inference on the type + * * `@addResolver` - add resolver options to a field. Args are `type` and + * `options`. Type can be `dateformat`, `link` and `fileByRelativePath`. + * + * Resolver types: + * * `dateformat` - add date formatting arguments. Accepts `formatString` and + * `locale` options that sets the defaults for this field + * * `link` - connect to a different Node. Arguments `by` and `from`, which + * define which field to compare to on a remote node and which field to use on + * the source node + * * `fileByRelativePath` - connect to a File node. Same arguments. The + * difference from link is that this normalizes the relative path to be + * relative from the path where source node is found. + * + * * * @example * exports.sourceNodes = ({ actions }) => { From 5bf9bb8c6c42cd67e259a9a13e9190571278b9cb Mon Sep 17 00:00:00 2001 From: stefanprobst Date: Tue, 14 May 2019 13:52:27 +0200 Subject: [PATCH 25/32] [schema] Allow registering custom field extensions (#13738) * Allow registering field extensions * Update packages/gatsby/src/utils/api-node-docs.js Co-Authored-By: stefanprobst * Update packages/gatsby/src/schema/schema.js Co-Authored-By: stefanprobst * Rename to createFieldExtension * Apply review suggestion * Remove addResolver extension * Add test * Remove public createFieldExtension API * Lint --- .../src/schema/__tests__/kitchen-sink.js | 4 +- .../src/schema/__tests__/queries-file.js | 2 +- .../gatsby/src/schema/add-field-resolvers.js | 84 ---------- .../gatsby/src/schema/extensions/index.js | 147 ++++++++++++++++++ .../src/schema/infer/__tests__/infer.js | 2 +- .../src/schema/infer/__tests__/merge-types.js | 60 ++++++- .../src/schema/infer/add-inferred-fields.js | 50 +++--- packages/gatsby/src/schema/infer/index.js | 10 +- packages/gatsby/src/schema/schema-composer.js | 10 +- packages/gatsby/src/schema/schema.js | 102 ++++-------- packages/gatsby/src/schema/types/date.js | 60 +++---- .../gatsby/src/schema/types/directives.js | 56 ------- 12 files changed, 303 insertions(+), 284 deletions(-) delete mode 100644 packages/gatsby/src/schema/add-field-resolvers.js create mode 100644 packages/gatsby/src/schema/extensions/index.js delete mode 100644 packages/gatsby/src/schema/types/directives.js diff --git a/packages/gatsby/src/schema/__tests__/kitchen-sink.js b/packages/gatsby/src/schema/__tests__/kitchen-sink.js index a6540bdcebf64..a5ddbb49be580 100644 --- a/packages/gatsby/src/schema/__tests__/kitchen-sink.js +++ b/packages/gatsby/src/schema/__tests__/kitchen-sink.js @@ -56,9 +56,9 @@ describe(`Kitchen sink schema test`, () => { payload: ` type PostsJson implements Node @infer { id: String! - time: Date @addResolver(type: "dateformat", options: { locale: "fi", formatString: "DD MMMM"}) + time: Date @dateformat(locale: "fi", formatString: "DD MMMM") code: String - image: File @addResolver(type: "fileByRelativePath") + image: File @fileByRelativePath } `, }) diff --git a/packages/gatsby/src/schema/__tests__/queries-file.js b/packages/gatsby/src/schema/__tests__/queries-file.js index f6e5a3afe0155..232c197042c34 100644 --- a/packages/gatsby/src/schema/__tests__/queries-file.js +++ b/packages/gatsby/src/schema/__tests__/queries-file.js @@ -143,7 +143,7 @@ describe(`Query fields of type File`, () => { files { name } } arrayOfArray { name } - arrayOfArrayOfObjects { + arrayOfArrayOfObjects { nested { name } diff --git a/packages/gatsby/src/schema/add-field-resolvers.js b/packages/gatsby/src/schema/add-field-resolvers.js deleted file mode 100644 index 5b4959cf83f0f..0000000000000 --- a/packages/gatsby/src/schema/add-field-resolvers.js +++ /dev/null @@ -1,84 +0,0 @@ -const _ = require(`lodash`) -const { defaultFieldResolver } = require(`graphql`) -const { dateResolver } = require(`./types/date`) -const { link, fileByPath } = require(`./resolvers`) - -export const addFieldResolvers = ({ - schemaComposer, - typeComposer, - parentSpan, -}) => { - typeComposer.getFieldNames().forEach(fieldName => { - let field = typeComposer.getField(fieldName) - - const extensions = typeComposer.getFieldExtensions(fieldName) - if ( - !field.resolve && - extensions.addResolver && - _.isObject(extensions.addResolver) - ) { - const options = extensions.addResolver.options || {} - switch (extensions.addResolver.type) { - case `dateformat`: { - addDateResolver({ - typeComposer, - fieldName, - options, - }) - break - } - case `link`: { - typeComposer.extendField(fieldName, { - resolve: link({ from: options.from, by: options.by }), - }) - break - } - case `fileByRelativePath`: { - typeComposer.extendField(fieldName, { - resolve: fileByPath({ from: options.from }), - }) - break - } - } - } - - if (extensions.proxyFrom) { - // XXX(freiksenet): get field again cause it will be changed because of above - field = typeComposer.getField(fieldName) - const resolver = field.resolve || defaultFieldResolver - typeComposer.extendField(fieldName, { - resolve: (source, args, context, info) => - resolver(source, args, context, { - ...info, - fieldName: extensions.proxyFrom, - }), - }) - } - }) - return typeComposer -} - -const addDateResolver = ({ - typeComposer, - fieldName, - options: { formatString, locale }, -}) => { - const field = typeComposer.getField(fieldName) - - let fieldConfig = { - resolve: dateResolver.resolve, - } - if (!field.args || _.isEmpty(field.args)) { - fieldConfig.args = { - ...dateResolver.args, - } - if (formatString) { - fieldConfig.args.formatString.defaultValue = formatString - } - if (locale) { - fieldConfig.args.locale.defaultValue = locale - } - } - - typeComposer.extendField(fieldName, fieldConfig) -} diff --git a/packages/gatsby/src/schema/extensions/index.js b/packages/gatsby/src/schema/extensions/index.js new file mode 100644 index 0000000000000..d52d2588b346c --- /dev/null +++ b/packages/gatsby/src/schema/extensions/index.js @@ -0,0 +1,147 @@ +const { + GraphQLBoolean, + GraphQLNonNull, + GraphQLDirective, + GraphQLString, + DirectiveLocation, + defaultFieldResolver, +} = require(`graphql`) + +const { link, fileByPath } = require(`../resolvers`) +const { getDateResolver } = require(`../types/date`) + +// Reserved for internal use +const internalExtensionNames = [`createdFrom`, `directives`, `infer`, `plugin`] + +const typeExtensions = { + infer: { + description: `Infer field types from field values.`, + args: { + noDefaultResolvers: { + type: GraphQLBoolean, + description: `Don't add default resolvers to defined fields.`, + deprecationReason: `noDefaultResolvers is deprecated, annotate individual fields.`, + }, + }, + }, + dontInfer: { + description: `Do not infer field types from field values.`, + args: { + noDefaultResolvers: { + type: GraphQLBoolean, + description: `Don't add default resolvers to defined fields.`, + deprecationReason: `noDefaultResolvers is deprecated, annotate individual fields.`, + }, + }, + }, +} + +const fieldExtensions = { + dateformat: { + description: `Add date formating options.`, + args: { + formatString: { type: GraphQLString }, + locale: { type: GraphQLString }, + }, + process(args, fieldConfig) { + return getDateResolver(args) + }, + }, + + link: { + description: `Link to node by foreign-key relation.`, + args: { + by: { + type: new GraphQLNonNull(GraphQLString), + defaultValue: `id`, + }, + from: { + type: GraphQLString, + }, + }, + process(args, fieldConfig) { + return { + resolve: link(args), + } + }, + }, + + fileByRelativePath: { + description: `Link to File node by relative path.`, + args: { + from: { + type: GraphQLString, + }, + }, + process(args, fieldConfig) { + return { + resolve: fileByPath(args), + } + }, + }, + + proxyFrom: { + description: `Proxy resolver from another field.`, + process(from, fieldConfig) { + const resolver = fieldConfig.resolve || defaultFieldResolver + return { + resolve(source, args, context, info) { + return resolver(source, args, context, { + ...info, + fieldName: from, + }) + }, + } + }, + }, +} + +const toDirectives = ({ extensions, locations }) => + Object.keys(extensions).map(name => { + const extension = extensions[name] + const { args, description } = extension + return new GraphQLDirective({ name, args, description, locations }) + }) + +const addDirectives = ({ schemaComposer }) => { + const fieldDirectives = toDirectives({ + extensions: fieldExtensions, + locations: [DirectiveLocation.FIELD_DEFINITION], + }) + fieldDirectives.forEach(directive => schemaComposer.addDirective(directive)) + const typeDirectives = toDirectives({ + extensions: typeExtensions, + locations: [DirectiveLocation.OBJECT], + }) + typeDirectives.forEach(directive => schemaComposer.addDirective(directive)) +} + +const processFieldExtensions = ({ + schemaComposer, + typeComposer, + parentSpan, +}) => { + typeComposer.getFieldNames().forEach(fieldName => { + const extensions = typeComposer.getFieldExtensions(fieldName) + Object.keys(extensions) + .filter(name => !internalExtensionNames.includes(name)) + .sort(a => a === `proxyFrom`) // Ensure `proxyFrom` is run last + .forEach(name => { + const { process } = fieldExtensions[name] || {} + if (process) { + // Always get fresh field config as it will have been changed + // by previous field extension + const prevFieldConfig = typeComposer.getFieldConfig(fieldName) + typeComposer.extendField( + fieldName, + process(extensions[name], prevFieldConfig) + ) + } + }) + }) +} + +module.exports = { + addDirectives, + processFieldExtensions, +} diff --git a/packages/gatsby/src/schema/infer/__tests__/infer.js b/packages/gatsby/src/schema/infer/__tests__/infer.js index acda4be1bab69..fba26902540ca 100644 --- a/packages/gatsby/src/schema/infer/__tests__/infer.js +++ b/packages/gatsby/src/schema/infer/__tests__/infer.js @@ -320,7 +320,7 @@ describe(`GraphQL type inference`, () => { ` with_space with_hyphen - with_resolver(formatString:"DD.MM.YYYY") + with_resolver(formatString: "DD.MM.YYYY") _123 _456 { testingTypeNameCreation diff --git a/packages/gatsby/src/schema/infer/__tests__/merge-types.js b/packages/gatsby/src/schema/infer/__tests__/merge-types.js index 9abc21da2b141..4291ede5bd369 100644 --- a/packages/gatsby/src/schema/infer/__tests__/merge-types.js +++ b/packages/gatsby/src/schema/infer/__tests__/merge-types.js @@ -528,12 +528,12 @@ describe(`merges explicit and inferred type definitions`, () => { it(`adds explicit resolvers through directives`, async () => { const typeDefs = ` type Test implements Node @infer { - explicitDate: Date @addResolver(type: "dateformat") + explicitDate: Date @dateformat } type LinkTest implements Node @infer { - link: Test! @addResolver(type: "link") - links: [Test!]! @addResolver(type: "link") + link: Test! @link + links: [Test!]! @link } ` store.dispatch({ type: `CREATE_TYPES`, payload: typeDefs }) @@ -551,7 +551,59 @@ describe(`merges explicit and inferred type definitions`, () => { expect(inferDate.resolve).toBeDefined() }) - it(`adds explicit resolvers through extensions`, async () => {}) + it(`adds explicit resolvers through extensions`, async () => { + const typeDefs = [ + buildObjectType({ + name: `Test`, + interfaces: [`Node`], + extensions: { + infer: true, + }, + fields: { + explicitDate: { + type: `Date`, + extensions: { + dateformat: {}, + }, + }, + }, + }), + buildObjectType({ + name: `LinkTest`, + interfaces: [`Node`], + extensions: { + infer: true, + }, + fields: { + link: { + type: `Test!`, + extensions: { + link: {}, + }, + }, + links: { + type: `[Test!]!`, + extensions: { + link: true, + }, + }, + }, + }), + ] + store.dispatch({ type: `CREATE_TYPES`, payload: typeDefs }) + await build({}) + const { schema } = store.getState() + + const { link, links } = schema.getType(`LinkTest`).getFields() + expect(link.type.toString()).toBe(`Test!`) + expect(links.type.toString()).toBe(`[Test!]!`) + expect(link.resolve).toBeDefined() + expect(links.resolve).toBeDefined() + + const { explicitDate, inferDate } = schema.getType(`Test`).getFields() + expect(explicitDate.resolve).toBeDefined() + expect(inferDate.resolve).toBeDefined() + }) it(`honors array depth when merging types`, async () => { const typeDefs = ` diff --git a/packages/gatsby/src/schema/infer/add-inferred-fields.js b/packages/gatsby/src/schema/infer/add-inferred-fields.js index 0da2257b793b8..3e8b8f7374e2f 100644 --- a/packages/gatsby/src/schema/infer/add-inferred-fields.js +++ b/packages/gatsby/src/schema/infer/add-inferred-fields.js @@ -20,11 +20,9 @@ const addInferredFields = ({ typeComposer, defaults: { shouldAddFields: true, - // FIXME: This is a behavioral change shouldAddDefaultResolvers: typeComposer.hasExtension(`infer`) ? false : true, - // shouldAddDefaultResolvers: true, }, }) addInferredFieldsImpl({ @@ -107,21 +105,30 @@ const addInferredFieldsImpl = ({ // and the field has neither args nor resolver explicitly defined. const field = typeComposer.getField(key) if ( - !typeComposer.hasFieldExtension(key, `addResolver`) && field.type.toString().replace(/[[\]!]/g, ``) === fieldConfig.type.toString() && _.isEmpty(field.args) && !field.resolve ) { - const extension = - fieldConfig.extensions && fieldConfig.extensions.addResolver - if (extension) { - typeComposer.setFieldExtension(key, `addResolver`, extension) - report.warn( - `Deprecation warning - adding inferred resolver for field ` + - `${typeComposer}.${key}. In Gatsby v3, only fields with a ` + - `\`addResolver\` extension/directive will get a resolver.` - ) + const { extensions } = fieldConfig + if (extensions) { + Object.keys(extensions) + .filter(name => + // It is okay to list allowed extensions explicitly here, + // since this is deprecated anyway and won't change. + [`dateformat`, `fileByRelativePath`, `link`].includes(name) + ) + .forEach(name => { + if (!typeComposer.hasFieldExtension(key, name)) { + typeComposer.setFieldExtension(key, name, extensions[name]) + report.warn( + `Deprecation warning - adding inferred resolver for field ` + + `${typeComposer.getTypeName()}.${key}. In Gatsby v3, ` + + `only fields with an explicit directive/extension will ` + + `get a resolver.` + ) + } + }) } } } @@ -228,7 +235,7 @@ const getFieldConfigFromMapping = ({ typeMapping, selector }) => { return { type, extensions: { - addResolver: { type: `link`, options: { by: path.join(`.`) || `id` } }, + link: { by: path.join(`.`) || `id` }, }, } } @@ -282,20 +289,11 @@ const getFieldConfigFromFieldNameConvention = ({ return { type, extensions: { - addResolver: { - type: `link`, - options: { - by: foreignKey || `id`, - from: key, - }, - }, + link: { by: foreignKey || `id`, from: key }, }, } } -const DATE_EXTENSION = { addResolver: { type: `dateformat` } } -const FILE_EXTENSION = { addResolver: { type: `fileByRelativePath` } } - const getSimpleFieldConfig = ({ schemaComposer, typeComposer, @@ -314,19 +312,19 @@ const getSimpleFieldConfig = ({ return { type: is32BitInteger(value) ? `Int` : `Float` } case `string`: if (isDate(value)) { - return { type: `Date`, extensions: DATE_EXTENSION } + return { type: `Date`, extensions: { dateformat: {} } } } if (isFile(nodeStore, selector, value)) { // NOTE: For arrays of files, where not every path references // a File node in the db, it is semi-random if the field is // inferred as File or String, since the exampleValue only has // the first entry (which could point to an existing file or not). - return { type: `File`, extensions: FILE_EXTENSION } + return { type: `File`, extensions: { fileByRelativePath: {} } } } return { type: `String` } case `object`: if (value instanceof Date) { - return { type: `Date`, extensions: DATE_EXTENSION } + return { type: `Date`, extensions: { dateformat: {} } } } if (value instanceof String) { return { type: `String` } diff --git a/packages/gatsby/src/schema/infer/index.js b/packages/gatsby/src/schema/infer/index.js index 031141d138624..49c2a720dd626 100644 --- a/packages/gatsby/src/schema/infer/index.js +++ b/packages/gatsby/src/schema/infer/index.js @@ -33,7 +33,7 @@ const addInferredTypes = ({ : true if (runInfer) { if (!typeComposer.hasInterface(`Node`)) { - noNodeInterfaceTypes.push(typeComposer) + noNodeInterfaceTypes.push(typeName) } typesToInfer.push(typeComposer) } @@ -46,16 +46,16 @@ const addInferredTypes = ({ }) if (noNodeInterfaceTypes.length > 0) { - noNodeInterfaceTypes.forEach(type => { + noNodeInterfaceTypes.forEach(typeName => { report.warn( - `Type \`${type}\` declared in \`createTypes\` looks like a node, ` + + `Type \`${typeName}\` declared in \`createTypes\` looks like a node, ` + `but doesn't implement a \`Node\` interface. It's likely that you should ` + `add the \`Node\` interface to your type def:\n\n` + - `\`type ${type} implements Node { ... }\`\n\n` + + `\`type ${typeName} implements Node { ... }\`\n\n` + `If you know that you don't want it to be a node (which would mean no ` + `root queries to retrieve it), you can explicitly disable inference ` + `for it:\n\n` + - `\`type ${type} @dontInfer { ... }\`` + `\`type ${typeName} @dontInfer { ... }\`` ) }) report.panic(`Building schema failed`) diff --git a/packages/gatsby/src/schema/schema-composer.js b/packages/gatsby/src/schema/schema-composer.js index 21e391faf1c8f..8d62f3ebade26 100644 --- a/packages/gatsby/src/schema/schema-composer.js +++ b/packages/gatsby/src/schema/schema-composer.js @@ -1,20 +1,14 @@ const { SchemaComposer, GraphQLJSON } = require(`graphql-compose`) const { getNodeInterface } = require(`./types/node-interface`) const { GraphQLDate } = require(`./types/date`) -const { - InferDirective, - DontInferDirective, - AddResolver, -} = require(`./types/directives`) +const { addDirectives } = require(`./extensions`) const createSchemaComposer = () => { const schemaComposer = new SchemaComposer() getNodeInterface({ schemaComposer }) schemaComposer.addAsComposer(GraphQLDate) schemaComposer.addAsComposer(GraphQLJSON) - schemaComposer.addDirective(InferDirective) - schemaComposer.addDirective(DontInferDirective) - schemaComposer.addDirective(AddResolver) + addDirectives({ schemaComposer }) return schemaComposer } diff --git a/packages/gatsby/src/schema/schema.js b/packages/gatsby/src/schema/schema.js index f1a0dc32e58c0..e95c4406f7618 100644 --- a/packages/gatsby/src/schema/schema.js +++ b/packages/gatsby/src/schema/schema.js @@ -5,14 +5,12 @@ const { isIntrospectionType, defaultFieldResolver, assertValidName, - Kind, } = require(`graphql`) const { ObjectTypeComposer, InterfaceTypeComposer, UnionTypeComposer, InputTypeComposer, - GraphQLJSON, } = require(`graphql-compose`) const apiRunner = require(`../utils/api-runner-node`) @@ -20,7 +18,7 @@ const report = require(`gatsby-cli/lib/reporter`) const { addNodeInterfaceFields } = require(`./types/node-interface`) const { addInferredType, addInferredTypes } = require(`./infer`) const { findOne, findManyPaginated } = require(`./resolvers`) -const { addFieldResolvers } = require(`./add-field-resolvers`) +const { processFieldExtensions } = require(`./extensions`) const { getPagination } = require(`./types/pagination`) const { getSortInput } = require(`./types/sort`) const { getFilterInput } = require(`./types/filter`) @@ -121,22 +119,19 @@ const processTypeComposer = async ({ nodeStore, parentSpan, }) => { - if ( - typeComposer instanceof ObjectTypeComposer && - typeComposer.hasInterface(`Node`) - ) { - await addFieldResolvers({ schemaComposer, typeComposer, parentSpan }) - await addNodeInterfaceFields({ schemaComposer, typeComposer, parentSpan }) - await addResolvers({ schemaComposer, typeComposer, parentSpan }) - await addConvenienceChildrenFields({ - schemaComposer, - typeComposer, - nodeStore, - parentSpan, - }) - await addTypeToRootQuery({ schemaComposer, typeComposer, parentSpan }) - } else if (typeComposer instanceof ObjectTypeComposer) { - await addFieldResolvers({ schemaComposer, typeComposer, parentSpan }) + if (typeComposer instanceof ObjectTypeComposer) { + await processFieldExtensions({ schemaComposer, typeComposer, parentSpan }) + if (typeComposer.hasInterface(`Node`)) { + await addNodeInterfaceFields({ schemaComposer, typeComposer, parentSpan }) + await addResolvers({ schemaComposer, typeComposer, parentSpan }) + await addConvenienceChildrenFields({ + schemaComposer, + typeComposer, + nodeStore, + parentSpan, + }) + await addTypeToRootQuery({ schemaComposer, typeComposer, parentSpan }) + } } } @@ -210,30 +205,22 @@ const processAddedType = ({ typeComposer.setExtension(`plugin`, plugin ? plugin.name : null) if (createdFrom === `sdl`) { - const ast = typeComposer.getType().astNode - if (ast && ast.directives) { - ast.directives.forEach(directive => { - if (directive.name.value === `infer`) { - typeComposer.setExtension(`infer`, true) - const addDefaultResolvers = getNoDefaultResolvers(directive) - if (addDefaultResolvers != null) { + const directives = typeComposer.getDirectives() + directives.forEach(({ name, args }) => { + switch (name) { + case `infer`: + case `dontInfer`: + typeComposer.setExtension(`infer`, name === `infer`) + if (args.noDefaultResolvers != null) { typeComposer.setExtension( `addDefaultResolvers`, - addDefaultResolvers + !args.noDefaultResolvers ) } - } else if (directive.name.value === `dontInfer`) { - typeComposer.setExtension(`infer`, false) - const addDefaultResolvers = getNoDefaultResolvers(directive) - if (addDefaultResolvers != null) { - typeComposer.setExtension( - `addDefaultResolvers`, - addDefaultResolvers - ) - } - } - }) - } + break + default: + } + }) } if ( @@ -249,46 +236,25 @@ const processAddedType = ({ ) if (createdFrom === `sdl`) { - const field = typeComposer.getField(fieldName) - if (field.astNode && field.astNode.directives) { - field.astNode.directives.forEach(directive => { - if (directive.name.value === `addResolver`) { - const options = {} - directive.arguments.forEach(argument => { - options[argument.name.value] = GraphQLJSON.parseLiteral( - argument.value - ) - }) - typeComposer.setFieldExtension(fieldName, `addResolver`, options) - } - }) - } + const directives = typeComposer.getFieldDirectives(fieldName) + directives.forEach(({ name, args }) => { + typeComposer.setFieldExtension(fieldName, name, args) + }) } }) } if (typeComposer.hasExtension(`addDefaultResolvers`)) { report.warn( - `Deprecation warning - "noDefaultResolvers" is deprecated. In Gatsby 3, defined fields won't get resolvers, unless "addResolver" directive/extension is used.` + `Deprecation warning - "noDefaultResolvers" is deprecated. In Gatsby 3, ` + + `defined fields won't get resolvers, unless explicitly added with a ` + + `directive/extension.` ) } return typeComposer } -const getNoDefaultResolvers = directive => { - const noDefaultResolvers = directive.arguments.find( - ({ name }) => name.value === `noDefaultResolvers` - ) - if (noDefaultResolvers) { - if (noDefaultResolvers.value.kind === Kind.BOOLEAN) { - return !noDefaultResolvers.value.value - } - } - - return null -} - const checkIsAllowedTypeName = name => { invariant( name !== `Node`, @@ -543,8 +509,6 @@ const addResolvers = ({ schemaComposer, typeComposer }) => { sort: sortInputTC, skip: `Int`, limit: `Int`, - // page: `Int`, - // perPage: { type: `Int`, defaultValue: 20 }, }, resolve: findManyPaginated(typeName), }) diff --git a/packages/gatsby/src/schema/types/date.js b/packages/gatsby/src/schema/types/date.js index e17ec8a9be4c2..4c34d8838de3c 100644 --- a/packages/gatsby/src/schema/types/date.js +++ b/packages/gatsby/src/schema/types/date.js @@ -215,44 +215,48 @@ const formatDate = ({ return normalizedDate } -const dateResolver = { - type: `Date`, - args: { - formatString: { - type: GraphQLString, - description: oneLine` +const getDateResolver = defaults => { + const { locale, formatString } = defaults + return { + 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` + defaultValue: formatString, + }, + fromNow: { + type: GraphQLBoolean, + description: oneLine` Returns a string generated with Moment.js' \`fromNow\` function`, - }, - difference: { - type: GraphQLString, - description: oneLine` + }, + 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 + Defaults to "milliseconds" but you can also pass in as the measurement "years", "months", "weeks", "days", "hours", "minutes", and "seconds".`, - }, - locale: { - type: GraphQLString, - description: oneLine` + }, + locale: { + type: GraphQLString, + description: oneLine` Configures the locale Moment.js will use to format the date.`, + defaultValue: locale, + }, }, - }, - resolve(source, args, context, { fieldName }) { - const date = source[fieldName] - if (date == null) return null + resolve(source, args, context, { fieldName }) { + const date = source[fieldName] + if (date == null) return null - return Array.isArray(date) - ? date.map(d => formatDate({ date: d, ...args })) - : formatDate({ date, ...args }) - }, + return Array.isArray(date) + ? date.map(d => formatDate({ date: d, ...args })) + : formatDate({ date, ...args }) + }, + } } -module.exports = { GraphQLDate, dateResolver, isDate, looksLikeADate } +module.exports = { GraphQLDate, getDateResolver, isDate, looksLikeADate } diff --git a/packages/gatsby/src/schema/types/directives.js b/packages/gatsby/src/schema/types/directives.js deleted file mode 100644 index 7fd7e929fecbe..0000000000000 --- a/packages/gatsby/src/schema/types/directives.js +++ /dev/null @@ -1,56 +0,0 @@ -const { - GraphQLBoolean, - GraphQLNonNull, - GraphQLDirective, - GraphQLString, - DirectiveLocation, -} = require(`graphql`) -const { GraphQLJSON } = require(`graphql-compose`) - -const InferDirective = new GraphQLDirective({ - name: `infer`, - description: `Infer fields for this type from nodes.`, - locations: [DirectiveLocation.OBJECT], - args: { - noDefaultResolvers: { - type: GraphQLBoolean, - description: `Don't add default resolvers to defined fields.`, - deprecationReason: `noDefaultResolvers is deprecated, annotate individual fields.`, - }, - }, -}) - -const DontInferDirective = new GraphQLDirective({ - name: `dontInfer`, - description: `Do not infer additional fields for this type from nodes.`, - locations: [DirectiveLocation.OBJECT], - args: { - noDefaultResolvers: { - type: GraphQLBoolean, - description: `Don't add default resolvers to defined fields.`, - deprecationReason: `noDefaultResolvers is deprecated, annotate individual fields.`, - }, - }, -}) - -const AddResolver = new GraphQLDirective({ - name: `addResolver`, - description: `Add a resolver specified by "type" to field`, - locations: [DirectiveLocation.FIELD_DEFINITION], - args: { - type: { - type: new GraphQLNonNull(GraphQLString), - description: `Type of the resolver. Types available by default are: "date", "link" and "relativeFile".`, - }, - options: { - type: GraphQLJSON, - description: `Resolver options. Vary based on resolver type.`, - }, - }, -}) - -module.exports = { - InferDirective, - DontInferDirective, - AddResolver, -} From aee5186bc1b4fbedc65d78274a5d95da0051e13b Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Wed, 15 May 2019 10:03:39 +0300 Subject: [PATCH 26/32] Fix all addResolver uses --- .../index.md | 22 ++++++------- packages/gatsby/src/redux/actions.js | 33 +++++++++---------- .../__snapshots__/kitchen-sink.js.snap | 2 +- .../__tests__/fixtures/kitchen-sink.json | 2 +- .../src/schema/__tests__/kitchen-sink.js | 2 +- 5 files changed, 29 insertions(+), 32 deletions(-) rename docs/blog/{2019-04-23-improvements-to-schema-customization => 2019-05-17-improvements-to-schema-customization}/index.md (83%) diff --git a/docs/blog/2019-04-23-improvements-to-schema-customization/index.md b/docs/blog/2019-05-17-improvements-to-schema-customization/index.md similarity index 83% rename from docs/blog/2019-04-23-improvements-to-schema-customization/index.md rename to docs/blog/2019-05-17-improvements-to-schema-customization/index.md index 725691a1cbb5e..4a40ef9f72add 100644 --- a/docs/blog/2019-04-23-improvements-to-schema-customization/index.md +++ b/docs/blog/2019-05-17-improvements-to-schema-customization/index.md @@ -1,6 +1,6 @@ --- title: Improvements to Schema Customization API - Available in Gatsby 2.4.0 -date: 2019-04-23 +date: 2019-05-17 author: Mikhail Novikov tags: - schema @@ -9,7 +9,7 @@ tags: Today we are releasing further improvements to the schema customization that [we've released in version 2.2.0](/blog/2019-03-18-releasing-new-schema-customization). You can use them with Gatsby 2.4.0. -It is now possible to indicate to Gatsby, that you want to add a resolver to an explicitly defined fields. Use `addResolver` to add default arguments or/and resolvers to fields. In addition, when `@dontInfer` is set, Gatsby will no longer run inference for marked type, allowing one to improve performance for large data sets. +It is now possible to indicate to Gatsby, that you want to add a resolver to an explicitly defined fields. Use extensions like `@link` and `@dateformat` to add default arguments or/and resolvers to fields. In addition, when `@dontInfer` is set, Gatsby will no longer run inference for marked type, allowing one to improve performance for large data sets. ## Summary @@ -26,7 +26,7 @@ Therefore we have some changes to the way we do inferrence. In addition, we are First of all, we are deprecating `noDefaultResolvers`. It was an argument of `infer` and `dontInfer`. We feel it was confusing and in some cases it didn't even actually add resolvers :). We will support `noDefaultResolvers` until version 3, after which `@infer` behaviour (see below) will become a default and `noDefaultResolvers` will be removed. -We didn't want to break things, so we keep old default behaviour, even though we think it's not optimal. Add explicit `@infer` and `@addResolver` to fields to be future proof. +We didn't want to break things, so we keep old default behaviour, even though we think it's not optimal. Add explicit `@infer` and resolver extensions (like `@link`) to fields to be future proof. ### Default (deprecated, removed in v3) @@ -38,13 +38,13 @@ Type gets all inferred fields added. If type has defined fields of types `Date`, Applies with `@infer` or `@infer(noDefaultResolvers: true)`. -Type gets all inferred fields added. Existing fields won't automatically get resolvers (use `@addResolver` directive). +Type gets all inferred fields added. Existing fields won't automatically get resolvers (use resolver extensions). ### No inferrence Applies with `@dontInfer` or `@dontInfer(noDefaultResolvers: true)`. -Inferrence won't run at all. Existing fields won't automatically get resolvers (use @addResolver directive). +Inferrence won't run at all. Existing fields won't automatically get resolvers (use resolver extensions). ### No new fields with default resolvers (deprecated, removed in v3) @@ -58,9 +58,9 @@ Add resolver and resolver options (such as arguments) to the given field. ```graphql type MyType @infer { - date: Date @addResolver(type: "dateformat", options: { formatString: "DD MMM", locale: "fi" }) - image: File @addResolver(type: "fileByRelativePath") - authorByEmail: Author @addResolver(type: "link", { by: "email" }) + date: Date @dateformat(formatString: "DD MMM", locale: "fi") + image: File @fileByRelativePath + authorByEmail: Author @link(by: "email") } ``` @@ -78,9 +78,9 @@ schema.createObjectType({ date: { type: "Date", extensions: { - addResolver: { - type: "dateformat", - options: { formatString: "DD MMM", locale: "fi" }, + dateformat: { + formatString: "DD MMM", + locale: "fi", }, }, }, diff --git a/packages/gatsby/src/redux/actions.js b/packages/gatsby/src/redux/actions.js index 94f52f363a0a0..fcf7a1087fa41 100644 --- a/packages/gatsby/src/redux/actions.js +++ b/packages/gatsby/src/redux/actions.js @@ -1265,23 +1265,24 @@ import type GatsbyGraphQLType from "../schema/types/type-builders" * with inferred field types, and default field resolvers for `Date` (which * adds formatting options) and `File` (which resolves the field value as * a `relativePath` foreign-key field) are added. This behavior can be - * customised with `@infer`, `@dontInfer` and `@addResolvers` directives. + * customised with `@infer`, `@dontInfer` directives or extensions. Fields + * may be assigned resolver (and other option like args) with additional + * directives. Currently `@dateformat`, `@link` and `@fileByRelativePath` are + * available. * * * Schema customization controls: * * `@infer` - run inference on the type and add fields that don't exist on the * defined type to it. * * `@dontInfer` - don't run any inference on the type - * * `@addResolver` - add resolver options to a field. Args are `type` and - * `options`. Type can be `dateformat`, `link` and `fileByRelativePath`. * - * Resolver types: - * * `dateformat` - add date formatting arguments. Accepts `formatString` and + * Extensions to add resolver options: + * * `@dateformat` - add date formatting arguments. Accepts `formatString` and * `locale` options that sets the defaults for this field - * * `link` - connect to a different Node. Arguments `by` and `from`, which + * * `@link` - connect to a different Node. Arguments `by` and `from`, which * define which field to compare to on a remote node and which field to use on * the source node - * * `fileByRelativePath` - connect to a File node. Same arguments. The + * * `@fileByRelativePath` - connect to a File node. Same arguments. The * difference from link is that this normalizes the relative path to be * relative from the path where source node is found. * @@ -1303,8 +1304,8 @@ import type GatsbyGraphQLType from "../schema/types/type-builders" * """ * type Frontmatter @infer { * title: String! - * author: AuthorJson! @addResolver(type: "link") - * date: Date! @addResolver(type: "dateformat") + * author: AuthorJson! @link + * date: Date! @dateformat * published: Boolean! * tags: [String!]! * } @@ -1315,7 +1316,7 @@ import type GatsbyGraphQLType from "../schema/types/type-builders" * # Does not include automatically inferred fields * type AuthorJson implements Node @dontInfer { * name: String! - * birthday: Date! @addResolver(type: "dateformat") + * birthday: Date! @date(locale: "ru") * } * ` * createTypes(typeDefs) @@ -1347,17 +1348,13 @@ import type GatsbyGraphQLType from "../schema/types/type-builders" * author: { * type: 'AuthorJson' * extensions: { - * addResolver: { - * type: 'link', - * }, + * link: {}, * }, * } * date: { * type: 'Date!' * extensions: { - * addResolver: { - * type: 'date', - * }, + * dateformat: {}, * }, * }, * published: 'Boolean!', @@ -1371,8 +1368,8 @@ import type GatsbyGraphQLType from "../schema/types/type-builders" * birthday: { * type: 'Date!' * extensions: { - * addResolver: { - * type: 'date', + * date: { + * locale: 'ru', * }, * }, * }, diff --git a/packages/gatsby/src/schema/__tests__/__snapshots__/kitchen-sink.js.snap b/packages/gatsby/src/schema/__tests__/__snapshots__/kitchen-sink.js.snap index c08c144dd6613..d8ab564253b2e 100644 --- a/packages/gatsby/src/schema/__tests__/__snapshots__/kitchen-sink.js.snap +++ b/packages/gatsby/src/schema/__tests__/__snapshots__/kitchen-sink.js.snap @@ -3,7 +3,7 @@ exports[`Kitchen sink schema test passes kitchen sink query 1`] = ` Object { "data": Object { - "addResolvers": Array [ + "createResolvers": Array [ Object { "code": "BdiU-TTFP4h", "id": "1685001452849004065", diff --git a/packages/gatsby/src/schema/__tests__/fixtures/kitchen-sink.json b/packages/gatsby/src/schema/__tests__/fixtures/kitchen-sink.json index c9ac2f76726e1..0446c7912550b 100644 --- a/packages/gatsby/src/schema/__tests__/fixtures/kitchen-sink.json +++ b/packages/gatsby/src/schema/__tests__/fixtures/kitchen-sink.json @@ -1060,7 +1060,7 @@ "nodeAPIs": [ "createPages", "sourceNodes", - "addResolvers" + "createResolvers" ], "browserAPIs": [ "shouldUpdateScroll", diff --git a/packages/gatsby/src/schema/__tests__/kitchen-sink.js b/packages/gatsby/src/schema/__tests__/kitchen-sink.js index a5ddbb49be580..f26a11632fa7d 100644 --- a/packages/gatsby/src/schema/__tests__/kitchen-sink.js +++ b/packages/gatsby/src/schema/__tests__/kitchen-sink.js @@ -110,7 +110,7 @@ describe(`Kitchen sink schema test`, () => { idWithDecoration likes } - addResolvers: likedEnough { + createResolvers: likedEnough { id likes code From daa2af3255131d94de065127b215068e82a4a412 Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Wed, 15 May 2019 10:04:30 +0300 Subject: [PATCH 27/32] v2.5.0-rc.1 --- packages/gatsby/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 04ca61a86f26f..018ac2fecdaac 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -1,7 +1,7 @@ { "name": "gatsby", "description": "Blazing fast modern site generator for React", - "version": "2.4.0-alpha.3", + "version": "2.5.0-rc.1", "author": "Kyle Mathews ", "bin": { "gatsby": "./dist/bin/gatsby.js" From 65e75046b7a5ba1cb5a6a538386d4e7fd857fb8b Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Wed, 15 May 2019 12:03:28 +0300 Subject: [PATCH 28/32] Fix blog post --- .../index.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/blog/2019-05-17-improvements-to-schema-customization/index.md b/docs/blog/2019-05-17-improvements-to-schema-customization/index.md index 4a40ef9f72add..a1ba15dee435a 100644 --- a/docs/blog/2019-05-17-improvements-to-schema-customization/index.md +++ b/docs/blog/2019-05-17-improvements-to-schema-customization/index.md @@ -52,9 +52,18 @@ Applies with `@dontInfer(noDefaultResolvers: false)` Inferrence will run, but fields won't be added. If type has defined fields of types `Date`, `File` and any other node, and we inferred that they should have resolvers/args, resolvers/args will be added to type with a warning. -## `addResolver` - -Add resolver and resolver options (such as arguments) to the given field. +## Resolver extensions + +Add resolver and resolver options (such as arguments) to the given field. There are currently 3 extensions available. + +- `@dateformat` - add date formatting arguments. Accepts `formatString` and + `locale` options that sets the defaults for this field +- `@link` - connect to a different Node. Arguments `by` and `from`, which + define which field to compare to on a remote node and which field to use on + the source node +- `@fileByRelativePath` - connect to a File node. Same arguments. The + difference from link is that this normalizes the relative path to be + relative from the path where source node is found. ```graphql type MyType @infer { From b6c61e544390adb1c2753c7924663f0c1e4ea5f3 Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Wed, 15 May 2019 14:20:25 +0300 Subject: [PATCH 29/32] Apply suggestions from code review Co-Authored-By: Mike Allanson --- .../index.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/blog/2019-05-17-improvements-to-schema-customization/index.md b/docs/blog/2019-05-17-improvements-to-schema-customization/index.md index a1ba15dee435a..91c11c34a35fa 100644 --- a/docs/blog/2019-05-17-improvements-to-schema-customization/index.md +++ b/docs/blog/2019-05-17-improvements-to-schema-customization/index.md @@ -1,5 +1,5 @@ --- -title: Improvements to Schema Customization API - Available in Gatsby 2.4.0 +title: Improvements to Schema Customization API - Available in Gatsby 2.5.0 date: 2019-05-17 author: Mikhail Novikov tags: @@ -13,14 +13,16 @@ It is now possible to indicate to Gatsby, that you want to add a resolver to an ## Summary -After about a month of testing schema customization both here and in pre-release we determined a couple of issues. We set up to do this because we wanted to remove uncertainty in people's schemas, when the data changes. However, the original design allowed some uncertainties anyway. In addition, we have made inferrence a more heavy process, trading performance for consistency and didn't really provide a way to opt out of it completely. To summarize: +After about a month of testing schema customization both here and in pre-release we determined a couple of issues. The original aim of our schema customisation work was to remove uncertainty in people's schemas when their data changes. + +However, the original design allowed some uncertainties anyway. In addition, it made inference a more heavy process, trading performance for consistency without providing a way to opt out completely. To summarize, the schema customization work released in Gatsby 2.2.0 had the following issues: - Resolvers and arguments of fields like Date and File was determined by inferred data - There was no easy way to use arguments/resolvers to override the above - Inferrence was run even when `@dontInfer` flag was on - There was no way to control inference outside of SDL, eg in Type Builders -Therefore we have some changes to the way we do inferrence. In addition, we are deprecating some of the features introduced is 2.2.0 and will remove them in Gatsby 3. +Therefore we have some changes to the way we do inferrence. In addition, we are deprecating some of the features introduced in 2.2.0 and will remove them in Gatsby 3. ## noDefaultResolvers and inferrence modes From e76784c9b4db3218ab1f80c9c7e92115fcf85e19 Mon Sep 17 00:00:00 2001 From: stefanprobst Date: Wed, 15 May 2019 13:25:23 +0200 Subject: [PATCH 30/32] Update packages/gatsby/src/redux/actions.js --- packages/gatsby/src/redux/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gatsby/src/redux/actions.js b/packages/gatsby/src/redux/actions.js index fcf7a1087fa41..0ffb7aaca5f4c 100644 --- a/packages/gatsby/src/redux/actions.js +++ b/packages/gatsby/src/redux/actions.js @@ -1368,7 +1368,7 @@ import type GatsbyGraphQLType from "../schema/types/type-builders" * birthday: { * type: 'Date!' * extensions: { - * date: { + * dateformat: { * locale: 'ru', * }, * }, From 69f179ac1f0544f15adcea8290213281e7e92e2d Mon Sep 17 00:00:00 2001 From: stefanprobst Date: Wed, 15 May 2019 13:25:33 +0200 Subject: [PATCH 31/32] Update packages/gatsby/src/redux/actions.js --- packages/gatsby/src/redux/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gatsby/src/redux/actions.js b/packages/gatsby/src/redux/actions.js index 0ffb7aaca5f4c..cc09861447507 100644 --- a/packages/gatsby/src/redux/actions.js +++ b/packages/gatsby/src/redux/actions.js @@ -1316,7 +1316,7 @@ import type GatsbyGraphQLType from "../schema/types/type-builders" * # Does not include automatically inferred fields * type AuthorJson implements Node @dontInfer { * name: String! - * birthday: Date! @date(locale: "ru") + * birthday: Date! @dateformat(locale: "ru") * } * ` * createTypes(typeDefs) From f338bddb065cbddd64bbc05e9b73a393793a2b20 Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Wed, 15 May 2019 14:28:36 +0300 Subject: [PATCH 32/32] More blog post improvements --- .../index.md | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/docs/blog/2019-05-17-improvements-to-schema-customization/index.md b/docs/blog/2019-05-17-improvements-to-schema-customization/index.md index 91c11c34a35fa..728672ce38233 100644 --- a/docs/blog/2019-05-17-improvements-to-schema-customization/index.md +++ b/docs/blog/2019-05-17-improvements-to-schema-customization/index.md @@ -13,7 +13,7 @@ It is now possible to indicate to Gatsby, that you want to add a resolver to an ## Summary -After about a month of testing schema customization both here and in pre-release we determined a couple of issues. The original aim of our schema customisation work was to remove uncertainty in people's schemas when their data changes. +After about a month of testing schema customization both here and in pre-release we determined a couple of issues. The original aim of our schema customisation work was to remove uncertainty in people's schemas when their data changes. However, the original design allowed some uncertainties anyway. In addition, it made inference a more heavy process, trading performance for consistency without providing a way to opt out completely. To summarize, the schema customization work released in Gatsby 2.2.0 had the following issues: @@ -24,37 +24,62 @@ However, the original design allowed some uncertainties anyway. In addition, it Therefore we have some changes to the way we do inferrence. In addition, we are deprecating some of the features introduced in 2.2.0 and will remove them in Gatsby 3. -## noDefaultResolvers and inferrence modes +## Changes in Gatsby 2.5.0 + +### noDefaultResolvers and inferrence modes First of all, we are deprecating `noDefaultResolvers`. It was an argument of `infer` and `dontInfer`. We feel it was confusing and in some cases it didn't even actually add resolvers :). We will support `noDefaultResolvers` until version 3, after which `@infer` behaviour (see below) will become a default and `noDefaultResolvers` will be removed. We didn't want to break things, so we keep old default behaviour, even though we think it's not optimal. Add explicit `@infer` and resolver extensions (like `@link`) to fields to be future proof. -### Default (deprecated, removed in v3) +#### Default (deprecated, removed in v3) Applies with no `@infer` and no `@dontInfer` on a type. Equals to `@infer(noDefaultResolvers: false)`. Type gets all inferred fields added. If type has defined fields of types `Date`, `File` and any other node, and we inferred that they should have resolver options, resolver options will be added to type with a warning. -### Strict inference (future default in v3) +#### Strict inference (future default in v3) Applies with `@infer` or `@infer(noDefaultResolvers: true)`. Type gets all inferred fields added. Existing fields won't automatically get resolvers (use resolver extensions). -### No inferrence +#### No inferrence Applies with `@dontInfer` or `@dontInfer(noDefaultResolvers: true)`. Inferrence won't run at all. Existing fields won't automatically get resolvers (use resolver extensions). -### No new fields with default resolvers (deprecated, removed in v3) +#### No new fields with default resolvers (deprecated, removed in v3) Applies with `@dontInfer(noDefaultResolvers: false)` Inferrence will run, but fields won't be added. If type has defined fields of types `Date`, `File` and any other node, and we inferred that they should have resolvers/args, resolvers/args will be added to type with a warning. -## Resolver extensions +### Migrating your code + +Here are suggested changes to your code if you are using schema customization already. Your code will work in Gatsby 2.5.0, but those changes will ensure it stays compatible with Gatsby 3.0 + +1. Add resolver directives to fields +2. Add `@infer` or `@dontInfer` to your type if you don't have it already + +```graphql:title=before +type MyType { + date: Date + image: File + authorByEmail: AuthorJson +} +``` + +```graphql:title=after +type MyType @infer { + date: Date @dateformat + image: File @fileByRelativePath + authorByEmail: Author @link +} +``` + +### Resolver extensions Add resolver and resolver options (such as arguments) to the given field. There are currently 3 extensions available. @@ -101,7 +126,7 @@ schema.createObjectType({ ## Conclusions -With these improvements we hope we'll solve most of the issues that people are having with new schema customization. We are working on further improvements, like allowing users and plugins to define their own extensions (see [PR #13738](https://github.com/gatsbyjs/gatsby/pull/13738)). +With these improvements we hope we'll solve most of the issues that people are having with new schema customization. We want more feedback about this from you - please write a message to [schema customization umbrella issue](https://github.com/gatsbyjs/gatsby/issues/12272) if you encounter any problems. We are working on further improvements, like allowing users and plugins to define their own extensions (see [PR #13738](https://github.com/gatsbyjs/gatsby/pull/13738)). Useful links: