Skip to content

Commit

Permalink
backport[v16]: Implement OneOf Input Objects via @oneOf directive (#…
Browse files Browse the repository at this point in the history
…4124)

Co-authored-by: Erik Kessler <erik.kessler1@gmail.com>
Co-authored-by: Benedikt Franke <benedikt.franke@mll.com>
Co-authored-by: Michael Hayes <michael@hayes.io>
Co-authored-by: Mike Ciesielka <maciesielka@comcast.net>
  • Loading branch information
5 people authored Jun 21, 2024
1 parent c35130e commit 29144f7
Show file tree
Hide file tree
Showing 28 changed files with 814 additions and 7 deletions.
6 changes: 6 additions & 0 deletions src/__testUtils__/kitchenSinkSDL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Foo implements Bar & Baz & Two {
five(argument: [String] = ["string", "string"]): String
six(argument: InputType = {key: "value"}): Type
seven(argument: Int = null): Type
eight(argument: OneOfInputType): Type
}
type AnnotatedObject @onObject(arg: "value") {
Expand Down Expand Up @@ -116,6 +117,11 @@ input InputType {
answer: Int = 42
}
input OneOfInputType @oneOf {
string: String
int: Int
}
input AnnotatedInput @onInputObject {
annotatedField: Type @onInputFieldDefinition
}
Expand Down
185 changes: 185 additions & 0 deletions src/execution/__tests__/oneof-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { describe, it } from 'mocha';

import { expectJSON } from '../../__testUtils__/expectJSON';

import { parse } from '../../language/parser';

import { buildSchema } from '../../utilities/buildASTSchema';

import type { ExecutionResult } from '../execute';
import { execute } from '../execute';

const schema = buildSchema(`
type Query {
test(input: TestInputObject!): TestObject
}
input TestInputObject @oneOf {
a: String
b: Int
}
type TestObject {
a: String
b: Int
}
`);

function executeQuery(
query: string,
rootValue: unknown,
variableValues?: { [variable: string]: unknown },
): ExecutionResult | Promise<ExecutionResult> {
return execute({ schema, document: parse(query), rootValue, variableValues });
}

describe('Execute: Handles OneOf Input Objects', () => {
describe('OneOf Input Objects', () => {
const rootValue = {
test({ input }: { input: { a?: string; b?: number } }) {
return input;
},
};

it('accepts a good default value', () => {
const query = `
query ($input: TestInputObject! = {a: "abc"}) {
test(input: $input) {
a
b
}
}
`;
const result = executeQuery(query, rootValue);

expectJSON(result).toDeepEqual({
data: {
test: {
a: 'abc',
b: null,
},
},
});
});

it('rejects a bad default value', () => {
const query = `
query ($input: TestInputObject! = {a: "abc", b: 123}) {
test(input: $input) {
a
b
}
}
`;
const result = executeQuery(query, rootValue);

expectJSON(result).toDeepEqual({
data: {
test: null,
},
errors: [
{
locations: [{ column: 23, line: 3 }],
message:
// This type of error would be caught at validation-time
// hence the vague error message here.
'Argument "input" of non-null type "TestInputObject!" must not be null.',
path: ['test'],
},
],
});
});

it('accepts a good variable', () => {
const query = `
query ($input: TestInputObject!) {
test(input: $input) {
a
b
}
}
`;
const result = executeQuery(query, rootValue, { input: { a: 'abc' } });

expectJSON(result).toDeepEqual({
data: {
test: {
a: 'abc',
b: null,
},
},
});
});

it('accepts a good variable with an undefined key', () => {
const query = `
query ($input: TestInputObject!) {
test(input: $input) {
a
b
}
}
`;
const result = executeQuery(query, rootValue, {
input: { a: 'abc', b: undefined },
});

expectJSON(result).toDeepEqual({
data: {
test: {
a: 'abc',
b: null,
},
},
});
});

it('rejects a variable with multiple non-null keys', () => {
const query = `
query ($input: TestInputObject!) {
test(input: $input) {
a
b
}
}
`;
const result = executeQuery(query, rootValue, {
input: { a: 'abc', b: 123 },
});

expectJSON(result).toDeepEqual({
errors: [
{
locations: [{ column: 16, line: 2 }],
message:
'Variable "$input" got invalid value { a: "abc", b: 123 }; Exactly one key must be specified for OneOf type "TestInputObject".',
},
],
});
});

it('rejects a variable with multiple nullable keys', () => {
const query = `
query ($input: TestInputObject!) {
test(input: $input) {
a
b
}
}
`;
const result = executeQuery(query, rootValue, {
input: { a: 'abc', b: null },
});

expectJSON(result).toDeepEqual({
errors: [
{
locations: [{ column: 16, line: 2 }],
message:
'Variable "$input" got invalid value { a: "abc", b: null }; Exactly one key must be specified for OneOf type "TestInputObject".',
},
],
});
});
});
});
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export {
GraphQLSkipDirective,
GraphQLDeprecatedDirective,
GraphQLSpecifiedByDirective,
GraphQLOneOfDirective,
// "Enum" of Type Kinds
TypeKind,
// Constant Deprecation Reason
Expand Down
6 changes: 6 additions & 0 deletions src/language/__tests__/schema-printer-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ describe('Printer: SDL document', () => {
five(argument: [String] = ["string", "string"]): String
six(argument: InputType = {key: "value"}): Type
seven(argument: Int = null): Type
eight(argument: OneOfInputType): Type
}
type AnnotatedObject @onObject(arg: "value") {
Expand Down Expand Up @@ -143,6 +144,11 @@ describe('Printer: SDL document', () => {
answer: Int = 42
}
input OneOfInputType @oneOf {
string: String
int: Int
}
input AnnotatedInput @onInputObject {
annotatedField: Type @onInputFieldDefinition
}
Expand Down
106 changes: 106 additions & 0 deletions src/type/__tests__/introspection-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,17 @@ describe('Introspection', () => {
isDeprecated: false,
deprecationReason: null,
},
{
name: 'isOneOf',
args: [],
type: {
kind: 'SCALAR',
name: 'Boolean',
ofType: null,
},
isDeprecated: false,
deprecationReason: null,
},
],
inputFields: null,
interfaces: [],
Expand Down Expand Up @@ -989,6 +1000,12 @@ describe('Introspection', () => {
},
],
},
{
name: 'oneOf',
isRepeatable: false,
locations: ['INPUT_OBJECT'],
args: [],
},
],
},
},
Expand Down Expand Up @@ -1519,6 +1536,95 @@ describe('Introspection', () => {
});
});

