Skip to content

Commit 2986c2d

Browse files
lillyfwangleebyron
authored andcommitted
Allow interface type extensions (#1222)
* Allow interface type extensions * Do not extend object types that implement the interface * Added more tests in schema validation after interface extensions * Added more test cases to extendSchema, removed some tests from validate
1 parent 974ebab commit 2986c2d

File tree

3 files changed

+398
-9
lines changed

3 files changed

+398
-9
lines changed

src/type/__tests__/validation-test.js

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,117 @@ describe('Type System: Objects can only implement unique interfaces', () => {
896896
});
897897
});
898898

899+
describe('Type System: Interface extensions should be valid', () => {
900+
it('rejects an Object implementing the extended interface due to missing field', () => {
901+
const schema = buildSchema(`
902+
type Query {
903+
test: AnotherObject
904+
}
905+
906+
interface AnotherInterface {
907+
field: String
908+
}
909+
910+
type AnotherObject implements AnotherInterface {
911+
field: String
912+
}
913+
`);
914+
const extendedSchema = extendSchema(
915+
schema,
916+
parse(`
917+
extend interface AnotherInterface {
918+
newField: String
919+
}
920+
`),
921+
);
922+
expect(validateSchema(extendedSchema)).to.containSubset([
923+
{
924+
message:
925+
'Interface field AnotherInterface.newField expected but AnotherObject does not provide it.',
926+
locations: [{ line: 3, column: 11 }, { line: 10, column: 7 }],
927+
},
928+
]);
929+
});
930+
931+
it('rejects an Object implementing the extended interface due to missing field args', () => {
932+
const schema = buildSchema(`
933+
type Query {
934+
test: AnotherObject
935+
}
936+
937+
interface AnotherInterface {
938+
field: String
939+
}
940+
941+
type AnotherObject implements AnotherInterface {
942+
field: String
943+
}
944+
`);
945+
const extendedSchema = extendSchema(
946+
schema,
947+
parse(`
948+
extend interface AnotherInterface {
949+
newField(test: Boolean): String
950+
}
951+
952+
extend type AnotherObject {
953+
newField: String
954+
}
955+
`),
956+
);
957+
expect(validateSchema(extendedSchema)).to.containSubset([
958+
{
959+
message:
960+
'Interface field argument AnotherInterface.newField(test:) expected but AnotherObject.newField does not provide it.',
961+
locations: [{ line: 3, column: 20 }, { line: 7, column: 11 }],
962+
},
963+
]);
964+
});
965+
966+
it('rejects Objects implementing the extended interface due to mismatching interface type', () => {
967+
const schema = buildSchema(`
968+
type Query {
969+
test: AnotherObject
970+
}
971+
972+
interface AnotherInterface {
973+
field: String
974+
}
975+
976+
type AnotherObject implements AnotherInterface {
977+
field: String
978+
}
979+
`);
980+
const extendedSchema = extendSchema(
981+
schema,
982+
parse(`
983+
extend interface AnotherInterface {
984+
newInterfaceField: NewInterface
985+
}
986+
987+
interface NewInterface {
988+
newField: String
989+
}
990+
991+
interface MismatchingInterface {
992+
newField: String
993+
}
994+
995+
extend type AnotherObject {
996+
newInterfaceField: MismatchingInterface
997+
}
998+
`),
999+
);
1000+
expect(validateSchema(extendedSchema)).to.containSubset([
1001+
{
1002+
message:
1003+
'Interface field AnotherInterface.newInterfaceField expects type NewInterface but AnotherObject.newInterfaceField is type MismatchingInterface.',
1004+
locations: [{ line: 3, column: 30 }, { line: 15, column: 30 }],
1005+
},
1006+
]);
1007+
});
1008+
});
1009+
8991010
describe('Type System: Interface fields must have output types', () => {
9001011
function schemaWithInterfaceFieldOfType(fieldType) {
9011012
const BadInterfaceType = new GraphQLInterfaceType({

src/utilities/__tests__/extendSchema-test.js

Lines changed: 238 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,220 @@ describe('extendSchema', () => {
801801
`);
802802
});
803803

804+
it('extends interfaces by adding new fields', () => {
805+
const ast = parse(`
806+
extend interface SomeInterface {
807+
newField: String
808+
}
809+
810+
extend type Bar {
811+
newField: String
812+
}
813+
814+
extend type Foo {
815+
newField: String
816+
}
817+
`);
818+
const originalPrint = printSchema(testSchema);
819+
const extendedSchema = extendSchema(testSchema, ast);
820+
expect(extendedSchema).to.not.equal(testSchema);
821+
expect(printSchema(testSchema)).to.equal(originalPrint);
822+
expect(printSchema(extendedSchema)).to.equal(dedent`
823+
type Bar implements SomeInterface {
824+
name: String
825+
some: SomeInterface
826+
foo: Foo
827+
newField: String
828+
}
829+
830+
type Biz {
831+
fizz: String
832+
}
833+
834+
type Foo implements SomeInterface {
835+
name: String
836+
some: SomeInterface
837+
tree: [Foo]!
838+
newField: String
839+
}
840+
841+
type Query {
842+
foo: Foo
843+
someUnion: SomeUnion
844+
someEnum: SomeEnum
845+
someInterface(id: ID!): SomeInterface
846+
}
847+
848+
enum SomeEnum {
849+
ONE
850+
TWO
851+
}
852+
853+
interface SomeInterface {
854+
name: String
855+
some: SomeInterface
856+
newField: String
857+
}
858+
859+
union SomeUnion = Foo | Biz
860+
`);
861+
});
862+
863+
it('allows extension of interface with missing Object fields', () => {
864+
const ast = parse(`
865+
extend interface SomeInterface {
866+
newObject: NewObject
867+
newInterface: NewInterface
868+
newUnion: NewUnion
869+
newScalar: NewScalar
870+
newTree: [Foo]!
871+
newField(arg1: String, arg2: NewEnum!): String
872+
}
873+
874+
type NewObject implements NewInterface {
875+
baz: String
876+
}
877+
878+
type NewOtherObject {
879+
fizz: Int
880+
}
881+
882+
interface NewInterface {
883+
baz: String
884+
}
885+
886+
union NewUnion = NewObject | NewOtherObject
887+
888+
scalar NewScalar
889+
890+
enum NewEnum {
891+
OPTION_A
892+
OPTION_B
893+
}
894+
`);
895+
const originalPrint = printSchema(testSchema);
896+
const extendedSchema = extendSchema(testSchema, ast);
897+
expect(extendedSchema).to.not.equal(testSchema);
898+
expect(printSchema(testSchema)).to.equal(originalPrint);
899+
expect(printSchema(extendedSchema)).to.equal(dedent`
900+
type Bar implements SomeInterface {
901+
name: String
902+
some: SomeInterface
903+
foo: Foo
904+
}
905+
906+
type Biz {
907+
fizz: String
908+
}
909+
910+
type Foo implements SomeInterface {
911+
name: String
912+
some: SomeInterface
913+
tree: [Foo]!
914+
}
915+
916+
enum NewEnum {
917+
OPTION_A
918+
OPTION_B
919+
}
920+
921+
interface NewInterface {
922+
baz: String
923+
}
924+
925+
type NewObject implements NewInterface {
926+
baz: String
927+
}
928+
929+
type NewOtherObject {
930+
fizz: Int
931+
}
932+
933+
scalar NewScalar
934+
935+
union NewUnion = NewObject | NewOtherObject
936+
937+
type Query {
938+
foo: Foo
939+
someUnion: SomeUnion
940+
someEnum: SomeEnum
941+
someInterface(id: ID!): SomeInterface
942+
}
943+
944+
enum SomeEnum {
945+
ONE
946+
TWO
947+
}
948+
949+
interface SomeInterface {
950+
name: String
951+
some: SomeInterface
952+
newObject: NewObject
953+
newInterface: NewInterface
954+
newUnion: NewUnion
955+
newScalar: NewScalar
956+
newTree: [Foo]!
957+
newField(arg1: String, arg2: NewEnum!): String
958+
}
959+
960+
union SomeUnion = Foo | Biz
961+
`);
962+
});
963+
964+
it('extends interfaces multiple times', () => {
965+
const ast = parse(`
966+
extend interface SomeInterface {
967+
newFieldA: Int
968+
}
969+
970+
extend interface SomeInterface {
971+
newFieldB(test: Boolean): String
972+
}
973+
`);
974+
const originalPrint = printSchema(testSchema);
975+
const extendedSchema = extendSchema(testSchema, ast);
976+
expect(extendedSchema).to.not.equal(testSchema);
977+
expect(printSchema(testSchema)).to.equal(originalPrint);
978+
expect(printSchema(extendedSchema)).to.equal(dedent`
979+
type Bar implements SomeInterface {
980+
name: String
981+
some: SomeInterface
982+
foo: Foo
983+
}
984+
985+
type Biz {
986+
fizz: String
987+
}
988+
989+
type Foo implements SomeInterface {
990+
name: String
991+
some: SomeInterface
992+
tree: [Foo]!
993+
}
994+
995+
type Query {
996+
foo: Foo
997+
someUnion: SomeUnion
998+
someEnum: SomeEnum
999+
someInterface(id: ID!): SomeInterface
1000+
}
1001+
1002+
enum SomeEnum {
1003+
ONE
1004+
TWO
1005+
}
1006+
1007+
interface SomeInterface {
1008+
name: String
1009+
some: SomeInterface
1010+
newFieldA: Int
1011+
newFieldB(test: Boolean): String
1012+
}
1013+
1014+
union SomeUnion = Foo | Biz
1015+
`);
1016+
});
1017+
8041018
it('may extend mutations and subscriptions', () => {
8051019
const mutationSchema = new GraphQLSchema({
8061020
query: new GraphQLObjectType({
@@ -994,6 +1208,18 @@ describe('extendSchema', () => {
9941208
);
9951209
});
9961210

1211+
it('does not allow extending an unknown interface type', () => {
1212+
const ast = parse(`
1213+
extend interface UnknownInterfaceType {
1214+
baz: String
1215+
}
1216+
`);
1217+
expect(() => extendSchema(testSchema, ast)).to.throw(
1218+
'Cannot extend interface "UnknownInterfaceType" because it does not ' +
1219+
'exist in the existing schema.',
1220+
);
1221+
});
1222+
9971223
it('maintains configuration of the original schema object', () => {
9981224
const testSchemaWithLegacyNames = new GraphQLSchema({
9991225
query: new GraphQLObjectType({
@@ -1014,7 +1240,7 @@ describe('extendSchema', () => {
10141240
});
10151241

10161242
describe('does not allow extending a non-object type', () => {
1017-
it('not an interface', () => {
1243+
it('not an object', () => {
10181244
const ast = parse(`
10191245
extend type SomeInterface {
10201246
baz: String
@@ -1025,6 +1251,17 @@ describe('extendSchema', () => {
10251251
);
10261252
});
10271253

1254+
it('not an interface', () => {
1255+
const ast = parse(`
1256+
extend interface Foo {
1257+
baz: String
1258+
}
1259+
`);
1260+
expect(() => extendSchema(testSchema, ast)).to.throw(
1261+
'Cannot extend non-interface type "Foo".',
1262+
);
1263+
});
1264+
10281265
it('not a scalar', () => {
10291266
const ast = parse(`
10301267
extend type String {

0 commit comments

Comments
 (0)