Skip to content

Commit 8cfa3de

Browse files
erikkessler1benjie
andauthored
Implement OneOf Input Objects via @oneOf directive (#3513)
Co-authored-by: Benjie Gillam <benjie@jemjie.com> Closes graphql/graphql-wg#648
1 parent d766c8e commit 8cfa3de

22 files changed

+752
-6
lines changed

src/__testUtils__/kitchenSinkSDL.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type Foo implements Bar & Baz & Two {
2727
five(argument: [String] = ["string", "string"]): String
2828
six(argument: InputType = {key: "value"}): Type
2929
seven(argument: Int = null): Type
30+
eight(argument: OneOfInputType): Type
3031
}
3132
3233
type AnnotatedObject @onObject(arg: "value") {
@@ -116,6 +117,11 @@ input InputType {
116117
answer: Int = 42
117118
}
118119
120+
input OneOfInputType @oneOf {
121+
string: String
122+
int: Int
123+
}
124+
119125
input AnnotatedInput @onInputObject {
120126
annotatedField: Type @onInputFieldDefinition
121127
}

src/execution/__tests__/oneof-test.ts

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { describe, it } from 'mocha';
2+
3+
import { expectJSON } from '../../__testUtils__/expectJSON.js';
4+
5+
import { parse } from '../../language/parser.js';
6+
7+
import { buildSchema } from '../../utilities/buildASTSchema.js';
8+
9+
import type { ExecutionResult } from '../execute.js';
10+
import { execute } from '../execute.js';
11+
12+
const schema = buildSchema(`
13+
type Query {
14+
test(input: TestInputObject!): TestObject
15+
}
16+
17+
input TestInputObject @oneOf {
18+
a: String
19+
b: Int
20+
}
21+
22+
type TestObject {
23+
a: String
24+
b: Int
25+
}
26+
`);
27+
28+
function executeQuery(
29+
query: string,
30+
rootValue: unknown,
31+
variableValues?: { [variable: string]: unknown },
32+
): ExecutionResult | Promise<ExecutionResult> {
33+
return execute({ schema, document: parse(query), rootValue, variableValues });
34+
}
35+
36+
describe('Execute: Handles OneOf Input Objects', () => {
37+
describe('OneOf Input Objects', () => {
38+
const rootValue = {
39+
test({ input }: { input: { a?: string; b?: number } }) {
40+
return input;
41+
},
42+
};
43+
44+
it('accepts a good default value', () => {
45+
const query = `
46+
query ($input: TestInputObject! = {a: "abc"}) {
47+
test(input: $input) {
48+
a
49+
b
50+
}
51+
}
52+
`;
53+
const result = executeQuery(query, rootValue);
54+
55+
expectJSON(result).toDeepEqual({
56+
data: {
57+
test: {
58+
a: 'abc',
59+
b: null,
60+
},
61+
},
62+
});
63+
});
64+
65+
it('rejects a bad default value', () => {
66+
const query = `
67+
query ($input: TestInputObject! = {a: "abc", b: 123}) {
68+
test(input: $input) {
69+
a
70+
b
71+
}
72+
}
73+
`;
74+
const result = executeQuery(query, rootValue);
75+
76+
expectJSON(result).toDeepEqual({
77+
data: {
78+
test: null,
79+
},
80+
errors: [
81+
{
82+
locations: [{ column: 23, line: 3 }],
83+
message:
84+
// This type of error would be caught at validation-time
85+
// hence the vague error message here.
86+
'Argument "input" of non-null type "TestInputObject!" must not be null.',
87+
path: ['test'],
88+
},
89+
],
90+
});
91+
});
92+
93+
it('accepts a good variable', () => {
94+
const query = `
95+
query ($input: TestInputObject!) {
96+
test(input: $input) {
97+
a
98+
b
99+
}
100+
}
101+
`;
102+
const result = executeQuery(query, rootValue, { input: { a: 'abc' } });
103+
104+
expectJSON(result).toDeepEqual({
105+
data: {
106+
test: {
107+
a: 'abc',
108+
b: null,
109+
},
110+
},
111+
});
112+
});
113+
114+
it('accepts a good variable with an undefined key', () => {
115+
const query = `
116+
query ($input: TestInputObject!) {
117+
test(input: $input) {
118+
a
119+
b
120+
}
121+
}
122+
`;
123+
const result = executeQuery(query, rootValue, {
124+
input: { a: 'abc', b: undefined },
125+
});
126+
127+
expectJSON(result).toDeepEqual({
128+
data: {
129+
test: {
130+
a: 'abc',
131+
b: null,
132+
},
133+
},
134+
});
135+
});
136+
137+
it('rejects a variable with multiple non-null keys', () => {
138+
const query = `
139+
query ($input: TestInputObject!) {
140+
test(input: $input) {
141+
a
142+
b
143+
}
144+
}
145+
`;
146+
const result = executeQuery(query, rootValue, {
147+
input: { a: 'abc', b: 123 },
148+
});
149+
150+
expectJSON(result).toDeepEqual({
151+
errors: [
152+
{
153+
locations: [{ column: 16, line: 2 }],
154+
message:
155+
'Variable "$input" got invalid value { a: "abc", b: 123 }; Exactly one key must be specified for OneOf type "TestInputObject".',
156+
},
157+
],
158+
});
159+
});
160+
161+
it('rejects a variable with multiple nullable keys', () => {
162+
const query = `
163+
query ($input: TestInputObject!) {
164+
test(input: $input) {
165+
a
166+
b
167+
}
168+
}
169+
`;
170+
const result = executeQuery(query, rootValue, {
171+
input: { a: 'abc', b: null },
172+
});
173+
174+
expectJSON(result).toDeepEqual({
175+
errors: [
176+
{
177+
locations: [{ column: 16, line: 2 }],
178+
message:
179+
'Variable "$input" got invalid value { a: "abc", b: null }; Exactly one key must be specified for OneOf type "TestInputObject".',
180+
},
181+
],
182+
});
183+
});
184+
});
185+
});

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export {
6666
GraphQLStreamDirective,
6767
GraphQLDeprecatedDirective,
6868
GraphQLSpecifiedByDirective,
69+
GraphQLOneOfDirective,
6970
// "Enum" of Type Kinds
7071
TypeKind,
7172
// Constant Deprecation Reason

src/language/__tests__/schema-printer-test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ describe('Printer: SDL document', () => {
6161
five(argument: [String] = ["string", "string"]): String
6262
six(argument: InputType = { key: "value" }): Type
6363
seven(argument: Int = null): Type
64+
eight(argument: OneOfInputType): Type
6465
}
6566
6667
type AnnotatedObject @onObject(arg: "value") {
@@ -143,6 +144,11 @@ describe('Printer: SDL document', () => {
143144
answer: Int = 42
144145
}
145146
147+
input OneOfInputType @oneOf {
148+
string: String
149+
int: Int
150+
}
151+
146152
input AnnotatedInput @onInputObject {
147153
annotatedField: Type @onInputFieldDefinition
148154
}

src/type/__tests__/introspection-test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,17 @@ describe('Introspection', () => {
372372
isDeprecated: false,
373373
deprecationReason: null,
374374
},
375+
{
376+
name: 'isOneOf',
377+
args: [],
378+
type: {
379+
kind: 'SCALAR',
380+
name: 'Boolean',
381+
ofType: null,
382+
},
383+
isDeprecated: false,
384+
deprecationReason: null,
385+
},
375386
],
376387
inputFields: null,
377388
interfaces: [],
@@ -989,6 +1000,12 @@ describe('Introspection', () => {
9891000
},
9901001
],
9911002
},
1003+
{
1004+
name: 'oneOf',
1005+
isRepeatable: false,
1006+
locations: ['INPUT_OBJECT'],
1007+
args: [],
1008+
},
9921009
],
9931010
},
9941011
},
@@ -1519,6 +1536,95 @@ describe('Introspection', () => {
15191536
});
15201537
});
15211538

