diff --git a/.eslintrc b/.eslintrc index f0988229eb..7c1c981597 100644 --- a/.eslintrc +++ b/.eslintrc @@ -182,7 +182,7 @@ "no-unneeded-ternary": 2, "no-unreachable": 2, "no-unused-expressions": 2, - "no-unused-vars": [2, {"vars": "all", "args": "after-used", "argsIgnorePattern": "^_"}], + "no-unused-vars": [2, {"vars": "all", "args": "after-used", "argsIgnorePattern": "^_", "ignoreRestSiblings": true}], "no-use-before-define": 0, "no-useless-call": 2, "no-useless-escape": 2, diff --git a/src/execution/__tests__/variables-test.js b/src/execution/__tests__/variables-test.js index 8d7c8c12ed..8af2b57b3d 100644 --- a/src/execution/__tests__/variables-test.js +++ b/src/execution/__tests__/variables-test.js @@ -13,6 +13,7 @@ import { GraphQLSchema, GraphQLObjectType, GraphQLInputObjectType, + GraphQLInputUnionType, GraphQLList, GraphQLString, GraphQLNonNull, @@ -59,6 +60,35 @@ const TestNestedInputObject = new GraphQLInputObjectType({ }, }); +const TestUnionInputObjectA = new GraphQLInputObjectType({ + name: 'TestUnionInputObjectA', + fields: { + a: { type: GraphQLString }, + b: { type: GraphQLList(GraphQLString) }, + }, +}); + +const TestUnionInputObjectB = new GraphQLInputObjectType({ + name: 'TestUnionInputObjectB', + fields: { + c: { type: GraphQLNonNull(GraphQLString) }, + d: { type: TestComplexScalar }, + }, +}); + +const TestInputUnion = new GraphQLInputUnionType({ + name: 'TestInputUnion', + types: [TestUnionInputObjectA, TestUnionInputObjectB], +}); + +const TestNestedInputUnion = new GraphQLInputObjectType({ + name: 'TestNestedInputUnion', + fields: { + na: { type: GraphQLNonNull(TestInputUnion) }, + nb: { type: GraphQLNonNull(GraphQLString) }, + }, +}); + const TestType = new GraphQLObjectType({ name: 'TestType', fields: { @@ -67,6 +97,11 @@ const TestType = new GraphQLObjectType({ args: { input: { type: TestInputObject } }, resolve: (_, { input }) => input && JSON.stringify(input), }, + fieldWithUnionInput: { + type: GraphQLString, + args: { input: { type: TestInputUnion } }, + resolve: (_, { input }) => input && JSON.stringify(input), + }, fieldWithNullableStringInput: { type: GraphQLString, args: { input: { type: GraphQLString } }, @@ -87,6 +122,15 @@ const TestType = new GraphQLObjectType({ args: { input: { type: TestNestedInputObject, + }, + }, + resolve: (_, { input }) => input && JSON.stringify(input), + }, + fieldWithNestedUnionInput: { + type: GraphQLString, + args: { + input: { + type: TestNestedInputUnion, defaultValue: 'Hello World', }, }, @@ -229,6 +273,115 @@ describe('Execute: Handles inputs', () => { }); }); + describe('using inline structs with union input', () => { + it('executes with complex input', async () => { + const doc = ` + { + a: fieldWithUnionInput(input: {__inputname: "TestUnionInputObjectA", a: "foo", b: ["bar"]}) + b: fieldWithUnionInput(input: {__inputname: "TestUnionInputObjectB", c: "baz"}) + } + `; + const ast = parse(doc); + + expect(await execute(schema, ast)).to.deep.equal({ + data: { + a: '{"__inputname":"TestUnionInputObjectA","a":"foo","b":["bar"]}', + b: '{"__inputname":"TestUnionInputObjectB","c":"baz"}', + }, + }); + }); + + it('properly parses single value to list', async () => { + const doc = ` + { + fieldWithUnionInput(input: {__inputname: "TestUnionInputObjectA", a: "foo", b: "bar"}) + } + `; + const ast = parse(doc); + + expect(await execute(schema, ast)).to.deep.equal({ + data: { + fieldWithUnionInput: + '{"__inputname":"TestUnionInputObjectA","a":"foo","b":["bar"]}', + }, + }); + }); + + it('properly parses null value to null', async () => { + const doc = ` + { + a: fieldWithUnionInput(input: {__inputname: "TestUnionInputObjectA", a: null, b: null, c: "C", d: null}) + b: fieldWithUnionInput(input: {__inputname: "TestUnionInputObjectB", c: "C", d: null}) + } + `; + const ast = parse(doc); + + expect(await execute(schema, ast)).to.deep.equal({ + data: { + a: '{"__inputname":"TestUnionInputObjectA","a":null,"b":null}', + b: '{"__inputname":"TestUnionInputObjectB","c":"C","d":null}', + }, + }); + }); + + it('properly parses null value in list', async () => { + const doc = ` + { + fieldWithUnionInput(input: {__inputname: "TestUnionInputObjectA", b: ["A",null,"C"]}) + } + `; + const ast = parse(doc); + + expect(await execute(schema, ast)).to.deep.equal({ + data: { + fieldWithUnionInput: + '{"__inputname":"TestUnionInputObjectA","b":["A",null,"C"]}', + }, + }); + }); + + it('does not use incorrect value', async () => { + const doc = ` + { + fieldWithUnionInput(input: ["foo", "bar", "baz"]) + } + `; + const ast = parse(doc); + + const result = await execute(schema, ast); + + expect(result).to.containSubset({ + data: { + fieldWithUnionInput: null, + }, + errors: [ + { + message: + 'Argument "input" has invalid value ["foo", "bar", "baz"].', + path: ['fieldWithUnionInput'], + locations: [{ line: 3, column: 38 }], + }, + ], + }); + }); + + it('properly runs parseLiteral on complex scalar types', async () => { + const doc = ` + { + fieldWithUnionInput(input: {__inputname:"TestUnionInputObjectB", c: "foo", d: "SerializedValue"}) + } + `; + const ast = parse(doc); + + expect(await execute(schema, ast)).to.deep.equal({ + data: { + fieldWithUnionInput: + '{"__inputname":"TestUnionInputObjectB","c":"foo","d":"DeserializedValue"}', + }, + }); + }); + }); + describe('using variables', () => { const doc = ` query q($input: TestInputObject) { @@ -341,7 +494,7 @@ describe('Execute: Handles inputs', () => { it('errors on deep nested errors and with many errors', async () => { const nestedDoc = ` query q($input: TestNestedInputObject) { - fieldWithNestedObjectInput(input: $input) + fieldWithNestedInputObject(input: $input) } `; const nestedAst = parse(nestedDoc); @@ -388,6 +541,186 @@ describe('Execute: Handles inputs', () => { }); }); }); + + describe('using variables with union input', () => { + const doc = ` + query q($input: TestInputUnion) { + fieldWithUnionInput(input: $input) + } + `; + const ast = parse(doc); + + it('executes with complex input', async () => { + const params = { + input: { a: 'foo', b: ['bar'], __inputname: 'TestUnionInputObjectA' }, + }; + const result = await execute(schema, ast, null, null, params); + + expect(result).to.deep.equal({ + data: { + fieldWithUnionInput: + '{"__inputname":"TestUnionInputObjectA","a":"foo","b":["bar"]}', + }, + }); + }); + + it('uses default value when not provided', async () => { + const withDefaultsAST = parse(` + query q($input: TestInputUnion = {__inputname:"TestUnionInputObjectA", a: "foo", b: ["bar"], c: "baz"}) { + fieldWithUnionInput(input: $input) + } + `); + + const result = await execute(schema, withDefaultsAST); + + expect(result).to.deep.equal({ + data: { + fieldWithUnionInput: + '{"__inputname":"TestUnionInputObjectA","a":"foo","b":["bar"]}', + }, + }); + }); + + it('properly parses single value to list', async () => { + const params = { + input: { a: 'foo', b: 'bar', __inputname: 'TestUnionInputObjectA' }, + }; + const result = await execute(schema, ast, null, null, params); + + expect(result).to.deep.equal({ + data: { + fieldWithUnionInput: + '{"__inputname":"TestUnionInputObjectA","a":"foo","b":["bar"]}', + }, + }); + }); + + it('executes with complex scalar input', async () => { + const params = { + input: { + c: 'foo', + d: 'SerializedValue', + __inputname: 'TestUnionInputObjectB', + }, + }; + const result = await execute(schema, ast, null, null, params); + + expect(result).to.deep.equal({ + data: { + fieldWithUnionInput: + '{"__inputname":"TestUnionInputObjectB","c":"foo","d":"DeserializedValue"}', + }, + }); + }); + + it('errors on null for nested non-null', async () => { + const params = { + input: { __inputname: 'TestUnionInputObjectB', c: null }, + }; + + const result = await execute(schema, ast, null, null, params); + expect(result).to.deep.equal({ + errors: [ + { + message: + 'Variable "$input" got invalid value ' + + '{"__inputname":"TestUnionInputObjectB","c":null}; ' + + 'Expected non-nullable type String! not to be null at value.c.', + locations: [{ line: 2, column: 17 }], + path: undefined, + }, + ], + }); + }); + + it('errors on incorrect type', async () => { + const params = { input: 'foo bar' }; + + const result = await execute(schema, ast, null, null, params); + expect(result).to.deep.equal({ + errors: [ + { + message: + 'Variable "$input" got invalid value "foo bar"; ' + + 'Expected type TestInputUnion to be an object.', + locations: [{ line: 2, column: 17 }], + path: undefined, + }, + ], + }); + }); + + it('errors on omission of nested non-null', async () => { + const params = { input: { __inputname: 'TestUnionInputObjectB' } }; + + const result = await execute(schema, ast, null, null, params); + expect(result).to.deep.equal({ + errors: [ + { + message: + 'Variable "$input" got invalid value {"__inputname":"TestUnionInputObjectB"}; ' + + 'Field value.c of required type String! was not provided.', + locations: [{ line: 2, column: 17 }], + path: undefined, + }, + ], + }); + }); + + it('errors on deep nested errors and with many errors', async () => { + const nestedDoc = ` + query q($input: TestNestedInputObject) { + fieldWithNestedUnionInput(input: $input) + } + `; + const nestedAst = parse(nestedDoc); + const params = { input: { na: { a: 'foo' } } }; + + const result = await execute(schema, nestedAst, null, null, params); + expect(result).to.deep.equal({ + errors: [ + { + message: + 'Variable "$input" got invalid value {"na":{"a":"foo"}}; ' + + 'Field value.na.c of required type String! was not provided.', + locations: [{ line: 2, column: 19 }], + path: undefined, + }, + { + message: + 'Variable "$input" got invalid value {"na":{"a":"foo"}}; ' + + 'Field value.nb of required type String! was not provided.', + locations: [{ line: 2, column: 19 }], + path: undefined, + }, + ], + }); + }); + + it('errors on addition of unknown input field', async () => { + const params = { + input: { + __inputname: 'TestUnionInputObjectB', + c: 'baz', + extra: 'dog', + }, + }; + + const result = await execute(schema, ast, null, null, params); + expect(result).to.deep.equal({ + errors: [ + { + message: + 'Variable "$input" got invalid value ' + + '{"__inputname":"TestUnionInputObjectB","c":"baz","extra":"dog"}; ' + + 'Field "extra" is not defined by type TestUnionInputObjectB.', + locations: [{ line: 2, column: 17 }], + path: undefined, + }, + ], + }); + }); + }); }); describe('Handles nullable scalars', () => { diff --git a/src/index.js b/src/index.js index 15efb8e43b..bcbbc3f4f6 100644 --- a/src/index.js +++ b/src/index.js @@ -43,6 +43,7 @@ export { GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, + GraphQLInputUnionType, GraphQLEnumType, GraphQLInputObjectType, GraphQLList, @@ -86,6 +87,7 @@ export { isObjectType, isInterfaceType, isUnionType, + isInputUnionType, isEnumType, isInputObjectType, isListType, @@ -107,6 +109,7 @@ export { assertObjectType, assertInterfaceType, assertUnionType, + assertInputUnionType, assertEnumType, assertInputObjectType, assertListType, @@ -165,6 +168,7 @@ export type { GraphQLTypeResolver, GraphQLUnionTypeConfig, GraphQLDirectiveConfig, + GraphQLInputUnionTypeConfig, } from './type'; // Parse and operate on GraphQL language source files. @@ -241,6 +245,7 @@ export type { InputValueDefinitionNode, InterfaceTypeDefinitionNode, UnionTypeDefinitionNode, + InputUnionTypeDefinitionNode, EnumTypeDefinitionNode, EnumValueDefinitionNode, InputObjectTypeDefinitionNode, @@ -392,4 +397,5 @@ export type { IntrospectionType, IntrospectionTypeRef, IntrospectionUnionType, + IntrospectionInputUnionType, } from './utilities'; diff --git a/src/language/__tests__/kitchen-sink.graphql b/src/language/__tests__/kitchen-sink.graphql index 6fcf394bf3..b7ccafa7b3 100644 --- a/src/language/__tests__/kitchen-sink.graphql +++ b/src/language/__tests__/kitchen-sink.graphql @@ -3,7 +3,7 @@ # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. -query queryName($foo: ComplexType, $site: Site = MOBILE) { +query queryName($foo: ComplexType, $site: Site = MOBILE, $inputUnion: InputOrAnnotated) { whoever123is: node(id: [123, 456]) { id , ... on User @defer { diff --git a/src/language/__tests__/lexer-test.js b/src/language/__tests__/lexer-test.js index 559701864b..b69731dc10 100644 --- a/src/language/__tests__/lexer-test.js +++ b/src/language/__tests__/lexer-test.js @@ -62,7 +62,24 @@ describe('Lexer', () => { // NB: util.inspect used to suck if (parseFloat(process.version.slice(1)) > 0.1) { expect(require('util').inspect(token)).to.equal( - "{ kind: 'Name', value: 'foo', line: 1, column: 1 }", + `Tok { + kind: 'Name', + start: 0, + end: 3, + line: 1, + column: 1, + value: 'foo', + prev: + Tok { + kind: '', + start: 0, + end: 0, + line: 0, + column: 0, + value: undefined, + prev: null, + next: [Circular] }, + next: null }`, ); } }); diff --git a/src/language/__tests__/parser-test.js b/src/language/__tests__/parser-test.js index a3486e72ad..92caed9aa3 100644 --- a/src/language/__tests__/parser-test.js +++ b/src/language/__tests__/parser-test.js @@ -408,7 +408,7 @@ describe('Parser', () => { // NB: util.inspect used to suck if (parseFloat(process.version.slice(1)) > 0.1) { expect(require('util').inspect(result.loc)).to.equal( - '{ start: 0, end: 6 }', + "Loc {\n start: 0,\n end: 6,\n startToken:\n Tok {\n kind: '',\n start: 0,\n end: 0,\n line: 0,\n column: 0,\n value: undefined,\n prev: null,\n next:\n Tok {\n kind: '{',\n start: 0,\n end: 1,\n line: 1,\n column: 1,\n value: undefined,\n prev: [Circular],\n next: [Tok] } },\n endToken:\n Tok {\n kind: '',\n start: 6,\n end: 6,\n line: 1,\n column: 7,\n value: undefined,\n prev:\n Tok {\n kind: '}',\n start: 5,\n end: 6,\n line: 1,\n column: 6,\n value: undefined,\n prev: [Tok],\n next: [Circular] },\n next: null },\n source:\n Source {\n body: '{ id }',\n name: 'GraphQL request',\n locationOffset: { line: 1, column: 1 } } }", ); } }); diff --git a/src/language/__tests__/printer-test.js b/src/language/__tests__/printer-test.js index 14b75e32ea..871bdbc85f 100644 --- a/src/language/__tests__/printer-test.js +++ b/src/language/__tests__/printer-test.js @@ -146,7 +146,7 @@ describe('Printer: Query document', () => { const printed = print(ast); expect(printed).to.equal(dedent` - query queryName($foo: ComplexType, $site: Site = MOBILE) { + query queryName($foo: ComplexType, $site: Site = MOBILE, $inputUnion: InputOrAnnotated) { whoever123is: node(id: [123, 456]) { id ... on User @defer { diff --git a/src/language/__tests__/schema-kitchen-sink.graphql b/src/language/__tests__/schema-kitchen-sink.graphql index f94f47c8e5..8ccdee1093 100644 --- a/src/language/__tests__/schema-kitchen-sink.graphql +++ b/src/language/__tests__/schema-kitchen-sink.graphql @@ -20,6 +20,7 @@ type Foo implements Bar & Baz { five(argument: [String] = ["string", "string"]): String six(argument: InputType = {key: "value"}): Type seven(argument: Int = null): Type + eight(argument: AnnotatedInput): Type } type AnnotatedObject @onObject(arg: "value") { @@ -102,6 +103,8 @@ extend input InputType { other: Float = 1.23e4 } +inputUnion InputOrAnnotated = InputType | AnnotatedInput + extend input InputType @onInputObject directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT diff --git a/src/language/__tests__/schema-parser-test.js b/src/language/__tests__/schema-parser-test.js index 1c9fe0f144..680bdff811 100644 --- a/src/language/__tests__/schema-parser-test.js +++ b/src/language/__tests__/schema-parser-test.js @@ -770,6 +770,25 @@ input Hello { ); }); + it('inputUnion object', () => { + const body = ` +inputUnion HelloWorld = Hello | World`; + const doc = parse(body); + const expected = [ + { + kind: 'InputUnionTypeDefinition', + name: nameNode('HelloWorld', { start: 12, end: 22 }), + directives: [], + types: [ + typeNode('Hello', { start: 25, end: 30 }), + typeNode('World', { start: 33, end: 38 }), + ], + loc: { start: 1, end: 38 }, + }, + ]; + expect(printJson(doc.definitions)).to.equal(printJson(expected)); + }); + it('Option: allowLegacySDLEmptyFields supports type with empty fields', () => { const body = 'type Hello { }'; expect(() => parse(body)).to.throw('Syntax Error: Expected Name, found }'); diff --git a/src/language/__tests__/schema-printer-test.js b/src/language/__tests__/schema-printer-test.js index cb3a3cd4c6..f32093cb2c 100644 --- a/src/language/__tests__/schema-printer-test.js +++ b/src/language/__tests__/schema-printer-test.js @@ -64,6 +64,7 @@ describe('Printer: SDL document', () => { five(argument: [String] = ["string", "string"]): String six(argument: InputType = {key: "value"}): Type seven(argument: Int = null): Type + eight(argument: AnnotatedInput): Type } type AnnotatedObject @onObject(arg: "value") { @@ -146,6 +147,8 @@ describe('Printer: SDL document', () => { other: Float = 1.23e4 } + inputUnion InputOrAnnotated = InputType | AnnotatedInput + extend input InputType @onInputObject directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT diff --git a/src/language/__tests__/visitor-test.js b/src/language/__tests__/visitor-test.js index 725b5e468e..7980225597 100644 --- a/src/language/__tests__/visitor-test.js +++ b/src/language/__tests__/visitor-test.js @@ -482,6 +482,16 @@ describe('Visitor', () => { ['enter', 'EnumValue', 'defaultValue', 'VariableDefinition'], ['leave', 'EnumValue', 'defaultValue', 'VariableDefinition'], ['leave', 'VariableDefinition', 1, undefined], + ['enter', 'VariableDefinition', 2, undefined], + ['enter', 'Variable', 'variable', 'VariableDefinition'], + ['enter', 'Name', 'name', 'Variable'], + ['leave', 'Name', 'name', 'Variable'], + ['leave', 'Variable', 'variable', 'VariableDefinition'], + ['enter', 'NamedType', 'type', 'VariableDefinition'], + ['enter', 'Name', 'name', 'NamedType'], + ['leave', 'Name', 'name', 'NamedType'], + ['leave', 'NamedType', 'type', 'VariableDefinition'], + ['leave', 'VariableDefinition', 2, undefined], ['enter', 'SelectionSet', 'selectionSet', 'OperationDefinition'], ['enter', 'Field', 0, undefined], ['enter', 'Name', 'alias', 'Field'], diff --git a/src/language/ast.js b/src/language/ast.js index bec5ae3d57..f08387454c 100644 --- a/src/language/ast.js +++ b/src/language/ast.js @@ -149,6 +149,7 @@ export type ASTNode = | InputValueDefinitionNode | InterfaceTypeDefinitionNode | UnionTypeDefinitionNode + | InputUnionTypeDefinitionNode | EnumTypeDefinitionNode | EnumValueDefinitionNode | InputObjectTypeDefinitionNode @@ -156,6 +157,7 @@ export type ASTNode = | ObjectTypeExtensionNode | InterfaceTypeExtensionNode | UnionTypeExtensionNode + | InputUnionTypeExtensionNode | EnumTypeExtensionNode | InputObjectTypeExtensionNode | DirectiveDefinitionNode; @@ -196,6 +198,7 @@ export type ASTKindToNode = { InputValueDefinition: InputValueDefinitionNode, InterfaceTypeDefinition: InterfaceTypeDefinitionNode, UnionTypeDefinition: UnionTypeDefinitionNode, + InputUnionTypeDefinition: InputUnionTypeDefinitionNode, EnumTypeDefinition: EnumTypeDefinitionNode, EnumValueDefinition: EnumValueDefinitionNode, InputObjectTypeDefinition: InputObjectTypeDefinitionNode, @@ -203,6 +206,7 @@ export type ASTKindToNode = { ObjectTypeExtension: ObjectTypeExtensionNode, InterfaceTypeExtension: InterfaceTypeExtensionNode, UnionTypeExtension: UnionTypeExtensionNode, + InputUnionTypeExtension: InputUnionTypeExtensionNode, EnumTypeExtension: EnumTypeExtensionNode, InputObjectTypeExtension: InputObjectTypeExtensionNode, DirectiveDefinition: DirectiveDefinitionNode, @@ -440,6 +444,7 @@ export type TypeDefinitionNode = | ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode | UnionTypeDefinitionNode + | InputUnionTypeDefinitionNode | EnumTypeDefinitionNode | InputObjectTypeDefinitionNode; @@ -499,6 +504,15 @@ export type UnionTypeDefinitionNode = { +types?: $ReadOnlyArray, }; +export type InputUnionTypeDefinitionNode = { + +kind: 'InputUnionTypeDefinition', + +loc?: Location, + +description?: StringValueNode, + +name: NameNode, + +directives?: $ReadOnlyArray, + +types?: $ReadOnlyArray, +}; + export type EnumTypeDefinitionNode = { +kind: 'EnumTypeDefinition', +loc?: Location, @@ -532,6 +546,7 @@ export type TypeExtensionNode = | ObjectTypeExtensionNode | InterfaceTypeExtensionNode | UnionTypeExtensionNode + | InputUnionTypeExtensionNode | EnumTypeExtensionNode | InputObjectTypeExtensionNode; @@ -567,6 +582,14 @@ export type UnionTypeExtensionNode = { +types?: $ReadOnlyArray, }; +export type InputUnionTypeExtensionNode = { + +kind: 'InputUnionTypeExtension', + +loc?: Location, + +name: NameNode, + +directives?: $ReadOnlyArray, + +types?: $ReadOnlyArray, +}; + export type EnumTypeExtensionNode = { +kind: 'EnumTypeExtension', +loc?: Location, diff --git a/src/language/directiveLocation.js b/src/language/directiveLocation.js index ebabf773bc..a66dd20d55 100644 --- a/src/language/directiveLocation.js +++ b/src/language/directiveLocation.js @@ -27,6 +27,7 @@ export const DirectiveLocation = { ARGUMENT_DEFINITION: 'ARGUMENT_DEFINITION', INTERFACE: 'INTERFACE', UNION: 'UNION', + INPUT_UNION: 'INPUT_UNION', ENUM: 'ENUM', ENUM_VALUE: 'ENUM_VALUE', INPUT_OBJECT: 'INPUT_OBJECT', diff --git a/src/language/index.js b/src/language/index.js index f8bd4bfbb6..b228364edd 100644 --- a/src/language/index.js +++ b/src/language/index.js @@ -72,6 +72,7 @@ export type { InputValueDefinitionNode, InterfaceTypeDefinitionNode, UnionTypeDefinitionNode, + InputUnionTypeDefinitionNode, EnumTypeDefinitionNode, EnumValueDefinitionNode, InputObjectTypeDefinitionNode, diff --git a/src/language/kinds.js b/src/language/kinds.js index f36f945afe..890e1d1c1b 100644 --- a/src/language/kinds.js +++ b/src/language/kinds.js @@ -62,6 +62,7 @@ export const FIELD_DEFINITION = 'FieldDefinition'; export const INPUT_VALUE_DEFINITION = 'InputValueDefinition'; export const INTERFACE_TYPE_DEFINITION = 'InterfaceTypeDefinition'; export const UNION_TYPE_DEFINITION = 'UnionTypeDefinition'; +export const INPUT_UNION_TYPE_DEFINITION = 'InputUnionTypeDefinition'; export const ENUM_TYPE_DEFINITION = 'EnumTypeDefinition'; export const ENUM_VALUE_DEFINITION = 'EnumValueDefinition'; export const INPUT_OBJECT_TYPE_DEFINITION = 'InputObjectTypeDefinition'; @@ -72,6 +73,7 @@ export const SCALAR_TYPE_EXTENSION = 'ScalarTypeExtension'; export const OBJECT_TYPE_EXTENSION = 'ObjectTypeExtension'; export const INTERFACE_TYPE_EXTENSION = 'InterfaceTypeExtension'; export const UNION_TYPE_EXTENSION = 'UnionTypeExtension'; +export const INPUT_UNION_TYPE_EXTENSION = 'InputUnionTypeExtension'; export const ENUM_TYPE_EXTENSION = 'EnumTypeExtension'; export const INPUT_OBJECT_TYPE_EXTENSION = 'InputObjectTypeExtension'; diff --git a/src/language/parser.js b/src/language/parser.js index afa5d6ecab..098ef5c2e4 100644 --- a/src/language/parser.js +++ b/src/language/parser.js @@ -49,6 +49,7 @@ import type { InputValueDefinitionNode, InterfaceTypeDefinitionNode, UnionTypeDefinitionNode, + InputUnionTypeDefinitionNode, EnumTypeDefinitionNode, EnumValueDefinitionNode, InputObjectTypeDefinitionNode, @@ -57,6 +58,7 @@ import type { ObjectTypeExtensionNode, InterfaceTypeExtensionNode, UnionTypeExtensionNode, + InputUnionTypeExtensionNode, EnumTypeExtensionNode, InputObjectTypeExtensionNode, DirectiveDefinitionNode, @@ -95,6 +97,7 @@ import { INPUT_VALUE_DEFINITION, INTERFACE_TYPE_DEFINITION, UNION_TYPE_DEFINITION, + INPUT_UNION_TYPE_DEFINITION, ENUM_TYPE_DEFINITION, ENUM_VALUE_DEFINITION, INPUT_OBJECT_TYPE_DEFINITION, @@ -102,6 +105,7 @@ import { OBJECT_TYPE_EXTENSION, INTERFACE_TYPE_EXTENSION, UNION_TYPE_EXTENSION, + INPUT_UNION_TYPE_EXTENSION, ENUM_TYPE_EXTENSION, INPUT_OBJECT_TYPE_EXTENSION, DIRECTIVE_DEFINITION, @@ -268,6 +272,7 @@ function parseDefinition(lexer: Lexer<*>): DefinitionNode { case 'type': case 'interface': case 'union': + case 'inputUnion': case 'enum': case 'input': case 'extend': @@ -804,6 +809,7 @@ export function parseNamedType(lexer: Lexer<*>): NamedTypeNode { * - ObjectTypeDefinition * - InterfaceTypeDefinition * - UnionTypeDefinition + * - InputUnionTypeDefinition * - EnumTypeDefinition * - InputObjectTypeDefinition */ @@ -823,6 +829,8 @@ function parseTypeSystemDefinition(lexer: Lexer<*>): TypeSystemDefinitionNode { return parseInterfaceTypeDefinition(lexer); case 'union': return parseUnionTypeDefinition(lexer); + case 'inputUnion': + return parseInputUnionTypeDefinition(lexer); case 'enum': return parseEnumTypeDefinition(lexer); case 'input': @@ -1076,6 +1084,29 @@ function parseUnionTypeDefinition(lexer: Lexer<*>): UnionTypeDefinitionNode { }; } +/** + * InputUnionTypeDefinition : + * - Description? inputUnion Name Directives[Const]? UnionMemberTypes? + */ +function parseInputUnionTypeDefinition( + lexer: Lexer<*>, +): InputUnionTypeDefinitionNode { + const start = lexer.token; + const description = parseDescription(lexer); + expectKeyword(lexer, 'inputUnion'); + const name = parseName(lexer); + const directives = parseDirectives(lexer, true); + const types = parseUnionMemberTypes(lexer); + return { + kind: INPUT_UNION_TYPE_DEFINITION, + description, + name, + directives, + types, + loc: loc(lexer, start), + }; +} + /** * UnionMemberTypes : * - = `|`? NamedType @@ -1205,6 +1236,8 @@ function parseTypeExtension(lexer: Lexer<*>): TypeExtensionNode { return parseInterfaceTypeExtension(lexer); case 'union': return parseUnionTypeExtension(lexer); + case 'inputUnion': + return parseInputUnionTypeExtension(lexer); case 'enum': return parseEnumTypeExtension(lexer); case 'input': @@ -1317,6 +1350,32 @@ function parseUnionTypeExtension(lexer: Lexer<*>): UnionTypeExtensionNode { }; } +/** + * InputUnionTypeExtension : + * - extend inputUnion Name Directives[Const]? UnionMemberTypes + * - extend inputUnion Name Directives[Const] + */ +function parseInputUnionTypeExtension( + lexer: Lexer<*>, +): InputUnionTypeExtensionNode { + const start = lexer.token; + expectKeyword(lexer, 'extend'); + expectKeyword(lexer, 'inputUnion'); + const name = parseName(lexer); + const directives = parseDirectives(lexer, true); + const types = parseUnionMemberTypes(lexer); + if (directives.length === 0 && types.length === 0) { + throw unexpected(lexer); + } + return { + kind: INPUT_UNION_TYPE_EXTENSION, + name, + directives, + types, + loc: loc(lexer, start), + }; +} + /** * EnumTypeExtension : * - extend enum Name Directives[Const]? EnumValuesDefinition diff --git a/src/language/printer.js b/src/language/printer.js index daf984d7e8..1627d57fca 100644 --- a/src/language/printer.js +++ b/src/language/printer.js @@ -161,6 +161,18 @@ const printDocASTReducer = { ), ), + InputUnionTypeDefinition: addDescription(({ name, directives, types }) => + join( + [ + 'inputUnion', + name, + join(directives, ' '), + types && types.length !== 0 ? '= ' + join(types, ' | ') : '', + ], + ' ', + ), + ), + EnumTypeDefinition: addDescription(({ name, directives, values }) => join(['enum', name, join(directives, ' '), block(values)], ' '), ), @@ -208,6 +220,17 @@ const printDocASTReducer = { InputObjectTypeExtension: ({ name, directives, fields }) => join(['extend input', name, join(directives, ' '), block(fields)], ' '), + InputUnionTypeExtension: ({ name, directives, types }) => + join( + [ + 'extend inputUnion', + name, + join(directives, ' '), + types && types.length !== 0 ? '= ' + join(types, ' | ') : '', + ], + ' ', + ), + DirectiveDefinition: addDescription( ({ name, arguments: args, locations }) => 'directive @' + diff --git a/src/language/visitor.js b/src/language/visitor.js index 31937548ee..8793c059ef 100644 --- a/src/language/visitor.js +++ b/src/language/visitor.js @@ -119,6 +119,7 @@ export const QueryDocumentKeys = { ], InterfaceTypeDefinition: ['description', 'name', 'directives', 'fields'], UnionTypeDefinition: ['description', 'name', 'directives', 'types'], + InputUnionTypeDefinition: ['description', 'name', 'directives', 'types'], EnumTypeDefinition: ['description', 'name', 'directives', 'values'], EnumValueDefinition: ['description', 'name', 'directives'], InputObjectTypeDefinition: ['description', 'name', 'directives', 'fields'], @@ -127,6 +128,7 @@ export const QueryDocumentKeys = { ObjectTypeExtension: ['name', 'interfaces', 'directives', 'fields'], InterfaceTypeExtension: ['name', 'directives', 'fields'], UnionTypeExtension: ['name', 'directives', 'types'], + InputUnionTypeExtension: ['name', 'directives', 'types'], EnumTypeExtension: ['name', 'directives', 'values'], InputObjectTypeExtension: ['name', 'directives', 'fields'], diff --git a/src/type/__tests__/definition-test.js b/src/type/__tests__/definition-test.js index a7c70cd6ac..98af3bd19b 100644 --- a/src/type/__tests__/definition-test.js +++ b/src/type/__tests__/definition-test.js @@ -13,6 +13,7 @@ import { GraphQLInterfaceType, GraphQLObjectType, GraphQLUnionType, + GraphQLInputUnionType, GraphQLList, GraphQLNonNull, GraphQLInt, @@ -95,6 +96,10 @@ const InterfaceType = new GraphQLInterfaceType({ name: 'Interface' }); const UnionType = new GraphQLUnionType({ name: 'Union', types: [ObjectType] }); const EnumType = new GraphQLEnumType({ name: 'Enum', values: { foo: {} } }); const InputObjectType = new GraphQLInputObjectType({ name: 'InputObject' }); +const InputUnionType = new GraphQLInputUnionType({ + name: 'InputUnion', + types: [InputObjectType], +}); const ScalarType = new GraphQLScalarType({ name: 'Scalar', serialize() {}, @@ -343,6 +348,7 @@ describe('Type System: Example', () => { expect(String(BlogArticle)).to.equal('Article'); expect(String(InterfaceType)).to.equal('Interface'); expect(String(UnionType)).to.equal('Union'); + expect(String(InputUnionType)).to.equal('InputUnion'); expect(String(EnumType)).to.equal('Enum'); expect(String(InputObjectType)).to.equal('InputObject'); expect(String(GraphQLNonNull(GraphQLInt))).to.equal('Int!'); diff --git a/src/type/__tests__/introspection-test.js b/src/type/__tests__/introspection-test.js index 6106bcd1c3..cfa9d30d50 100644 --- a/src/type/__tests__/introspection-test.js +++ b/src/type/__tests__/introspection-test.js @@ -15,6 +15,7 @@ import { GraphQLObjectType, GraphQLList, GraphQLInputObjectType, + GraphQLInputUnionType, GraphQLString, GraphQLEnumType, } from '../../'; @@ -911,6 +912,89 @@ describe('Introspection', () => { }); }); + it('introspects on input union', () => { + const TestInputObject = new GraphQLInputObjectType({ + name: 'TestInputObject', + fields: { + a: { type: GraphQLString, defaultValue: 'tes\t de\fault' }, + b: { type: GraphQLList(GraphQLString) }, + c: { type: GraphQLString, defaultValue: null }, + }, + }); + const TestInputObject2 = new GraphQLInputObjectType({ + name: 'TestInputObject2', + fields: { + a: { type: GraphQLString, defaultValue: 'tes\t de\fault' }, + b: { type: GraphQLList(GraphQLString) }, + d: { type: GraphQLString, defaultValue: null }, + }, + }); + + const TestInputUnion = new GraphQLInputUnionType({ + name: 'TestInputUnion', + types: [TestInputObject, TestInputObject2], + }); + + const TestType = new GraphQLObjectType({ + name: 'TestType', + fields: { + field: { + type: GraphQLString, + args: { inputUnion: { type: TestInputUnion } }, + resolve: (_, { inputUnion }) => JSON.stringify(inputUnion), + }, + }, + }); + + const schema = new GraphQLSchema({ query: TestType }); + const request = ` + { + __type(name: "TestInputUnion") { + kind + name + possibleTypes { ...TypeRef } + } + } + + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + `; + return expect(graphqlSync(schema, request)).to.deep.equal({ + data: { + __type: { + kind: 'INPUT_UNION', + name: 'TestInputUnion', + possibleTypes: [ + { + kind: 'INPUT_OBJECT', + name: 'TestInputObject', + ofType: null, + }, + { + kind: 'INPUT_OBJECT', + name: 'TestInputObject2', + ofType: null, + }, + ], + }, + }, + }); + }); + it('supports the __type root field', () => { const TestType = new GraphQLObjectType({ name: 'TestType', @@ -1322,6 +1406,12 @@ describe('Introspection', () => { '`possibleTypes` is a valid field.', name: 'UNION', }, + { + description: + 'Indicates this type is a union of input objects. ' + + '`possibleTypes` is a valid field.', + name: 'INPUT_UNION', + }, { description: 'Indicates this type is an enum. ' + diff --git a/src/type/__tests__/predicate-test.js b/src/type/__tests__/predicate-test.js index 148e42e240..c655d41707 100644 --- a/src/type/__tests__/predicate-test.js +++ b/src/type/__tests__/predicate-test.js @@ -15,6 +15,7 @@ import { GraphQLInterfaceType, GraphQLObjectType, GraphQLUnionType, + GraphQLInputUnionType, GraphQLList, GraphQLNonNull, GraphQLString, @@ -23,6 +24,7 @@ import { isObjectType, isInterfaceType, isUnionType, + isInputUnionType, isEnumType, isInputObjectType, isListType, @@ -40,6 +42,7 @@ import { assertObjectType, assertInterfaceType, assertUnionType, + assertInputUnionType, assertEnumType, assertInputObjectType, assertListType, @@ -59,6 +62,10 @@ import { const ObjectType = new GraphQLObjectType({ name: 'Object' }); const InterfaceType = new GraphQLInterfaceType({ name: 'Interface' }); const UnionType = new GraphQLUnionType({ name: 'Union', types: [ObjectType] }); +const InputUnionType = new GraphQLInputUnionType({ + name: 'Union', + types: [new GraphQLInputObjectType({ name: 'InputUnionObject' })], +}); const EnumType = new GraphQLEnumType({ name: 'Enum', values: { foo: {} } }); const InputObjectType = new GraphQLInputObjectType({ name: 'InputObject' }); const ScalarType = new GraphQLScalarType({ @@ -166,6 +173,30 @@ describe('Type predicates', () => { }); }); + describe('isInputUnionType', () => { + it('returns true for input union type', () => { + expect(isInputUnionType(InputUnionType)).to.equal(true); + expect(() => assertInputUnionType(InputUnionType)).not.to.throw(); + }); + + it('returns false for union type', () => { + expect(isInputUnionType(UnionType)).to.equal(false); + expect(() => assertInputUnionType(UnionType)).to.throw(); + }); + + it('returns false for wrapped input union type', () => { + expect(isInputUnionType(GraphQLList(InputUnionType))).to.equal(false); + expect(() => + assertInputUnionType(GraphQLList(InputUnionType)), + ).to.throw(); + }); + + it('returns false for non-union type', () => { + expect(isInputUnionType(ObjectType)).to.equal(false); + expect(() => assertInputUnionType(ObjectType)).to.throw(); + }); + }); + describe('isEnumType', () => { it('returns true for enum type', () => { expect(isEnumType(EnumType)).to.equal(true); diff --git a/src/type/__tests__/validation-test.js b/src/type/__tests__/validation-test.js index d3a0c55c68..64537cae0b 100644 --- a/src/type/__tests__/validation-test.js +++ b/src/type/__tests__/validation-test.js @@ -13,6 +13,7 @@ import { GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, + GraphQLInputUnionType, GraphQLEnumType, GraphQLInputObjectType, GraphQLList, @@ -60,6 +61,11 @@ const SomeInputObjectType = new GraphQLInputObjectType({ }, }); +const SomeInputUnionType = new GraphQLInputUnionType({ + name: 'SomeInputUnion', + types: [SomeInputObjectType], +}); + function withModifiers(types) { return types .concat(types.map(type => GraphQLList(type))) @@ -76,7 +82,10 @@ const outputTypes = withModifiers([ SomeInterfaceType, ]); -const notOutputTypes = withModifiers([SomeInputObjectType]).concat(Number); +const notOutputTypes = withModifiers([ + SomeInputObjectType, + SomeInputUnionType, +]).concat(Number); const inputTypes = withModifiers([ GraphQLString, diff --git a/src/type/definition.js b/src/type/definition.js index 2e54f99ca2..99d8ce4610 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -20,6 +20,7 @@ import type { InputValueDefinitionNode, InterfaceTypeDefinitionNode, UnionTypeDefinitionNode, + InputUnionTypeDefinitionNode, EnumTypeDefinitionNode, EnumValueDefinitionNode, InputObjectTypeDefinitionNode, @@ -44,6 +45,7 @@ export type GraphQLType = | GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType + | GraphQLInputUnionType | GraphQLEnumType | GraphQLInputObjectType | GraphQLList @@ -55,6 +57,7 @@ export function isType(type: mixed): boolean %checks { isObjectType(type) || isInterfaceType(type) || isUnionType(type) || + isInputUnionType(type) || isEnumType(type) || isInputObjectType(type) || isListType(type) || @@ -131,6 +134,21 @@ export function assertUnionType(type: mixed): GraphQLUnionType { return type; } +declare function isInputUnionType(type: mixed): boolean %checks(type instanceof + GraphQLInputUnionType); +// eslint-disable-next-line no-redeclare +export function isInputUnionType(type) { + return instanceOf(type, GraphQLInputUnionType); +} + +export function assertInputUnionType(type: mixed): GraphQLUnionType { + invariant( + isInputUnionType(type), + `Expected ${String(type)} to be a GraphQL Input Union type.`, + ); + return type; +} + declare function isEnumType(type: mixed): boolean %checks(type instanceof GraphQLEnumType); // eslint-disable-next-line no-redeclare @@ -198,11 +216,13 @@ export type GraphQLInputType = | GraphQLScalarType | GraphQLEnumType | GraphQLInputObjectType + | GraphQLInputUnionType | GraphQLList | GraphQLNonNull< | GraphQLScalarType | GraphQLEnumType | GraphQLInputObjectType + | GraphQLInputUnionType | GraphQLList, >; @@ -211,6 +231,7 @@ export function isInputType(type: mixed): boolean %checks { isScalarType(type) || isEnumType(type) || isInputObjectType(type) || + isInputUnionType(type) || (isWrappingType(type) && isInputType(type.ofType)) ); } @@ -304,7 +325,7 @@ export function assertCompositeType(type: mixed): GraphQLCompositeType { export type GraphQLAbstractType = GraphQLInterfaceType | GraphQLUnionType; export function isAbstractType(type: mixed): boolean %checks { - return isInterfaceType(type) || isUnionType(type); + return isInterfaceType(type) || isUnionType(type) || isInputUnionType(type); } export function assertAbstractType(type: mixed): GraphQLAbstractType { @@ -341,6 +362,7 @@ export type GraphQLNullableType = | GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType + | GraphQLInputUnionType | GraphQLEnumType | GraphQLInputObjectType | GraphQLList; @@ -376,6 +398,7 @@ export type GraphQLNamedType = | GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType + | GraphQLInputUnionType | GraphQLEnumType | GraphQLInputObjectType; @@ -385,6 +408,7 @@ export function isNamedType(type: mixed): boolean %checks { isObjectType(type) || isInterfaceType(type) || isUnionType(type) || + isInputUnionType(type) || isEnumType(type) || isInputObjectType(type) ); @@ -1224,3 +1248,87 @@ export type GraphQLInputField = { }; export type GraphQLInputFieldMap = ObjMap; + +/** + * Input Union Type Definition + * + * An input union type defines a heterogeneous set of input object types + * possible to fulfill an input field. The `__inputname` must be specified + * in the input object. + * + * Example: + * + * const PetInputType = new GraphQLInputUnionType({ + * name: 'PetInput', + * types: [ DogInputType, CatInputType ], + * }); + * + */ +export class GraphQLInputUnionType { + name: string; + description: ?string; + astNode: ?InputUnionTypeDefinitionNode; + + _typeConfig: GraphQLInputUnionTypeConfig; + _types: Array; + _typeMap: ObjMap; + + constructor(config: GraphQLInputUnionTypeConfig): void { + this.name = config.name; + this.description = config.description; + this.astNode = config.astNode; + this._typeConfig = config; + invariant(typeof config.name === 'string', 'Must provide name.'); + if (config.resolveType) { + invariant( + typeof config.resolveType === 'function', + `${this.name} must provide "resolveType" as a function.`, + ); + } + } + + getTypes(): Array { + return ( + this._types || + (this._types = Object.keys(this.getTypeMap()).map( + typename => this._typeMap[typename], + )) + ); + } + + getTypeMap(): ObjMap { + return this._typeMap || (this._typeMap = this._defineTypeMap()); + } + + _defineTypeMap(): ObjMap { + const types = resolveThunk(this._typeConfig.types) || []; + invariant( + Array.isArray(types), + 'Must provide Array of types or a function which returns ' + + `such an array for Input Union ${this.name}.`, + ); + const resultTypeMap = Object.create(null); + types.forEach(type => { + resultTypeMap[type.name] = type; + }); + return resultTypeMap; + } + + toString(): string { + return this.name; + } + + toJSON: () => string; + inspect: () => string; +} + +// Also provide toJSON and inspect aliases for toString. +GraphQLInputUnionType.prototype.toJSON = GraphQLInputUnionType.prototype.inspect = + GraphQLInputUnionType.prototype.toString; + +export type GraphQLInputUnionTypeConfig = { + name: string, + types: Thunk>, + description?: ?string, + astNode?: ?InputUnionTypeDefinitionNode, +}; diff --git a/src/type/index.js b/src/type/index.js index 4672841fa8..eb09d02ce3 100644 --- a/src/type/index.js +++ b/src/type/index.js @@ -23,6 +23,7 @@ export { isObjectType, isInterfaceType, isUnionType, + isInputUnionType, isEnumType, isInputObjectType, isListType, @@ -41,6 +42,7 @@ export { assertObjectType, assertInterfaceType, assertUnionType, + assertInputUnionType, assertEnumType, assertInputObjectType, assertListType, @@ -61,6 +63,7 @@ export { GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, + GraphQLInputUnionType, GraphQLEnumType, GraphQLInputObjectType, } from './definition'; @@ -155,6 +158,7 @@ export type { GraphQLScalarTypeConfig, GraphQLTypeResolver, GraphQLUnionTypeConfig, + GraphQLInputUnionTypeConfig, } from './definition'; export { validateSchema, assertValidSchema } from './validate'; diff --git a/src/type/introspection.js b/src/type/introspection.js index 759f4b8f2b..182ceba88e 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -17,6 +17,7 @@ import { isObjectType, isInterfaceType, isUnionType, + isInputUnionType, isEnumType, isInputObjectType, isListType, @@ -181,6 +182,10 @@ export const __DirectiveLocation = new GraphQLEnumType({ value: DirectiveLocation.UNION, description: 'Location adjacent to a union definition.', }, + INPUT_UNION: { + value: DirectiveLocation.INPUT_UNION, + description: 'Location adjacent to an input union definition.', + }, ENUM: { value: DirectiveLocation.ENUM, description: 'Location adjacent to an enum definition.', @@ -224,6 +229,8 @@ export const __Type = new GraphQLObjectType({ return TypeKind.INTERFACE; } else if (isUnionType(type)) { return TypeKind.UNION; + } else if (isInputUnionType(type)) { + return TypeKind.INPUT_UNION; } else if (isEnumType(type)) { return TypeKind.ENUM; } else if (isInputObjectType(type)) { @@ -333,6 +340,7 @@ export const __InputValue = new GraphQLObjectType({ name: { type: GraphQLNonNull(GraphQLString) }, description: { type: GraphQLString }, type: { type: GraphQLNonNull(__Type) }, + possibleTypes: { type: new GraphQLList(__Type) }, defaultValue: { type: GraphQLString, description: @@ -368,6 +376,7 @@ export const TypeKind = { OBJECT: 'OBJECT', INTERFACE: 'INTERFACE', UNION: 'UNION', + INPUT_UNION: 'INPUT_UNION', ENUM: 'ENUM', INPUT_OBJECT: 'INPUT_OBJECT', LIST: 'LIST', @@ -401,6 +410,12 @@ export const __TypeKind = new GraphQLEnumType({ 'Indicates this type is a union. ' + '`possibleTypes` is a valid field.', }, + INPUT_UNION: { + value: TypeKind.INPUT_UNION, + description: + 'Indicates this type is a union of input objects. ' + + '`possibleTypes` is a valid field.', + }, ENUM: { value: TypeKind.ENUM, description: diff --git a/src/type/schema.js b/src/type/schema.js index d25a9c69d2..fb6ae172f9 100644 --- a/src/type/schema.js +++ b/src/type/schema.js @@ -11,6 +11,7 @@ import { isObjectType, isInterfaceType, isUnionType, + isInputUnionType, isInputObjectType, isWrappingType, } from './definition'; @@ -188,6 +189,10 @@ export class GraphQLSchema { if (isUnionType(abstractType)) { return abstractType.getTypes(); } + if (isInputUnionType(abstractType)) { + return abstractType.getTypes(); + } + return this._implementations[(abstractType: GraphQLInterfaceType).name]; } @@ -270,6 +275,10 @@ function typeMapReducer(map: TypeMap, type: ?GraphQLType): TypeMap { reducedMap = type.getTypes().reduce(typeMapReducer, reducedMap); } + if (isInputUnionType(type)) { + reducedMap = type.getTypes().reduce(typeMapReducer, reducedMap); + } + if (isObjectType(type)) { reducedMap = type.getInterfaces().reduce(typeMapReducer, reducedMap); } diff --git a/src/type/validate.js b/src/type/validate.js index 4d6feaa9f6..385dea682e 100644 --- a/src/type/validate.js +++ b/src/type/validate.js @@ -11,6 +11,7 @@ import { isObjectType, isInterfaceType, isUnionType, + isInputUnionType, isEnumType, isInputObjectType, isNonNullType, @@ -22,6 +23,7 @@ import type { GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, + GraphQLInputUnionType, GraphQLEnumType, GraphQLInputObjectType, } from './definition'; @@ -200,7 +202,7 @@ function validateDirectives(context: SchemaValidationContext): void { argNames[argName] = true; // Ensure the type is an input type. - if (!isInputType(arg.type)) { + if (!isInputType(arg.type) && !isInputUnionType(arg.type)) { context.reportError( `The type of @${directive.name}(${argName}:) must be Input Type ` + `but got: ${String(arg.type)}.`, @@ -261,6 +263,9 @@ function validateTypes(context: SchemaValidationContext): void { } else if (isUnionType(type)) { // Ensure Unions include valid member types. validateUnionMembers(context, type); + } else if (isInputUnionType(type)) { + // Ensure Unions include valid member types. + validateInputUnionMembers(context, type); } else if (isEnumType(type)) { // Ensure Enums have valid values. validateEnumValues(context, type); @@ -330,7 +335,7 @@ function validateFields( argNames[argName] = true; // Ensure the type is an input type - if (!isInputType(arg.type)) { + if (!isInputType(arg.type) && !isInputUnionType(arg.type)) { context.reportError( `The type of ${type.name}.${fieldName}(${argName}:) must be Input ` + `Type but got: ${String(arg.type)}.`, @@ -463,6 +468,44 @@ function validateObjectImplementsInterface( }); } +function validateInputUnionMembers( + context: SchemaValidationContext, + inputUnion: GraphQLInputUnionType, +): void { + const memberTypes = inputUnion.getTypes(); + + if (memberTypes.length === 0) { + context.reportError( + `Input Union type ${ + inputUnion.name + } must define one or more member types.`, + inputUnion.astNode, + ); + } + + const includedTypeNames = Object.create(null); + memberTypes.forEach(memberType => { + if (includedTypeNames[memberType.name]) { + context.reportError( + `Input Union type ${inputUnion.name} can only include type ` + + `${memberType.name} once.`, + getInputUnionMemberTypeNodes(inputUnion, memberType.name), + ); + return; // continue loop + } + includedTypeNames[memberType.name] = true; + if (!isInputObjectType(memberType)) { + context.reportError( + `Input Union type ${ + inputUnion.name + } can only include Input Object types, ` + + `it cannot include ${String(memberType)}.`, + getInputUnionMemberTypeNodes(inputUnion, String(memberType)), + ); + } + }); +} + function validateUnionMembers( context: SchemaValidationContext, union: GraphQLUnionType, @@ -557,7 +600,7 @@ function validateInputFields( // TODO: Ensure they are unique per field. // Ensure the type is an input type - if (!isInputType(field.type)) { + if (!isInputType(field.type) && !isInputUnionType(field.type)) { context.reportError( `The type of ${inputObj.name}.${fieldName} must be Input Type ` + `but got: ${String(field.type)}.`, @@ -721,6 +764,17 @@ function getUnionMemberTypeNodes( ); } +function getInputUnionMemberTypeNodes( + inputUnion: GraphQLInputUnionType, + typeName: string, +): ?$ReadOnlyArray { + return ( + inputUnion.astNode && + inputUnion.astNode.types && + inputUnion.astNode.types.filter(type => type.name.value === typeName) + ); +} + function getEnumValueNodes( enumType: GraphQLEnumType, valueName: string, diff --git a/src/utilities/__tests__/buildClientSchema-test.js b/src/utilities/__tests__/buildClientSchema-test.js index 04d2342440..b71c359cf7 100644 --- a/src/utilities/__tests__/buildClientSchema-test.js +++ b/src/utilities/__tests__/buildClientSchema-test.js @@ -18,6 +18,7 @@ import { GraphQLUnionType, GraphQLEnumType, GraphQLInputObjectType, + GraphQLInputUnionType, GraphQLList, GraphQLNonNull, GraphQLInt, @@ -569,6 +570,63 @@ describe('Type System: build schema from introspection', () => { await testSchema(schema); }); + it('builds a schema with an input union argument', async () => { + const GeomTypeEnum = new GraphQLEnumType({ + name: 'GeomTypeEnum', + description: 'GeoJSON geometry types', + values: { + Point: { + value: 1, + }, + LineString: { + value: 2, + }, + Polygon: { + value: 3, + }, + }, + }); + + const GeoJSONLineString = new GraphQLInputObjectType({ + name: 'GeoLine', + fields: { + type: { type: GeomTypeEnum }, + coordinates: { type: GraphQLList(GraphQLList(GraphQLInt)) }, + }, + }); + + const GeoJSONPoint = new GraphQLInputObjectType({ + name: 'GeoPoint', + fields: { + type: { type: GeomTypeEnum }, + coordinates: { type: GraphQLList(GraphQLInt) }, + }, + }); + + const GeoJSONGeometry = new GraphQLInputUnionType({ + name: 'GeoJSONGeometry', + types: [GeoJSONLineString, GeoJSONPoint], + }); + + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'ArgFields', + fields: { + example: { + type: GraphQLString, + args: { + geometry: { + type: GeoJSONGeometry, + }, + }, + }, + }, + }), + }); + + await testSchema(schema); + }); + it('builds a schema with custom directives', async () => { const schema = new GraphQLSchema({ query: new GraphQLObjectType({ diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index 262d1b983a..5636cb728c 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -715,6 +715,9 @@ describe('Type System Printer', () => { """Location adjacent to a union definition.""" UNION + """Location adjacent to an input union definition.""" + INPUT_UNION + """Location adjacent to an enum definition.""" ENUM @@ -762,6 +765,7 @@ describe('Type System Printer', () => { name: String! description: String type: __Type! + possibleTypes: [__Type] """ A GraphQL-formatted string representing the default value for this input value. @@ -835,6 +839,11 @@ describe('Type System Printer', () => { """Indicates this type is a union. \`possibleTypes\` is a valid field.""" UNION + """ + Indicates this type is a union of input objects. \`possibleTypes\` is a valid field. + """ + INPUT_UNION + """Indicates this type is an enum. \`enumValues\` is a valid field.""" ENUM @@ -950,6 +959,9 @@ describe('Type System Printer', () => { # Location adjacent to a union definition. UNION + # Location adjacent to an input union definition. + INPUT_UNION + # Location adjacent to an enum definition. ENUM @@ -991,6 +1003,7 @@ describe('Type System Printer', () => { name: String! description: String type: __Type! + possibleTypes: [__Type] # A GraphQL-formatted string representing the default value for this input value. defaultValue: String @@ -1050,6 +1063,9 @@ describe('Type System Printer', () => { # Indicates this type is a union. \`possibleTypes\` is a valid field. UNION + # Indicates this type is a union of input objects. \`possibleTypes\` is a valid field. + INPUT_UNION + # Indicates this type is an enum. \`enumValues\` is a valid field. ENUM diff --git a/src/utilities/astFromValue.js b/src/utilities/astFromValue.js index d3cfe59e02..987f0dc60f 100644 --- a/src/utilities/astFromValue.js +++ b/src/utilities/astFromValue.js @@ -18,6 +18,7 @@ import { isScalarType, isEnumType, isInputObjectType, + isInputUnionType, isListType, isNonNullType, } from '../type/definition'; @@ -79,6 +80,13 @@ export function astFromValue(value: mixed, type: GraphQLInputType): ?ValueNode { return astFromValue(_value, itemType); } + // Ensure the input value is valid + if (isInputUnionType(type)) { + throw new TypeError( + 'Input Unions are not supported as a direct input value', + ); + } + // Populate the fields of the input object by creating ASTs from each value // in the JavaScript object according to the fields in the input type. if (isInputObjectType(type)) { diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 16ed9a99f4..dd4e6add10 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -34,6 +34,7 @@ import type { EnumTypeDefinitionNode, EnumValueDefinitionNode, InputObjectTypeDefinitionNode, + InputUnionTypeDefinitionNode, DirectiveDefinitionNode, StringValueNode, Location, @@ -49,6 +50,7 @@ import { GraphQLUnionType, GraphQLEnumType, GraphQLInputObjectType, + GraphQLInputUnionType, } from '../type/definition'; import { GraphQLList, GraphQLNonNull } from '../type/wrappers'; @@ -160,6 +162,7 @@ export function buildASTSchema( case Kind.INTERFACE_TYPE_DEFINITION: case Kind.ENUM_TYPE_DEFINITION: case Kind.UNION_TYPE_DEFINITION: + case Kind.INPUT_UNION_TYPE_DEFINITION: case Kind.INPUT_OBJECT_TYPE_DEFINITION: const typeName = d.name.value; if (nodeMap[typeName]) { @@ -341,6 +344,8 @@ export class ASTDefinitionBuilder { return this._makeScalarDef(def); case Kind.INPUT_OBJECT_TYPE_DEFINITION: return this._makeInputObjectDef(def); + case Kind.INPUT_UNION_TYPE_DEFINITION: + return this._makeInputUnionDef(def); default: throw new Error(`Type kind "${def.kind}" not supported.`); } @@ -455,6 +460,15 @@ export class ASTDefinitionBuilder { astNode: def, }); } + + _makeInputUnionDef(def: InputUnionTypeDefinitionNode) { + return new GraphQLInputUnionType({ + name: def.name.value, + description: getDescription(def, this._options), + astNode: def, + types: def.types ? def.types.map(t => (this.buildType(t): any)) : [], + }); + } } /** diff --git a/src/utilities/buildClientSchema.js b/src/utilities/buildClientSchema.js index cc883b3a88..869b2a98e7 100644 --- a/src/utilities/buildClientSchema.js +++ b/src/utilities/buildClientSchema.js @@ -19,10 +19,12 @@ import { DirectiveLocation } from '../language/directiveLocation'; import { isInputType, isOutputType, + isInputObjectType, GraphQLScalarType, GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, + GraphQLInputUnionType, GraphQLEnumType, GraphQLInputObjectType, assertNullableType, @@ -52,6 +54,7 @@ import type { IntrospectionObjectType, IntrospectionInterfaceType, IntrospectionUnionType, + IntrospectionInputUnionType, IntrospectionEnumType, IntrospectionInputObjectType, IntrospectionTypeRef, @@ -154,6 +157,17 @@ export function buildClientSchema( return type; } + function getInputObjectType( + typeRef: IntrospectionInputObjectType, + ): GraphQLInputObjectType { + const type = getType(typeRef); + invariant( + isInputObjectType(type), + 'Introspection must provide input type for arguments.', + ); + return type; + } + function getOutputType( typeRef: IntrospectionOutputTypeRef, ): GraphQLOutputType { @@ -192,6 +206,8 @@ export function buildClientSchema( return buildInterfaceDef(type); case TypeKind.UNION: return buildUnionDef(type); + case TypeKind.INPUT_UNION: + return buildInputUnionDef(type); case TypeKind.ENUM: return buildEnumDef(type); case TypeKind.INPUT_OBJECT: @@ -258,6 +274,22 @@ export function buildClientSchema( }); } + function buildInputUnionDef( + inputUnionIntrospection: IntrospectionInputUnionType, + ): GraphQLInputUnionType { + if (!inputUnionIntrospection.possibleTypes) { + throw new Error( + 'Introspection result missing possibleTypes: ' + + JSON.stringify(inputUnionIntrospection), + ); + } + return new GraphQLInputUnionType({ + name: inputUnionIntrospection.name, + description: inputUnionIntrospection.description, + types: inputUnionIntrospection.possibleTypes.map(getInputObjectType), + }); + } + function buildEnumDef( enumIntrospection: IntrospectionEnumType, ): GraphQLEnumType { @@ -333,6 +365,7 @@ export function buildClientSchema( } function buildInputValue(inputValueIntrospection) { + const type = getInputType(inputValueIntrospection.type); const defaultValue = inputValueIntrospection.defaultValue ? valueFromAST(parseValue(inputValueIntrospection.defaultValue), type) diff --git a/src/utilities/coerceValue.js b/src/utilities/coerceValue.js index c5ba791044..ea378de5eb 100644 --- a/src/utilities/coerceValue.js +++ b/src/utilities/coerceValue.js @@ -18,6 +18,7 @@ import { isScalarType, isEnumType, isInputObjectType, + isInputUnionType, isListType, isNonNullType, } from '../type/definition'; @@ -132,83 +133,51 @@ export function coerceValue( return coercedItem.errors ? coercedItem : ofValue([coercedItem.value]); } - if (isInputObjectType(type)) { + if (isInputUnionType(type)) { if (typeof value !== 'object') { + return expectObject(type, blameNode, path); + } + const inputName = + typeof value.__inputname !== 'string' ? '' : value.__inputname; + const validTypeMap = type.getTypeMap(); + if (!inputName || !validTypeMap[inputName]) { return ofErrors([ coercionError( - `Expected type ${type.name} to be an object`, + `Expected ${type.name}.__inputname to be one of ${Object.keys( + validTypeMap, + ).join(', ')}. Saw "${inputName}"`, blameNode, path, ), ]); } - let errors; - const coercedValue = {}; - const fields = type.getFields(); - - // Ensure every defined field is valid. - for (const fieldName in fields) { - if (hasOwnProperty.call(fields, fieldName)) { - const field = fields[fieldName]; - const fieldValue = value[fieldName]; - if (isInvalid(fieldValue)) { - if (!isInvalid(field.defaultValue)) { - coercedValue[fieldName] = field.defaultValue; - } else if (isNonNullType(field.type)) { - errors = add( - errors, - coercionError( - `Field ${printPath(atPath(path, fieldName))} of required ` + - `type ${String(field.type)} was not provided`, - blameNode, - ), - ); - } - } else { - const coercedField = coerceValue( - fieldValue, - field.type, - blameNode, - atPath(path, fieldName), - ); - if (coercedField.errors) { - errors = add(errors, coercedField.errors); - } else if (!errors) { - coercedValue[fieldName] = coercedField.value; - } - } - } - } + const { __inputname, ...rest } = value; + return coerceObject(validTypeMap[inputName], rest, path, blameNode, { + __inputname: inputName, + }); + } - // Ensure every provided field is defined. - for (const fieldName in value) { - if (hasOwnProperty.call(value, fieldName)) { - if (!fields[fieldName]) { - const suggestions = suggestionList(fieldName, Object.keys(fields)); - const didYouMean = - suggestions.length !== 0 - ? `did you mean ${orList(suggestions)}?` - : undefined; - errors = add( - errors, - coercionError( - `Field "${fieldName}" is not defined by type ${type.name}`, - blameNode, - path, - didYouMean, - ), - ); - } - } + if (isInputObjectType(type)) { + if (typeof value !== 'object') { + return expectObject(type, blameNode, path); } - - return errors ? ofErrors(errors) : ofValue(coercedValue); + return coerceObject(type, value, path, blameNode); } /* istanbul ignore next */ throw new Error(`Unexpected type: ${(type: empty)}.`); } +function expectObject(type, blameNode, path) { + return ofErrors([ + coercionError( + `Expected type ${type.name} to be an object`, + blameNode, + path, + ), + ]); +} + function ofValue(value) { return { errors: undefined, value }; } @@ -254,4 +223,67 @@ function printPath(path) { return pathStr ? 'value' + pathStr : ''; } +function coerceObject(type, value, path, blameNode, coercedValue = {}) { + let errors; + const fields = type.getFields(); + + // Ensure every defined field is valid. + for (const fieldName in fields) { + if (hasOwnProperty.call(fields, fieldName)) { + const field = fields[fieldName]; + const fieldValue = value[fieldName]; + if (isInvalid(fieldValue)) { + if (!isInvalid(field.defaultValue)) { + coercedValue[fieldName] = field.defaultValue; + } else if (isNonNullType(field.type)) { + errors = add( + errors, + coercionError( + `Field ${printPath(atPath(path, fieldName))} of required ` + + `type ${String(field.type)} was not provided`, + blameNode, + ), + ); + } + } else { + const coercedField = coerceValue( + fieldValue, + field.type, + blameNode, + atPath(path, fieldName), + ); + if (coercedField.errors) { + errors = add(errors, coercedField.errors); + } else if (!errors) { + coercedValue[fieldName] = coercedField.value; + } + } + } + } + + // Ensure every provided field is defined. + for (const fieldName in value) { + if (hasOwnProperty.call(value, fieldName)) { + if (!fields[fieldName]) { + const suggestions = suggestionList(fieldName, Object.keys(fields)); + const didYouMean = + suggestions.length !== 0 + ? `did you mean ${orList(suggestions)}?` + : undefined; + errors = add( + errors, + coercionError( + `Field "${fieldName}" is not defined by type ${type.name}`, + blameNode, + path, + didYouMean, + ), + ); + } + } + } + + return errors ? ofErrors(errors) : ofValue(coercedValue); +} + const hasOwnProperty = Object.prototype.hasOwnProperty; diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index 673999001f..b27cfc2aac 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -98,6 +98,7 @@ export function extendSchema( case Kind.INTERFACE_TYPE_DEFINITION: case Kind.ENUM_TYPE_DEFINITION: case Kind.UNION_TYPE_DEFINITION: + case Kind.INPUT_UNION_TYPE_DEFINITION: case Kind.SCALAR_TYPE_DEFINITION: case Kind.INPUT_OBJECT_TYPE_DEFINITION: // Sanity check that none of the defined types conflict with the @@ -153,6 +154,7 @@ export function extendSchema( case Kind.SCALAR_TYPE_EXTENSION: case Kind.INTERFACE_TYPE_EXTENSION: case Kind.UNION_TYPE_EXTENSION: + case Kind.INPUT_UNION_TYPE_EXTENSION: case Kind.ENUM_TYPE_EXTENSION: case Kind.INPUT_OBJECT_TYPE_EXTENSION: throw new Error( diff --git a/src/utilities/findBreakingChanges.js b/src/utilities/findBreakingChanges.js index b5787df2d4..333c1cb01e 100644 --- a/src/utilities/findBreakingChanges.js +++ b/src/utilities/findBreakingChanges.js @@ -12,6 +12,7 @@ import { isObjectType, isInterfaceType, isUnionType, + isInputUnionType, isEnumType, isInputObjectType, isNonNullType, @@ -39,6 +40,7 @@ export const BreakingChangeType = { TYPE_CHANGED_KIND: 'TYPE_CHANGED_KIND', TYPE_REMOVED: 'TYPE_REMOVED', TYPE_REMOVED_FROM_UNION: 'TYPE_REMOVED_FROM_UNION', + TYPE_REMOVED_FROM_INPUT_UNION: 'TYPE_REMOVED_FROM_INPUT_UNION', VALUE_REMOVED_FROM_ENUM: 'VALUE_REMOVED_FROM_ENUM', ARG_REMOVED: 'ARG_REMOVED', ARG_CHANGED_KIND: 'ARG_CHANGED_KIND', @@ -56,6 +58,7 @@ export const DangerousChangeType = { VALUE_ADDED_TO_ENUM: 'VALUE_ADDED_TO_ENUM', INTERFACE_ADDED_TO_OBJECT: 'INTERFACE_ADDED_TO_OBJECT', TYPE_ADDED_TO_UNION: 'TYPE_ADDED_TO_UNION', + TYPE_ADDED_TO_INPUT_UNION: 'TYPE_ADDED_TO_INPUT_UNION', NULLABLE_INPUT_FIELD_ADDED: 'NULLABLE_INPUT_FIELD_ADDED', NULLABLE_ARG_ADDED: 'NULLABLE_ARG_ADDED', }; @@ -286,6 +289,9 @@ function typeKindName(type: GraphQLNamedType): string { if (isUnionType(type)) { return 'a Union type'; } + if (isInputUnionType(type)) { + return 'an Input Union type'; + } if (isEnumType(type)) { return 'an Enum type'; } diff --git a/src/utilities/index.js b/src/utilities/index.js index 90d0795fe4..5dd9b2028b 100644 --- a/src/utilities/index.js +++ b/src/utilities/index.js @@ -24,6 +24,7 @@ export type { IntrospectionObjectType, IntrospectionInterfaceType, IntrospectionUnionType, + IntrospectionInputUnionType, IntrospectionEnumType, IntrospectionInputObjectType, IntrospectionTypeRef, diff --git a/src/utilities/introspectionQuery.js b/src/utilities/introspectionQuery.js index 4071250998..b71297c54b 100644 --- a/src/utilities/introspectionQuery.js +++ b/src/utilities/introspectionQuery.js @@ -75,6 +75,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { ${descriptions ? 'description' : ''} type { ...TypeRef } defaultValue + possibleTypes { ...TypeRef } } fragment TypeRef on __Type { @@ -131,6 +132,7 @@ export type IntrospectionType = | IntrospectionObjectType | IntrospectionInterfaceType | IntrospectionUnionType + | IntrospectionInputUnionType | IntrospectionEnumType | IntrospectionInputObjectType; @@ -144,6 +146,7 @@ export type IntrospectionOutputType = export type IntrospectionInputType = | IntrospectionScalarType | IntrospectionEnumType + | IntrospectionInputUnionType | IntrospectionInputObjectType; export type IntrospectionScalarType = { @@ -181,6 +184,13 @@ export type IntrospectionUnionType = { >, }; +export type IntrospectionInputUnionType = { + +kind: 'INPUT_UNION', + +name: string, + +description?: ?string, + +possibleTypes: $ReadOnlyArray, +}; + export type IntrospectionEnumType = { +kind: 'ENUM', +name: string, @@ -254,6 +264,7 @@ export type IntrospectionInputValue = {| +description?: ?string, +type: IntrospectionInputTypeRef, +defaultValue: ?string, + +possibleTypes: $ReadOnlyArray, |}; export type IntrospectionEnumValue = {| diff --git a/src/utilities/schemaPrinter.js b/src/utilities/schemaPrinter.js index 6dc1e189bc..595ee12ba6 100644 --- a/src/utilities/schemaPrinter.js +++ b/src/utilities/schemaPrinter.js @@ -17,6 +17,7 @@ import { isObjectType, isInterfaceType, isUnionType, + isInputUnionType, isEnumType, isInputObjectType, } from '../type/definition'; @@ -27,6 +28,7 @@ import type { GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, + GraphQLInputUnionType, GraphQLInputObjectType, } from '../type/definition'; import { GraphQLString, isSpecifiedScalarType } from '../type/scalars'; @@ -160,6 +162,8 @@ export function printType(type: GraphQLNamedType, options?: Options): string { return printInterface(type, options); } else if (isUnionType(type)) { return printUnion(type, options); + } else if (isInputUnionType(type)) { + return printInputUnion(type, options); } else if (isEnumType(type)) { return printEnum(type, options); } else if (isInputObjectType(type)) { @@ -204,6 +208,13 @@ function printUnion(type: GraphQLUnionType, options): string { ); } +function printInputUnion(type: GraphQLInputUnionType, options): string { + return ( + printDescription(options, type) + + `inputUnion ${type.name} = ${type.getTypes().join(' | ')}` + ); +} + function printEnum(type: GraphQLEnumType, options): string { return ( printDescription(options, type) + diff --git a/src/utilities/valueFromAST.js b/src/utilities/valueFromAST.js index 9cf859bd42..6eef712cc3 100644 --- a/src/utilities/valueFromAST.js +++ b/src/utilities/valueFromAST.js @@ -15,6 +15,7 @@ import { isScalarType, isEnumType, isInputObjectType, + isInputUnionType, isListType, isNonNullType, } from '../type/definition'; @@ -111,36 +112,35 @@ export function valueFromAST( return [coercedValue]; } + if (isInputUnionType(type)) { + if (valueNode.kind !== Kind.OBJECT) { + return; // Invalid: intentionally return no value. + } + const fieldNodes = keyMap( + (valueNode: ObjectValueNode).fields, + field => field.name.value, + ); + const inputType = getTargetInputType(type.getTypeMap(), fieldNodes); + if (!inputType) { + return; // Invalid: intentionally return no value. + } + const fields = inputType.getFields(); + const { __inputname, ...rest } = fieldNodes; + const initialObj = Object.create(null); + initialObj['__inputname'] = inputType.name; + return coerceObject(fields, rest, variables, initialObj); + } + if (isInputObjectType(type)) { if (valueNode.kind !== Kind.OBJECT) { return; // Invalid: intentionally return no value. } - const coercedObj = Object.create(null); const fields = type.getFields(); const fieldNodes = keyMap( (valueNode: ObjectValueNode).fields, field => field.name.value, ); - const fieldNames = Object.keys(fields); - for (let i = 0; i < fieldNames.length; i++) { - const fieldName = fieldNames[i]; - const field = fields[fieldName]; - const fieldNode = fieldNodes[fieldName]; - if (!fieldNode || isMissingVariable(fieldNode.value, variables)) { - if (!isInvalid(field.defaultValue)) { - coercedObj[fieldName] = field.defaultValue; - } else if (isNonNullType(field.type)) { - return; // Invalid: intentionally return no value. - } - continue; - } - const fieldValue = valueFromAST(fieldNode.value, field.type, variables); - if (isInvalid(fieldValue)) { - return; // Invalid: intentionally return no value. - } - coercedObj[fieldName] = fieldValue; - } - return coercedObj; + return coerceObject(fields, fieldNodes, variables); } if (isEnumType(type)) { @@ -182,3 +182,38 @@ function isMissingVariable(valueNode, variables) { (!variables || isInvalid(variables[(valueNode: VariableNode).name.value])) ); } + +function getTargetInputType(inputTypeMap, fieldNodes) { + const inputTypeNode = fieldNodes.__inputname; + if (inputTypeNode && inputTypeNode.value.kind === Kind.STRING) { + return inputTypeMap[inputTypeNode.value.value]; + } +} + +function coerceObject( + fields, + fieldNodes, + variables, + coercedObj = Object.create(null), +) { + const fieldNames = Object.keys(fields); + for (let i = 0; i < fieldNames.length; i++) { + const fieldName = fieldNames[i]; + const field = fields[fieldName]; + const fieldNode = fieldNodes[fieldName]; + if (!fieldNode || isMissingVariable(fieldNode.value, variables)) { + if (!isInvalid(field.defaultValue)) { + coercedObj[fieldName] = field.defaultValue; + } else if (isNonNullType(field.type)) { + return; // Invalid: intentionally return no value. + } + continue; + } + const fieldValue = valueFromAST(fieldNode.value, field.type, variables); + if (isInvalid(fieldValue)) { + return; // Invalid: intentionally return no value. + } + coercedObj[fieldName] = fieldValue; + } + return coercedObj; +} diff --git a/src/validation/rules/KnownDirectives.js b/src/validation/rules/KnownDirectives.js index a513fd1e0f..19d52444e6 100644 --- a/src/validation/rules/KnownDirectives.js +++ b/src/validation/rules/KnownDirectives.js @@ -98,6 +98,9 @@ function getDirectiveLocationForASTPath(ancestors) { case Kind.UNION_TYPE_DEFINITION: case Kind.UNION_TYPE_EXTENSION: return DirectiveLocation.UNION; + case Kind.INPUT_UNION_TYPE_DEFINITION: + case Kind.INPUT_UNION_TYPE_EXTENSION: + return DirectiveLocation.INPUT_UNION; case Kind.ENUM_TYPE_DEFINITION: case Kind.ENUM_TYPE_EXTENSION: return DirectiveLocation.ENUM;