Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

move variable position validation for OneOf to VariablesInAllowedPositionRule #4194

Merged
merged 1 commit into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/validation/ValidationContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type NodeWithSelectionSet = OperationDefinitionNode | FragmentDefinitionNode;
interface VariableUsage {
readonly node: VariableNode;
readonly type: Maybe<GraphQLInputType>;
readonly parentType: Maybe<GraphQLInputType>;
readonly defaultValue: GraphQLDefaultValueUsage | undefined;
readonly fragmentVariableDefinition: Maybe<VariableDefinitionNode>;
}
Expand Down Expand Up @@ -226,13 +227,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 },
],
},
]);
});
});
yaacovCR marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -40,17 +39,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 @@ -84,13 +73,7 @@ export function ValuesOfCorrectTypeRule(
}

if (type.isOneOf) {
validateOneOfInputObject(
context,
node,
type,
fieldNodeMap,
variableDefinitions,
);
validateOneOfInputObject(context, node, type, fieldNodeMap);
}
},
ObjectField(node) {
Expand Down Expand Up @@ -189,7 +172,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 @@ -206,29 +188,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 @@ -11,7 +11,11 @@ import type {
GraphQLDefaultValueUsage,
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 @@ -42,6 +46,7 @@ export function VariablesInAllowedPositionRule(
for (const {
node,
type,
parentType,
defaultValue,
fragmentVariableDefinition,
} of usages) {
Expand Down Expand Up @@ -78,6 +83,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 @@ -90,8 +110,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
Loading