1539+
it('identifies oneOf for input objects', () => {
1540+
const schema = buildSchema(`
1541+
input SomeInputObject @oneOf {
1542+
a: String
1543+
}
1544+
1545+
input AnotherInputObject {
1546+
a: String
1547+
b: String
1548+
}
1549+
1550+
type Query {
1551+
someField(someArg: SomeInputObject): String
1552+
anotherField(anotherArg: AnotherInputObject): String
1553+
}
1554+
`);
1555+
1556+
const source = `
1557+
{
1558+
oneOfInputObject: __type(name: "SomeInputObject") {
1559+
isOneOf
1560+
}
1561+
inputObject: __type(name: "AnotherInputObject") {
1562+
isOneOf
1563+
}
1564+
}
1565+
`;
1566+
1567+
expect(graphqlSync({ schema, source })).to.deep.equal({
1568+
data: {
1569+
oneOfInputObject: {
1570+
isOneOf: true,
1571+
},
1572+
inputObject: {
1573+
isOneOf: false,
1574+
},
1575+
},
1576+
});
1577+
});
1578+
1579+
it('returns null for oneOf for other types', () => {
1580+
const schema = buildSchema(`
1581+
type SomeObject implements SomeInterface {
1582+
fieldA: String
1583+
}
1584+
enum SomeEnum {
1585+
SomeObject
1586+
}
1587+
interface SomeInterface {
1588+
fieldA: String
1589+
}
1590+
union SomeUnion = SomeObject
1591+
type Query {
1592+
someField(enum: SomeEnum): SomeUnion
1593+
anotherField(enum: SomeEnum): SomeInterface
1594+
}
1595+
`);
1596+
1597+
const source = `
1598+
{
1599+
object: __type(name: "SomeObject") {
1600+
isOneOf
1601+
}
1602+
enum: __type(name: "SomeEnum") {
1603+
isOneOf
1604+
}
1605+
interface: __type(name: "SomeInterface") {
1606+
isOneOf
1607+
}
1608+
scalar: __type(name: "String") {
1609+
isOneOf
1610+
}
1611+
union: __type(name: "SomeUnion") {
1612+
isOneOf
1613+
}
1614+
}
1615+
`;
1616+
1617+
expect(graphqlSync({ schema, source })).to.deep.equal({
1618+
data: {
1619+
object: { isOneOf: null },
1620+
enum: { isOneOf: null },
1621+
interface: { isOneOf: null },
1622+
scalar: { isOneOf: null },
1623+
union: { isOneOf: null },
1624+
},
1625+
});
1626+
});
1627+
15221628
it('fails as expected on the __type root field without an arg', () => {
15231629
const schema = buildSchema(`
15241630
type Query {

0 commit comments

Comments
 (0)