Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow unions to declare implementation of interfaces #3527

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/__testUtils__/kitchenSinkSDL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,28 @@ extend union Feed = Photo | Video

extend union Feed @onUnion

interface Node {
id: ID
}

interface Resource {
url: String
}

extend type Photo implements Node {
id: ID
url: String
}

extend type Video implements Node {
id: ID
url: String
}

union Media implements Node = Photo | Video

extend union Media implements Resource

scalar CustomScalar

scalar AnnotatedScalar @onScalar
Expand Down
16 changes: 10 additions & 6 deletions src/execution/__tests__/union-interface-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ const CatType: GraphQLObjectType = new GraphQLObjectType({

const PetType = new GraphQLUnionType({
name: 'Pet',
interfaces: [MammalType, LifeType, NamedType],
types: [DogType, CatType],
resolveType(value) {
if (value instanceof Dog) {
Expand Down Expand Up @@ -211,8 +212,13 @@ describe('Execute: Union and intersection types', () => {
Pet: {
kind: 'UNION',
name: 'Pet',
fields: null,
interfaces: null,
fields: [
{ name: 'progeny' },
{ name: 'mother' },
{ name: 'father' },
{ name: 'name' },
],
interfaces: [{ name: 'Mammal' }, { name: 'Life' }, { name: 'Named' }],
possibleTypes: [{ name: 'Dog' }, { name: 'Cat' }],
enumValues: null,
inputFields: null,
Expand Down Expand Up @@ -264,12 +270,11 @@ describe('Execute: Union and intersection types', () => {
name
pets {
__typename
name
... on Dog {
name
barks
}
... on Cat {
name
meows
}
}
Expand Down Expand Up @@ -436,12 +441,11 @@ describe('Execute: Union and intersection types', () => {

fragment PetFields on Pet {
__typename
name
... on Dog {
name
barks
}
... on Cat {
name
meows
}
}
Expand Down
19 changes: 19 additions & 0 deletions src/jsutils/mapValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { ObjMap, ReadOnlyObjMap } from './ObjMap';

/**
* Creates an object map from an array of `maps` with the same keys as each `map`
* in `maps` and values generated by running each value of `map` thru `fn`.
*/
export function mapValues<T, V>(
maps: ReadonlyArray<ReadOnlyObjMap<T>>,
fn: (value: T, key: string) => V,
): ObjMap<V> {
const result = Object.create(null);

for (const map of maps) {
for (const key of Object.keys(map)) {
result[key] = fn(map[key], key);
}
}
return result;
}
117 changes: 117 additions & 0 deletions src/language/__tests__/schema-parser-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,24 @@ describe('Schema Parser', () => {
});
});

it('Union extension without types', () => {
const doc = parse('extend union HelloOrGoodbye implements Greeting');
expectJSON(doc).toDeepEqual({
kind: 'Document',
definitions: [
{
kind: 'UnionTypeExtension',
name: nameNode('HelloOrGoodbye', { start: 13, end: 27 }),
interfaces: [typeNode('Greeting', { start: 39, end: 47 })],
directives: [],
types: [],
loc: { start: 0, end: 47 },
},
],
loc: { start: 0, end: 47 },
});
});

it('Object extension without fields followed by extension', () => {
const doc = parse(`
extend type Hello implements Greeting
Expand Down Expand Up @@ -323,6 +341,36 @@ describe('Schema Parser', () => {
});
});

it('Union extension without types followed by extension', () => {
const doc = parse(`
extend union HelloOrGoodbye implements Greeting

extend union HelloOrGoodbye implements SecondGreeting
`);
expectJSON(doc).toDeepEqual({
kind: 'Document',
definitions: [
{
kind: 'UnionTypeExtension',
name: nameNode('HelloOrGoodbye', { start: 20, end: 34 }),
interfaces: [typeNode('Greeting', { start: 46, end: 54 })],
directives: [],
types: [],
loc: { start: 7, end: 54 },
},
{
kind: 'UnionTypeExtension',
name: nameNode('HelloOrGoodbye', { start: 75, end: 89 }),
interfaces: [typeNode('SecondGreeting', { start: 101, end: 115 })],
directives: [],
types: [],
loc: { start: 62, end: 115 },
},
],
loc: { start: 0, end: 120 },
});
});

it('Object extension do not include descriptions', () => {
expectSyntaxError(`
"Description"
Expand Down Expand Up @@ -517,6 +565,26 @@ describe('Schema Parser', () => {
});
});

it('Simple union inheriting interface', () => {
const doc = parse('union Hello implements World = Subtype');

expectJSON(doc).toDeepEqual({
kind: 'Document',
definitions: [
{
kind: 'UnionTypeDefinition',
name: nameNode('Hello', { start: 6, end: 11 }),
description: undefined,
interfaces: [typeNode('World', { start: 23, end: 28 })],
directives: [],
types: [typeNode('Subtype', { start: 31, end: 38 })],
loc: { start: 0, end: 38 },
},
],
loc: { start: 0, end: 38 },
});
});

it('Simple type inheriting multiple interfaces', () => {
const doc = parse('type Hello implements Wo & rld { field: String }');

Expand Down Expand Up @@ -574,6 +642,29 @@ describe('Schema Parser', () => {
});
});

it('Simple union inheriting multiple interfaces', () => {
const doc = parse('union Hello implements Wo & rld = Subtype');

expectJSON(doc).toDeepEqual({
kind: 'Document',
definitions: [
{
kind: 'UnionTypeDefinition',
name: nameNode('Hello', { start: 6, end: 11 }),
description: undefined,
interfaces: [
typeNode('Wo', { start: 23, end: 25 }),
typeNode('rld', { start: 28, end: 31 }),
],
directives: [],
types: [typeNode('Subtype', { start: 34, end: 41 })],
loc: { start: 0, end: 41 },
},
],
loc: { start: 0, end: 41 },
});
});

it('Simple type inheriting multiple interfaces with leading ampersand', () => {
const doc = parse('type Hello implements & Wo & rld { field: String }');

Expand Down Expand Up @@ -633,6 +724,29 @@ describe('Schema Parser', () => {
});
});

it('Simple union inheriting multiple interfaces with leading ampersand', () => {
const doc = parse('union Hello implements & Wo & rld = Subtype');

expectJSON(doc).toDeepEqual({
kind: 'Document',
definitions: [
{
kind: 'UnionTypeDefinition',
name: nameNode('Hello', { start: 6, end: 11 }),
description: undefined,
interfaces: [
typeNode('Wo', { start: 25, end: 27 }),
typeNode('rld', { start: 30, end: 33 }),
],
directives: [],
types: [typeNode('Subtype', { start: 36, end: 43 })],
loc: { start: 0, end: 43 },
},
],
loc: { start: 0, end: 43 },
});
});

it('Single value enum', () => {
const doc = parse('enum Hello { WORLD }');

Expand Down Expand Up @@ -880,6 +994,7 @@ describe('Schema Parser', () => {
kind: 'UnionTypeDefinition',
name: nameNode('Hello', { start: 6, end: 11 }),
description: undefined,
interfaces: [],
directives: [],
types: [typeNode('World', { start: 14, end: 19 })],
loc: { start: 0, end: 19 },
Expand All @@ -899,6 +1014,7 @@ describe('Schema Parser', () => {
kind: 'UnionTypeDefinition',
name: nameNode('Hello', { start: 6, end: 11 }),
description: undefined,
interfaces: [],
directives: [],
types: [
typeNode('Wo', { start: 14, end: 16 }),
Expand All @@ -921,6 +1037,7 @@ describe('Schema Parser', () => {
kind: 'UnionTypeDefinition',
name: nameNode('Hello', { start: 6, end: 11 }),
description: undefined,
interfaces: [],
directives: [],
types: [
typeNode('Wo', { start: 16, end: 18 }),
Expand Down
22 changes: 22 additions & 0 deletions src/language/__tests__/schema-printer-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,28 @@ describe('Printer: SDL document', () => {

extend union Feed @onUnion

interface Node {
id: ID
}

interface Resource {
url: String
}

extend type Photo implements Node {
id: ID
url: String
}

extend type Video implements Node {
id: ID
url: String
}

union Media implements Node = Photo | Video

extend union Media implements Resource

scalar CustomScalar

scalar AnnotatedScalar @onScalar
Expand Down
12 changes: 10 additions & 2 deletions src/language/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,13 @@ export const QueryDocumentKeys: {
'directives',
'fields',
],
UnionTypeDefinition: ['description', 'name', 'directives', 'types'],
UnionTypeDefinition: [
'description',
'name',
'interfaces',
'directives',
'types',
],
EnumTypeDefinition: ['description', 'name', 'directives', 'values'],
EnumValueDefinition: ['description', 'name', 'directives'],
InputObjectTypeDefinition: ['description', 'name', 'directives', 'fields'],
Expand All @@ -274,7 +280,7 @@ export const QueryDocumentKeys: {
ScalarTypeExtension: ['name', 'directives'],
ObjectTypeExtension: ['name', 'interfaces', 'directives', 'fields'],
InterfaceTypeExtension: ['name', 'interfaces', 'directives', 'fields'],
UnionTypeExtension: ['name', 'directives', 'types'],
UnionTypeExtension: ['name', 'interfaces', 'directives', 'types'],
EnumTypeExtension: ['name', 'directives', 'values'],
InputObjectTypeExtension: ['name', 'directives', 'fields'],
};
Expand Down Expand Up @@ -624,6 +630,7 @@ export interface UnionTypeDefinitionNode {
readonly loc?: Location;
readonly description?: StringValueNode;
readonly name: NameNode;
readonly interfaces?: ReadonlyArray<NamedTypeNode>;
readonly directives?: ReadonlyArray<ConstDirectiveNode>;
readonly types?: ReadonlyArray<NamedTypeNode>;
}
Expand Down Expand Up @@ -716,6 +723,7 @@ export interface UnionTypeExtensionNode {
readonly kind: Kind.UNION_TYPE_EXTENSION;
readonly loc?: Location;
readonly name: NameNode;
readonly interfaces?: ReadonlyArray<NamedTypeNode>;
readonly directives?: ReadonlyArray<ConstDirectiveNode>;
readonly types?: ReadonlyArray<NamedTypeNode>;
}
Expand Down
10 changes: 9 additions & 1 deletion src/language/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -970,12 +970,14 @@ export class Parser {
const description = this.parseDescription();
this.expectKeyword('union');
const name = this.parseName();
const interfaces = this.parseImplementsInterfaces();
const directives = this.parseConstDirectives();
const types = this.parseUnionMemberTypes();
return this.node<UnionTypeDefinitionNode>(start, {
kind: Kind.UNION_TYPE_DEFINITION,
description,
name,
interfaces,
directives,
types,
});
Expand Down Expand Up @@ -1249,14 +1251,20 @@ export class Parser {
this.expectKeyword('extend');
this.expectKeyword('union');
const name = this.parseName();
const interfaces = this.parseImplementsInterfaces();
const directives = this.parseConstDirectives();
const types = this.parseUnionMemberTypes();
if (directives.length === 0 && types.length === 0) {
if (
interfaces.length === 0 &&
directives.length === 0 &&
types.length === 0
) {
throw this.unexpected();
}
return this.node<UnionTypeExtensionNode>(start, {
kind: Kind.UNION_TYPE_EXTENSION,
name,
interfaces,
directives,
types,
});
Expand Down
Loading