Skip to content

Commit fcb08f8

Browse files
committed
allow unions to declare implementation of interfaces
WIP: more tests required complete code coverage is already there, but goal is to have a test where union implements an interface wherever there is a test for an interface implementing interface
1 parent 5f247e0 commit fcb08f8

14 files changed

+273
-16
lines changed

src/execution/__tests__/union-interface-test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ describe('Execute: Union and intersection types', () => {
212212
kind: 'UNION',
213213
name: 'Pet',
214214
fields: null,
215-
interfaces: null,
215+
interfaces: [],
216216
possibleTypes: [{ name: 'Dog' }, { name: 'Cat' }],
217217
enumValues: null,
218218
inputFields: null,

src/language/__tests__/schema-parser-test.ts

+117
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,24 @@ describe('Schema Parser', () => {
230230
});
231231
});
232232

233+
it('Union extension without types', () => {
234+
const doc = parse('extend union HelloOrGoodbye implements Greeting');
235+
expectJSON(doc).toDeepEqual({
236+
kind: 'Document',
237+
definitions: [
238+
{
239+
kind: 'UnionTypeExtension',
240+
name: nameNode('HelloOrGoodbye', { start: 13, end: 27 }),
241+
interfaces: [typeNode('Greeting', { start: 39, end: 47 })],
242+
directives: [],
243+
types: [],
244+
loc: { start: 0, end: 47 },
245+
},
246+
],
247+
loc: { start: 0, end: 47 },
248+
});
249+
});
250+
233251
it('Object extension without fields followed by extension', () => {
234252
const doc = parse(`
235253
extend type Hello implements Greeting
@@ -323,6 +341,36 @@ describe('Schema Parser', () => {
323341
});
324342
});
325343

344+
it('Union extension without types followed by extension', () => {
345+
const doc = parse(`
346+
extend union HelloOrGoodbye implements Greeting
347+
348+
extend union HelloOrGoodbye implements SecondGreeting
349+
`);
350+
expectJSON(doc).toDeepEqual({
351+
kind: 'Document',
352+
definitions: [
353+
{
354+
kind: 'UnionTypeExtension',
355+
name: nameNode('HelloOrGoodbye', { start: 20, end: 34 }),
356+
interfaces: [typeNode('Greeting', { start: 46, end: 54 })],
357+
directives: [],
358+
types: [],
359+
loc: { start: 7, end: 54 },
360+
},
361+
{
362+
kind: 'UnionTypeExtension',
363+
name: nameNode('HelloOrGoodbye', { start: 75, end: 89 }),
364+
interfaces: [typeNode('SecondGreeting', { start: 101, end: 115 })],
365+
directives: [],
366+
types: [],
367+
loc: { start: 62, end: 115 },
368+
},
369+
],
370+
loc: { start: 0, end: 120 },
371+
});
372+
});
373+
326374
it('Object extension do not include descriptions', () => {
327375
expectSyntaxError(`
328376
"Description"
@@ -517,6 +565,26 @@ describe('Schema Parser', () => {
517565
});
518566
});
519567

568+
it('Simple union inheriting interface', () => {
569+
const doc = parse('union Hello implements World = Subtype');
570+
571+
expectJSON(doc).toDeepEqual({
572+
kind: 'Document',
573+
definitions: [
574+
{
575+
kind: 'UnionTypeDefinition',
576+
name: nameNode('Hello', { start: 6, end: 11 }),
577+
description: undefined,
578+
interfaces: [typeNode('World', { start: 23, end: 28 })],
579+
directives: [],
580+
types: [typeNode('Subtype', { start: 31, end: 38 })],
581+
loc: { start: 0, end: 38 },
582+
},
583+
],
584+
loc: { start: 0, end: 38 },
585+
});
586+
});
587+
520588
it('Simple type inheriting multiple interfaces', () => {
521589
const doc = parse('type Hello implements Wo & rld { field: String }');
522590

@@ -574,6 +642,29 @@ describe('Schema Parser', () => {
574642
});
575643
});
576644

645+
it('Simple union inheriting multiple interfaces', () => {
646+
const doc = parse('union Hello implements Wo & rld = Subtype');
647+
648+
expectJSON(doc).toDeepEqual({
649+
kind: 'Document',
650+
definitions: [
651+
{
652+
kind: 'UnionTypeDefinition',
653+
name: nameNode('Hello', { start: 6, end: 11 }),
654+
description: undefined,
655+
interfaces: [
656+
typeNode('Wo', { start: 23, end: 25 }),
657+
typeNode('rld', { start: 28, end: 31 }),
658+
],
659+
directives: [],
660+
types: [typeNode('Subtype', { start: 34, end: 41 })],
661+
loc: { start: 0, end: 41 },
662+
},
663+
],
664+
loc: { start: 0, end: 41 },
665+
});
666+
});
667+
577668
it('Simple type inheriting multiple interfaces with leading ampersand', () => {
578669
const doc = parse('type Hello implements & Wo & rld { field: String }');
579670

@@ -633,6 +724,29 @@ describe('Schema Parser', () => {
633724
});
634725
});
635726

727+
it('Simple union inheriting multiple interfaces with leading ampersand', () => {
728+
const doc = parse('union Hello implements & Wo & rld = Subtype');
729+
730+
expectJSON(doc).toDeepEqual({
731+
kind: 'Document',
732+
definitions: [
733+
{
734+
kind: 'UnionTypeDefinition',
735+
name: nameNode('Hello', { start: 6, end: 11 }),
736+
description: undefined,
737+
interfaces: [
738+
typeNode('Wo', { start: 25, end: 27 }),
739+
typeNode('rld', { start: 30, end: 33 }),
740+
],
741+
directives: [],
742+
types: [typeNode('Subtype', { start: 36, end: 43 })],
743+
loc: { start: 0, end: 43 },
744+
},
745+
],
746+
loc: { start: 0, end: 43 },
747+
});
748+
});
749+
636750
it('Single value enum', () => {
637751
const doc = parse('enum Hello { WORLD }');
638752

@@ -880,6 +994,7 @@ describe('Schema Parser', () => {
880994
kind: 'UnionTypeDefinition',
881995
name: nameNode('Hello', { start: 6, end: 11 }),
882996
description: undefined,
997+
interfaces: [],
883998
directives: [],
884999
types: [typeNode('World', { start: 14, end: 19 })],
8851000
loc: { start: 0, end: 19 },
@@ -899,6 +1014,7 @@ describe('Schema Parser', () => {
8991014
kind: 'UnionTypeDefinition',
9001015
name: nameNode('Hello', { start: 6, end: 11 }),
9011016
description: undefined,
1017+
interfaces: [],
9021018
directives: [],
9031019
types: [
9041020
typeNode('Wo', { start: 14, end: 16 }),
@@ -921,6 +1037,7 @@ describe('Schema Parser', () => {
9211037
kind: 'UnionTypeDefinition',
9221038
name: nameNode('Hello', { start: 6, end: 11 }),
9231039
description: undefined,
1040+
interfaces: [],
9241041
directives: [],
9251042
types: [
9261043
typeNode('Wo', { start: 16, end: 18 }),

src/language/ast.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,13 @@ export const QueryDocumentKeys: {
262262
'directives',
263263
'fields',
264264
],
265-
UnionTypeDefinition: ['description', 'name', 'directives', 'types'],
265+
UnionTypeDefinition: [
266+
'description',
267+
'name',
268+
'interfaces',
269+
'directives',
270+
'types',
271+
],
266272
EnumTypeDefinition: ['description', 'name', 'directives', 'values'],
267273
EnumValueDefinition: ['description', 'name', 'directives'],
268274
InputObjectTypeDefinition: ['description', 'name', 'directives', 'fields'],
@@ -274,7 +280,7 @@ export const QueryDocumentKeys: {
274280
ScalarTypeExtension: ['name', 'directives'],
275281
ObjectTypeExtension: ['name', 'interfaces', 'directives', 'fields'],
276282
InterfaceTypeExtension: ['name', 'interfaces', 'directives', 'fields'],
277-
UnionTypeExtension: ['name', 'directives', 'types'],
283+
UnionTypeExtension: ['name', 'interfaces', 'directives', 'types'],
278284
EnumTypeExtension: ['name', 'directives', 'values'],
279285
InputObjectTypeExtension: ['name', 'directives', 'fields'],
280286
};
@@ -624,6 +630,7 @@ export interface UnionTypeDefinitionNode {
624630
readonly loc?: Location;
625631
readonly description?: StringValueNode;
626632
readonly name: NameNode;
633+
readonly interfaces?: ReadonlyArray<NamedTypeNode>;
627634
readonly directives?: ReadonlyArray<ConstDirectiveNode>;
628635
readonly types?: ReadonlyArray<NamedTypeNode>;
629636
}
@@ -716,6 +723,7 @@ export interface UnionTypeExtensionNode {
716723
readonly kind: Kind.UNION_TYPE_EXTENSION;
717724
readonly loc?: Location;
718725
readonly name: NameNode;
726+
readonly interfaces?: ReadonlyArray<NamedTypeNode>;
719727
readonly directives?: ReadonlyArray<ConstDirectiveNode>;
720728
readonly types?: ReadonlyArray<NamedTypeNode>;
721729
}

src/language/parser.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -970,12 +970,14 @@ export class Parser {
970970
const description = this.parseDescription();
971971
this.expectKeyword('union');
972972
const name = this.parseName();
973+
const interfaces = this.parseImplementsInterfaces();
973974
const directives = this.parseConstDirectives();
974975
const types = this.parseUnionMemberTypes();
975976
return this.node<UnionTypeDefinitionNode>(start, {
976977
kind: Kind.UNION_TYPE_DEFINITION,
977978
description,
978979
name,
980+
interfaces,
979981
directives,
980982
types,
981983
});
@@ -1249,14 +1251,20 @@ export class Parser {
12491251
this.expectKeyword('extend');
12501252
this.expectKeyword('union');
12511253
const name = this.parseName();
1254+
const interfaces = this.parseImplementsInterfaces();
12521255
const directives = this.parseConstDirectives();
12531256
const types = this.parseUnionMemberTypes();
1254-
if (directives.length === 0 && types.length === 0) {
1257+
if (
1258+
interfaces.length === 0 &&
1259+
directives.length === 0 &&
1260+
types.length === 0
1261+
) {
12551262
throw this.unexpected();
12561263
}
12571264
return this.node<UnionTypeExtensionNode>(start, {
12581265
kind: Kind.UNION_TYPE_EXTENSION,
12591266
name,
1267+
interfaces,
12601268
directives,
12611269
types,
12621270
});

src/type/__tests__/schema-test.ts

+25
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
GraphQLList,
1414
GraphQLObjectType,
1515
GraphQLScalarType,
16+
GraphQLUnionType,
1617
} from '../definition';
1718
import { GraphQLDirective } from '../directives';
1819
import { GraphQLBoolean, GraphQLInt, GraphQLString } from '../scalars';
@@ -212,6 +213,30 @@ describe('Type System: Schema', () => {
212213
expect(schema.getType('SomeSubtype')).to.equal(SomeSubtype);
213214
});
214215

216+
it("includes unions's thunk subtypes in the type map", () => {
217+
const SomeUnion = new GraphQLUnionType({
218+
name: 'SomeUnion',
219+
types: () => [SomeSubtype],
220+
interfaces: () => [SomeInterface],
221+
});
222+
223+
const SomeInterface = new GraphQLInterfaceType({
224+
name: 'SomeInterface',
225+
fields: {},
226+
});
227+
228+
const SomeSubtype = new GraphQLObjectType({
229+
name: 'SomeSubtype',
230+
fields: {},
231+
});
232+
233+
const schema = new GraphQLSchema({ types: [SomeUnion] });
234+
235+
expect(schema.getType('SomeUnion')).to.equal(SomeUnion);
236+
expect(schema.getType('SomeInterface')).to.equal(SomeInterface);
237+
expect(schema.getType('SomeSubtype')).to.equal(SomeSubtype);
238+
});
239+
215240
it('includes nested input objects in the map', () => {
216241
const NestedInputObject = new GraphQLInputObjectType({
217242
name: 'NestedInputObject',

src/type/definition.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,9 @@ export class GraphQLObjectType<TSource = any, TContext = any> {
826826

827827
function defineInterfaces(
828828
config: Readonly<
829-
GraphQLObjectTypeConfig<any, any> | GraphQLInterfaceTypeConfig<any, any>
829+
| GraphQLObjectTypeConfig<any, any>
830+
| GraphQLInterfaceTypeConfig<any, any>
831+
| GraphQLUnionTypeConfig<any, any>
830832
>,
831833
): ReadonlyArray<GraphQLInterfaceType> {
832834
const interfaces = resolveReadonlyArrayThunk(config.interfaces ?? []);
@@ -1247,6 +1249,7 @@ export class GraphQLUnionType {
12471249
extensionASTNodes: ReadonlyArray<UnionTypeExtensionNode>;
12481250

12491251
private _types: ThunkReadonlyArray<GraphQLObjectType>;
1252+
private _interfaces: ThunkReadonlyArray<GraphQLInterfaceType>;
12501253

12511254
constructor(config: Readonly<GraphQLUnionTypeConfig<any, any>>) {
12521255
this.name = assertName(config.name);
@@ -1257,6 +1260,7 @@ export class GraphQLUnionType {
12571260
this.extensionASTNodes = config.extensionASTNodes ?? [];
12581261

12591262
this._types = defineTypes.bind(undefined, config);
1263+
this._interfaces = defineInterfaces.bind(undefined, config);
12601264
devAssert(
12611265
config.resolveType == null || typeof config.resolveType === 'function',
12621266
`${this.name} must provide "resolveType" as a function, ` +
@@ -1275,10 +1279,18 @@ export class GraphQLUnionType {
12751279
return this._types;
12761280
}
12771281

1282+
getInterfaces(): ReadonlyArray<GraphQLInterfaceType> {
1283+
if (typeof this._interfaces === 'function') {
1284+
this._interfaces = this._interfaces();
1285+
}
1286+
return this._interfaces;
1287+
}
1288+
12781289
toConfig(): GraphQLUnionTypeNormalizedConfig {
12791290
return {
12801291
name: this.name,
12811292
description: this.description,
1293+
interfaces: this.getInterfaces(),
12821294
types: this.getTypes(),
12831295
resolveType: this.resolveType,
12841296
extensions: this.extensions,
@@ -1310,6 +1322,7 @@ function defineTypes(
13101322
export interface GraphQLUnionTypeConfig<TSource, TContext> {
13111323
name: string;
13121324
description?: Maybe<string>;
1325+
interfaces?: ThunkReadonlyArray<GraphQLInterfaceType>;
13131326
types: ThunkReadonlyArray<GraphQLObjectType>;
13141327
/**
13151328
* Optionally provide a custom type resolver function. If one is not provided,
@@ -1324,6 +1337,7 @@ export interface GraphQLUnionTypeConfig<TSource, TContext> {
13241337

13251338
interface GraphQLUnionTypeNormalizedConfig
13261339
extends GraphQLUnionTypeConfig<any, any> {
1340+
interfaces: ReadonlyArray<GraphQLInterfaceType>;
13271341
types: ReadonlyArray<GraphQLObjectType>;
13281342
extensions: Readonly<GraphQLUnionTypeExtensions>;
13291343
extensionASTNodes: ReadonlyArray<UnionTypeExtensionNode>;

src/type/introspection.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ import { print } from '../language/printer';
66

77
import { astFromValue } from '../utilities/astFromValue';
88

9-
import type {
9+
import {
1010
GraphQLEnumValue,
1111
GraphQLField,
1212
GraphQLFieldConfigMap,
1313
GraphQLInputField,
1414
GraphQLNamedType,
1515
GraphQLType,
16+
isCompositeType,
1617
} from './definition';
1718
import {
1819
GraphQLEnumType,
@@ -275,7 +276,7 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({
275276
interfaces: {
276277
type: new GraphQLList(new GraphQLNonNull(__Type)),
277278
resolve(type) {
278-
if (isObjectType(type) || isInterfaceType(type)) {
279+
if (isCompositeType(type)) {
279280
return type.getInterfaces();
280281
}
281282
},

0 commit comments

Comments
 (0)