Skip to content

Commit a3dbb17

Browse files
committed
Move variable position validation for OneOf fields to VariablesInAllowedPosition Rule
1 parent 4dfae39 commit a3dbb17

5 files changed

+60
-54
lines changed

src/validation/ValidationContext.ts

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type NodeWithSelectionSet = OperationDefinitionNode | FragmentDefinitionNode;
3535
interface VariableUsage {
3636
readonly node: VariableNode;
3737
readonly type: Maybe<GraphQLInputType>;
38+
readonly parentType: Maybe<GraphQLInputType>;
3839
readonly defaultValue: GraphQLDefaultValueUsage | undefined;
3940
readonly fragmentVariableDefinition: Maybe<VariableDefinitionNode>;
4041
}
@@ -226,13 +227,15 @@ export class ValidationContext extends ASTValidationContext {
226227
newUsages.push({
227228
node: variable,
228229
type: typeInfo.getInputType(),
230+
parentType: typeInfo.getParentInputType(),
229231
defaultValue: undefined, // fragment variables have a variable default but no location default, which is what this default value represents
230232
fragmentVariableDefinition,
231233
});
232234
} else {
233235
newUsages.push({
234236
node: variable,
235237
type: typeInfo.getInputType(),
238+
parentType: typeInfo.getParentInputType(),
236239
defaultValue: typeInfo.getDefaultValue(),
237240
fragmentVariableDefinition: undefined,
238241
});

src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts

-16
Original file line numberDiff line numberDiff line change
@@ -1094,22 +1094,6 @@ describe('Validate: Values of correct type', () => {
10941094
]);
10951095
});
10961096

