diff --git a/src/execution/__tests__/variables-test.ts b/src/execution/__tests__/variables-test.ts index 1a580e7822..78d206dff9 100644 --- a/src/execution/__tests__/variables-test.ts +++ b/src/execution/__tests__/variables-test.ts @@ -404,6 +404,38 @@ describe('Execute: Handles inputs', () => { }); }); + it('preserves explicit null variables within input object literals', () => { + const result = executeQuery( + ` + query q($input: String) { + fieldWithObjectInput(input: { a: $input, c: "baz" }) + }`, + { input: null }, + ); + + expect(result).to.deep.equal({ + data: { + fieldWithObjectInput: '{ a: null, c: "baz" }', + }, + }); + }); + + it('treats explicitly undefined variable values as omitted', () => { + const result = executeQuery( + ` + query q($input: String = "Default value") { + fieldWithNullableStringInput(input: $input) + }`, + { input: undefined }, + ); + + expect(result).to.deep.equal({ + data: { + fieldWithNullableStringInput: '"Default value"', + }, + }); + }); + it('uses default value when not provided', () => { const result = executeQuery(` query ($input: TestInputObject = {a: "foo", b: ["bar"], c: "baz"}) { diff --git a/src/utilities/__tests__/coerceInputValue-test.ts b/src/utilities/__tests__/coerceInputValue-test.ts index 61202e3f68..23ea89a9a8 100644 --- a/src/utilities/__tests__/coerceInputValue-test.ts +++ b/src/utilities/__tests__/coerceInputValue-test.ts @@ -640,6 +640,16 @@ describe('coerceInputLiteral', () => { { int: 42, requiredBool: true }, ); }); + + it('preserves explicit null variables in input object fields', () => { + testWithVariables( + '($foo: Boolean)', + { foo: null }, + '{ int: $foo, requiredBool: true }', + testInputObj, + { int: null, requiredBool: true }, + ); + }); }); describe('coerceDefaultValue', () => { diff --git a/src/utilities/coerceInputValue.ts b/src/utilities/coerceInputValue.ts index 4d0b1ad268..f93fd26edd 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -229,11 +229,11 @@ export function coerceInputLiteral( if ( !fieldNode || (fieldNode.value.kind === Kind.VARIABLE && - getCoercedVariableValue( + isMissingVariable( fieldNode.value, variableValues, fragmentVariableValues, - ) == null) + )) ) { if (isRequiredInputField(field)) { return; // Invalid: intentionally return no value. @@ -296,6 +296,19 @@ function getCoercedVariableValue( return variableValues?.coerced[varName]; } +function isMissingVariable( + variableNode: VariableNode, + variableValues: Maybe, + fragmentVariableValues: Maybe, +): boolean { + const varName = variableNode.name.value; + const scopedValues = + fragmentVariableValues?.sources[varName] !== undefined + ? fragmentVariableValues.coerced + : variableValues?.coerced; + return scopedValues?.[varName] === undefined; +} + interface InputValue { type: GraphQLInputType; default?: GraphQLDefaultInput | undefined;