From 248f7e59ac938556e4882fa9fa36b299b7db7413 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Tue, 25 Mar 2025 18:48:06 +0100 Subject: [PATCH] Ensure we validate for using nullable variables in oneOf input fields --- src/validation/ValidationContext.ts | 2 ++ .../VariablesInAllowedPositionRule-test.ts | 31 +++++++++++++++++++ .../rules/VariablesInAllowedPositionRule.ts | 21 +++++++++++-- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/validation/ValidationContext.ts b/src/validation/ValidationContext.ts index f6944a6ebd..7884031c9d 100644 --- a/src/validation/ValidationContext.ts +++ b/src/validation/ValidationContext.ts @@ -33,6 +33,7 @@ interface VariableUsage { readonly node: VariableNode; readonly type: Maybe; readonly defaultValue: Maybe; + readonly parentType: Maybe; } /** @@ -209,6 +210,7 @@ export class ValidationContext extends ASTValidationContext { node: variable, type: typeInfo.getInputType(), defaultValue: typeInfo.getDefaultValue(), + parentType: typeInfo.getParentInputType(), }); }, }), diff --git a/src/validation/__tests__/VariablesInAllowedPositionRule-test.ts b/src/validation/__tests__/VariablesInAllowedPositionRule-test.ts index 090f1680c4..6fc3d59c39 100644 --- a/src/validation/__tests__/VariablesInAllowedPositionRule-test.ts +++ b/src/validation/__tests__/VariablesInAllowedPositionRule-test.ts @@ -358,3 +358,34 @@ describe('Validate: Variables are in allowed positions', () => { }); }); }); + +describe('Validates OneOf Input Objects', () => { + it('Allows exactly one non-nullable variable', () => { + expectValid(` + query ($string: String!) { + complicatedArgs { + oneOfArgField(oneOfArg: { stringField: $string }) + } + } + `); + }); + + it('Forbids one nullable variable', () => { + expectErrors(` + query ($string: String) { + complicatedArgs { + oneOfArgField(oneOfArg: { stringField: $string }) + } + } + `).toDeepEqual([ + { + message: + 'Variable "$string" is of type "String" but must be non-nullable to be used for OneOf Input Object "OneOfInput".', + locations: [ + { line: 2, column: 14 }, + { line: 4, column: 50 }, + ], + }, + ]); + }); +}); diff --git a/src/validation/rules/VariablesInAllowedPositionRule.ts b/src/validation/rules/VariablesInAllowedPositionRule.ts index a0b7e991a6..3f4cb51c27 100644 --- a/src/validation/rules/VariablesInAllowedPositionRule.ts +++ b/src/validation/rules/VariablesInAllowedPositionRule.ts @@ -8,7 +8,11 @@ import { Kind } from '../../language/kinds'; import type { ASTVisitor } from '../../language/visitor'; import type { GraphQLType } from '../../type/definition'; -import { isNonNullType } from '../../type/definition'; +import { + isInputObjectType, + isNonNullType, + isNullableType, +} from '../../type/definition'; import type { GraphQLSchema } from '../../type/schema'; import { isTypeSubTypeOf } from '../../utilities/typeComparators'; @@ -36,7 +40,7 @@ export function VariablesInAllowedPositionRule( leave(operation) { const usages = context.getRecursiveVariableUsages(operation); - for (const { node, type, defaultValue } of usages) { + for (const { node, type, defaultValue, parentType } of usages) { const varName = node.name.value; const varDef = varDefMap[varName]; if (varDef && type) { @@ -66,6 +70,19 @@ export function VariablesInAllowedPositionRule( ), ); } + + if ( + isInputObjectType(parentType) && + parentType.isOneOf && + isNullableType(varType) + ) { + context.reportError( + new GraphQLError( + `Variable "$${varName}" is of type "${varType}" but must be non-nullable to be used for OneOf Input Object "${parentType}".`, + { nodes: [varDef, node] }, + ), + ); + } } } },