1097-
it('Exactly one nullable variable', () => {
1098-
expectErrors(`
1099-
query ($string: String) {
1100-
complicatedArgs {
1101-
oneOfArgField(oneOfArg: { stringField: $string })
1102-
}
1103-
}
1104-
`).toDeepEqual([
1105-
{
1106-
message:
1107-
'Variable "$string" must be non-nullable to be used for OneOf Input Object "OneOfInput".',
1108-
locations: [{ line: 4, column: 37 }],
1109-
},
1110-
]);
1111-
});
1112-
11131097
it('More than one field', () => {
11141098
expectErrors(`
11151099
{

src/validation/__tests__/VariablesInAllowedPositionRule-test.ts

+31
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,37 @@ describe('Validate: Variables are in allowed positions', () => {
365365
});
366366
});
367367

368+
describe('Validates OneOf Input Objects', () => {
369+
it('Allows exactly one non-nullable variable', () => {
370+
expectValid(`
371+
query ($string: String!) {
372+
complicatedArgs {
373+
oneOfArgField(oneOfArg: { stringField: $string })
374+
}
375+
}
376+
`);
377+
});
378+
379+
it('Forbids one nullable variable', () => {
380+
expectErrors(`
381+
query ($string: String) {
382+
complicatedArgs {
383+
oneOfArgField(oneOfArg: { stringField: $string })
384+
}
385+
}
386+
`).toDeepEqual([
387+
{
388+
message:
389+
'Variable "$string" is of type "String" but must be non-nullable to be used for OneOf Input Object "OneOfInput".',
390+
locations: [
391+
{ line: 2, column: 16 },
392+
{ line: 4, column: 52 },
393+
],
394+
},
395+
]);
396+
});
397+
});
398+
368399
describe('Fragment arguments are validated', () => {
369400
it('Boolean => Boolean', () => {
370401
expectValid(`

src/validation/rules/ValuesOfCorrectTypeRule.ts

+1-36
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import type {
88
ObjectFieldNode,
99
ObjectValueNode,
1010
ValueNode,
11-
VariableDefinitionNode,
1211
} from '../../language/ast.js';
1312
import { Kind } from '../../language/kinds.js';
1413
import { print } from '../../language/printer.js';
@@ -40,17 +39,7 @@ import type { ValidationContext } from '../ValidationContext.js';
4039
export function ValuesOfCorrectTypeRule(
4140
context: ValidationContext,
4241
): ASTVisitor {
43-
let variableDefinitions: { [key: string]: VariableDefinitionNode } = {};
44-
4542
return {
46-
OperationDefinition: {
47-
enter() {
48-
variableDefinitions = {};
49-
},
50-
},
51-
VariableDefinition(definition) {
52-
variableDefinitions[definition.variable.name.value] = definition;
53-
},
5443
ListValue(node) {
5544
// Note: TypeInfo will traverse into a list's item type, so look to the
5645
// parent input type to check if it is a list.
@@ -84,13 +73,7 @@ export function ValuesOfCorrectTypeRule(
8473
}
8574

8675
if (type.isOneOf) {
87-
validateOneOfInputObject(
88-
context,
89-
node,
90-
type,
91-
fieldNodeMap,
92-
variableDefinitions,
93-
);
76+
validateOneOfInputObject(context, node, type, fieldNodeMap);
9477
}
9578
},
9679
ObjectField(node) {
@@ -189,7 +172,6 @@ function validateOneOfInputObject(
189172
node: ObjectValueNode,
190173
type: GraphQLInputObjectType,
191174
fieldNodeMap: Map<string, ObjectFieldNode>,
192-
variableDefinitions: { [key: string]: VariableDefinitionNode },
193175
): void {
194176
const keys = Array.from(fieldNodeMap.keys());
195177
const isNotExactlyOneField = keys.length !== 1;
@@ -206,29 +188,12 @@ function validateOneOfInputObject(
206188

207189
const value = fieldNodeMap.get(keys[0])?.value;
208190
const isNullLiteral = !value || value.kind === Kind.NULL;
209-
const isVariable = value?.kind === Kind.VARIABLE;
210191

211192
if (isNullLiteral) {
212193
context.reportError(
213194
new GraphQLError(`Field "${type}.${keys[0]}" must be non-null.`, {
214195
nodes: [node],
215196
}),
216197
);
217-
return;
218-
}
219-
220-
if (isVariable) {
221-
const variableName = value.name.value;
222-
const definition = variableDefinitions[variableName];
223-
const isNullableVariable = definition.type.kind !== Kind.NON_NULL_TYPE;
224-
225-
if (isNullableVariable) {
226-
context.reportError(
227-
new GraphQLError(
228-
`Variable "$${variableName}" must be non-nullable to be used for OneOf Input Object "${type}".`,
229-
{ nodes: [node] },
230-
),
231-
);
232-
}
233198
}
234199
}

src/validation/rules/VariablesInAllowedPositionRule.ts

+25-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import type {
1111
GraphQLDefaultValueUsage,
1212
GraphQLType,
1313
} from '../../type/definition.js';
14-
import { isNonNullType } from '../../type/definition.js';
14+
import {
15+
isInputObjectType,
16+
isNonNullType,
17+
isNullableType,
18+
} from '../../type/definition.js';
1519
import type { GraphQLSchema } from '../../type/schema.js';
1620

1721
import { isTypeSubTypeOf } from '../../utilities/typeComparators.js';
@@ -42,6 +46,7 @@ export function VariablesInAllowedPositionRule(
4246
for (const {
4347
node,
4448
type,
49+
parentType,
4550
defaultValue,
4651
fragmentVariableDefinition,
4752
} of usages) {
@@ -78,6 +83,21 @@ export function VariablesInAllowedPositionRule(
7883
),
7984
);
8085
}
86+
87+
if (
88+
isInputObjectType(parentType) &&
89+
parentType.isOneOf &&
90+
isNullableType(varType)
91+
) {
92+
const varTypeStr = inspect(varType);
93+
const parentTypeStr = inspect(parentType);
94+
context.reportError(
95+
new GraphQLError(
96+
`Variable "$${varName}" is of type "${varTypeStr}" but must be non-nullable to be used for OneOf Input Object "${parentTypeStr}".`,
97+
{ nodes: [varDef, node] },
98+
),
99+
);
100+
}
81101
}
82102
}
83103
},
@@ -90,8 +110,11 @@ export function VariablesInAllowedPositionRule(
90110

91111
/**
92112
* Returns true if the variable is allowed in the location it was found,
93-
* which includes considering if default values exist for either the variable
113+
* including considering if default values exist for either the variable
94114
* or the location at which it is located.
115+
*
116+
* OneOf Input Object Type fields are considered separately above to
117+
* provide a more descriptive error message.
95118
*/
96119
function allowedVariableUsage(
97120
schema: GraphQLSchema,

0 commit comments

Comments
 (0)