it('identifies oneOf for input objects', () => {
const schema = buildSchema(`
input SomeInputObject @oneOf {
a: String
}
input AnotherInputObject {
a: String
b: String
}
type Query {
someField(someArg: SomeInputObject): String
anotherField(anotherArg: AnotherInputObject): String
}
`);

const source = `
{
oneOfInputObject: __type(name: "SomeInputObject") {
isOneOf
}
inputObject: __type(name: "AnotherInputObject") {
isOneOf
}
}
`;

expect(graphqlSync({ schema, source })).to.deep.equal({
data: {
oneOfInputObject: {
isOneOf: true,
},
inputObject: {
isOneOf: false,
},
},
});
});

it('returns null for oneOf for other types', () => {
const schema = buildSchema(`
type SomeObject implements SomeInterface {
fieldA: String
}
enum SomeEnum {
SomeObject
}
interface SomeInterface {
fieldA: String
}
union SomeUnion = SomeObject
type Query {
someField(enum: SomeEnum): SomeUnion
anotherField(enum: SomeEnum): SomeInterface
}
`);

const source = `
{
object: __type(name: "SomeObject") {
isOneOf
}
enum: __type(name: "SomeEnum") {
isOneOf
}
interface: __type(name: "SomeInterface") {
isOneOf
}
scalar: __type(name: "String") {
isOneOf
}
union: __type(name: "SomeUnion") {
isOneOf
}
}
`;

expect(graphqlSync({ schema, source })).to.deep.equal({
data: {
object: { isOneOf: null },
enum: { isOneOf: null },
interface: { isOneOf: null },
scalar: { isOneOf: null },
union: { isOneOf: null },
},
});
});

it('fails as expected on the __type root field without an arg', () => {
const schema = buildSchema(`
type Query {
Expand Down
Loading

0 comments on commit 29144f7

Please sign in to comment.