diff --git a/src/utilities/__tests__/buildASTSchema-test.ts b/src/utilities/__tests__/buildASTSchema-test.ts index 21de5434c2..3ea90c9b35 100644 --- a/src/utilities/__tests__/buildASTSchema-test.ts +++ b/src/utilities/__tests__/buildASTSchema-test.ts @@ -509,6 +509,26 @@ describe('Schema Builder', () => { expect(cycleSDL(sdl)).to.equal(sdl); }); + it('builds recursive input object field defaults', () => { + const sdl = dedent` + type Query { + depth(person: PersonInput): Int + } + + input PersonInput { + parent: PersonInput = {parent: {parent: {parent: null}}} + } + `; + + const schema = buildSchema(sdl); + const personInput = assertInputObjectType(schema.getType('PersonInput')); + + expect(personInput.getFields().parent.defaultValue).to.deep.equal({ + parent: { parent: { parent: null } }, + }); + expect(printSchema(schema)).to.equal(sdl); + }); + it('Simple argument field with default', () => { const sdl = dedent` type Query { diff --git a/src/utilities/__tests__/extendSchema-test.ts b/src/utilities/__tests__/extendSchema-test.ts index 7891a47793..fc55883727 100644 --- a/src/utilities/__tests__/extendSchema-test.ts +++ b/src/utilities/__tests__/extendSchema-test.ts @@ -271,6 +271,38 @@ describe('extendSchema', () => { `); }); + it('extends inputs with recursive field defaults', () => { + const schema = buildSchema(` + type Query { + someInput(arg: PersonInput): String + } + + input PersonInput { + parent: PersonInput + } + `); + const extensionSDL = dedent` + extend input PersonInput { + ancestor: PersonInput = {parent: { parent: { parent: null } } } + } + `; + const extendedSchema = extendSchema(schema, parse(extensionSDL)); + const personInput = assertInputObjectType( + extendedSchema.getType('PersonInput'), + ); + + expect(personInput.getFields().ancestor.defaultValue).to.deep.equal({ + parent: { parent: { parent: null } }, + }); + expect(validateSchema(extendedSchema)).to.deep.equal([]); + expectSchemaChanges(schema, extendedSchema).to.equal(dedent` + input PersonInput { + parent: PersonInput + ancestor: PersonInput = {parent: {parent: {parent: null}}} + } + `); + }); + it('extends scalars by adding new directives', () => { const schema = buildSchema(` type Query { diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 9dc9fc77ee..86cd345fd2 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -298,15 +298,27 @@ export function extendSchemaImpl( const config = type.toConfig(); const extensions = typeExtensionsMap[config.name] ?? []; + let fields: GraphQLInputFieldConfigMap | undefined; + return new GraphQLInputObjectType({ ...config, - fields: () => ({ - ...mapValue(config.fields, (field) => ({ - ...field, - type: replaceType(field.type), - })), - ...buildInputFieldMap(extensions), - }), + fields: () => { + if (fields === undefined) { + fields = Object.create(null) as GraphQLInputFieldConfigMap; + Object.assign( + fields, + mapValue(config.fields, (field) => ({ + ...field, + type: replaceType(field.type), + })), + ); + extendInputFieldMap(fields, extensions); + // Default values must be applied _after_ all fields are known to + // prevent issues with recursion. + applyInputFieldDefaultValues(fields, extensions); + } + return fields; + }, extensionASTNodes: config.extensionASTNodes.concat(extensions), }); } @@ -545,12 +557,12 @@ export function extendSchemaImpl( return argConfigMap; } - function buildInputFieldMap( + function extendInputFieldMap( + inputFieldMap: GraphQLInputFieldConfigMap, nodes: ReadonlyArray< InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode >, - ): GraphQLInputFieldConfigMap { - const inputFieldMap = Object.create(null); + ): void { for (const node of nodes) { // FIXME: https://github.com/graphql/graphql-js/issues/2203 const fieldsNodes = /* c8 ignore next */ node.fields ?? []; @@ -564,13 +576,34 @@ export function extendSchemaImpl( inputFieldMap[field.name.value] = { type, description: field.description?.value, - defaultValue: valueFromAST(field.defaultValue, type), + // `defaultValue` will be populated later by applyInputFieldDefaultValues + defaultValue: undefined, deprecationReason: getDeprecationReason(field), astNode: field, }; } } - return inputFieldMap; + } + + function applyInputFieldDefaultValues( + inputFieldMap: GraphQLInputFieldConfigMap, + nodes: ReadonlyArray< + InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode + >, + ): void { + for (const node of nodes) { + // FIXME: https://github.com/graphql/graphql-js/issues/2203 + const fieldsNodes = /* c8 ignore next */ node.fields ?? []; + + for (const field of fieldsNodes) { + const type = inputFieldMap[field.name.value].type; + + inputFieldMap[field.name.value].defaultValue = valueFromAST( + field.defaultValue, + type, + ); + } + } } function buildEnumValueMap( @@ -686,10 +719,20 @@ export function extendSchemaImpl( case Kind.INPUT_OBJECT_TYPE_DEFINITION: { const allNodes = [astNode, ...extensionASTNodes]; + let fields: GraphQLInputFieldConfigMap | undefined; return new GraphQLInputObjectType({ name, description: astNode.description?.value, - fields: () => buildInputFieldMap(allNodes), + fields: () => { + if (fields === undefined) { + fields = Object.create(null) as GraphQLInputFieldConfigMap; + extendInputFieldMap(fields, allNodes); + // Default values must be applied _after_ all fields are known to + // prevent issues with recursion. + applyInputFieldDefaultValues(fields, allNodes); + } + return fields; + }, astNode, extensionASTNodes, isOneOf: isOneOf(astNode),