diff --git a/src/__fixtures__/schema-kitchen-sink.graphql b/src/__fixtures__/schema-kitchen-sink.graphql index 648f9200f6..a670f6b3cc 100644 --- a/src/__fixtures__/schema-kitchen-sink.graphql +++ b/src/__fixtures__/schema-kitchen-sink.graphql @@ -7,7 +7,7 @@ schema { This is a description of the `Foo` type. """ -type Foo implements Bar & Baz { +type Foo implements Bar & Baz & Two { "Description of the `one` field." one: Type """ @@ -50,12 +50,18 @@ interface AnnotatedInterface @onInterface { interface UndefinedInterface -extend interface Bar { +extend interface Bar implements Two { two(argument: InputType!): Type } extend interface Bar @onInterface +interface Baz implements Bar & Two { + one: Type + two(argument: InputType!): Type + four(argument: String = "string"): String +} + union Feed = | Story | Article diff --git a/src/execution/__tests__/union-interface-test.js b/src/execution/__tests__/union-interface-test.js index ace2a7b8d4..7f9bd4223a 100644 --- a/src/execution/__tests__/union-interface-test.js +++ b/src/execution/__tests__/union-interface-test.js @@ -19,20 +19,32 @@ import { execute } from '../execute'; class Dog { name: string; barks: boolean; + mother: ?Dog; + father: ?Dog; + progeny: Array; constructor(name, barks) { this.name = name; this.barks = barks; + this.mother = null; + this.father = null; + this.progeny = []; } } class Cat { name: string; meows: boolean; + mother: ?Cat; + father: ?Cat; + progeny: Array; constructor(name, meows) { this.name = name; this.meows = meows; + this.mother = null; + this.father = null; + this.progeny = []; } } @@ -55,23 +67,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, }); @@ -90,12 +125,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, }); @@ -105,7 +143,13 @@ const schema = new GraphQLSchema({ }); const garfield = new Cat('Garfield', false); +garfield.mother = new Cat("Garfield's Mom", false); +garfield.mother.progeny = [garfield]; + const odie = new Dog('Odie', true); +odie.mother = new Dog("Odie's Mom", true); +odie.mother.progeny = [odie]; + const liz = new Person('Liz'); const john = new Person('John', [garfield, odie], [liz, odie]); @@ -122,6 +166,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 @@ -140,7 +193,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, @@ -178,8 +240,16 @@ describe('Execute: Union and intersection types', () => { __typename: 'Person', name: 'John', pets: [ - { __typename: 'Cat', name: 'Garfield', meows: false }, - { __typename: 'Dog', name: 'Odie', barks: true }, + { + __typename: 'Cat', + name: 'Garfield', + meows: false, + }, + { + __typename: 'Dog', + name: 'Odie', + barks: true, + }, ], }, }); @@ -210,8 +280,16 @@ describe('Execute: Union and intersection types', () => { __typename: 'Person', name: 'John', pets: [ - { __typename: 'Cat', name: 'Garfield', meows: false }, - { __typename: 'Dog', name: 'Odie', barks: true }, + { + __typename: 'Cat', + name: 'Garfield', + meows: false, + }, + { + __typename: 'Dog', + name: 'Odie', + barks: true, + }, ], }, }); @@ -259,6 +337,20 @@ describe('Execute: Union and intersection types', () => { ... on Cat { meows } + + ... on Mammal { + mother { + __typename + ... on Dog { + name + barks + } + ... on Cat { + name + meows + } + } + } } } `); @@ -268,8 +360,17 @@ describe('Execute: Union and intersection types', () => { __typename: 'Person', name: 'John', friends: [ - { __typename: 'Person', name: 'Liz' }, - { __typename: 'Dog', name: 'Odie', barks: true }, + { + __typename: 'Person', + name: 'Liz', + mother: null, + }, + { + __typename: 'Dog', + name: 'Odie', + barks: true, + mother: { __typename: 'Dog', name: "Odie's Mom", barks: true }, + }, ], }, }); @@ -280,7 +381,14 @@ describe('Execute: Union and intersection types', () => { { __typename name - pets { ...PetFields } + pets { + ...PetFields, + ...on Mammal { + mother { + ...ProgenyFields + } + } + } friends { ...FriendFields } } @@ -306,6 +414,12 @@ describe('Execute: Union and intersection types', () => { meows } } + + fragment ProgenyFields on Life { + progeny { + __typename + } + } `); expect(execute(schema, ast, john)).to.deep.equal({ @@ -313,12 +427,29 @@ describe('Execute: Union and intersection types', () => { __typename: 'Person', name: 'John', pets: [ - { __typename: 'Cat', name: 'Garfield', meows: false }, - { __typename: 'Dog', name: 'Odie', barks: true }, + { + __typename: 'Cat', + name: 'Garfield', + meows: false, + mother: { progeny: [{ __typename: 'Cat' }] }, + }, + { + __typename: 'Dog', + name: 'Odie', + barks: true, + mother: { progeny: [{ __typename: 'Dog' }] }, + }, ], friends: [ - { __typename: 'Person', name: 'Liz' }, - { __typename: 'Dog', name: 'Odie', barks: true }, + { + __typename: 'Person', + name: 'Liz', + }, + { + __typename: 'Dog', + name: 'Odie', + barks: true, + }, ], }, }); diff --git a/src/execution/execute.js b/src/execution/execute.js index 024cb0f5a9..8059719508 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -583,7 +583,7 @@ function doesFragmentConditionMatch( return true; } if (isAbstractType(conditionalType)) { - return exeContext.schema.isPossibleType(conditionalType, type); + return exeContext.schema.isSubType(conditionalType, type); } return false; } @@ -1021,7 +1021,7 @@ function ensureValidRuntimeType( ); } - if (!exeContext.schema.isPossibleType(returnType, runtimeType)) { + if (!exeContext.schema.isSubType(returnType, runtimeType)) { throw new GraphQLError( `Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`, fieldNodes, diff --git a/src/language/__tests__/schema-parser-test.js b/src/language/__tests__/schema-parser-test.js index 9cc04b1d11..9abcc05d21 100644 --- a/src/language/__tests__/schema-parser-test.js +++ b/src/language/__tests__/schema-parser-test.js @@ -169,7 +169,7 @@ describe('Schema Parser', () => { }); }); - it('Extension without fields', () => { + it('Object extension without fields', () => { const doc = parse('extend type Hello implements Greeting'); expect(toJSONDeep(doc)).to.deep.equal({ @@ -188,7 +188,25 @@ describe('Schema Parser', () => { }); }); - it('Extension without fields followed by extension', () => { + it('Interface extension without fields', () => { + const doc = parse('extend interface Hello implements Greeting'); + expect(toJSONDeep(doc)).to.deep.equal({ + 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 }, + }); + }); + + it('Object extension without fields followed by extension', () => { const doc = parse(` extend type Hello implements Greeting @@ -226,7 +244,51 @@ describe('Schema Parser', () => { }); }); - it('Extension do not include descriptions', () => { + it('Interface extension without fields followed by extension', () => { + const doc = parse(` + extend interface Hello implements Greeting + + extend interface Hello implements SecondGreeting + `); + expect(toJSONDeep(doc)).to.deep.equal({ + 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 }, + }); + }); + + it('Object extension without anything throws', () => { + expectSyntaxError('extend type Hello').to.deep.equal({ + message: 'Syntax Error: Unexpected .', + locations: [{ line: 1, column: 18 }], + }); + }); + + it('Interface extension without anything throws', () => { + expectSyntaxError('extend interface Hello').to.deep.equal({ + message: 'Syntax Error: Unexpected .', + locations: [{ line: 1, column: 23 }], + }); + }); + + it('Object extension do not include descriptions', () => { expectSyntaxError(` "Description" extend type Hello { @@ -247,6 +309,27 @@ describe('Schema Parser', () => { }); }); + it('Interface extension do not include descriptions', () => { + expectSyntaxError(` + "Description" + extend interface Hello { + world: String + } + `).to.deep.equal({ + message: 'Syntax Error: Unexpected Name "extend".', + locations: [{ line: 3, column: 7 }], + }); + + expectSyntaxError(` + extend "Description" interface Hello { + world: String + } + `).to.deep.equal({ + message: 'Syntax Error: Unexpected String "Description".', + locations: [{ line: 2, column: 14 }], + }); + }); + it('Schema extension', () => { const body = ` extend schema { @@ -339,6 +422,31 @@ describe('Schema Parser', () => { }); }); + it('Simple interface inheriting interface', () => { + const doc = parse('interface Hello implements World { field: String }'); + expect(toJSONDeep(doc)).to.deep.equal({ + kind: 'Document', + definitions: [ + { + kind: 'InterfaceTypeDefinition', + name: nameNode('Hello', { start: 10, end: 15 }), + description: undefined, + 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 }, + }); + }); + it('Simple type inheriting interface', () => { const doc = parse('type Hello implements World { field: String }'); @@ -394,6 +502,34 @@ describe('Schema Parser', () => { }); }); + it('Simple interface inheriting multiple interfaces', () => { + const doc = parse('interface Hello implements Wo & rld { field: String }'); + expect(toJSONDeep(doc)).to.deep.equal({ + kind: 'Document', + definitions: [ + { + kind: 'InterfaceTypeDefinition', + name: nameNode('Hello', { start: 10, end: 15 }), + description: undefined, + 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 }, + }); + }); + it('Simple type inheriting multiple interfaces with leading ampersand', () => { const doc = parse('type Hello implements & Wo & rld { field: String }'); @@ -423,6 +559,36 @@ describe('Schema Parser', () => { }); }); + it('Simple interface inheriting multiple interfaces with leading ampersand', () => { + const doc = parse( + 'interface Hello implements & Wo & rld { field: String }', + ); + expect(toJSONDeep(doc)).to.deep.equal({ + kind: 'Document', + definitions: [ + { + kind: 'InterfaceTypeDefinition', + name: nameNode('Hello', { start: 10, end: 15 }), + description: undefined, + 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 }, + }); + }); + it('Single value enum', () => { const doc = parse('enum Hello { WORLD }'); @@ -478,6 +644,7 @@ describe('Schema Parser', () => { kind: 'InterfaceTypeDefinition', name: nameNode('Hello', { start: 10, end: 15 }), description: undefined, + interfaces: [], directives: [], fields: [ fieldNode( diff --git a/src/language/__tests__/schema-printer-test.js b/src/language/__tests__/schema-printer-test.js index 407445f216..3993f1bf6d 100644 --- a/src/language/__tests__/schema-printer-test.js +++ b/src/language/__tests__/schema-printer-test.js @@ -47,7 +47,7 @@ describe('Printer: SDL document', () => { This is a description of the \`Foo\` type. """ - type Foo implements Bar & Baz { + type Foo implements Bar & Baz & Two { "Description of the \`one\` field." one: Type """This is a description of the \`two\` field.""" @@ -86,12 +86,18 @@ describe('Printer: SDL document', () => { interface UndefinedInterface - extend interface Bar { + extend interface Bar implements Two { two(argument: InputType!): Type } extend interface Bar @onInterface + interface Baz implements Bar & Two { + 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 c32a199fd7..20a6529df3 100644 --- a/src/language/ast.js +++ b/src/language/ast.js @@ -488,6 +488,7 @@ export type InterfaceTypeDefinitionNode = { +loc?: Location, +description?: StringValueNode, +name: NameNode, + +interfaces?: $ReadOnlyArray, +directives?: $ReadOnlyArray, +fields?: $ReadOnlyArray, ... @@ -589,6 +590,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 ce38a0f773..c2472def7c 100644 --- a/src/language/parser.js +++ b/src/language/parser.js @@ -964,12 +964,14 @@ class Parser { const description = this.parseDescription(); this.expectKeyword('interface'); const name = this.parseName(); + const interfaces = this.parseImplementsInterfaces(); const directives = this.parseDirectives(true); const fields = this.parseFieldsDefinition(); return { kind: Kind.INTERFACE_TYPE_DEFINITION, description, name, + interfaces, directives, fields, loc: this.loc(start), @@ -1215,22 +1217,29 @@ class Parser { /** * 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 */ parseInterfaceTypeExtension(): InterfaceTypeExtensionNode { const start = this._lexer.token; this.expectKeyword('extend'); this.expectKeyword('interface'); const name = this.parseName(); + const interfaces = this.parseImplementsInterfaces(); const directives = this.parseDirectives(true); const fields = this.parseFieldsDefinition(); - if (directives.length === 0 && fields.length === 0) { + if ( + interfaces.length === 0 && + directives.length === 0 && + fields.length === 0 + ) { throw this.unexpected(); } return { kind: Kind.INTERFACE_TYPE_EXTENSION, name, + interfaces, directives, fields, loc: this.loc(start), diff --git a/src/language/printer.js b/src/language/printer.js index a96ff3f3d8..c66cbc0d96 100644 --- a/src/language/printer.js +++ b/src/language/printer.js @@ -148,8 +148,18 @@ const printDocASTReducer: any = { ), ), - 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 }) => @@ -206,8 +216,17 @@ const printDocASTReducer: any = { ' ', ), - 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 db96ee7da8..c2c26f5f21 100644 --- a/src/language/visitor.js +++ b/src/language/visitor.js @@ -113,7 +113,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 e1f5c5a484..f589ecab8f 100644 --- a/src/type/__tests__/definition-test.js +++ b/src/type/__tests__/definition-test.js @@ -441,6 +441,50 @@ describe('Type System: Interfaces', () => { ).not.to.throw(); }); + it('accepts an Interface type with an array of interfaces', () => { + const implementing = new GraphQLInterfaceType({ + name: 'AnotherInterface', + fields: {}, + interfaces: [InterfaceType], + }); + expect(implementing.getInterfaces()).to.deep.equal([InterfaceType]); + }); + + it('accepts an Interface type with interfaces as a function returning an array', () => { + const implementing = new GraphQLInterfaceType({ + name: 'AnotherInterface', + fields: {}, + interfaces: () => [InterfaceType], + }); + expect(implementing.getInterfaces()).to.deep.equal([InterfaceType]); + }); + + it('rejects an Interface type with incorrectly typed interfaces', () => { + const objType = new GraphQLInterfaceType({ + name: 'AnotherInterface', + fields: {}, + // $DisableFlowOnNegativeTest + interfaces: {}, + }); + expect(() => objType.getInterfaces()).to.throw( + 'AnotherInterface interfaces must be an Array or a function which returns an Array.', + ); + }); + + it('rejects an Interface type with interfaces as a function returning an incorrect type', () => { + const objType = new GraphQLInterfaceType({ + name: 'AnotherInterface', + fields: {}, + // $DisableFlowOnNegativeTest + interfaces() { + return {}; + }, + }); + expect(() => objType.getInterfaces()).to.throw( + 'AnotherInterface interfaces must be an Array or a function which returns an Array.', + ); + }); + it('rejects an Interface type with an incorrect type for resolveType', () => { expect( () => diff --git a/src/type/__tests__/introspection-test.js b/src/type/__tests__/introspection-test.js index 2c7b35a850..43d597671c 100644 --- a/src/type/__tests__/introspection-test.js +++ b/src/type/__tests__/introspection-test.js @@ -1357,7 +1357,7 @@ describe('Introspection', () => { }, { description: - '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.', name: 'INTERFACE', }, { diff --git a/src/type/__tests__/schema-test.js b/src/type/__tests__/schema-test.js index 7ac983d16b..c10ef8041f 100644 --- a/src/type/__tests__/schema-test.js +++ b/src/type/__tests__/schema-test.js @@ -181,6 +181,12 @@ describe('Type System: Schema', () => { const SomeInterface = new GraphQLInterfaceType({ name: 'SomeInterface', fields: {}, + interfaces: () => [AnotherInterface], + }); + + const AnotherInterface = new GraphQLInterfaceType({ + name: 'AnotherInterface', + fields: {}, }); const SomeSubtype = new GraphQLObjectType({ @@ -192,6 +198,7 @@ describe('Type System: Schema', () => { const schema = new GraphQLSchema({ types: [SomeSubtype] }); expect(schema.getType('SomeInterface')).to.equal(SomeInterface); + expect(schema.getType('AnotherInterface')).to.equal(AnotherInterface); expect(schema.getType('SomeSubtype')).to.equal(SomeSubtype); }); diff --git a/src/type/__tests__/validation-test.js b/src/type/__tests__/validation-test.js index 903b374dba..f9ace4e85a 100644 --- a/src/type/__tests__/validation-test.js +++ b/src/type/__tests__/validation-test.js @@ -1909,4 +1909,478 @@ describe('Objects must adhere to Interface they implement', () => { }, ]); }); + + it('rejects an Object missing a transitive interface', () => { + const schema = buildSchema(` + type Query { + test: AnotherObject + } + + interface SuperInterface { + field: String! + } + + interface AnotherInterface implements SuperInterface { + field: String! + } + + type AnotherObject implements AnotherInterface { + field: String! + } + `); + expect(validateSchema(schema)).to.deep.equal([ + { + message: + 'Type AnotherObject must implement SuperInterface because it is implemented by AnotherInterface.', + locations: [{ line: 10, column: 45 }, { line: 14, column: 37 }], + }, + ]); + }); +}); + +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 along 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: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + anotherField: String + } + `); + expect(validateSchema(schema)).to.deep.equal([ + { + 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.deep.equal([ + { + 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.deep.equal([ + { + 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 + } + + interface 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 + } + + interface 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.deep.equal([ + { + 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.deep.equal([ + { + 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.deep.equal([ + { + 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(baseArg: String): String + } + + interface ChildInterface implements ParentInterface { + field( + baseArg: String, + requiredArg: String! + optionalArg1: String, + optionalArg2: String = "", + ): String + } + `); + expect(validateSchema(schema)).to.deep.equal([ + { + message: + 'Object field ChildInterface.field includes required argument requiredArg that is missing from the Interface field ParentInterface.field.', + locations: [{ line: 13, column: 11 }, { 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.deep.equal([ + { + 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.deep.equal([ + { + 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.deep.equal([ + { + 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 Object missing a transitive interface', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + interface SuperInterface { + field: String! + } + + interface ParentInterface implements SuperInterface { + field: String! + } + + interface ChildInterface implements ParentInterface { + field: String! + } + `); + expect(validateSchema(schema)).to.deep.equal([ + { + message: + 'Type ChildInterface must implement SuperInterface because it is implemented by ParentInterface.', + locations: [{ line: 10, column: 44 }, { line: 14, column: 43 }], + }, + ]); + }); + + it('rejects a self reference interface', () => { + const schema = buildSchema(` + type Query { + test: FooInterface + } + + interface FooInterface implements FooInterface { + field: String + } + `); + + expect(validateSchema(schema)).to.deep.equal([ + { + message: + 'Type FooInterface cannot implement itself because it would create a circular reference.', + locations: [{ line: 6, column: 41 }], + }, + ]); + }); + + it('rejects a circular Interface implementation', () => { + const schema = buildSchema(` + type Query { + test: FooInterface + } + + interface FooInterface implements BarIntereface { + field: String + } + + interface BarIntereface implements FooInterface { + field: String + } + `); + + expect(validateSchema(schema)).to.deep.equal([ + { + message: + 'Type FooInterface cannot implement BarIntereface because it would create a circular reference.', + locations: [{ line: 10, column: 42 }, { line: 6, column: 41 }], + }, + { + message: + 'Type BarIntereface cannot implement FooInterface because it would create a circular reference.', + locations: [{ line: 6, column: 41 }, { line: 10, column: 42 }], + }, + ]); + }); }); diff --git a/src/type/definition.js b/src/type/definition.js index ffa21c7a52..f7ea7b520c 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -739,7 +739,9 @@ defineToStringTag(GraphQLObjectType); defineToJSON(GraphQLObjectType); function defineInterfaces( - config: GraphQLObjectTypeConfig, + config: + | GraphQLObjectTypeConfig + | GraphQLInterfaceTypeConfig, ): Array { const interfaces = resolveThunk(config.interfaces) || []; devAssert( @@ -979,6 +981,7 @@ export class GraphQLInterfaceType { extensionASTNodes: ?$ReadOnlyArray; _fields: Thunk>; + _interfaces: Thunk>; constructor(config: GraphQLInterfaceTypeConfig<*, *>): void { this.name = config.name; @@ -989,6 +992,7 @@ export class GraphQLInterfaceType { this.extensionASTNodes = undefineIfEmpty(config.extensionASTNodes); this._fields = defineFieldMap.bind(undefined, config); + this._interfaces = defineInterfaces.bind(undefined, config); devAssert(typeof config.name === 'string', 'Must provide name.'); devAssert( config.resolveType == null || typeof config.resolveType === 'function', @@ -1004,8 +1008,16 @@ export class GraphQLInterfaceType { return this._fields; } + getInterfaces(): Array { + if (typeof this._interfaces === 'function') { + this._interfaces = this._interfaces(); + } + return this._interfaces; + } + toConfig(): {| ...GraphQLInterfaceTypeConfig<*, *>, + interfaces: Array, fields: GraphQLFieldConfigMap<*, *>, extensions: ?ReadOnlyObjMap, extensionASTNodes: ?$ReadOnlyArray, @@ -1013,6 +1025,7 @@ export class GraphQLInterfaceType { return { name: this.name, description: this.description, + interfaces: this.getInterfaces(), fields: fieldsToFieldsConfig(this.getFields()), resolveType: this.resolveType, extensions: this.extensions, @@ -1033,6 +1046,7 @@ defineToJSON(GraphQLInterfaceType); export type GraphQLInterfaceTypeConfig = {| name: string, description?: ?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 2a6ddd2173..a48010e32e 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -240,7 +240,7 @@ export const __Type = new GraphQLObjectType({ interfaces: { type: GraphQLList(GraphQLNonNull(__Type)), resolve(type) { - if (isObjectType(type)) { + if (isObjectType(type) || isInterfaceType(type)) { return type.getInterfaces(); } }, @@ -398,7 +398,7 @@ export const __TypeKind = new GraphQLEnumType({ INTERFACE: { value: TypeKind.INTERFACE, description: - '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.', }, UNION: { value: TypeKind.UNION, diff --git a/src/type/schema.js b/src/type/schema.js index 8e06078d1b..d1cc3dd174 100644 --- a/src/type/schema.js +++ b/src/type/schema.js @@ -32,6 +32,7 @@ import { type GraphQLNamedType, type GraphQLAbstractType, type GraphQLObjectType, + type GraphQLInterfaceType, isObjectType, isInterfaceType, isUnionType, @@ -129,8 +130,8 @@ export class GraphQLSchema { _subscriptionType: ?GraphQLObjectType; _directives: $ReadOnlyArray; _typeMap: TypeMap; - _implementations: ObjMap>; - _possibleTypeMap: ObjMap>; + _implementations: ObjMap; + _subTypeMap: ObjMap>; // Used as a cache for validateSchema(). __validationErrors: ?$ReadOnlyArray; @@ -186,24 +187,10 @@ export class GraphQLSchema { // Storing the resulting map for reference by the schema. this._typeMap = typeMap; - this._possibleTypeMap = Object.create(null); + this._subTypeMap = Object.create(null); // Keep track of all implementations by interface name. - this._implementations = Object.create(null); - for (const type of objectValues(this._typeMap)) { - if (isObjectType(type)) { - for (const iface of type.getInterfaces()) { - if (isInterfaceType(iface)) { - const impls = this._implementations[iface.name]; - if (impls) { - impls.push(type); - } else { - this._implementations[iface.name] = [type]; - } - } - } - } - } + this._implementations = collectImplementations(objectValues(typeMap)); } getQueryType(): ?GraphQLObjectType { @@ -229,25 +216,50 @@ export class GraphQLSchema { getPossibleTypes( abstractType: GraphQLAbstractType, ): $ReadOnlyArray { - if (isUnionType(abstractType)) { - return abstractType.getTypes(); - } - return this._implementations[abstractType.name] || []; + return isUnionType(abstractType) + ? abstractType.getTypes() + : this.getImplementations(abstractType).objects; } + getImplementations( + interfaceType: GraphQLInterfaceType, + ): InterfaceImplementations { + return this._implementations[interfaceType.name]; + } + + // @deprecated: use isSubType instead - will be removed in v16. isPossibleType( abstractType: GraphQLAbstractType, possibleType: GraphQLObjectType, ): boolean { - if (this._possibleTypeMap[abstractType.name] == null) { - const map = Object.create(null); - for (const type of this.getPossibleTypes(abstractType)) { - map[type.name] = true; + return this.isSubType(abstractType, possibleType); + } + + isSubType( + abstractType: GraphQLAbstractType, + maybeSubType: GraphQLObjectType | GraphQLInterfaceType, + ): boolean { + let map = this._subTypeMap[abstractType.name]; + if (map === undefined) { + map = Object.create(null); + + if (isUnionType(abstractType)) { + for (const type of abstractType.getTypes()) { + map[type.name] = true; + } + } else { + const implementations = this.getImplementations(abstractType); + for (const type of implementations.objects) { + map[type.name] = true; + } + for (const type of implementations.interfaces) { + map[type.name] = true; + } } - this._possibleTypeMap[abstractType.name] = map; - } - return this._possibleTypeMap[abstractType.name][possibleType.name] != null; + this._subTypeMap[abstractType.name] = map; + } + return map[maybeSubType.name] !== undefined; } getDirectives(): $ReadOnlyArray { @@ -285,6 +297,11 @@ defineToStringTag(GraphQLSchema); type TypeMap = ObjMap; +type InterfaceImplementations = {| + objects: $ReadOnlyArray, + interfaces: $ReadOnlyArray, +|}; + export type GraphQLSchemaValidationOptions = {| /** * When building a schema from a GraphQL service's introspection result, it @@ -308,6 +325,52 @@ export type GraphQLSchemaConfig = {| ...GraphQLSchemaValidationOptions, |}; +function collectImplementations( + types: $ReadOnlyArray, +): ObjMap { + const implementations = Object.create(null); + + for (const type of types) { + if (isInterfaceType(type)) { + if (implementations[type.name] === undefined) { + implementations[type.name] = { objects: [], interfaces: [] }; + } + + // Store implementations by interface. + for (const iface of type.getInterfaces()) { + if (isInterfaceType(iface)) { + const impls = implementations[iface.name]; + if (impls === undefined) { + implementations[iface.name] = { + objects: [], + interfaces: [type], + }; + } else { + impls.interfaces.push(type); + } + } + } + } else if (isObjectType(type)) { + // Store implementations by objects. + for (const iface of type.getInterfaces()) { + if (isInterfaceType(iface)) { + const impls = implementations[iface.name]; + if (impls === undefined) { + implementations[iface.name] = { + objects: [type], + interfaces: [], + }; + } else { + impls.objects.push(type); + } + } + } + } + } + + return implementations; +} + function typeMapReducer(map: TypeMap, type: ?GraphQLType): TypeMap { if (!type) { return map; @@ -331,11 +394,9 @@ function typeMapReducer(map: TypeMap, type: ?GraphQLType): TypeMap { reducedMap = namedType.getTypes().reduce(typeMapReducer, reducedMap); } - if (isObjectType(namedType)) { + if (isObjectType(namedType) || isInterfaceType(namedType)) { reducedMap = namedType.getInterfaces().reduce(typeMapReducer, reducedMap); - } - if (isObjectType(namedType) || isInterfaceType(namedType)) { for (const field of objectValues(namedType.getFields())) { const fieldArgTypes = field.args.map(arg => arg.type); reducedMap = fieldArgTypes.reduce(typeMapReducer, reducedMap); diff --git a/src/type/validate.js b/src/type/validate.js index 9e64855d65..033334b8ff 100644 --- a/src/type/validate.js +++ b/src/type/validate.js @@ -235,10 +235,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); @@ -313,65 +316,75 @@ function validateFields( } } -function validateObjectInterfaces( +function validateInterfaces( context: SchemaValidationContext, - object: GraphQLObjectType, + type: GraphQLObjectType | GraphQLInterfaceType, ): void { - const implementedTypeNames = Object.create(null); - for (const iface of object.getInterfaces()) { + const ifaceTypeNames = Object.create(null); + for (const iface of type.getInterfaces()) { if (!isInterfaceType(iface)) { context.reportError( - `Type ${inspect(object)} must only implement Interface types, ` + + `Type ${inspect(type)} must only implement Interface types, ` + `it cannot implement ${inspect(iface)}.`, - getAllImplementsInterfaceNodes(object, iface), + getAllImplementsInterfaceNodes(type, iface), + ); + continue; + } + + if (type === iface) { + context.reportError( + `Type ${type.name} cannot implement itself because it would create a circular reference.`, + getAllImplementsInterfaceNodes(type, iface), ); continue; } - if (implementedTypeNames[iface.name]) { + if (ifaceTypeNames[iface.name]) { context.reportError( - `Type ${object.name} can only implement ${iface.name} once.`, - getAllImplementsInterfaceNodes(object, iface), + `Type ${type.name} can only implement ${iface.name} once.`, + getAllImplementsInterfaceNodes(type, iface), ); continue; } - implementedTypeNames[iface.name] = true; - validateObjectImplementsInterface(context, object, iface); + ifaceTypeNames[iface.name] = true; + + validateTypeImplementsAncestors(context, type, iface); + validateTypeImplementsInterface(context, type, iface); } } -function validateObjectImplementsInterface( +function validateTypeImplementsInterface( context: SchemaValidationContext, - object: GraphQLObjectType, + type: GraphQLObjectType | GraphQLInterfaceType, iface: GraphQLInterfaceType, ): void { - const objectFieldMap = object.getFields(); + const typeFieldMap = type.getFields(); // Assert each interface field is implemented. for (const ifaceField of objectValues(iface.getFields())) { const fieldName = ifaceField.name; - const objectField = objectFieldMap[fieldName]; + const typeField = typeFieldMap[fieldName]; - // Assert interface field exists on object. - if (!objectField) { + // Assert interface field exists on type. + if (!typeField) { context.reportError( - `Interface field ${iface.name}.${fieldName} expected but ${object.name} does not provide it.`, - [ifaceField.astNode, ...getAllNodes(object)], + `Interface field ${iface.name}.${fieldName} expected but ${type.name} does not provide it.`, + [ifaceField.astNode, ...getAllNodes(type)], ); continue; } - // Assert interface field type is satisfied by object field type, by being + // Assert interface field type is satisfied by type field type, by being // a valid subtype. (covariant) - if (!isTypeSubTypeOf(context.schema, objectField.type, ifaceField.type)) { + if (!isTypeSubTypeOf(context.schema, typeField.type, ifaceField.type)) { context.reportError( `Interface field ${iface.name}.${fieldName} expects type ` + - `${inspect(ifaceField.type)} but ${object.name}.${fieldName} ` + - `is type ${inspect(objectField.type)}.`, + `${inspect(ifaceField.type)} but ${type.name}.${fieldName} ` + + `is type ${inspect(typeField.type)}.`, [ ifaceField.astNode && ifaceField.astNode.type, - objectField.astNode && objectField.astNode.type, + typeField.astNode && typeField.astNode.type, ], ); } @@ -379,13 +392,13 @@ function validateObjectImplementsInterface( // Assert each interface field arg is implemented. for (const ifaceArg of ifaceField.args) { const argName = ifaceArg.name; - const objectArg = find(objectField.args, arg => arg.name === argName); + const typeArg = find(typeField.args, arg => arg.name === argName); // Assert interface field arg exists on object field. - if (!objectArg) { + if (!typeArg) { context.reportError( - `Interface field argument ${iface.name}.${fieldName}(${argName}:) expected but ${object.name}.${fieldName} does not provide it.`, - [ifaceArg.astNode, objectField.astNode], + `Interface field argument ${iface.name}.${fieldName}(${argName}:) expected but ${type.name}.${fieldName} does not provide it.`, + [ifaceArg.astNode, typeField.astNode], ); continue; } @@ -393,15 +406,15 @@ function validateObjectImplementsInterface( // Assert interface field arg type matches object field arg type. // (invariant) // TODO: change to contravariant? - if (!isEqualType(ifaceArg.type, objectArg.type)) { + if (!isEqualType(ifaceArg.type, typeArg.type)) { context.reportError( `Interface field argument ${iface.name}.${fieldName}(${argName}:) ` + `expects type ${inspect(ifaceArg.type)} but ` + - `${object.name}.${fieldName}(${argName}:) is type ` + - `${inspect(objectArg.type)}.`, + `${type.name}.${fieldName}(${argName}:) is type ` + + `${inspect(typeArg.type)}.`, [ ifaceArg.astNode && ifaceArg.astNode.type, - objectArg.astNode && objectArg.astNode.type, + typeArg.astNode && typeArg.astNode.type, ], ); } @@ -410,19 +423,40 @@ function validateObjectImplementsInterface( } // Assert additional arguments must not be required. - for (const objectArg of objectField.args) { - const argName = objectArg.name; + for (const typeArg of typeField.args) { + const argName = typeArg.name; const ifaceArg = find(ifaceField.args, arg => arg.name === argName); - if (!ifaceArg && isRequiredArgument(objectArg)) { + if (!ifaceArg && isRequiredArgument(typeArg)) { context.reportError( - `Object field ${object.name}.${fieldName} includes required argument ${argName} that is missing from the Interface field ${iface.name}.${fieldName}.`, - [objectArg.astNode, ifaceField.astNode], + `Object field ${type.name}.${fieldName} includes required argument ${argName} that is missing from the Interface field ${iface.name}.${fieldName}.`, + [typeArg.astNode, ifaceField.astNode], ); } } } } +function validateTypeImplementsAncestors( + context: SchemaValidationContext, + type: GraphQLObjectType | GraphQLInterfaceType, + iface: GraphQLInterfaceType, +): void { + const ifaceInterfaces = type.getInterfaces(); + for (const transitive of iface.getInterfaces()) { + if (ifaceInterfaces.indexOf(transitive) === -1) { + context.reportError( + transitive === type + ? `Type ${type.name} cannot implement ${iface.name} because it would create a circular reference.` + : `Type ${type.name} must implement ${transitive.name} because it is implemented by ${iface.name}.`, + [ + ...getAllImplementsInterfaceNodes(iface, transitive), + ...getAllImplementsInterfaceNodes(type, iface), + ], + ); + } + } +} + function validateUnionMembers( context: SchemaValidationContext, union: GraphQLUnionType, @@ -589,7 +623,7 @@ function getAllSubNodes( } function getAllImplementsInterfaceNodes( - type: GraphQLObjectType, + type: GraphQLObjectType | GraphQLInterfaceType, iface: GraphQLInterfaceType, ): $ReadOnlyArray { return getAllSubNodes(type, typeNode => typeNode.interfaces).filter( diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index 83edc697a0..5d956bd56c 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -285,6 +285,12 @@ describe('Schema Builder', () => { const sdl = dedent` interface EmptyInterface `; + + const definition = parse(sdl).definitions[0]; + expect( + definition.kind === 'InterfaceTypeDefinition' && definition.interfaces, + ).to.deep.equal([], 'The interfaces property must be an empty array.'); + expect(cycleSDL(sdl)).to.equal(sdl); }); @@ -301,6 +307,27 @@ describe('Schema Builder', () => { expect(cycleSDL(sdl)).to.equal(sdl); }); + it('Simple interface heirarchy', () => { + const sdl = dedent` + schema { + query: Child + } + + interface Child implements Parent { + str: String + } + + type Hello implements Parent & Child { + str: String + } + + interface Parent { + str: String + } + `; + expect(cycleSDL(sdl)).to.equal(sdl); + }); + it('Empty enum', () => { const sdl = dedent` enum EmptyEnum @@ -632,6 +659,23 @@ describe('Schema Builder', () => { expect(cycleSDL(sdl)).to.equal(sdl); }); + it('Unreferenced interface implementing referenced interface', () => { + const sdl = dedent` + interface Child implements Parent { + key: String + } + + interface Parent { + key: String + } + + type Query { + iface: Parent + } + `; + expect(cycleSDL(sdl)).to.equal(sdl); + }); + it('Unreferenced type implementing referenced union', () => { const sdl = dedent` type Concrete { diff --git a/src/utilities/__tests__/buildClientSchema-test.js b/src/utilities/__tests__/buildClientSchema-test.js index b67feecb84..c736eef9a1 100644 --- a/src/utilities/__tests__/buildClientSchema-test.js +++ b/src/utilities/__tests__/buildClientSchema-test.js @@ -211,6 +211,36 @@ describe('Type System: build schema from introspection', () => { expect(cycleIntrospection(sdl)).to.equal(sdl); }); + it('builds a schema with an interface heirarchy', () => { + const sdl = dedent` + type Dog implements Friendly & Named { + bestFriend: Friendly + name: String + } + + interface Friendly implements Named { + """The best friend of this friendly thing""" + bestFriend: Friendly + name: String + } + + type Human implements Friendly & Named { + bestFriend: Friendly + name: String + } + + interface Named { + name: String + } + + type Query { + friendly: Friendly + } + `; + + expect(cycleIntrospection(sdl)).to.equal(sdl); + }); + it('builds a schema with an implicit interface', () => { const sdl = dedent` type Dog implements Friendly { @@ -552,6 +582,10 @@ describe('Type System: build schema from introspection', () => { foo(bar: String): String } + interface SomeInterface { + foo: String + } + union SomeUnion = Query enum SomeEnum { FOO } @@ -647,6 +681,20 @@ describe('Type System: build schema from introspection', () => { ); }); + it('Legacy support for interfaces with null as interfaces field', () => { + const introspection = introspectionFromSchema(dummySchema); + const someInterfaceIntrospection = introspection.__schema.types.find( + ({ name }) => name === 'SomeInterface', + ); + + expect(someInterfaceIntrospection).to.have.property('interfaces'); + // $DisableFlowOnNegativeTest + someInterfaceIntrospection.interfaces = null; + + const clientSchema = buildClientSchema(introspection); + expect(printSchema(clientSchema)).to.equal(printSchema(dummySchema)); + }); + it('throws when missing fields', () => { const introspection = introspectionFromSchema(dummySchema); const queryTypeIntrospection = introspection.__schema.types.find( diff --git a/src/utilities/__tests__/extendSchema-test.js b/src/utilities/__tests__/extendSchema-test.js index 690051307b..ab188d45a1 100644 --- a/src/utilities/__tests__/extendSchema-test.js +++ b/src/utilities/__tests__/extendSchema-test.js @@ -41,18 +41,21 @@ const testSchema = buildSchema(` scalar SomeScalar interface SomeInterface { - name: String some: SomeInterface } - type Foo implements SomeInterface { + interface AnotherInterface implements SomeInterface { name: String - some: SomeInterface + some: AnotherInterface + } + + type Foo implements AnotherInterface & SomeInterface { + name: String + some: AnotherInterface tree: [Foo]! } type Bar implements SomeInterface { - name: String some: SomeInterface foo: Foo } @@ -194,9 +197,9 @@ describe('extendSchema', () => { } `); expect(printTestSchemaChanges(extendedSchema)).to.equal(dedent` - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! newField: String } @@ -633,9 +636,9 @@ describe('extendSchema', () => { } `); expect(printTestSchemaChanges(extendedSchema)).to.equal(dedent` - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! newField(arg1: String, arg2: NewInputObj!): String } @@ -655,9 +658,9 @@ describe('extendSchema', () => { } `); expect(printTestSchemaChanges(extendedSchema)).to.equal(dedent` - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! newField(arg1: SomeEnum!): SomeEnum } @@ -713,9 +716,9 @@ describe('extendSchema', () => { } `); expect(printTestSchemaChanges(extendedSchema)).to.equal(dedent` - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! newObject: NewObject newInterface: NewInterface @@ -759,9 +762,9 @@ describe('extendSchema', () => { } `); expect(printTestSchemaChanges(extendedSchema)).to.equal(dedent` - type Foo implements SomeInterface & NewInterface { + type Foo implements AnotherInterface & SomeInterface & NewInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! baz: String } @@ -865,6 +868,10 @@ describe('extendSchema', () => { newField: String } + extend interface AnotherInterface { + newField: String + } + extend type Bar { newField: String } @@ -874,28 +881,66 @@ describe('extendSchema', () => { } `); expect(printTestSchemaChanges(extendedSchema)).to.equal(dedent` - type Bar implements SomeInterface { + interface AnotherInterface implements SomeInterface { name: String + some: AnotherInterface + newField: String + } + + type Bar implements SomeInterface { some: SomeInterface foo: Foo newField: String } - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! newField: String } interface SomeInterface { - name: String some: SomeInterface newField: String } `); }); + it('extends interfaces by adding new implemted interfaces', () => { + const extendedSchema = extendTestSchema(` + interface NewInterface { + newField: String + } + + extend interface AnotherInterface implements NewInterface { + newField: String + } + + extend type Foo implements NewInterface { + newField: String + } + `); + expect(printTestSchemaChanges(extendedSchema)).to.equal(dedent` + interface AnotherInterface implements SomeInterface & NewInterface { + name: String + some: AnotherInterface + newField: String + } + + type Foo implements AnotherInterface & SomeInterface & NewInterface { + name: String + some: AnotherInterface + tree: [Foo]! + newField: String + } + + interface NewInterface { + newField: String + } + `); + }); + it('allows extension of interface with missing Object fields', () => { const extendedSchema = extendTestSchema(` extend interface SomeInterface { @@ -908,7 +953,6 @@ describe('extendSchema', () => { expect(printTestSchemaChanges(extendedSchema)).to.equal(dedent` interface SomeInterface { - name: String some: SomeInterface newField: String } @@ -927,7 +971,6 @@ describe('extendSchema', () => { `); expect(printTestSchemaChanges(extendedSchema)).to.equal(dedent` interface SomeInterface { - name: String some: SomeInterface newFieldA: Int newFieldB(test: Boolean): String diff --git a/src/utilities/__tests__/findBreakingChanges-test.js b/src/utilities/__tests__/findBreakingChanges-test.js index 420ee42b68..b3fd7dae6e 100644 --- a/src/utilities/__tests__/findBreakingChanges-test.js +++ b/src/utilities/__tests__/findBreakingChanges-test.js @@ -592,12 +592,33 @@ describe('findBreakingChanges', () => { expect(findBreakingChanges(oldSchema, newSchema)).to.deep.equal([ { - type: BreakingChangeType.INTERFACE_REMOVED_FROM_OBJECT, + type: BreakingChangeType.IMPLEMENTED_INTERFACE_REMOVED, description: 'Type1 no longer implements interface Interface1.', }, ]); }); + it('should detect interfaces removed from interfaces', () => { + const oldSchema = buildSchema(` + interface Interface1 + + interface Interface2 implements Interface1 + `); + + const newSchema = buildSchema(` + interface Interface1 + + interface Interface2 + `); + + expect(findBreakingChanges(oldSchema, newSchema)).to.deep.equal([ + { + type: BreakingChangeType.IMPLEMENTED_INTERFACE_REMOVED, + description: 'Interface2 no longer implements interface Interface1.', + }, + ]); + }); + it('should ignore changes in order of interfaces', () => { const oldSchema = buildSchema(` interface FirstInterface @@ -704,7 +725,7 @@ describe('findBreakingChanges', () => { 'VALUE0 was removed from enum type EnumTypeThatLosesAValue.', }, { - type: BreakingChangeType.INTERFACE_REMOVED_FROM_OBJECT, + type: BreakingChangeType.IMPLEMENTED_INTERFACE_REMOVED, description: 'TypeThatLooseInterface1 no longer implements interface Interface1.', }, @@ -1021,12 +1042,36 @@ describe('findDangerousChanges', () => { expect(findDangerousChanges(oldSchema, newSchema)).to.deep.equal([ { - type: DangerousChangeType.INTERFACE_ADDED_TO_OBJECT, + type: DangerousChangeType.IMPLEMENTED_INTERFACE_ADDED, description: 'NewInterface added to interfaces implemented by Type1.', }, ]); }); + it('should detect interfaces added to interfaces', () => { + const oldSchema = buildSchema(` + interface OldInterface + interface NewInterface + + interface Interface1 implements OldInterface + `); + + const newSchema = buildSchema(` + interface OldInterface + interface NewInterface + + interface Interface1 implements OldInterface & NewInterface + `); + + expect(findDangerousChanges(oldSchema, newSchema)).to.deep.equal([ + { + type: DangerousChangeType.IMPLEMENTED_INTERFACE_ADDED, + description: + 'NewInterface added to interfaces implemented by Interface1.', + }, + ]); + }); + it('should detect if a type was added to a union type', () => { const oldSchema = buildSchema(` type Type1 @@ -1121,7 +1166,7 @@ describe('findDangerousChanges', () => { 'Type1.field1 arg argThatChangesDefaultValue has changed defaultValue from "test" to "Test".', }, { - type: DangerousChangeType.INTERFACE_ADDED_TO_OBJECT, + type: DangerousChangeType.IMPLEMENTED_INTERFACE_ADDED, description: 'Interface1 added to interfaces implemented by TypeThatGainsInterface1.', }, diff --git a/src/utilities/__tests__/lexicographicSortSchema-test.js b/src/utilities/__tests__/lexicographicSortSchema-test.js index 01839ce88b..705428a5c5 100644 --- a/src/utilities/__tests__/lexicographicSortSchema-test.js +++ b/src/utilities/__tests__/lexicographicSortSchema-test.js @@ -75,7 +75,7 @@ describe('lexicographicSortSchema', () => { dummy: String } - interface FooC { + interface FooC implements FooB & FooA { dummy: String } @@ -93,7 +93,7 @@ describe('lexicographicSortSchema', () => { 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 5f82f097db..decb2ebdc6 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -340,6 +340,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', @@ -723,7 +778,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 @@ -929,7 +984,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/__tests__/typeComparators-test.js b/src/utilities/__tests__/typeComparators-test.js index 12b19f58e2..f2123c576b 100644 --- a/src/utilities/__tests__/typeComparators-test.js +++ b/src/utilities/__tests__/typeComparators-test.js @@ -110,7 +110,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: { @@ -127,5 +127,30 @@ 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: 'Interface', + fields: { + field: { type: GraphQLString }, + }, + }); + const iface2 = new GraphQLInterfaceType({ + name: 'Interface2', + interfaces: [iface], + fields: { + field: { type: GraphQLString }, + }, + }); + const impl = new GraphQLObjectType({ + name: 'Object', + interfaces: [iface2, iface], + fields: { + field: { type: GraphQLString }, + }, + }); + const schema = testSchema({ field: { type: impl } }); + expect(isTypeSubTypeOf(schema, iface2, iface)).to.equal(true); + }); }); }); diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 6b7951144e..523b6d9096 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -346,8 +346,17 @@ export class ASTDefinitionBuilder { } _makeInterfaceDef(astNode: InterfaceTypeDefinitionNode) { + const interfaceNodes = astNode.interfaces; const fieldNodes = astNode.fields; + // Note: While this could make assertions to get the correctly typed + // values below, that would throw immediately while type system + // validation with validateSchema() will produce more actionable results. + const interfaces = + interfaceNodes && interfaceNodes.length > 0 + ? () => interfaceNodes.map(ref => (this.getNamedType(ref): any)) + : []; + const fields = fieldNodes && fieldNodes.length > 0 ? () => keyByNameNode(fieldNodes, field => this.buildField(field)) @@ -356,6 +365,7 @@ export class ASTDefinitionBuilder { return new GraphQLInterfaceType({ name: astNode.name.value, description: getDescription(astNode, this._options), + interfaces, fields, astNode, }); diff --git a/src/utilities/buildClientSchema.js b/src/utilities/buildClientSchema.js index 0ccccbb59d..bedb9118d0 100644 --- a/src/utilities/buildClientSchema.js +++ b/src/utilities/buildClientSchema.js @@ -232,19 +232,37 @@ export function buildClientSchema( }); } - function buildObjectDef( - objectIntrospection: IntrospectionObjectType, - ): GraphQLObjectType { - if (!objectIntrospection.interfaces) { + function buildImplementationsList( + implementingIntrospection: + | IntrospectionObjectType + | IntrospectionInterfaceType, + ) { + // TODO: Temprorary workaround until GraphQL ecosystem will fully support + // 'interfaces' on interface types. + if ( + implementingIntrospection.interfaces === null && + implementingIntrospection.kind === TypeKind.INTERFACE + ) { + return []; + } + + if (!implementingIntrospection.interfaces) { throw new Error( 'Introspection result missing interfaces: ' + - inspect(objectIntrospection), + inspect(implementingIntrospection), ); } + + return implementingIntrospection.interfaces.map(getInterfaceType); + } + + function buildObjectDef( + objectIntrospection: IntrospectionObjectType, + ): GraphQLObjectType { return new GraphQLObjectType({ name: objectIntrospection.name, description: objectIntrospection.description, - interfaces: () => objectIntrospection.interfaces.map(getInterfaceType), + interfaces: () => buildImplementationsList(objectIntrospection), fields: () => buildFieldDefMap(objectIntrospection), }); } @@ -255,6 +273,7 @@ export function buildClientSchema( return new GraphQLInterfaceType({ name: interfaceIntrospection.name, description: interfaceIntrospection.description, + interfaces: () => buildImplementationsList(interfaceIntrospection), fields: () => buildFieldDefMap(interfaceIntrospection), }); } diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index e8da8c4e5f..0c41cb4764 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -365,10 +365,18 @@ export function extendSchema( ): GraphQLInterfaceType { const config = type.toConfig(); const extensions = typeExtsMap[config.name] || []; + const interfaceNodes = flatMap(extensions, node => node.interfaces || []); const fieldNodes = flatMap(extensions, node => node.fields || []); return new GraphQLInterfaceType({ ...config, + interfaces: () => [ + ...type.getInterfaces().map(replaceNamedType), + // Note: While this could make early assertions to get the correctly + // typed values, that would throw immediately while type system + // validation with validateSchema() will produce more actionable results. + ...interfaceNodes.map(node => (astBuilder.getNamedType(node): any)), + ], fields: () => ({ ...mapValue(config.fields, extendField), ...keyValMap( diff --git a/src/utilities/findBreakingChanges.js b/src/utilities/findBreakingChanges.js index 98863f8402..b43c740075 100644 --- a/src/utilities/findBreakingChanges.js +++ b/src/utilities/findBreakingChanges.js @@ -42,7 +42,7 @@ export const BreakingChangeType = Object.freeze({ TYPE_REMOVED_FROM_UNION: 'TYPE_REMOVED_FROM_UNION', VALUE_REMOVED_FROM_ENUM: 'VALUE_REMOVED_FROM_ENUM', REQUIRED_INPUT_FIELD_ADDED: 'REQUIRED_INPUT_FIELD_ADDED', - INTERFACE_REMOVED_FROM_OBJECT: 'INTERFACE_REMOVED_FROM_OBJECT', + IMPLEMENTED_INTERFACE_REMOVED: 'IMPLEMENTED_INTERFACE_REMOVED', FIELD_REMOVED: 'FIELD_REMOVED', FIELD_CHANGED_KIND: 'FIELD_CHANGED_KIND', REQUIRED_ARG_ADDED: 'REQUIRED_ARG_ADDED', @@ -59,7 +59,7 @@ export const DangerousChangeType = Object.freeze({ TYPE_ADDED_TO_UNION: 'TYPE_ADDED_TO_UNION', OPTIONAL_INPUT_FIELD_ADDED: 'OPTIONAL_INPUT_FIELD_ADDED', OPTIONAL_ARG_ADDED: 'OPTIONAL_ARG_ADDED', - INTERFACE_ADDED_TO_OBJECT: 'INTERFACE_ADDED_TO_OBJECT', + IMPLEMENTED_INTERFACE_ADDED: 'IMPLEMENTED_INTERFACE_ADDED', ARG_DEFAULT_VALUE_CHANGE: 'ARG_DEFAULT_VALUE_CHANGE', }); @@ -191,9 +191,15 @@ function findTypeChanges( } else if (isInputObjectType(oldType) && isInputObjectType(newType)) { schemaChanges.push(...findInputObjectTypeChanges(oldType, newType)); } else if (isObjectType(oldType) && isObjectType(newType)) { - schemaChanges.push(...findObjectTypeChanges(oldType, newType)); + schemaChanges.push( + ...findFieldChanges(oldType, newType), + ...findImplementedInterfacesChanges(oldType, newType), + ); } else if (isInterfaceType(oldType) && isInterfaceType(newType)) { - schemaChanges.push(...findFieldChanges(oldType, newType)); + schemaChanges.push( + ...findFieldChanges(oldType, newType), + ...findImplementedInterfacesChanges(oldType, newType), + ); } else if (oldType.constructor !== newType.constructor) { schemaChanges.push({ type: BreakingChangeType.TYPE_CHANGED_KIND, @@ -304,23 +310,23 @@ function findEnumTypeChanges( return schemaChanges; } -function findObjectTypeChanges( - oldType: GraphQLObjectType, - newType: GraphQLObjectType, +function findImplementedInterfacesChanges( + oldType: GraphQLObjectType | GraphQLInterfaceType, + newType: GraphQLObjectType | GraphQLInterfaceType, ): Array { - const schemaChanges = findFieldChanges(oldType, newType); + const schemaChanges = []; const interfacesDiff = diff(oldType.getInterfaces(), newType.getInterfaces()); for (const newInterface of interfacesDiff.added) { schemaChanges.push({ - type: DangerousChangeType.INTERFACE_ADDED_TO_OBJECT, + type: DangerousChangeType.IMPLEMENTED_INTERFACE_ADDED, description: `${newInterface.name} added to interfaces implemented by ${oldType.name}.`, }); } for (const oldInterface of interfacesDiff.removed) { schemaChanges.push({ - type: BreakingChangeType.INTERFACE_REMOVED_FROM_OBJECT, + type: BreakingChangeType.IMPLEMENTED_INTERFACE_REMOVED, description: `${oldType.name} no longer implements interface ${oldInterface.name}.`, }); } diff --git a/src/utilities/getIntrospectionQuery.js b/src/utilities/getIntrospectionQuery.js index 5044afa743..54b0ad7a9b 100644 --- a/src/utilities/getIntrospectionQuery.js +++ b/src/utilities/getIntrospectionQuery.js @@ -160,6 +160,9 @@ export type IntrospectionInterfaceType = { +name: string, +description?: ?string, +fields: $ReadOnlyArray, + +interfaces: $ReadOnlyArray< + IntrospectionNamedTypeRef, + >, +possibleTypes: $ReadOnlyArray< IntrospectionNamedTypeRef, >, diff --git a/src/utilities/lexicographicSortSchema.js b/src/utilities/lexicographicSortSchema.js index 16723a86ca..c6a18cb52c 100644 --- a/src/utilities/lexicographicSortSchema.js +++ b/src/utilities/lexicographicSortSchema.js @@ -115,6 +115,7 @@ export function lexicographicSortSchema(schema: GraphQLSchema): GraphQLSchema { const config = type.toConfig(); return new GraphQLInterfaceType({ ...config, + interfaces: () => sortTypes(config.interfaces), fields: () => sortFields(config.fields), }); } else if (isUnionType(type)) { diff --git a/src/utilities/schemaPrinter.js b/src/utilities/schemaPrinter.js index 8b9e863ef7..b59d218d04 100644 --- a/src/utilities/schemaPrinter.js +++ b/src/utilities/schemaPrinter.js @@ -181,14 +181,20 @@ function printScalar(type: GraphQLScalarType, options): string { return printDescription(options, type) + `scalar ${type.name}`; } -function printObject(type: GraphQLObjectType, options): string { +function printImplementedInterfaces( + type: GraphQLObjectType | GraphQLInterfaceType, +): string { const interfaces = type.getInterfaces(); - const implementedInterfaces = interfaces.length + return interfaces.length ? ' implements ' + interfaces.map(i => i.name).join(' & ') : ''; +} + +function printObject(type: GraphQLObjectType, options): string { return ( printDescription(options, type) + - `type ${type.name}${implementedInterfaces}` + + `type ${type.name}` + + printImplementedInterfaces(type) + printFields(options, type) ); } @@ -197,6 +203,7 @@ function printInterface(type: GraphQLInterfaceType, options): string { return ( printDescription(options, type) + `interface ${type.name}` + + printImplementedInterfaces(type) + printFields(options, type) ); } diff --git a/src/utilities/typeComparators.js b/src/utilities/typeComparators.js index 9b70895bf0..18e4447812 100644 --- a/src/utilities/typeComparators.js +++ b/src/utilities/typeComparators.js @@ -4,6 +4,7 @@ import { type GraphQLSchema } from '../type/schema'; import { type GraphQLType, type GraphQLCompositeType, + isInterfaceType, isObjectType, isListType, isNonNullType, @@ -71,18 +72,13 @@ export function isTypeSubTypeOf( return false; } - // If superType type is an abstract type, maybeSubType type may be a currently - // possible object type. - if ( - isAbstractType(superType) && - isObjectType(maybeSubType) && - schema.isPossibleType(superType, maybeSubType) - ) { - return true; - } - + // If superType type is an abstract type, check if it is super type of maybeSubType. // Otherwise, the child type is not a valid subtype of the parent type. - return false; + return ( + isAbstractType(superType) && + (isInterfaceType(maybeSubType) || isObjectType(maybeSubType)) && + schema.isSubType(superType, maybeSubType) + ); } /** @@ -110,15 +106,15 @@ export function doTypesOverlap( // between possible concrete types of each. return schema .getPossibleTypes(typeA) - .some(type => schema.isPossibleType(typeB, type)); + .some(type => schema.isSubType(typeB, type)); } // Determine if the latter type is a possible concrete type of the former. - return schema.isPossibleType(typeA, typeB); + return schema.isSubType(typeA, typeB); } if (isAbstractType(typeB)) { // Determine if the former type is a possible concrete type of the latter. - return schema.isPossibleType(typeB, typeA); + return schema.isSubType(typeB, typeA); } // Otherwise the types do not overlap. diff --git a/src/validation/__tests__/FragmentsOnCompositeTypes-test.js b/src/validation/__tests__/FragmentsOnCompositeTypes-test.js index 1438be623b..46bb2daaac 100644 --- a/src/validation/__tests__/FragmentsOnCompositeTypes-test.js +++ b/src/validation/__tests__/FragmentsOnCompositeTypes-test.js @@ -41,6 +41,16 @@ describe('Validate: Fragments on composite types', () => { `); }); + it('interface is valid inline fragment type', () => { + expectValid(` + fragment validFragment on Mammal { + ... on Canine { + name + } + } + `); + }); + it('inline fragment without type is valid', () => { expectValid(` fragment validFragment on Pet { diff --git a/src/validation/__tests__/harness.js b/src/validation/__tests__/harness.js index c4f313504d..96561c1979 100644 --- a/src/validation/__tests__/harness.js +++ b/src/validation/__tests__/harness.js @@ -46,8 +46,22 @@ const Being = new GraphQLInterfaceType({ }), }); +const Mammal = new GraphQLInterfaceType({ + name: 'Mammal', + interfaces: [], + fields: () => ({ + mother: { + type: Mammal, + }, + father: { + type: Mammal, + }, + }), +}); + const Pet = new GraphQLInterfaceType({ name: 'Pet', + interfaces: [Being], fields: () => ({ name: { type: GraphQLString, @@ -58,11 +72,18 @@ const Pet = new GraphQLInterfaceType({ const Canine = new GraphQLInterfaceType({ name: 'Canine', + interfaces: [Mammal, Being], fields: () => ({ name: { type: GraphQLString, args: { surname: { type: GraphQLBoolean } }, }, + mother: { + type: Canine, + }, + father: { + type: Canine, + }, }), }); @@ -77,6 +98,7 @@ const DogCommand = new GraphQLEnumType({ const Dog = new GraphQLObjectType({ name: 'Dog', + interfaces: [Being, Pet, Mammal, Canine], fields: () => ({ name: { type: GraphQLString, @@ -104,8 +126,13 @@ const Dog = new GraphQLObjectType({ type: GraphQLBoolean, args: { x: { type: GraphQLInt }, y: { type: GraphQLInt } }, }, + mother: { + type: Dog, + }, + father: { + type: Dog, + }, }), - interfaces: [Being, Pet, Canine], }); const Cat = new GraphQLObjectType({ diff --git a/tstypes/language/ast.d.ts b/tstypes/language/ast.d.ts index d2072ab62e..2bf27aefbb 100644 --- a/tstypes/language/ast.d.ts +++ b/tstypes/language/ast.d.ts @@ -452,6 +452,7 @@ export interface InterfaceTypeDefinitionNode { readonly loc?: Location; readonly description?: StringValueNode; readonly name: NameNode; + readonly interfaces?: ReadonlyArray; readonly directives?: ReadonlyArray; readonly fields?: ReadonlyArray; } @@ -544,6 +545,7 @@ export interface InterfaceTypeExtensionNode { readonly kind: 'InterfaceTypeExtension'; readonly loc?: Location; readonly name: NameNode; + readonly interfaces?: ReadonlyArray; readonly directives?: ReadonlyArray; readonly fields?: ReadonlyArray; } diff --git a/tstypes/language/visitor.d.ts b/tstypes/language/visitor.d.ts index a71c01bba9..e9d1ea8071 100644 --- a/tstypes/language/visitor.d.ts +++ b/tstypes/language/visitor.d.ts @@ -122,7 +122,14 @@ export const QueryDocumentKeys: { 'defaultValue', 'directives' ]; - InterfaceTypeDefinition: ['description', 'name', 'directives', 'fields']; + // prettier-ignore + InterfaceTypeDefinition: [ + 'description', + 'name', + 'interfaces', + 'directives', + 'fields' + ]; UnionTypeDefinition: ['description', 'name', 'directives', 'types']; EnumTypeDefinition: ['description', 'name', 'directives', 'values']; EnumValueDefinition: ['description', 'name', 'directives']; @@ -134,7 +141,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/tstypes/type/definition.d.ts b/tstypes/type/definition.d.ts index 8ab6bc0983..01939fec1d 100644 --- a/tstypes/type/definition.d.ts +++ b/tstypes/type/definition.d.ts @@ -562,8 +562,10 @@ export class GraphQLInterfaceType { constructor(config: GraphQLInterfaceTypeConfig); getFields(): GraphQLFieldMap; + getInterfaces(): GraphQLInterfaceType[]; toConfig(): GraphQLInterfaceTypeConfig & { + interfaces: GraphQLInterfaceType[]; fields: GraphQLFieldConfigMap; extensions: Maybe>>; extensionASTNodes: ReadonlyArray; @@ -582,6 +584,7 @@ export interface GraphQLInterfaceTypeConfig< > { name: string; description?: Maybe; + interfaces?: Thunk>; fields: Thunk>; /** * Optionally provide a custom type resolver function. If one is not provided, diff --git a/tstypes/type/schema.d.ts b/tstypes/type/schema.d.ts index fce077a085..83d26e6230 100644 --- a/tstypes/type/schema.d.ts +++ b/tstypes/type/schema.d.ts @@ -6,6 +6,7 @@ import { GraphQLNamedType, GraphQLAbstractType, GraphQLObjectType, + GraphQLInterfaceType, } from './definition'; /** @@ -51,15 +52,26 @@ export class GraphQLSchema { getSubscriptionType(): Maybe; getTypeMap(): TypeMap; getType(name: string): Maybe; + getPossibleTypes( abstractType: GraphQLAbstractType, ): ReadonlyArray; + getImplementations( + interfaceType: GraphQLInterfaceType, + ): InterfaceImplementations; + + // @deprecated: use isSubType instead - will be removed in v16. isPossibleType( abstractType: GraphQLAbstractType, possibleType: GraphQLObjectType, ): boolean; + isSubType( + abstractType: GraphQLAbstractType, + maybeSubType: GraphQLNamedType, + ): boolean; + getDirectives(): ReadonlyArray; getDirective(name: string): Maybe; @@ -74,6 +86,11 @@ export class GraphQLSchema { type TypeMap = { [key: string]: GraphQLNamedType }; +type InterfaceImplementations = { + objects: ReadonlyArray; + interfaces: ReadonlyArray; +}; + export interface GraphQLSchemaValidationOptions { /** * When building a schema from a GraphQL service's introspection result, it diff --git a/tstypes/utilities/getIntrospectionQuery.d.ts b/tstypes/utilities/getIntrospectionQuery.d.ts index 2e5a601c9c..527d946d5d 100644 --- a/tstypes/utilities/getIntrospectionQuery.d.ts +++ b/tstypes/utilities/getIntrospectionQuery.d.ts @@ -66,6 +66,9 @@ export interface IntrospectionInterfaceType { readonly name: string; readonly description?: Maybe; readonly fields: ReadonlyArray; + readonly interfaces: ReadonlyArray< + IntrospectionNamedTypeRef + >; readonly possibleTypes: ReadonlyArray< IntrospectionNamedTypeRef >;