From af29e9abcf080f7f3a02e4db86f564ceb5642f26 Mon Sep 17 00:00:00 2001 From: Mike Marcacci Date: Tue, 30 Jan 2018 03:28:39 -0600 Subject: [PATCH 1/3] initial work on language and type --- .../__tests__/schema-kitchen-sink.graphql | 6 + src/language/__tests__/schema-parser-test.js | 173 ++++++- src/language/__tests__/schema-printer-test.js | 6 + src/language/ast.js | 2 + src/language/parser.js | 15 +- src/language/printer.js | 27 +- src/language/visitor.js | 10 +- src/type/__tests__/definition-test.js | 37 +- src/type/__tests__/introspection-test.js | 2 +- src/type/__tests__/validation-test.js | 453 +++++++++++++++++- src/type/definition.js | 11 +- src/type/introspection.js | 4 +- src/type/validate.js | 209 +++++--- src/utilities/__tests__/schemaPrinter-test.js | 4 +- src/utilities/buildASTSchema.js | 5 +- src/utilities/findBreakingChanges.js | 6 +- src/utilities/schemaPrinter.js | 6 +- 17 files changed, 869 insertions(+), 107 deletions(-) diff --git a/src/language/__tests__/schema-kitchen-sink.graphql b/src/language/__tests__/schema-kitchen-sink.graphql index f94f47c8e5..f71888091c 100644 --- a/src/language/__tests__/schema-kitchen-sink.graphql +++ b/src/language/__tests__/schema-kitchen-sink.graphql @@ -51,6 +51,12 @@ extend interface Bar { extend interface Bar @onInterface +interface Baz implements Bar { + one: Type + two(argument: InputType!): Type + four(argument: String = "string"): String +} + union Feed = Story | Article | Advert union AnnotatedUnion @onUnion = A | B diff --git a/src/language/__tests__/schema-parser-test.js b/src/language/__tests__/schema-parser-test.js index 1c9fe0f144..1de0a1fdd9 100644 --- a/src/language/__tests__/schema-parser-test.js +++ b/src/language/__tests__/schema-parser-test.js @@ -183,7 +183,7 @@ extend type Hello { expect(printJson(doc)).to.equal(printJson(expected)); }); - it('Extension without fields', () => { + it('Object extension without fields', () => { const body = 'extend type Hello implements Greeting'; const doc = parse(body); const expected = { @@ -203,7 +203,27 @@ extend type Hello { expect(printJson(doc)).to.equal(printJson(expected)); }); - it('Extension without fields followed by extension', () => { + it('Interface extension without fields', () => { + const body = 'extend interface Hello implements Greeting'; + const doc = parse(body); + const expected = { + kind: 'Document', + definitions: [ + { + kind: 'InterfaceTypeExtension', + name: nameNode('Hello', { start: 17, end: 22 }), + interfaces: [typeNode('Greeting', { start: 34, end: 42 })], + directives: [], + fields: [], + loc: { start: 0, end: 42 }, + }, + ], + loc: { start: 0, end: 42 }, + }; + expect(printJson(doc)).to.equal(printJson(expected)); + }); + + it('Object extension without fields followed by extension', () => { const body = ` extend type Hello implements Greeting @@ -235,14 +255,53 @@ extend type Hello { expect(printJson(doc)).to.equal(printJson(expected)); }); - it('Extension without anything throws', () => { + it('Interface extension without fields followed by extension', () => { + const body = ` + extend interface Hello implements Greeting + + extend interface Hello implements SecondGreeting + `; + const doc = parse(body); + const expected = { + kind: 'Document', + definitions: [ + { + kind: 'InterfaceTypeExtension', + name: nameNode('Hello', { start: 24, end: 29 }), + interfaces: [typeNode('Greeting', { start: 41, end: 49 })], + directives: [], + fields: [], + loc: { start: 7, end: 49 }, + }, + { + kind: 'InterfaceTypeExtension', + name: nameNode('Hello', { start: 74, end: 79 }), + interfaces: [typeNode('SecondGreeting', { start: 91, end: 105 })], + directives: [], + fields: [], + loc: { start: 57, end: 105 }, + }, + ], + loc: { start: 0, end: 110 }, + }; + expect(printJson(doc)).to.equal(printJson(expected)); + }); + + it('Object extension without anything throws', () => { expectSyntaxError('extend type Hello', 'Unexpected ', { line: 1, column: 18, }); }); - it('Extension do not include descriptions', () => { + it('Interface extension without anything throws', () => { + expectSyntaxError('extend interface Hello', 'Unexpected ', { + line: 1, + column: 23, + }); + }); + + it('Object extension do not include descriptions', () => { expectSyntaxError( ` "Description" @@ -263,6 +322,27 @@ extend type Hello { ); }); + it('Interface extension do not include descriptions', () => { + expectSyntaxError( + ` + "Description" + extend interface Hello { + world: String + }`, + 'Unexpected Name "extend"', + { line: 3, column: 7 }, + ); + + expectSyntaxError( + ` + extend "Description" interface Hello { + world: String + }`, + 'Unexpected String "Description"', + { line: 2, column: 14 }, + ); + }); + it('Simple non-null type', () => { const body = ` type Hello { @@ -322,6 +402,32 @@ type Hello { expect(printJson(doc)).to.equal(printJson(expected)); }); + it('Simple interface inheriting interface', () => { + const body = 'interface Hello implements World { field: String }'; + const doc = parse(body); + const expected = { + kind: 'Document', + definitions: [ + { + kind: 'InterfaceTypeDefinition', + name: nameNode('Hello', { start: 10, end: 15 }), + interfaces: [typeNode('World', { start: 27, end: 32 })], + directives: [], + fields: [ + fieldNode( + nameNode('field', { start: 35, end: 40 }), + typeNode('String', { start: 42, end: 48 }), + { start: 35, end: 48 }, + ), + ], + loc: { start: 0, end: 50 }, + }, + ], + loc: { start: 0, end: 50 }, + }; + expect(printJson(doc)).to.equal(printJson(expected)); + }); + it('Simple type inheriting multiple interfaces', () => { const body = 'type Hello implements Wo & rld { field: String }'; const doc = parse(body); @@ -351,6 +457,35 @@ type Hello { expect(printJson(doc)).to.equal(printJson(expected)); }); + it('Simple interface inheriting multiple interfaces', () => { + const body = 'interface Hello implements Wo & rld { field: String }'; + const doc = parse(body); + const expected = { + kind: 'Document', + definitions: [ + { + kind: 'InterfaceTypeDefinition', + name: nameNode('Hello', { start: 10, end: 15 }), + interfaces: [ + typeNode('Wo', { start: 27, end: 29 }), + typeNode('rld', { start: 32, end: 35 }), + ], + directives: [], + fields: [ + fieldNode( + nameNode('field', { start: 38, end: 43 }), + typeNode('String', { start: 45, end: 51 }), + { start: 38, end: 51 }, + ), + ], + loc: { start: 0, end: 53 }, + }, + ], + loc: { start: 0, end: 53 }, + }; + expect(printJson(doc)).to.equal(printJson(expected)); + }); + it('Simple type inheriting multiple interfaces with leading ampersand', () => { const body = 'type Hello implements & Wo & rld { field: String }'; const doc = parse(body); @@ -380,6 +515,35 @@ type Hello { expect(printJson(doc)).to.equal(printJson(expected)); }); + it('Simple interface inheriting multiple interfaces with leading ampersand', () => { + const body = 'interface Hello implements & Wo & rld { field: String }'; + const doc = parse(body); + const expected = { + kind: 'Document', + definitions: [ + { + kind: 'InterfaceTypeDefinition', + name: nameNode('Hello', { start: 10, end: 15 }), + interfaces: [ + typeNode('Wo', { start: 29, end: 31 }), + typeNode('rld', { start: 34, end: 37 }), + ], + directives: [], + fields: [ + fieldNode( + nameNode('field', { start: 40, end: 45 }), + typeNode('String', { start: 47, end: 53 }), + { start: 40, end: 53 }, + ), + ], + loc: { start: 0, end: 55 }, + }, + ], + loc: { start: 0, end: 55 }, + }; + expect(printJson(doc)).to.equal(printJson(expected)); + }); + it('Single value enum', () => { const body = 'enum Hello { WORLD }'; const doc = parse(body); @@ -433,6 +597,7 @@ interface Hello { { kind: 'InterfaceTypeDefinition', name: nameNode('Hello', { start: 11, end: 16 }), + interfaces: [], directives: [], fields: [ fieldNode( diff --git a/src/language/__tests__/schema-printer-test.js b/src/language/__tests__/schema-printer-test.js index cb3a3cd4c6..bba6642a2b 100644 --- a/src/language/__tests__/schema-printer-test.js +++ b/src/language/__tests__/schema-printer-test.js @@ -95,6 +95,12 @@ describe('Printer: SDL document', () => { extend interface Bar @onInterface + interface Baz implements Bar { + one: Type + two(argument: InputType!): Type + four(argument: String = "string"): String + } + union Feed = Story | Article | Advert union AnnotatedUnion @onUnion = A | B diff --git a/src/language/ast.js b/src/language/ast.js index 86d0d52438..fa02666875 100644 --- a/src/language/ast.js +++ b/src/language/ast.js @@ -458,6 +458,7 @@ export type InterfaceTypeDefinitionNode = { +loc?: Location, +description?: StringValueNode, +name: NameNode, + +interfaces?: $ReadOnlyArray, +directives?: $ReadOnlyArray, +fields?: $ReadOnlyArray, }; @@ -527,6 +528,7 @@ export type InterfaceTypeExtensionNode = { +kind: 'InterfaceTypeExtension', +loc?: Location, +name: NameNode, + +interfaces?: $ReadOnlyArray, +directives?: $ReadOnlyArray, +fields?: $ReadOnlyArray, }; diff --git a/src/language/parser.js b/src/language/parser.js index cca271a420..70fb324267 100644 --- a/src/language/parser.js +++ b/src/language/parser.js @@ -1000,12 +1000,14 @@ function parseInterfaceTypeDefinition( const description = parseDescription(lexer); expectKeyword(lexer, 'interface'); const name = parseName(lexer); + const interfaces = parseImplementsInterfaces(lexer); const directives = parseDirectives(lexer, true); const fields = parseFieldsDefinition(lexer); return { kind: Kind.INTERFACE_TYPE_DEFINITION, description, name, + interfaces, directives, fields, loc: loc(lexer, start), @@ -1226,8 +1228,9 @@ function parseObjectTypeExtension(lexer: Lexer<*>): ObjectTypeExtensionNode { /** * InterfaceTypeExtension : - * - extend interface Name Directives[Const]? FieldsDefinition - * - extend interface Name Directives[Const] + * - extend interface Name ImplementsInterfaces? Directives[Const]? FieldsDefinition + * - extend interface Name ImplementsInterfaces? Directives[Const] + * - extend interface Name ImplementsInterfaces */ function parseInterfaceTypeExtension( lexer: Lexer<*>, @@ -1236,14 +1239,20 @@ function parseInterfaceTypeExtension( expectKeyword(lexer, 'extend'); expectKeyword(lexer, 'interface'); const name = parseName(lexer); + const interfaces = parseImplementsInterfaces(lexer); const directives = parseDirectives(lexer, true); const fields = parseFieldsDefinition(lexer); - if (directives.length === 0 && fields.length === 0) { + if ( + interfaces.length === 0 && + directives.length === 0 && + fields.length === 0 + ) { throw unexpected(lexer); } return { kind: Kind.INTERFACE_TYPE_EXTENSION, name, + interfaces, directives, fields, loc: loc(lexer, start), diff --git a/src/language/printer.js b/src/language/printer.js index daf984d7e8..55554f47c9 100644 --- a/src/language/printer.js +++ b/src/language/printer.js @@ -145,8 +145,18 @@ const printDocASTReducer = { ), ), - InterfaceTypeDefinition: addDescription(({ name, directives, fields }) => - join(['interface', name, join(directives, ' '), block(fields)], ' '), + InterfaceTypeDefinition: addDescription( + ({ name, interfaces, directives, fields }) => + join( + [ + 'interface', + name, + wrap('implements ', join(interfaces, ' & ')), + join(directives, ' '), + block(fields), + ], + ' ', + ), ), UnionTypeDefinition: addDescription(({ name, directives, types }) => @@ -188,8 +198,17 @@ const printDocASTReducer = { ' ', ), - InterfaceTypeExtension: ({ name, directives, fields }) => - join(['extend interface', name, join(directives, ' '), block(fields)], ' '), + InterfaceTypeExtension: ({ name, interfaces, directives, fields }) => + join( + [ + 'extend interface', + name, + wrap('implements ', join(interfaces, ' & ')), + join(directives, ' '), + block(fields), + ], + ' ', + ), UnionTypeExtension: ({ name, directives, types }) => join( diff --git a/src/language/visitor.js b/src/language/visitor.js index 31937548ee..b48eaea1b9 100644 --- a/src/language/visitor.js +++ b/src/language/visitor.js @@ -117,7 +117,13 @@ export const QueryDocumentKeys = { 'defaultValue', 'directives', ], - InterfaceTypeDefinition: ['description', 'name', 'directives', 'fields'], + InterfaceTypeDefinition: [ + 'description', + 'name', + 'interfaces', + 'directives', + 'fields', + ], UnionTypeDefinition: ['description', 'name', 'directives', 'types'], EnumTypeDefinition: ['description', 'name', 'directives', 'values'], EnumValueDefinition: ['description', 'name', 'directives'], @@ -125,7 +131,7 @@ export const QueryDocumentKeys = { ScalarTypeExtension: ['name', 'directives'], ObjectTypeExtension: ['name', 'interfaces', 'directives', 'fields'], - InterfaceTypeExtension: ['name', 'directives', 'fields'], + InterfaceTypeExtension: ['name', 'interfaces', 'directives', 'fields'], UnionTypeExtension: ['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..427b4440c9 100644 --- a/src/type/__tests__/definition-test.js +++ b/src/type/__tests__/definition-test.js @@ -281,40 +281,60 @@ describe('Type System: Example', () => { }); it('includes interface possible types in the type map', () => { - const SomeInterface = new GraphQLInterfaceType({ - name: 'SomeInterface', + const ParentInterface = new GraphQLInterfaceType({ + name: 'ParentInterface', fields: { f: { type: GraphQLInt }, }, }); + const ChildInterface = new GraphQLInterfaceType({ + name: 'ChildInterface', + fields: { + f: { type: GraphQLInt }, + g: { type: GraphQLInt }, + }, + interfaces: [ParentInterface], + }); + const SomeSubtype = new GraphQLObjectType({ name: 'SomeSubtype', fields: { f: { type: GraphQLInt }, + g: { type: GraphQLInt }, }, - interfaces: [SomeInterface], + interfaces: [ParentInterface, ChildInterface], }); const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', fields: { - iface: { type: SomeInterface }, + iface: { type: ParentInterface }, }, }), types: [SomeSubtype], }); + expect(schema.getTypeMap().ChildInterface).to.equal(ChildInterface); expect(schema.getTypeMap().SomeSubtype).to.equal(SomeSubtype); }); it("includes interfaces' thunk subtypes in the type map", () => { - const SomeInterface = new GraphQLInterfaceType({ - name: 'SomeInterface', + const ParentInterface = new GraphQLInterfaceType({ + name: 'ParentInterface', + fields: { + f: { type: GraphQLInt }, + }, + }); + + const ChildInterface = new GraphQLInterfaceType({ + name: 'ChildInterface', fields: { f: { type: GraphQLInt }, + g: { type: GraphQLInt }, }, + interfaces: () => [ParentInterface], }); const SomeSubtype = new GraphQLObjectType({ @@ -322,19 +342,20 @@ describe('Type System: Example', () => { fields: { f: { type: GraphQLInt }, }, - interfaces: () => [SomeInterface], + interfaces: () => [ParentInterface, ChildInterface], }); const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', fields: { - iface: { type: SomeInterface }, + iface: { type: ParentInterface }, }, }), types: [SomeSubtype], }); + expect(schema.getTypeMap().ChildInterface).to.equal(ChildInterface); expect(schema.getTypeMap().SomeSubtype).to.equal(SomeSubtype); }); diff --git a/src/type/__tests__/introspection-test.js b/src/type/__tests__/introspection-test.js index 6106bcd1c3..e822324cc3 100644 --- a/src/type/__tests__/introspection-test.js +++ b/src/type/__tests__/introspection-test.js @@ -1313,7 +1313,7 @@ describe('Introspection', () => { { description: 'Indicates this type is an interface. ' + - '`fields` and `possibleTypes` are valid fields.', + '`fields`, `interfaces`, and `possibleTypes` are valid fields.', name: 'INTERFACE', }, { diff --git a/src/type/__tests__/validation-test.js b/src/type/__tests__/validation-test.js index d3a0c55c68..ac5cdae09b 100644 --- a/src/type/__tests__/validation-test.js +++ b/src/type/__tests__/validation-test.js @@ -1115,6 +1115,418 @@ describe('Type System: Input Object fields must have input types', () => { }); }); +describe('Interfaces must adhere to Interface they implement', () => { + it('accepts an Interface which implements an Interface', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field(input: String): String + } + `); + expect(validateSchema(schema)).to.deep.equal([]); + }); + + it('accepts an Interface which implements an Interface with more fields', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field(input: String): String + anotherField: String + } + `); + expect(validateSchema(schema)).to.deep.equal([]); + }); + + it('accepts an Interface which implements an Interface field along with additional optional arguments', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field(input: String, anotherInput: String): String + } + `); + expect(validateSchema(schema)).to.deep.equal([]); + }); + + it('rejects an Interface missing an Interface field', () => { + const schema = buildSchema(` + type Query { + test: SomeObject + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + anotherField: String + } + + type SomeObject implements ParentInterface & ChildInterface { + field(input: String, anotherInput: String): String + anotherField: String + } + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'Interface field ParentInterface.field expected but ' + + 'ChildInterface does not provide it.', + locations: [{ line: 7, column: 9 }, { line: 10, column: 7 }], + }, + ]); + }); + + it('rejects an Interface with an incorrectly typed Interface field', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field(input: String): Int + } + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'Interface field ParentInterface.field expects type String but ' + + 'ChildInterface.field is type Int.', + locations: [{ line: 7, column: 31 }, { line: 11, column: 31 }], + }, + ]); + }); + + it('rejects an Interface with a differently typed Interface field', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + type A { foo: String } + type B { foo: String } + + interface ParentInterface { + field: A + } + + interface ChildInterface implements ParentInterface { + field: B + } + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'Interface field ParentInterface.field expects type A but ' + + 'ChildInterface.field is type B.', + locations: [{ line: 10, column: 16 }, { line: 14, column: 16 }], + }, + ]); + }); + + it('accepts an Interface with a subtyped Interface field (interface)', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + interface ParentInterface { + field: ParentInterface + } + + type ChildInterface implements ParentInterface { + field: ChildInterface + } + `); + expect(validateSchema(schema)).to.deep.equal([]); + }); + + it('accepts an Interface with a subtyped Interface field (union)', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + type SomeObject { + field: String + } + + union SomeUnionType = SomeObject + + interface ParentInterface { + field: SomeUnionType + } + + type ChildInterface implements ParentInterface { + field: SomeObject + } + `); + expect(validateSchema(schema)).to.deep.equal([]); + }); + + it('rejects an Interface missing an Interface argument', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field: String + } + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'Interface field argument ParentInterface.field(input:) expected ' + + 'but ChildInterface.field does not provide it.', + locations: [{ line: 7, column: 15 }, { line: 11, column: 9 }], + }, + ]); + }); + + it('rejects an Interface with an incorrectly typed Interface argument', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field(input: Int): String + } + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'Interface field argument ParentInterface.field(input:) expects ' + + 'type String but ChildInterface.field(input:) is type Int.', + locations: [{ line: 7, column: 22 }, { line: 11, column: 22 }], + }, + ]); + }); + + it('rejects an Interface with both an incorrectly typed field and argument', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field(input: Int): Int + } + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'Interface field ParentInterface.field expects type String but ' + + 'ChildInterface.field is type Int.', + locations: [{ line: 7, column: 31 }, { line: 11, column: 28 }], + }, + { + message: + 'Interface field argument ParentInterface.field(input:) expects ' + + 'type String but ChildInterface.field(input:) is type Int.', + locations: [{ line: 7, column: 22 }, { line: 11, column: 22 }], + }, + ]); + }); + + it('rejects an Interface which implements an Interface field along with additional required arguments', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field(input: String, anotherInput: String!): String + } + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'Field argument ChildInterface.field(anotherInput:) is of ' + + 'required type String! but is not also provided by the Interface ' + + 'field ParentInterface.field.', + locations: [{ line: 11, column: 44 }, { line: 7, column: 9 }], + }, + ]); + }); + + it('accepts an Interface with an equivalently wrapped Interface field type', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + interface ParentInterface { + field: [String]! + } + + interface ChildInterface implements ParentInterface { + field: [String]! + } + `); + expect(validateSchema(schema)).to.deep.equal([]); + }); + + it('rejects an Interface with a non-list Interface field list type', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + interface ParentInterface { + field: [String] + } + + interface ChildInterface implements ParentInterface { + field: String + } + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'Interface field ParentInterface.field expects type [String] ' + + 'but ChildInterface.field is type String.', + locations: [{ line: 7, column: 16 }, { line: 11, column: 16 }], + }, + ]); + }); + + it('rejects an Interface with a list Interface field non-list type', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + interface ParentInterface { + field: String + } + + interface ChildInterface implements ParentInterface { + field: [String] + } + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'Interface field ParentInterface.field expects type String but ' + + 'ChildInterface.field is type [String].', + locations: [{ line: 7, column: 16 }, { line: 11, column: 16 }], + }, + ]); + }); + + it('accepts an Interface with a subset non-null Interface field type', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + interface ParentInterface { + field: String + } + + interface ChildInterface implements ParentInterface { + field: String! + } + `); + expect(validateSchema(schema)).to.deep.equal([]); + }); + + it('rejects an Interface with a superset nullable Interface field type', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + interface ParentInterface { + field: String! + } + + interface ChildInterface implements ParentInterface { + field: String + } + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'Interface field ParentInterface.field expects type String! ' + + 'but ChildInterface.field is type String.', + locations: [{ line: 7, column: 16 }, { line: 11, column: 16 }], + }, + ]); + }); + + it('rejects an Interface that does not implement all its ancestors', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + interface ParentInterface { + field: String + } + + interface ChildInterface implements ParentInterface { + field: String + anotherField: String + } + + interface GrandchildInterface implements ChildInterface { + field: String + anotherField: String + } + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'Type GrandchildInterface must implement ParentInterface because it is implemented by ChildInterface', + locations: [{ line: 15, column: 48 }], + }, + ]); + }); +}); + describe('Objects must adhere to Interface they implement', () => { it('accepts an Object which implements an Interface', () => { const schema = buildSchema(` @@ -1378,7 +1790,7 @@ describe('Objects must adhere to Interface they implement', () => { expect(validateSchema(schema)).to.containSubset([ { message: - 'Object field argument AnotherObject.field(anotherInput:) is of ' + + 'Field argument AnotherObject.field(anotherInput:) is of ' + 'required type String! but is not also provided by the Interface ' + 'field AnotherInterface.field.', locations: [{ line: 11, column: 44 }, { line: 7, column: 9 }], @@ -1491,4 +1903,43 @@ describe('Objects must adhere to Interface they implement', () => { }, ]); }); + + it('rejects an Object that does not implement all its ancestors', () => { + const schema = buildSchema(` + type Query { + test: AnotherObject + } + + interface ParentInterface { + field: String + } + + interface ChildInterface implements ParentInterface { + field: String + anotherField: String + } + + interface GrandchildInterface implements ChildInterface & ParentInterface { + field: String + anotherField: String + } + + type AnotherObject implements GrandchildInterface { + field: String + anotherField: String + } + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'Type AnotherObject must implement ChildInterface because it is implemented by GrandchildInterface', + locations: [{ line: 20, column: 37 }], + }, + { + message: + 'Type AnotherObject must implement ParentInterface because it is implemented by ChildInterface, GrandchildInterface', + locations: [{ line: 20, column: 37 }], + }, + ]); + }); }); diff --git a/src/type/definition.js b/src/type/definition.js index 9e3dfaa0b9..9252a68977 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -613,7 +613,7 @@ GraphQLObjectType.prototype.toJSON = GraphQLObjectType.prototype.inspect = GraphQLObjectType.prototype.toString; function defineInterfaces( - type: GraphQLObjectType, + type: GraphQLObjectType | GraphQLInterfaceType, interfacesThunk: Thunk>, ): Array { const interfaces = resolveThunk(interfacesThunk) || []; @@ -825,6 +825,7 @@ export class GraphQLInterfaceType { _typeConfig: GraphQLInterfaceTypeConfig<*, *>; _fields: GraphQLFieldMap<*, *>; + _interfaces: Array; constructor(config: GraphQLInterfaceTypeConfig<*, *>): void { this.name = config.name; @@ -849,6 +850,13 @@ export class GraphQLInterfaceType { ); } + getInterfaces(): Array { + return ( + this._interfaces || + (this._interfaces = defineInterfaces(this, this._typeConfig.interfaces)) + ); + } + toString(): string { return this.name; } @@ -863,6 +871,7 @@ GraphQLInterfaceType.prototype.toJSON = GraphQLInterfaceType.prototype.inspect = export type GraphQLInterfaceTypeConfig = { name: string, + interfaces?: Thunk>, fields: Thunk>, /** * Optionally provide a custom type resolver function. If one is not provided, diff --git a/src/type/introspection.js b/src/type/introspection.js index af78f42204..793853218c 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -257,7 +257,7 @@ export const __Type = new GraphQLObjectType({ interfaces: { type: GraphQLList(GraphQLNonNull(__Type)), resolve(type) { - if (isObjectType(type)) { + if (isObjectType(type) || isInterfaceType(type)) { return type.getInterfaces(); } }, @@ -389,7 +389,7 @@ export const __TypeKind = new GraphQLEnumType({ value: TypeKind.INTERFACE, description: 'Indicates this type is an interface. ' + - '`fields` and `possibleTypes` are valid fields.', + '`fields`, `interfaces`, and `possibleTypes` are valid fields.', }, UNION: { value: TypeKind.UNION, diff --git a/src/type/validate.js b/src/type/validate.js index decc7f3049..19ea86014c 100644 --- a/src/type/validate.js +++ b/src/type/validate.js @@ -253,10 +253,13 @@ function validateTypes(context: SchemaValidationContext): void { validateFields(context, type); // Ensure objects implement the interfaces they claim to. - validateObjectInterfaces(context, type); + validateInterfaces(context, type); } else if (isInterfaceType(type)) { // Ensure fields are valid. validateFields(context, type); + + // Ensure interfaces implement the interfaces they claim to. + validateInterfaces(context, type); } else if (isUnionType(type)) { // Ensure Unions include valid member types. validateUnionMembers(context, type); @@ -337,102 +340,124 @@ function validateFields( }); } -function validateObjectInterfaces( +function validateInterfaces( context: SchemaValidationContext, - object: GraphQLObjectType, + implementing: GraphQLObjectType | GraphQLInterfaceType, ): void { const implementedTypeNames = Object.create(null); - object.getInterfaces().forEach(iface => { - if (implementedTypeNames[iface.name]) { + implementing.getInterfaces().forEach(implemented => { + if (implementedTypeNames[implemented.name]) { context.reportError( - `Type ${object.name} can only implement ${iface.name} once.`, - getAllImplementsInterfaceNodes(object, iface), + `Type ${implementing.name} can only implement ${ + implemented.name + } once.`, + getAllImplementsInterfaceNodes(implementing, implemented), ); return; // continue loop } - implementedTypeNames[iface.name] = true; - validateObjectImplementsInterface(context, object, iface); + implementedTypeNames[implemented.name] = true; + validateImplementsInterface(context, implementing, implemented); }); } -function validateObjectImplementsInterface( +function validateImplementsInterface( context: SchemaValidationContext, - object: GraphQLObjectType, - iface: GraphQLInterfaceType, + implementing: GraphQLObjectType | GraphQLInterfaceType, + implemented: GraphQLInterfaceType, ): void { - if (!isInterfaceType(iface)) { + if (!isInterfaceType(implemented)) { context.reportError( - `Type ${String(object)} must only implement Interface types, ` + - `it cannot implement ${String(iface)}.`, - getImplementsInterfaceNode(object, iface), + `Type ${String(implementing)} must only implement Interface types, ` + + `it cannot implement ${String(implemented)}.`, + getImplementsInterfaceNode(implementing, implemented), ); return; } - const objectFieldMap = object.getFields(); - const ifaceFieldMap = iface.getFields(); + // Assert each ancestor interface is explicitly implemented + validateImplementsAncestors(context, implementing, implemented); + + const implementingFieldMap = implementing.getFields(); + const implementedFieldMap = implemented.getFields(); // Assert each interface field is implemented. - Object.keys(ifaceFieldMap).forEach(fieldName => { - const objectField = objectFieldMap[fieldName]; - const ifaceField = ifaceFieldMap[fieldName]; + Object.keys(implementedFieldMap).forEach(fieldName => { + const implementingField = implementingFieldMap[fieldName]; + const implementedField = implementedFieldMap[fieldName]; - // Assert interface field exists on object. - if (!objectField) { + // Assert interface field exists on implementing. + if (!implementingField) { context.reportError( - `Interface field ${iface.name}.${fieldName} expected but ` + - `${object.name} does not provide it.`, - [getFieldNode(iface, fieldName), object.astNode], + `Interface field ${implemented.name}.${fieldName} expected but ` + + `${implementing.name} does not provide it.`, + [getFieldNode(implemented, fieldName), implementing.astNode], ); // Continue loop over fields. return; } - // Assert interface field type is satisfied by object field type, by being + // Assert interface field type is satisfied by implementing field type, by being // a valid subtype. (covariant) - if (!isTypeSubTypeOf(context.schema, objectField.type, ifaceField.type)) { + if ( + !isTypeSubTypeOf( + context.schema, + implementingField.type, + implementedField.type, + ) + ) { context.reportError( - `Interface field ${iface.name}.${fieldName} expects type ` + - `${String(ifaceField.type)} but ${object.name}.${fieldName} ` + - `is type ${String(objectField.type)}.`, + `Interface field ${implemented.name}.${fieldName} expects type ` + + `${String(implementedField.type)} but ${ + implementing.name + }.${fieldName} ` + + `is type ${String(implementingField.type)}.`, [ - getFieldTypeNode(iface, fieldName), - getFieldTypeNode(object, fieldName), + getFieldTypeNode(implemented, fieldName), + getFieldTypeNode(implementing, fieldName), ], ); } // Assert each interface field arg is implemented. - ifaceField.args.forEach(ifaceArg => { - const argName = ifaceArg.name; - const objectArg = find(objectField.args, arg => arg.name === argName); + implementedField.args.forEach(implementedArg => { + const argName = implementedArg.name; + const implementingArg = find( + implementingField.args, + arg => arg.name === argName, + ); - // Assert interface field arg exists on object field. - if (!objectArg) { + // Assert interface field arg exists on implementing field. + if (!implementingArg) { context.reportError( - `Interface field argument ${iface.name}.${fieldName}(${argName}:) ` + - `expected but ${object.name}.${fieldName} does not provide it.`, + `Interface field argument ${ + implemented.name + }.${fieldName}(${argName}:) ` + + `expected but ${ + implementing.name + }.${fieldName} does not provide it.`, [ - getFieldArgNode(iface, fieldName, argName), - getFieldNode(object, fieldName), + getFieldArgNode(implemented, fieldName, argName), + getFieldNode(implementing, fieldName), ], ); // Continue loop over arguments. return; } - // Assert interface field arg type matches object field arg type. + // Assert interface field arg type matches implementing field arg type. // (invariant) // TODO: change to contravariant? - if (!isEqualType(ifaceArg.type, objectArg.type)) { + if (!isEqualType(implementedArg.type, implementingArg.type)) { context.reportError( - `Interface field argument ${iface.name}.${fieldName}(${argName}:) ` + - `expects type ${String(ifaceArg.type)} but ` + - `${object.name}.${fieldName}(${argName}:) is type ` + - `${String(objectArg.type)}.`, + `Interface field argument ${ + implemented.name + }.${fieldName}(${argName}:) ` + + `expects type ${String(implementedArg.type)} but ` + + `${implementing.name}.${fieldName}(${argName}:) is type ` + + `${String(implementingArg.type)}.`, [ - getFieldArgTypeNode(iface, fieldName, argName), - getFieldArgTypeNode(object, fieldName, argName), + getFieldArgTypeNode(implemented, fieldName, argName), + getFieldArgTypeNode(implementing, fieldName, argName), ], ); } @@ -441,17 +466,22 @@ function validateObjectImplementsInterface( }); // Assert additional arguments must not be required. - objectField.args.forEach(objectArg => { - const argName = objectArg.name; - const ifaceArg = find(ifaceField.args, arg => arg.name === argName); - if (!ifaceArg && isNonNullType(objectArg.type)) { + implementingField.args.forEach(implementingArg => { + const argName = implementingArg.name; + const implementedArg = find( + implementedField.args, + arg => arg.name === argName, + ); + if (!implementedArg && isNonNullType(implementingArg.type)) { context.reportError( - `Object field argument ${object.name}.${fieldName}(${argName}:) ` + - `is of required type ${String(objectArg.type)} but is not also ` + - `provided by the Interface field ${iface.name}.${fieldName}.`, + `Field argument ${implementing.name}.${fieldName}(${argName}:) ` + + `is of required type ${String( + implementingArg.type, + )} but is not also ` + + `provided by the Interface field ${implemented.name}.${fieldName}.`, [ - getFieldArgTypeNode(object, fieldName, argName), - getFieldNode(iface, fieldName), + getFieldArgTypeNode(implementing, fieldName, argName), + getFieldNode(implemented, fieldName), ], ); } @@ -459,6 +489,26 @@ function validateObjectImplementsInterface( }); } +function validateImplementsAncestors( + context: SchemaValidationContext, + implementing: GraphQLObjectType | GraphQLInterfaceType, + implemented: GraphQLInterfaceType, +): void { + const ancestors = getAncestors(implemented, new Map()); + ancestors.forEach((lineage, ancestor) => { + if (!implementing.getInterfaces().includes(ancestor)) { + context.reportError( + `Type ${implementing.name} must implement ${ + ancestor.name + } because it is implemented by ${Array.from(lineage) + .map(({ name }) => name) + .join(', ')}`, + getAllImplementsInterfaceNodes(implementing, implemented), + ); + } + }); +} + function validateUnionMembers( context: SchemaValidationContext, union: GraphQLUnionType, @@ -560,16 +610,6 @@ function validateInputFields( }); } -function getAllObjectNodes( - type: GraphQLObjectType, -): $ReadOnlyArray { - return type.astNode - ? type.extensionASTNodes - ? [type.astNode].concat(type.extensionASTNodes) - : [type.astNode] - : type.extensionASTNodes || []; -} - function getAllObjectOrInterfaceNodes( type: GraphQLObjectType | GraphQLInterfaceType, ): $ReadOnlyArray< @@ -586,23 +626,23 @@ function getAllObjectOrInterfaceNodes( } function getImplementsInterfaceNode( - type: GraphQLObjectType, - iface: GraphQLInterfaceType, + implementing: GraphQLObjectType | GraphQLInterfaceType, + implemented: GraphQLInterfaceType, ): ?NamedTypeNode { - return getAllImplementsInterfaceNodes(type, iface)[0]; + return getAllImplementsInterfaceNodes(implementing, implemented)[0]; } function getAllImplementsInterfaceNodes( - type: GraphQLObjectType, - iface: GraphQLInterfaceType, + implementing: GraphQLObjectType | GraphQLInterfaceType, + implemented: GraphQLInterfaceType, ): $ReadOnlyArray { const implementsNodes = []; - const astNodes = getAllObjectNodes(type); + const astNodes = getAllObjectOrInterfaceNodes(implementing); for (let i = 0; i < astNodes.length; i++) { const astNode = astNodes[i]; if (astNode && astNode.interfaces) { astNode.interfaces.forEach(node => { - if (node.name.value === iface.name) { + if (node.name.value === implemented.name) { implementsNodes.push(node); } }); @@ -724,3 +764,20 @@ function getEnumValueNodes( enumType.astNode.values.filter(value => value.name.value === valueName) ); } + +function getAncestors( + implementing: GraphQLObjectType | GraphQLInterfaceType, + ancestors: Map< + GraphQLInterfaceType, + Set, + >, +): Map> { + implementing.getInterfaces().forEach(implemented => { + const lineage = ancestors.get(implemented) || new Set(); + lineage.add(implementing); + ancestors.set(implemented, lineage); + getAncestors(implemented, ancestors); + }); + + return ancestors; +} diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index 66c8baabf5..3a69835224 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -813,7 +813,7 @@ describe('Type System Printer', () => { OBJECT """ - Indicates this type is an interface. \`fields\` and \`possibleTypes\` are valid fields. + Indicates this type is an interface. \`fields\`, \`interfaces\`, and \`possibleTypes\` are valid fields. """ INTERFACE @@ -1025,7 +1025,7 @@ describe('Type System Printer', () => { # Indicates this type is an object. \`fields\` and \`interfaces\` are valid fields. OBJECT - # Indicates this type is an interface. \`fields\` and \`possibleTypes\` are valid fields. + # Indicates this type is an interface. \`fields\`, \`interfaces\`, and \`possibleTypes\` are valid fields. INTERFACE # Indicates this type is a union. \`possibleTypes\` is a valid field. diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 8216a2e0f7..49890d2250 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -368,7 +368,9 @@ export class ASTDefinitionBuilder { : {}; } - _makeImplementedInterfaces(def: ObjectTypeDefinitionNode) { + _makeImplementedInterfaces( + def: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + ) { return ( def.interfaces && // Note: While this could make early assertions to get the correctly @@ -402,6 +404,7 @@ export class ASTDefinitionBuilder { name: def.name.value, description: getDescription(def, this._options), fields: () => this._makeFieldDefMap(def), + interfaces: () => this._makeImplementedInterfaces(def), astNode: def, }); } diff --git a/src/utilities/findBreakingChanges.js b/src/utilities/findBreakingChanges.js index b5787df2d4..3650fce4f3 100644 --- a/src/utilities/findBreakingChanges.js +++ b/src/utilities/findBreakingChanges.js @@ -640,7 +640,11 @@ export function findInterfacesRemovedFromObjectTypes( Object.keys(oldTypeMap).forEach(typeName => { const oldType = oldTypeMap[typeName]; const newType = newTypeMap[typeName]; - if (!isObjectType(oldType) || !isObjectType(newType)) { + + if ( + !(isObjectType(oldType) && isObjectType(newType)) || + !(isInterfaceType(oldType) && isInterfaceType(newType)) + ) { return; } diff --git a/src/utilities/schemaPrinter.js b/src/utilities/schemaPrinter.js index f52785e9d0..4d875df358 100644 --- a/src/utilities/schemaPrinter.js +++ b/src/utilities/schemaPrinter.js @@ -188,9 +188,13 @@ function printObject(type: GraphQLObjectType, options): string { } function printInterface(type: GraphQLInterfaceType, options): string { + const interfaces = type.getInterfaces(); + const implementedInterfaces = interfaces.length + ? ' implements ' + interfaces.map(i => i.name).join(' & ') + : ''; return ( printDescription(options, type) + - `interface ${type.name} {\n` + + `interface ${type.name}${implementedInterfaces} {\n` + printFields(options, type) + '\n' + '}' From f22a0c3968959b615d0b39e89fa610ea169509fe Mon Sep 17 00:00:00 2001 From: Mike Marcacci Date: Wed, 31 Jan 2018 14:55:29 -0600 Subject: [PATCH 2/3] additional work on utils --- src/type/schema.js | 61 ++++++- .../__tests__/buildASTSchema-test.js | 40 +++++ .../__tests__/buildClientSchema-test.js | 48 ++++++ src/utilities/__tests__/extendSchema-test.js | 150 +++++++++++------- .../__tests__/findBreakingChanges-test.js | 91 +++++++++++ .../__tests__/lexographicSortSchema-test.js | 4 +- src/utilities/__tests__/schemaPrinter-test.js | 55 +++++++ .../__tests__/typeComparators-test.js | 20 ++- src/utilities/buildClientSchema.js | 1 + src/utilities/extendSchema.js | 3 +- src/utilities/findBreakingChanges.js | 70 +++++++- src/utilities/introspectionQuery.js | 3 + src/utilities/lexographicSortSchema.js | 1 + src/utilities/typeComparators.js | 11 ++ 14 files changed, 493 insertions(+), 65 deletions(-) diff --git a/src/type/schema.js b/src/type/schema.js index 9482a9d116..8680b5cf93 100644 --- a/src/type/schema.js +++ b/src/type/schema.js @@ -78,8 +78,9 @@ export class GraphQLSchema { _subscriptionType: ?GraphQLObjectType; _directives: $ReadOnlyArray; _typeMap: TypeMap; - _implementations: ObjMap>; + _implementations: ObjMap>; _possibleTypeMap: ?ObjMap>; + _possibleImplementationsMap: ?ObjMap>; // Used as a cache for validateSchema(). __validationErrors: ?$ReadOnlyArray; // Referenced by validateSchema(). @@ -150,7 +151,7 @@ export class GraphQLSchema { this._implementations = Object.create(null); Object.keys(this._typeMap).forEach(typeName => { const type = this._typeMap[typeName]; - if (isObjectType(type)) { + if (isObjectType(type) || isInterfaceType(type)) { type.getInterfaces().forEach(iface => { const impls = this._implementations[iface.name]; if (impls) { @@ -189,7 +190,27 @@ export class GraphQLSchema { if (isUnionType(abstractType)) { return abstractType.getTypes(); } - return this._implementations[(abstractType: GraphQLInterfaceType).name]; + + const implementations = this._implementations[ + (abstractType: GraphQLInterfaceType).name + ]; + + const possibleTypes = []; + if (implementations) { + implementations.forEach(type => { + if (isObjectType(type)) { + possibleTypes.push(type); + } + }); + } + + return implementations && possibleTypes; + } + + getPossibleImplementations( + implemented: GraphQLInterfaceType, + ): $ReadOnlyArray { + return this._implementations[implemented.name]; } isPossibleType( @@ -218,6 +239,40 @@ export class GraphQLSchema { return Boolean(possibleTypeMap[abstractType.name][possibleType.name]); } + isPossibleImplementation( + implemented: GraphQLInterfaceType, + possibleImplementation: GraphQLObjectType | GraphQLInterfaceType, + ): boolean { + let possibleImplementationsMap = this._possibleImplementationsMap; + if (!possibleImplementationsMap) { + this._possibleImplementationsMap = possibleImplementationsMap = Object.create( + null, + ); + } + + if (!possibleImplementationsMap[implemented.name]) { + const possibleImplementations = this.getPossibleImplementations( + implemented, + ); + invariant( + Array.isArray(possibleImplementations), + `Could not find possible implementations for ${implemented.name} ` + + 'in schema. Check that schema.types is defined and is an array of ' + + 'all possible types in the schema.', + ); + possibleImplementationsMap[ + implemented.name + ] = possibleImplementations.reduce( + (map, impl) => ((map[impl.name] = true), map), + Object.create(null), + ); + } + + return Boolean( + possibleImplementationsMap[implemented.name][possibleImplementation.name], + ); + } + getDirectives(): $ReadOnlyArray { return this._directives; } diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index 2f1d0fe6de..a1bde2dd3a 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -322,6 +322,28 @@ describe('Schema Builder', () => { expect(output).to.equal(body); }); + it('Simple interface heirarchy', () => { + const body = dedent` + schema { + query: Child + } + + interface Child implements Parent { + str: String + } + + type Hello implements Parent & Child { + str: String + } + + interface Parent { + str: String + } + `; + const output = cycleOutput(body, 'Hello'); + expect(output).to.equal(body); + }); + it('Simple output enum', () => { const body = dedent` schema { @@ -678,6 +700,24 @@ describe('Schema Builder', () => { expect(output).to.equal(body); }); + it('Unreferenced interface implementing referenced interface', () => { + const body = dedent` + interface Child implements Parent { + key: String + } + + interface Parent { + key: String + } + + type Query { + iface: Parent + } + `; + const output = cycleOutput(body); + expect(output).to.equal(body); + }); + it('Unreferenced type implementing referenced union', () => { const body = dedent` type Concrete { diff --git a/src/utilities/__tests__/buildClientSchema-test.js b/src/utilities/__tests__/buildClientSchema-test.js index 04d2342440..1961b770ce 100644 --- a/src/utilities/__tests__/buildClientSchema-test.js +++ b/src/utilities/__tests__/buildClientSchema-test.js @@ -44,6 +44,7 @@ async function testSchema(serverSchema) { clientSchema, getIntrospectionQuery(), ); + expect(secondIntrospection).to.deep.equal(initialIntrospection); } @@ -223,6 +224,53 @@ describe('Type System: build schema from introspection', () => { await testSchema(schema); }); + it('builds a schema with an interface heirarchy', async () => { + const namedType = new GraphQLInterfaceType({ + name: 'Named', + fields: () => ({ + name: { type: new GraphQLNonNull(GraphQLString) }, + }), + }); + const friendlyType = new GraphQLInterfaceType({ + name: 'Friendly', + interfaces: [namedType], + fields: () => ({ + name: { type: new GraphQLNonNull(GraphQLString) }, + bestFriend: { + type: friendlyType, + description: 'The best friend of this friendly thing', + }, + }), + }); + const dogType = new GraphQLObjectType({ + name: 'Dog', + interfaces: [friendlyType, namedType], + fields: () => ({ + name: { type: new GraphQLNonNull(GraphQLString) }, + bestFriend: { type: friendlyType }, + }), + }); + const humanType = new GraphQLObjectType({ + name: 'Human', + interfaces: [friendlyType, namedType], + fields: () => ({ + name: { type: new GraphQLNonNull(GraphQLString) }, + bestFriend: { type: friendlyType }, + }), + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'WithInterface', + fields: { + friendly: { type: friendlyType }, + }, + }), + types: [dogType, humanType], + }); + + await testSchema(schema); + }); + it('builds a schema with an implicit interface', async () => { const friendlyType = new GraphQLInterfaceType({ name: 'Friendly', diff --git a/src/utilities/__tests__/extendSchema-test.js b/src/utilities/__tests__/extendSchema-test.js index 31d32d7077..9aadd42103 100644 --- a/src/utilities/__tests__/extendSchema-test.js +++ b/src/utilities/__tests__/extendSchema-test.js @@ -31,27 +31,35 @@ import { const SomeInterfaceType = new GraphQLInterfaceType({ name: 'SomeInterface', fields: () => ({ - name: { type: GraphQLString }, some: { type: SomeInterfaceType }, }), }); +const AnotherInterfaceType = new GraphQLInterfaceType({ + name: 'AnotherInterface', + interfaces: [SomeInterfaceType], + fields: () => ({ + name: { type: GraphQLString }, + some: { type: AnotherInterfaceType }, + }), +}); + const FooType = new GraphQLObjectType({ name: 'Foo', - interfaces: [SomeInterfaceType], + interfaces: [AnotherInterfaceType, SomeInterfaceType], fields: () => ({ name: { type: GraphQLString }, - some: { type: SomeInterfaceType }, + some: { type: AnotherInterfaceType }, tree: { type: GraphQLNonNull(GraphQLList(FooType)) }, }), }); const BarType = new GraphQLObjectType({ name: 'Bar', - interfaces: [SomeInterfaceType], + interfaces: [AnotherInterfaceType, SomeInterfaceType], fields: () => ({ name: { type: GraphQLString }, - some: { type: SomeInterfaceType }, + some: { type: AnotherInterfaceType }, foo: { type: FooType }, }), }); @@ -186,9 +194,14 @@ describe('extendSchema', () => { expect(extendedSchema).to.not.equal(testSchema); expect(printSchema(testSchema)).to.equal(originalPrint); expect(printSchema(extendedSchema)).to.equal(dedent` - type Bar implements SomeInterface { + interface AnotherInterface implements SomeInterface { name: String - some: SomeInterface + some: AnotherInterface + } + + type Bar implements AnotherInterface & SomeInterface { + name: String + some: AnotherInterface foo: Foo } @@ -196,9 +209,9 @@ describe('extendSchema', () => { fizz: String } - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! newField: String } @@ -216,7 +229,6 @@ describe('extendSchema', () => { } interface SomeInterface { - name: String some: SomeInterface } @@ -369,9 +381,14 @@ describe('extendSchema', () => { expect(extendedSchema).to.not.equal(testSchema); expect(printSchema(testSchema)).to.equal(originalPrint); expect(printSchema(extendedSchema)).to.equal(dedent` - type Bar implements SomeInterface { + interface AnotherInterface implements SomeInterface { name: String - some: SomeInterface + some: AnotherInterface + } + + type Bar implements AnotherInterface & SomeInterface { + name: String + some: AnotherInterface foo: Foo } @@ -379,9 +396,9 @@ describe('extendSchema', () => { fizz: String } - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! } @@ -398,7 +415,6 @@ describe('extendSchema', () => { } interface SomeInterface { - name: String some: SomeInterface } @@ -427,9 +443,14 @@ describe('extendSchema', () => { expect(extendedSchema).to.not.equal(testSchema); expect(printSchema(testSchema)).to.equal(originalPrint); expect(printSchema(extendedSchema)).to.equal(dedent` - type Bar implements SomeInterface { + interface AnotherInterface implements SomeInterface { name: String - some: SomeInterface + some: AnotherInterface + } + + type Bar implements AnotherInterface & SomeInterface { + name: String + some: AnotherInterface foo: Foo } @@ -437,9 +458,9 @@ describe('extendSchema', () => { fizz: String } - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! newField(arg1: String, arg2: NewInputObj!): String } @@ -463,7 +484,6 @@ describe('extendSchema', () => { } interface SomeInterface { - name: String some: SomeInterface } @@ -482,9 +502,14 @@ describe('extendSchema', () => { expect(extendedSchema).to.not.equal(testSchema); expect(printSchema(testSchema)).to.equal(originalPrint); expect(printSchema(extendedSchema)).to.equal(dedent` - type Bar implements SomeInterface { + interface AnotherInterface implements SomeInterface { name: String - some: SomeInterface + some: AnotherInterface + } + + type Bar implements AnotherInterface & SomeInterface { + name: String + some: AnotherInterface foo: Foo } @@ -492,9 +517,9 @@ describe('extendSchema', () => { fizz: String } - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! newField(arg1: SomeEnum!): SomeEnum } @@ -512,7 +537,6 @@ describe('extendSchema', () => { } interface SomeInterface { - name: String some: SomeInterface } @@ -522,9 +546,9 @@ describe('extendSchema', () => { it('extends objects by adding implemented interfaces', () => { const ast = parse(` - extend type Biz implements SomeInterface { + extend type Biz implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface } `); const originalPrint = printSchema(testSchema); @@ -532,21 +556,26 @@ describe('extendSchema', () => { expect(extendedSchema).to.not.equal(testSchema); expect(printSchema(testSchema)).to.equal(originalPrint); expect(printSchema(extendedSchema)).to.equal(dedent` - type Bar implements SomeInterface { + interface AnotherInterface implements SomeInterface { name: String - some: SomeInterface + some: AnotherInterface + } + + type Bar implements AnotherInterface & SomeInterface { + name: String + some: AnotherInterface foo: Foo } - type Biz implements SomeInterface { + type Biz implements AnotherInterface & SomeInterface { fizz: String name: String - some: SomeInterface + some: AnotherInterface } - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! } @@ -563,7 +592,6 @@ describe('extendSchema', () => { } interface SomeInterface { - name: String some: SomeInterface } @@ -608,9 +636,14 @@ describe('extendSchema', () => { expect(extendedSchema).to.not.equal(testSchema); expect(printSchema(testSchema)).to.equal(originalPrint); expect(printSchema(extendedSchema)).to.equal(dedent` - type Bar implements SomeInterface { + interface AnotherInterface implements SomeInterface { name: String - some: SomeInterface + some: AnotherInterface + } + + type Bar implements AnotherInterface & SomeInterface { + name: String + some: AnotherInterface foo: Foo } @@ -618,9 +651,9 @@ describe('extendSchema', () => { fizz: String } - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! newObject: NewObject newInterface: NewInterface @@ -664,7 +697,6 @@ describe('extendSchema', () => { } interface SomeInterface { - name: String some: SomeInterface } @@ -687,9 +719,14 @@ describe('extendSchema', () => { expect(extendedSchema).to.not.equal(testSchema); expect(printSchema(testSchema)).to.equal(originalPrint); expect(printSchema(extendedSchema)).to.equal(dedent` - type Bar implements SomeInterface { + interface AnotherInterface implements SomeInterface { name: String - some: SomeInterface + some: AnotherInterface + } + + type Bar implements AnotherInterface & SomeInterface { + name: String + some: AnotherInterface foo: Foo } @@ -697,9 +734,9 @@ describe('extendSchema', () => { fizz: String } - type Foo implements SomeInterface & NewInterface { + type Foo implements AnotherInterface & SomeInterface & NewInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! baz: String } @@ -721,7 +758,6 @@ describe('extendSchema', () => { } interface SomeInterface { - name: String some: SomeInterface } @@ -736,11 +772,15 @@ describe('extendSchema', () => { } extend type Biz implements SomeInterface { - name: String some: SomeInterface newFieldA: Int } + extend type Biz implements AnotherInterface { + name: String + some: AnotherInterface + } + extend type Biz { newFieldA: Int newFieldB: Float @@ -755,24 +795,29 @@ describe('extendSchema', () => { expect(extendedSchema).to.not.equal(testSchema); expect(printSchema(testSchema)).to.equal(originalPrint); expect(printSchema(extendedSchema)).to.equal(dedent` - type Bar implements SomeInterface { + interface AnotherInterface implements SomeInterface { name: String - some: SomeInterface + some: AnotherInterface + } + + type Bar implements AnotherInterface & SomeInterface { + name: String + some: AnotherInterface foo: Foo } - type Biz implements NewInterface & SomeInterface { + type Biz implements NewInterface & SomeInterface & AnotherInterface { fizz: String buzz: String - name: String - some: SomeInterface + some: AnotherInterface newFieldA: Int + name: String newFieldB: Float } - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! } @@ -793,7 +838,6 @@ describe('extendSchema', () => { } interface SomeInterface { - name: String some: SomeInterface } diff --git a/src/utilities/__tests__/findBreakingChanges-test.js b/src/utilities/__tests__/findBreakingChanges-test.js index e9164498b5..9786df62b8 100644 --- a/src/utilities/__tests__/findBreakingChanges-test.js +++ b/src/utilities/__tests__/findBreakingChanges-test.js @@ -1055,6 +1055,51 @@ describe('findBreakingChanges', () => { ]); }); + it('should detect interfaces removed from interfaces', () => { + const interface1 = new GraphQLInterfaceType({ + name: 'Interface1', + fields: { + field1: { type: GraphQLString }, + }, + }); + const oldInterfaceType = new GraphQLObjectType({ + name: 'Interface2', + interfaces: [interface1], + fields: { + field1: { + type: GraphQLString, + }, + }, + }); + + const newInterfaceType = new GraphQLObjectType({ + name: 'Interface2', + interfaces: [], + fields: { + field1: { + type: GraphQLString, + }, + }, + }); + + const oldSchema = new GraphQLSchema({ + query: queryType, + types: [oldInterfaceType], + }); + + const newSchema = new GraphQLSchema({ + query: queryType, + types: [newInterfaceType], + }); + + expect(findInterfacesRemovedFromObjectTypes(oldSchema, newSchema)).to.eql([ + { + description: 'Interface2 no longer implements interface Interface1.', + type: BreakingChangeType.INTERFACE_REMOVED_FROM_OBJECT, + }, + ]); + }); + it('should detect all breaking changes', () => { const typeThatGetsRemoved = new GraphQLObjectType({ name: 'TypeThatGetsRemoved', @@ -1611,6 +1656,52 @@ describe('findDangerousChanges', () => { ]); }); + it('should detect interfaces added to interfaces', () => { + const interface1 = new GraphQLInterfaceType({ + name: 'Interface1', + fields: { + field1: { type: GraphQLString }, + }, + }); + const oldInterfaceType = new GraphQLObjectType({ + name: 'Interface2', + interfaces: [], + fields: { + field1: { + type: GraphQLString, + }, + }, + }); + + const newInterfaceType = new GraphQLObjectType({ + name: 'Interface2', + interfaces: [interface1], + fields: { + field1: { + type: GraphQLString, + }, + }, + }); + + const oldSchema = new GraphQLSchema({ + query: queryType, + types: [oldInterfaceType], + }); + + const newSchema = new GraphQLSchema({ + query: queryType, + types: [newInterfaceType], + }); + + expect(findInterfacesAddedToObjectTypes(oldSchema, newSchema)).to.eql([ + { + description: + 'Interface1 added to interfaces implemented by Interface2.', + type: DangerousChangeType.INTERFACE_ADDED_TO_OBJECT, + }, + ]); + }); + it('should detect if a type was added to a union type', () => { const type1 = new GraphQLObjectType({ name: 'Type1', diff --git a/src/utilities/__tests__/lexographicSortSchema-test.js b/src/utilities/__tests__/lexographicSortSchema-test.js index 50a775ac90..e1a3f72ba7 100644 --- a/src/utilities/__tests__/lexographicSortSchema-test.js +++ b/src/utilities/__tests__/lexographicSortSchema-test.js @@ -78,7 +78,7 @@ describe('lexographicSortSchema', () => { dummy: String } - interface FooC { + interface FooC implements FooB & FooA { dummy: String } @@ -96,7 +96,7 @@ describe('lexographicSortSchema', () => { dummy: String } - interface FooC { + interface FooC implements FooA & FooB { dummy: String } diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index 3a69835224..6f4f85854d 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -374,6 +374,61 @@ describe('Type System Printer', () => { `); }); + it('Print Hierarchical Interface', () => { + const FooType = new GraphQLInterfaceType({ + name: 'Foo', + fields: { str: { type: GraphQLString } }, + }); + + const BaazType = new GraphQLInterfaceType({ + name: 'Baaz', + interfaces: [FooType], + fields: { + int: { type: GraphQLInt }, + str: { type: GraphQLString }, + }, + }); + + const BarType = new GraphQLObjectType({ + name: 'Bar', + fields: { + str: { type: GraphQLString }, + int: { type: GraphQLInt }, + }, + interfaces: [FooType, BaazType], + }); + + const Query = new GraphQLObjectType({ + name: 'Query', + fields: { bar: { type: BarType } }, + }); + + const Schema = new GraphQLSchema({ + query: Query, + types: [BarType], + }); + const output = printForTest(Schema); + expect(output).to.equal(dedent` + interface Baaz implements Foo { + int: Int + str: String + } + + type Bar implements Foo & Baaz { + str: String + int: Int + } + + interface Foo { + str: String + } + + type Query { + bar: Bar + } + `); + }); + it('Print Unions', () => { const FooType = new GraphQLObjectType({ name: 'Foo', diff --git a/src/utilities/__tests__/typeComparators-test.js b/src/utilities/__tests__/typeComparators-test.js index 7fafb0e152..b188fd299f 100644 --- a/src/utilities/__tests__/typeComparators-test.js +++ b/src/utilities/__tests__/typeComparators-test.js @@ -115,7 +115,7 @@ describe('typeComparators', () => { expect(isTypeSubTypeOf(schema, member, union)).to.equal(true); }); - it('implementation is subtype of interface', () => { + it('implementing object is subtype of interface', () => { const iface = new GraphQLInterfaceType({ name: 'Interface', fields: { @@ -132,5 +132,23 @@ describe('typeComparators', () => { const schema = testSchema({ field: { type: impl } }); expect(isTypeSubTypeOf(schema, impl, iface)).to.equal(true); }); + + it('implementing interface is subtype of interface', () => { + const iface = new GraphQLInterfaceType({ + name: 'Parent', + fields: { + field: { type: GraphQLString }, + }, + }); + const impl = new GraphQLInterfaceType({ + name: 'Child', + interfaces: [iface], + fields: { + field: { type: GraphQLString }, + }, + }); + const schema = testSchema({ field: { type: impl } }); + expect(isTypeSubTypeOf(schema, impl, iface)).to.equal(true); + }); }); }); diff --git a/src/utilities/buildClientSchema.js b/src/utilities/buildClientSchema.js index cc883b3a88..c33961f826 100644 --- a/src/utilities/buildClientSchema.js +++ b/src/utilities/buildClientSchema.js @@ -238,6 +238,7 @@ export function buildClientSchema( return new GraphQLInterfaceType({ name: interfaceIntrospection.name, description: interfaceIntrospection.description, + interfaces: interfaceIntrospection.interfaces.map(getInterfaceType), fields: () => buildFieldDefMap(interfaceIntrospection), }); } diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index f064894c17..11b3344ef1 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -289,6 +289,7 @@ export function extendSchema( return new GraphQLInterfaceType({ name: type.name, description: type.description, + interfaces: () => extendImplementedInterfaces(type), fields: () => extendFieldMap(type), astNode: type.astNode, resolveType: type.resolveType, @@ -306,7 +307,7 @@ export function extendSchema( } function extendImplementedInterfaces( - type: GraphQLObjectType, + type: GraphQLObjectType | GraphQLInterfaceType, ): Array { const interfaces = type.getInterfaces().map(getTypeFromDef); diff --git a/src/utilities/findBreakingChanges.js b/src/utilities/findBreakingChanges.js index 3650fce4f3..d35f71e320 100644 --- a/src/utilities/findBreakingChanges.js +++ b/src/utilities/findBreakingChanges.js @@ -88,6 +88,7 @@ export function findBreakingChanges( ...findValuesRemovedFromEnums(oldSchema, newSchema), ...findArgChanges(oldSchema, newSchema).breakingChanges, ...findInterfacesRemovedFromObjectTypes(oldSchema, newSchema), + ...findInterfacesRemovedFromInterfaces(oldSchema, newSchema), ...findRemovedDirectives(oldSchema, newSchema), ...findRemovedDirectiveArgs(oldSchema, newSchema), ...findAddedNonNullDirectiveArgs(oldSchema, newSchema), @@ -107,6 +108,7 @@ export function findDangerousChanges( ...findArgChanges(oldSchema, newSchema).dangerousChanges, ...findValuesAddedToEnums(oldSchema, newSchema), ...findInterfacesAddedToObjectTypes(oldSchema, newSchema), + ...findInterfacesAddedToInterfaces(oldSchema, newSchema), ...findTypesAddedToUnions(oldSchema, newSchema), ...findFieldsThatChangedTypeOnInputObjectTypes(oldSchema, newSchema) .dangerousChanges, @@ -640,11 +642,7 @@ export function findInterfacesRemovedFromObjectTypes( Object.keys(oldTypeMap).forEach(typeName => { const oldType = oldTypeMap[typeName]; const newType = newTypeMap[typeName]; - - if ( - !(isObjectType(oldType) && isObjectType(newType)) || - !(isInterfaceType(oldType) && isInterfaceType(newType)) - ) { + if (!isObjectType(oldType) || !isObjectType(newType)) { return; } @@ -695,6 +693,68 @@ export function findInterfacesAddedToObjectTypes( return interfacesAddedToObjectTypes; } +export function findInterfacesRemovedFromInterfaces( + oldSchema: GraphQLSchema, + newSchema: GraphQLSchema, +): Array { + const oldTypeMap = oldSchema.getTypeMap(); + const newTypeMap = newSchema.getTypeMap(); + const breakingChanges = []; + + Object.keys(oldTypeMap).forEach(typeName => { + const oldType = oldTypeMap[typeName]; + const newType = newTypeMap[typeName]; + if (!isInterfaceType(oldType) || !isInterfaceType(newType)) { + return; + } + + const oldInterfaces = oldType.getInterfaces(); + const newInterfaces = newType.getInterfaces(); + oldInterfaces.forEach(oldInterface => { + if (!newInterfaces.some(int => int.name === oldInterface.name)) { + breakingChanges.push({ + type: BreakingChangeType.INTERFACE_REMOVED_FROM_OBJECT, + description: + `${typeName} no longer implements interface ` + + `${oldInterface.name}.`, + }); + } + }); + }); + return breakingChanges; +} + +export function findInterfacesAddedToInterfaces( + oldSchema: GraphQLSchema, + newSchema: GraphQLSchema, +): Array { + const oldTypeMap = oldSchema.getTypeMap(); + const newTypeMap = newSchema.getTypeMap(); + const interfacesAddedToObjectTypes = []; + + Object.keys(newTypeMap).forEach(typeName => { + const oldType = oldTypeMap[typeName]; + const newType = newTypeMap[typeName]; + if (!isInterfaceType(oldType) || !isInterfaceType(newType)) { + return; + } + + const oldInterfaces = oldType.getInterfaces(); + const newInterfaces = newType.getInterfaces(); + newInterfaces.forEach(newInterface => { + if (!oldInterfaces.some(int => int.name === newInterface.name)) { + interfacesAddedToObjectTypes.push({ + type: DangerousChangeType.INTERFACE_ADDED_TO_OBJECT, + description: + `${newInterface.name} added to interfaces implemented ` + + `by ${typeName}.`, + }); + } + }); + }); + return interfacesAddedToObjectTypes; +} + export function findRemovedDirectives( oldSchema: GraphQLSchema, newSchema: GraphQLSchema, diff --git a/src/utilities/introspectionQuery.js b/src/utilities/introspectionQuery.js index 4071250998..92d3171f96 100644 --- a/src/utilities/introspectionQuery.js +++ b/src/utilities/introspectionQuery.js @@ -167,6 +167,9 @@ export type IntrospectionInterfaceType = { +name: string, +description?: ?string, +fields: $ReadOnlyArray, + +interfaces: $ReadOnlyArray< + IntrospectionNamedTypeRef, + >, +possibleTypes: $ReadOnlyArray< IntrospectionNamedTypeRef, >, diff --git a/src/utilities/lexographicSortSchema.js b/src/utilities/lexographicSortSchema.js index 0e6abc48a3..22cc04dfd5 100644 --- a/src/utilities/lexographicSortSchema.js +++ b/src/utilities/lexographicSortSchema.js @@ -134,6 +134,7 @@ export function lexographicSortSchema(schema: GraphQLSchema): GraphQLSchema { } else if (isInterfaceType(type)) { return new GraphQLInterfaceType({ name: type.name, + interfaces: sortTypes(type.getInterfaces()), fields: sortFields(type.getFields()), resolveType: type.resolveType, description: type.description, diff --git a/src/utilities/typeComparators.js b/src/utilities/typeComparators.js index 616a04723b..d183f5f776 100644 --- a/src/utilities/typeComparators.js +++ b/src/utilities/typeComparators.js @@ -8,6 +8,7 @@ */ import { + isInterfaceType, isObjectType, isListType, isNonNullType, @@ -87,6 +88,16 @@ export function isTypeSubTypeOf( return true; } + // If superType type is an abstract type, maybeSubType type may be a currently + // possible object type. + if ( + isInterfaceType(superType) && + (isObjectType(maybeSubType) || isInterfaceType(maybeSubType)) && + schema.isPossibleImplementation(superType, maybeSubType) + ) { + return true; + } + // Otherwise, the child type is not a valid subtype of the parent type. return false; } From c539fc455a94069e5b5a3fb7954ce4d3ef362f45 Mon Sep 17 00:00:00 2001 From: Mike Marcacci Date: Wed, 31 Jan 2018 15:56:21 -0600 Subject: [PATCH 3/3] add execution and valudation tests --- .../__tests__/union-interface-test.js | 64 ++++++++++++++++--- src/validation/__tests__/harness.js | 28 +++++++- 2 files changed, 81 insertions(+), 11 deletions(-) diff --git a/src/execution/__tests__/union-interface-test.js b/src/execution/__tests__/union-interface-test.js index 93f3a7eb2f..ab70dc2fc1 100644 --- a/src/execution/__tests__/union-interface-test.js +++ b/src/execution/__tests__/union-interface-test.js @@ -49,23 +49,46 @@ const NamedType = new GraphQLInterfaceType({ }, }); +const LifeType = new GraphQLInterfaceType({ + name: 'Life', + fields: () => ({ + progeny: { type: GraphQLList(LifeType) }, + }), +}); + +const MammalType = new GraphQLInterfaceType({ + name: 'Mammal', + interfaces: [LifeType], + fields: () => ({ + progeny: { type: GraphQLList(MammalType) }, + mother: { type: MammalType }, + father: { type: MammalType }, + }), +}); + const DogType = new GraphQLObjectType({ name: 'Dog', - interfaces: [NamedType], - fields: { + interfaces: [MammalType, LifeType, NamedType], + fields: () => ({ name: { type: GraphQLString }, barks: { type: GraphQLBoolean }, - }, + progeny: { type: GraphQLList(DogType) }, + mother: { type: DogType }, + father: { type: DogType }, + }), isTypeOf: value => value instanceof Dog, }); const CatType = new GraphQLObjectType({ name: 'Cat', - interfaces: [NamedType], - fields: { + interfaces: [MammalType, LifeType, NamedType], + fields: () => ({ name: { type: GraphQLString }, meows: { type: GraphQLBoolean }, - }, + progeny: { type: GraphQLList(CatType) }, + mother: { type: CatType }, + father: { type: CatType }, + }), isTypeOf: value => value instanceof Cat, }); @@ -84,12 +107,15 @@ const PetType = new GraphQLUnionType({ const PersonType = new GraphQLObjectType({ name: 'Person', - interfaces: [NamedType], - fields: { + interfaces: [NamedType, MammalType, LifeType], + fields: () => ({ name: { type: GraphQLString }, pets: { type: GraphQLList(PetType) }, friends: { type: GraphQLList(NamedType) }, - }, + progeny: { type: GraphQLList(PersonType) }, + mother: { type: PersonType }, + father: { type: PersonType }, + }), isTypeOf: value => value instanceof Person, }); @@ -116,6 +142,15 @@ describe('Execute: Union and intersection types', () => { enumValues { name } inputFields { name } } + Mammal: __type(name: "Mammal") { + kind + name + fields { name } + interfaces { name } + possibleTypes { name } + enumValues { name } + inputFields { name } + } Pet: __type(name: "Pet") { kind name @@ -134,7 +169,16 @@ describe('Execute: Union and intersection types', () => { kind: 'INTERFACE', name: 'Named', fields: [{ name: 'name' }], - interfaces: null, + interfaces: [], + possibleTypes: [{ name: 'Person' }, { name: 'Dog' }, { name: 'Cat' }], + enumValues: null, + inputFields: null, + }, + Mammal: { + kind: 'INTERFACE', + name: 'Mammal', + fields: [{ name: 'progeny' }, { name: 'mother' }, { name: 'father' }], + interfaces: [{ name: 'Life' }], possibleTypes: [{ name: 'Person' }, { name: 'Dog' }, { name: 'Cat' }], enumValues: null, inputFields: null, diff --git a/src/validation/__tests__/harness.js b/src/validation/__tests__/harness.js index 6ef9725db3..66227ee034 100644 --- a/src/validation/__tests__/harness.js +++ b/src/validation/__tests__/harness.js @@ -41,8 +41,21 @@ const Being = new GraphQLInterfaceType({ }), }); +const Mammal = new GraphQLInterfaceType({ + name: 'Mammal', + fields: () => ({ + mother: { + type: Mammal, + }, + father: { + type: Mammal, + }, + }), +}); + const Pet = new GraphQLInterfaceType({ name: 'Pet', + interfaces: [Being], fields: () => ({ name: { type: GraphQLString, @@ -53,11 +66,18 @@ const Pet = new GraphQLInterfaceType({ const Canine = new GraphQLInterfaceType({ name: 'Canine', + interfaces: [Being, Mammal], fields: () => ({ name: { type: GraphQLString, args: { surname: { type: GraphQLBoolean } }, }, + mother: { + type: Canine, + }, + father: { + type: Canine, + }, }), }); @@ -99,8 +119,14 @@ const Dog = new GraphQLObjectType({ type: GraphQLBoolean, args: { x: { type: GraphQLInt }, y: { type: GraphQLInt } }, }, + mother: { + type: Dog, + }, + father: { + type: Dog, + }, }), - interfaces: [Being, Pet, Canine], + interfaces: [Being, Pet, Mammal, Canine], }); const Cat = new GraphQLObjectType({