Skip to content

Commit

Permalink
Move variable position validation for OneOf fields to VariablesInAllo…
Browse files Browse the repository at this point in the history
…wedPosition Rule
  • Loading branch information
yaacovCR committed Sep 17, 2024
1 parent 7875552 commit cb33ff1
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 54 deletions.
3 changes: 3 additions & 0 deletions src/validation/ValidationContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type NodeWithSelectionSet = OperationDefinitionNode | FragmentDefinitionNode;
interface VariableUsage {
readonly node: VariableNode;
readonly type: Maybe<GraphQLInputType>;
readonly parentType: Maybe<GraphQLInputType>;
readonly defaultValue: Maybe<unknown>;
readonly fragmentVariableDefinition: Maybe<VariableDefinitionNode>;
}
Expand Down Expand Up @@ -225,13 +226,15 @@ export class ValidationContext extends ASTValidationContext {
newUsages.push({
node: variable,
type: typeInfo.getInputType(),
parentType: typeInfo.getParentInputType(),
defaultValue: undefined, // fragment variables have a variable default but no location default, which is what this default value represents
fragmentVariableDefinition,
});
} else {
newUsages.push({
node: variable,
type: typeInfo.getInputType(),
parentType: typeInfo.getParentInputType(),
defaultValue: typeInfo.getDefaultValue(),
fragmentVariableDefinition: undefined,
});
Expand Down
16 changes: 0 additions & 16 deletions src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1094,22 +1094,6 @@ describe('Validate: Values of correct type', () => {
]);
});

it('Exactly one nullable variable', () => {
expectErrors(`
query ($string: String) {
complicatedArgs {
oneOfArgField(oneOfArg: { stringField: $string })
}
}
`).toDeepEqual([
{
message:
'Variable "$string" must be non-nullable to be used for OneOf Input Object "OneOfInput".',
locations: [{ line: 4, column: 37 }],
},
]);
});

it('More than one field', () => {
expectErrors(`
{
Expand Down
31 changes: 31 additions & 0 deletions src/validation/__tests__/VariablesInAllowedPositionRule-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,37 @@ 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: 16 },
{ line: 4, column: 52 },
],
},
]);
});
});

describe('Fragment arguments are validated', () => {
it('Boolean => Boolean', () => {
expectValid(`
Expand Down
37 changes: 1 addition & 36 deletions src/validation/rules/ValuesOfCorrectTypeRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type {
ObjectFieldNode,
ObjectValueNode,
ValueNode,
VariableDefinitionNode,
} from '../../language/ast.js';
import { Kind } from '../../language/kinds.js';
import { print } from '../../language/printer.js';
Expand Down Expand Up @@ -38,17 +37,7 @@ import type { ValidationContext } from '../ValidationContext.js';
export function ValuesOfCorrectTypeRule(
context: ValidationContext,
): ASTVisitor {
let variableDefinitions: { [key: string]: VariableDefinitionNode } = {};

return {
OperationDefinition: {
enter() {
variableDefinitions = {};
},
},
VariableDefinition(definition) {
variableDefinitions[definition.variable.name.value] = definition;
},
ListValue(node) {
// Note: TypeInfo will traverse into a list's item type, so look to the
// parent input type to check if it is a list.
Expand Down Expand Up @@ -82,13 +71,7 @@ export function ValuesOfCorrectTypeRule(
}

if (type.isOneOf) {
validateOneOfInputObject(
context,
node,
type,
fieldNodeMap,
variableDefinitions,
);
validateOneOfInputObject(context, node, type, fieldNodeMap);
}
},
ObjectField(node) {
Expand Down Expand Up @@ -185,7 +168,6 @@ function validateOneOfInputObject(
node: ObjectValueNode,
type: GraphQLInputObjectType,
fieldNodeMap: Map<string, ObjectFieldNode>,
variableDefinitions: { [key: string]: VariableDefinitionNode },
): void {
const keys = Array.from(fieldNodeMap.keys());
const isNotExactlyOneField = keys.length !== 1;
Expand All @@ -202,29 +184,12 @@ function validateOneOfInputObject(

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

if (isNullLiteral) {
context.reportError(
new GraphQLError(`Field "${type}.${keys[0]}" must be non-null.`, {
nodes: [node],
}),
);
return;
}

if (isVariable) {
const variableName = value.name.value;
const definition = variableDefinitions[variableName];
const isNullableVariable = definition.type.kind !== Kind.NON_NULL_TYPE;

if (isNullableVariable) {
context.reportError(
new GraphQLError(
`Variable "$${variableName}" must be non-nullable to be used for OneOf Input Object "${type}".`,
{ nodes: [node] },
),
);
}
}
}
27 changes: 25 additions & 2 deletions src/validation/rules/VariablesInAllowedPositionRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import { Kind } from '../../language/kinds.js';
import type { ASTVisitor } from '../../language/visitor.js';

import type { GraphQLType } from '../../type/definition.js';
import { isNonNullType } from '../../type/definition.js';
import {
isInputObjectType,
isNonNullType,
isNullableType,
} from '../../type/definition.js';
import type { GraphQLSchema } from '../../type/schema.js';

import { isTypeSubTypeOf } from '../../utilities/typeComparators.js';
Expand Down Expand Up @@ -39,6 +43,7 @@ export function VariablesInAllowedPositionRule(
for (const {
node,
type,
parentType,
defaultValue,
fragmentVariableDefinition,
} of usages) {
Expand Down Expand Up @@ -75,6 +80,21 @@ export function VariablesInAllowedPositionRule(
),
);
}

if (
isInputObjectType(parentType) &&
parentType.isOneOf &&
isNullableType(varType)
) {
const varTypeStr = inspect(varType);
const parentTypeStr = inspect(parentType);
context.reportError(
new GraphQLError(
`Variable "$${varName}" is of type "${varTypeStr}" but must be non-nullable to be used for OneOf Input Object "${parentTypeStr}".`,
{ nodes: [varDef, node] },
),
);
}
}
}
},
Expand All @@ -87,8 +107,11 @@ export function VariablesInAllowedPositionRule(

/**
* Returns true if the variable is allowed in the location it was found,
* which includes considering if default values exist for either the variable
* including considering if default values exist for either the variable
* or the location at which it is located.
*
* OneOf Input Object Type fields are considered separately above to
* provide a more descriptive error message.
*/
function allowedVariableUsage(
schema: GraphQLSchema,
Expand Down

0 comments on commit cb33ff1

Please sign in to comment.