diff --git a/packages/gatsby/src/schema/__tests__/__snapshots__/print.js.snap b/packages/gatsby/src/schema/__tests__/__snapshots__/print.js.snap index 81b5bdfe03dca..e7816e0482521 100644 --- a/packages/gatsby/src/schema/__tests__/__snapshots__/print.js.snap +++ b/packages/gatsby/src/schema/__tests__/__snapshots__/print.js.snap @@ -120,6 +120,14 @@ input Baz { qux: Boolean } +type BarChild implements Node @childOf(types: [\\"Test\\"]) @dontInfer { + bar: String +} + +type FooChild implements Node @childOf(types: [\\"Test\\"]) @dontInfer { + bar: String +} + type Test implements Node @dontInfer { foo: Int }" @@ -148,6 +156,10 @@ type Nested { input Baz { qux: Boolean +} + +type BarChild implements Node @childOf(types: [\\"Test\\"]) @dontInfer { + bar: String }" `; @@ -266,6 +278,14 @@ type OneMoreTest implements Node @dontInfer { union ThisOrThat = AnotherTest | OneMoreTest +type BarChild implements Node @childOf(types: [\\"Test\\"]) @dontInfer { + bar: String +} + +type FooChild implements Node @childOf(types: [\\"Test\\"]) @dontInfer { + bar: String +} + type Test implements Node @dontInfer { foo: Int }" @@ -423,6 +443,14 @@ input Baz { qux: Boolean } +type BarChild implements Node @childOf(types: [\\"Test\\"]) @dontInfer { + bar: String +} + +type FooChild implements Node @childOf(types: [\\"Test\\"]) @dontInfer { + bar: String +} + type Test implements Node @dontInfer { foo: Int }" diff --git a/packages/gatsby/src/schema/__tests__/print.js b/packages/gatsby/src/schema/__tests__/print.js index dfccfddac0b6b..3fd0938988530 100644 --- a/packages/gatsby/src/schema/__tests__/print.js +++ b/packages/gatsby/src/schema/__tests__/print.js @@ -2,6 +2,8 @@ const { build } = require(`..`) import { buildObjectType } from "../types/type-builders" const { store } = require(`../../redux`) const { actions } = require(`../../redux/actions/restricted`) +const { actions: publicActions } = require(`../../redux/actions/public`) +const { createParentChildLink } = publicActions const { printTypeDefinitions } = actions jest.mock(`fs-extra`) @@ -41,14 +43,35 @@ jest.spyOn(global.Date.prototype, `toISOString`).mockReturnValue(`2019-01-01`) describe(`Print type definitions`, () => { beforeEach(() => { store.dispatch({ type: `DELETE_CACHE` }) - const node = { + const node1 = { id: `test1`, internal: { type: `Test`, }, + children: [], foo: 26, } - store.dispatch({ type: `CREATE_NODE`, payload: { ...node } }) + const node2 = { + id: `test2`, + parent: `test1`, + internal: { + type: `FooChild`, + }, + bar: `bar`, + } + const node3 = { + id: `test3`, + parent: `test1`, + internal: { + type: `BarChild`, + }, + bar: `bar`, + } + store.dispatch({ type: `CREATE_NODE`, payload: { ...node1 } }) + store.dispatch({ type: `CREATE_NODE`, payload: { ...node2 } }) + store.dispatch({ type: `CREATE_NODE`, payload: { ...node3 } }) + createParentChildLink({ parent: node1, child: node2 }) + createParentChildLink({ parent: node1, child: node3 }) const typeDefs = [] typeDefs.push(` type AnotherTest implements Node & ITest { @@ -96,6 +119,18 @@ describe(`Print type definitions`, () => { types: [`OneMoreTest`], }, }, + }), + buildObjectType({ + name: `BarChild`, + fields: { + id: `ID!`, + }, + interfaces: [`Node`], + extensions: { + childOf: { + types: [`Test`], + }, + }, }) ) store.dispatch({ @@ -108,6 +143,11 @@ describe(`Print type definitions`, () => { payload: typeDefs[1], plugin: { name: `gatsby-plugin-another-test` }, }) + store.dispatch({ + type: `CREATE_TYPES`, + payload: typeDefs[2], + plugin: { name: `gatsby-plugin-another-test` }, + }) }) it(`saves type definitions to default file`, async () => { diff --git a/packages/gatsby/src/schema/extensions/__tests__/child-relations.js b/packages/gatsby/src/schema/extensions/__tests__/child-relations.js index 07b865c374587..15ae28a4df983 100644 --- a/packages/gatsby/src/schema/extensions/__tests__/child-relations.js +++ b/packages/gatsby/src/schema/extensions/__tests__/child-relations.js @@ -186,6 +186,24 @@ describe(`Define parent-child relationships with field extensions`, () => { ) }) + it(`does not show deprecation warning for inferred child fields`, async () => { + dispatch( + createTypes(` + type Parent implements Node @dontInfer @mimeTypes(types: ["application/listenup"]) { + id: ID! + } + type Child implements Node @childOf(mimeTypes: ["application/listenup"]) { + id: ID! + } + type AnotherChild implements Node @childOf(types: ["Parent"]) { + id: ID! + } + `) + ) + await buildSchema() + expect(report.warn).toBeCalledTimes(0) + }) + it(`adds children[Field] field to parent type with childOf extension`, async () => { dispatch( createTypes(` diff --git a/packages/gatsby/src/schema/schema.js b/packages/gatsby/src/schema/schema.js index 00d1caee63e06..abaf7a482952d 100644 --- a/packages/gatsby/src/schema/schema.js +++ b/packages/gatsby/src/schema/schema.js @@ -146,6 +146,9 @@ const updateSchemaComposer = async ({ inferenceMetadata, parentSpan: activity.span, }) + addInferredChildOfExtensions({ + schemaComposer, + }) activity.end() activity = report.phantomActivity(`Processing types`, { @@ -202,11 +205,6 @@ const processTypeComposer = async ({ if (typeComposer.hasInterface(`Node`)) { await addNodeInterfaceFields({ schemaComposer, typeComposer, parentSpan }) - await addImplicitConvenienceChildrenFields({ - schemaComposer, - typeComposer, - parentSpan, - }) } await determineSearchableFields({ schemaComposer, @@ -999,15 +997,42 @@ const addConvenienceChildrenFields = ({ schemaComposer }) => { }) } -const addImplicitConvenienceChildrenFields = ({ - schemaComposer, - typeComposer, -}) => { +const isExplicitChild = ({ typeComposer, childTypeComposer }) => { + if (!childTypeComposer.hasExtension(`childOf`)) { + return false + } + const childOfExtension = childTypeComposer.getExtension(`childOf`) + const { types: parentMimeTypes = [] } = + typeComposer.getExtension(`mimeTypes`) ?? {} + + return ( + childOfExtension?.types?.includes(typeComposer.getTypeName()) || + childOfExtension?.mimeTypes?.some(mimeType => + parentMimeTypes.includes(mimeType) + ) + ) +} + +const addInferredChildOfExtensions = ({ schemaComposer }) => { + schemaComposer.forEach(typeComposer => { + if ( + typeComposer instanceof ObjectTypeComposer && + typeComposer.hasInterface(`Node`) + ) { + addInferredChildOfExtension({ + schemaComposer, + typeComposer, + }) + } + }) +} + +const addInferredChildOfExtension = ({ schemaComposer, typeComposer }) => { const shouldInfer = typeComposer.getExtension(`infer`) - // In Gatsby v3, when `@dontInfer` is set, children fields will not be - // created for parent-child relations set by plugins with + // In Gatsby v3, when `@dontInfer` is set, `@childOf` extension will not be + // automatically created for parent-child relations set by plugins with // `createParentChildLink`. With `@dontInfer`, only parent-child - // relations explicitly set with the `childOf` extension will be added. + // relations explicitly set with the `@childOf` extension will be added. // if (shouldInfer === false) return const parentTypeName = typeComposer.getTypeName() @@ -1016,39 +1041,46 @@ const addImplicitConvenienceChildrenFields = ({ const childNodesByType = groupChildNodesByType({ nodes }) Object.keys(childNodesByType).forEach(typeName => { - // Adding children fields to types with the `@dontInfer` extension is deprecated - if (shouldInfer === false) { - const childTypeComposer = schemaComposer.getAnyTC(typeName) - const childOfExtension = childTypeComposer.getExtension(`childOf`) + const childTypeComposer = schemaComposer.getAnyTC(typeName) + let childOfExtension = childTypeComposer.getExtension(`childOf`) - // Only warn when the parent-child relation has not been explicitly set with - if ( - !childOfExtension || - !childOfExtension.types.includes(parentTypeName) - ) { - const childField = fieldNames.convenienceChild(typeName) - const childrenField = fieldNames.convenienceChildren(typeName) - const childOfTypes = (childOfExtension?.types ?? []) - .concat(parentTypeName) - .map(name => `"${name}"`) - .join(`,`) - - report.warn( - `Deprecation warning: ` + - `In Gatsby v3 fields \`${parentTypeName}.${childField}\` and \`${parentTypeName}.${childrenField}\` ` + - `will not be added automatically because ` + - `type \`${typeName}\` does not explicitly list type \`${parentTypeName}\` in \`childOf\` extension.\n` + - `Add the following type definition to fix this:\n\n` + - ` type ${typeName} implements Node @childOf(types: [${childOfTypes}]) {\n` + - ` id: ID!\n` + - ` }\n\n` + - `https://www.gatsbyjs.com/docs/actions/#createTypes` - ) - } + if (isExplicitChild({ typeComposer, childTypeComposer })) { + return } + if (shouldInfer === false) { + // Adding children fields to types with the `@dontInfer` extension is deprecated + // Only warn when the parent-child relation has not been explicitly set with `childOf` directive + const childField = fieldNames.convenienceChild(typeName) + const childrenField = fieldNames.convenienceChildren(typeName) + const childOfTypes = (childOfExtension?.types ?? []) + .concat(parentTypeName) + .map(name => `"${name}"`) + .join(`,`) - typeComposer.addFields(createChildrenField(typeName)) - typeComposer.addFields(createChildField(typeName)) + report.warn( + `Deprecation warning: ` + + `In Gatsby v3 fields \`${parentTypeName}.${childField}\` and \`${parentTypeName}.${childrenField}\` ` + + `will not be added automatically because ` + + `type \`${typeName}\` does not explicitly list type \`${parentTypeName}\` in \`childOf\` extension.\n` + + `Add the following type definition to fix this:\n\n` + + ` type ${typeName} implements Node @childOf(types: [${childOfTypes}]) {\n` + + ` id: ID!\n` + + ` }\n\n` + + `https://www.gatsbyjs.com/docs/actions/#createTypes` + ) + } + // Set `@childOf` extension automatically + // This will cause convenience children fields like `childImageSharp` + // to be added in `addConvenienceChildrenFields` method. + // Also required for proper printing of the `@childOf` directive in the snapshot plugin + if (!childOfExtension) { + childOfExtension = {} + } + if (!childOfExtension.types) { + childOfExtension.types = [] + } + childOfExtension.types.push(parentTypeName) + childTypeComposer.setExtension(`childOf`, childOfExtension) }) }