Skip to content

Commit affae01

Browse files
yaacovCRerikwrede
authored andcommitted
move variable position validation for OneOf to VariablesInAllowedPositionRule (#4194)
### TLDR => let's consider moving validation of variable positions with respect to one of into the `VariablesInAllowedPositionRule`, i.e. out of the `ValuesOfCorrectTypeRule`. <hr> This work was pulled out of the work rebasing the Default Value Coercion/Validation PR stack at #3814 on the OneOf and Experimental Fragment Variables changes. In that PR stack (work originally done by @leebyron within #3049), Lee extracts the validation done by the `ValuesOfCorrectTypeRule` into a `validateInputLiteral()` utility that can be called at validation-time or at run-time. At validation time, `validateInputLiteral()` validates statically, is not supplied variables **or their types**, and does not validate variables, while at run-time it is supplied variables and does validate them. <hr> With OneOf Input Objects, we have a situation in which Input Objects will define technically nullable/optional input fields, but the addition of the `@oneOf` directive tells the validator/executor that we have to supply exactly one of these fields (and it cannot be given the value `null`). In essence, we are saying that when `@oneOf` is applied, the fields of the input object are no longer technically nullable positions, although they are still optional. This was for a combination of type reasoning, backwards compatibility, and simplicity, working overall within the constraints of GraphQL where only nullable fields are optional. So, to validate variable usage with OneOf Input Object, we need to make sure that a variable with a nullable type is not allowed to show up in a field position on a OneOf Input Object. Where should this be done? Currently, this is done within the `ValuesOfCorrectTypeRule`, which for this purpose was modified to collect the operation variable definitions. @leebyron 's changes in the abovementioned stack extracts this to `validateInputLiteral()`, where we run into a problem, we don't have access to the operation (or experimental fragment variables)! Lee's work preserving variable sources and definitions organizes the definitions by source, so if we are analyzing statically, we don't have the source or the definitions. There are two paths forwards. One idea is to modify Lee's work, and split the definitions from the sources, and supply them to `validateInputLiteral()` even when working statically, which complicates the signature of a number of functions. What if we take a step back, and ask ourselves if we should have really modified `ValuesOfCorrectTypeRule` to collect all those operation variable definitions? If we move this bit to `VariablesInAllowedPositionRule`, then we avoid the entire complexity. That's what this PR does. <hr> How did this happen anyway? Shouldn't it be clear from the spec change in which validation rule the changes should have gone? Actually....not exactly. According to [the proposed spec changes](graphql/graphql-spec#825), this work was not done within the `ValuesOfCorrectTypeRule` or the `VariablesInAllowedPositionRule`, but instead within a wholly new "OneOf Input Object Rule." That's not the way it got implemented, and in my opinion for good reason! I'm planning on separately submit some comments on the RFC to that effect, that we can eliminate the need for the new rule, and fold the changes into the existing `ValuesOfCorrectTypeRule` -- which basically says that if it can't coerce, it's not valid, and because of the coercion changes does not require any actual new text -- except within the `VariablesInAllowedPositionRule`. Anyway, TLDR TLDR => let's consider moving validation of variable positions with respect to one of into the `VariablesInAllowedPositionRule`, i.e. out of the `ValuesOfCorrectTypeRule`. Discussion of allowed variable positions just belongs within that rule, just look at the rule name. :)
1 parent 4543103 commit affae01

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
}
@@ -237,13 +238,15 @@ export class ValidationContext extends ASTValidationContext {
237238
newUsages.push({
238239
node: variable,
239240
type: typeInfo.getInputType(),
241+
parentType: typeInfo.getParentInputType(),
240242
defaultValue: undefined, // fragment variables have a variable default but no location default, which is what this default value represents
241243
fragmentVariableDefinition,
242244
});
243245
} else {
244246
newUsages.push({
245247
node: variable,
246248
type: typeInfo.getInputType(),
249+
parentType: typeInfo.getParentInputType(),
247250
defaultValue: typeInfo.getDefaultValue(),
248251
fragmentVariableDefinition: undefined,
249252
});

src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts

-16
Original file line numberDiff line numberDiff line change
@@ -1156,22 +1156,6 @@ describe('Validate: Values of correct type', () => {
11561156
]);
11571157
});
11581158

1159-
it('Exactly one nullable variable', () => {
1160-
expectErrors(`
1161-
query ($string: String) {
1162-
complicatedArgs {
1163-
oneOfArgField(oneOfArg: { stringField: $string })
1164-
}
1165-
}
1166-
`).toDeepEqual([
1167-
{
1168-
message:
1169-
'Variable "$string" must be non-nullable to be used for OneOf Input Object "OneOfInput".',
1170-
locations: [{ line: 4, column: 37 }],
1171-
},
1172-
]);
1173-
});
1174-
11751159
it('More than one field', () => {
11761160
expectErrors(`
11771161
{

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) {
@@ -191,7 +174,6 @@ function validateOneOfInputObject(
191174
node: ObjectValueNode,
192175
type: GraphQLInputObjectType,
193176
fieldNodeMap: Map<string, ObjectFieldNode>,
194-
variableDefinitions: { [key: string]: VariableDefinitionNode },
195177
): void {
196178
const keys = Array.from(fieldNodeMap.keys());
197179
const isNotExactlyOneField = keys.length !== 1;
@@ -208,29 +190,12 @@ function validateOneOfInputObject(
208190

209191
const value = fieldNodeMap.get(keys[0])?.value;
210192
const isNullLiteral = !value || value.kind === Kind.NULL;
211-
const isVariable = value?.kind === Kind.VARIABLE;
212193

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

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)