Skip to content

Commit 24f0d95

Browse files
committed
Enforce oneOf rules during the validation phase
This ensures that OneOf Input Objects have exactly one field provided with the correct non-null, type for that field. Additionally, a variable used in a OneOf Input Object must be non-nullable.
1 parent 25928e3 commit 24f0d95

File tree

3 files changed

+171
-1
lines changed

3 files changed

+171
-1
lines changed

src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,28 @@ describe('Validate: Values of correct type', () => {
874874
});
875875
});
876876

877+
describe('Valid oneOf input object value', () => {
878+
it('Exactly one field', () => {
879+
expectValid(`
880+
{
881+
complicatedArgs {
882+
oneOfArgField(oneOfArg: { stringField: "abc" })
883+
}
884+
}
885+
`);
886+
});
887+
888+
it('Exactly one non-nullable variable', () => {
889+
expectValid(`
890+
query ($string: String!) {
891+
complicatedArgs {
892+
oneOfArgField(oneOfArg: { stringField: $string })
893+
}
894+
}
895+
`);
896+
});
897+
});
898+
877899
describe('Invalid input object value', () => {
878900
it('Partial object, missing required', () => {
879901
expectErrors(`
@@ -1041,6 +1063,70 @@ describe('Validate: Values of correct type', () => {
10411063
});
10421064
});
10431065

1066+
describe('Invalid oneOf input object value', () => {
1067+
it('Invalid field type', () => {
1068+
expectErrors(`
1069+
{
1070+
complicatedArgs {
1071+
oneOfArgField(oneOfArg: { stringField: 2 })
1072+
}
1073+
}
1074+
`).toDeepEqual([
1075+
{
1076+
message: 'String cannot represent a non string value: 2',
1077+
locations: [{ line: 4, column: 52 }],
1078+
},
1079+
]);
1080+
});
1081+
1082+
it('Exactly one null field', () => {
1083+
expectErrors(`
1084+
{
1085+
complicatedArgs {
1086+
oneOfArgField(oneOfArg: { stringField: null })
1087+
}
1088+
}
1089+
`).toDeepEqual([
1090+
{
1091+
message: 'Field "OneOfInput.stringField" must be non-null.',
1092+
locations: [{ line: 4, column: 37 }],
1093+
},
1094+
]);
1095+
});
1096+
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+
1113+
it('More than one field', () => {
1114+
expectErrors(`
1115+
{
1116+
complicatedArgs {
1117+
oneOfArgField(oneOfArg: { stringField: "abc", intField: 123 })
1118+
}
1119+
}
1120+
`).toDeepEqual([
1121+
{
1122+
message:
1123+
'OneOf Input Object "OneOfInput" must specify exactly one key.',
1124+
locations: [{ line: 4, column: 37 }],
1125+
},
1126+
]);
1127+
});
1128+
});
1129+
10441130
describe('Directive arguments', () => {
10451131
it('with directives of valid types', () => {
10461132
expectValid(`

src/validation/__tests__/harness.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ export const testSchema: GraphQLSchema = buildSchema(`
7979
stringListField: [String]
8080
}
8181
82+
input OneOfInput @oneOf {
83+
stringField: String
84+
intField: Int
85+
}
86+
8287
type ComplicatedArgs {
8388
# TODO List
8489
# TODO Coercion
@@ -93,6 +98,7 @@ export const testSchema: GraphQLSchema = buildSchema(`
9398
stringListArgField(stringListArg: [String]): String
9499
stringListNonNullArgField(stringListNonNullArg: [String!]): String
95100
complexArgField(complexArg: ComplexInput): String
101+
oneOfArgField(oneOfArg: OneOfInput): String
96102
multipleReqs(req1: Int!, req2: Int!): String
97103
nonNullFieldWithDefault(arg: Int! = 0): String
98104
multipleOpts(opt1: Int = 0, opt2: Int = 0): String

src/validation/rules/ValuesOfCorrectTypeRule.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
import { didYouMean } from '../../jsutils/didYouMean';
22
import { inspect } from '../../jsutils/inspect';
33
import { keyMap } from '../../jsutils/keyMap';
4+
import type { ObjMap } from '../../jsutils/ObjMap';
45
import { suggestionList } from '../../jsutils/suggestionList';
56

67
import { GraphQLError } from '../../error/GraphQLError';
78

8-
import type { ValueNode } from '../../language/ast';
9+
import type {
10+
ObjectFieldNode,
11+
ObjectValueNode,
12+
ValueNode,
13+
VariableDefinitionNode,
14+
} from '../../language/ast';
15+
import { Kind } from '../../language/kinds';
916
import { print } from '../../language/printer';
1017
import type { ASTVisitor } from '../../language/visitor';
1118

19+
import type { GraphQLInputObjectType } from '../../type/definition';
1220
import {
1321
getNamedType,
1422
getNullableType,
@@ -32,7 +40,17 @@ import type { ValidationContext } from '../ValidationContext';
3240
export function ValuesOfCorrectTypeRule(
3341
context: ValidationContext,
3442
): ASTVisitor {
43+
let variableDefinitions: { [key: string]: VariableDefinitionNode } = {};
44+
3545
return {
46+
OperationDefinition: {
47+
enter() {
48+
variableDefinitions = {};
49+
},
50+
},
51+
VariableDefinition(definition) {
52+
variableDefinitions[definition.variable.name.value] = definition;
53+
},
3654
ListValue(node) {
3755
// Note: TypeInfo will traverse into a list's item type, so look to the
3856
// parent input type to check if it is a list.
@@ -62,6 +80,16 @@ export function ValuesOfCorrectTypeRule(
6280
);
6381
}
6482
}
83+
84+
if (type.isOneOf) {
85+
validateOneOfInputObject(
86+
context,
87+
node,
88+
type,
89+
fieldNodeMap,
90+
variableDefinitions,
91+
);
92+
}
6593
},
6694
ObjectField(node) {
6795
const parentType = getNamedType(context.getParentInputType());
@@ -155,3 +183,53 @@ function isValidValueNode(context: ValidationContext, node: ValueNode): void {
155183
}
156184
}
157185
}
186+
187+
function validateOneOfInputObject(
188+
context: ValidationContext,
189+
node: ObjectValueNode,
190+
type: GraphQLInputObjectType,
191+
fieldNodeMap: ObjMap<ObjectFieldNode>,
192+
variableDefinitions: { [key: string]: VariableDefinitionNode },
193+
): void {
194+
const keys = Object.keys(fieldNodeMap);
195+
const isNotExactlyOneField = keys.length !== 1;
196+
197+
if (isNotExactlyOneField) {
198+
context.reportError(
199+
new GraphQLError(
200+
`OneOf Input Object "${type.name}" must specify exactly one key.`,
201+
node,
202+
),
203+
);
204+
return;
205+
}
206+
207+
const value = fieldNodeMap[keys[0]].value;
208+
const isNullLiteral = value.kind === Kind.NULL;
209+
const isVariable = value.kind === Kind.VARIABLE;
210+
211+
if (isNullLiteral) {
212+
context.reportError(
213+
new GraphQLError(
214+
`Field "${type.name}.${keys[0]}" must be non-null.`,
215+
node,
216+
),
217+
);
218+
return;
219+
}
220+
221+
if (isVariable) {
222+
const variableName = value.name.value;
223+
const definition = variableDefinitions[variableName];
224+
const isNullableVariable = definition.type.kind !== Kind.NON_NULL_TYPE;
225+
226+
if (isNullableVariable) {
227+
context.reportError(
228+
new GraphQLError(
229+
`Variable "${variableName}" must be non-nullable to be used for OneOf Input Object "${type.name}".`,
230+
node,
231+
),
232+
);
233+
}
234+
}
235+
}

0 commit comments

Comments
 (0)