Skip to content

Commit

Permalink
allow unions to declare implementation of interfaces
Browse files Browse the repository at this point in the history
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
  • Loading branch information
yaacovCR committed Apr 5, 2022
1 parent 5f247e0 commit fcb08f8
Show file tree
Hide file tree
Showing 14 changed files with 273 additions and 16 deletions.
2 changes: 1 addition & 1 deletion src/execution/__tests__/union-interface-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ describe('Execute: Union and intersection types', () => {
kind: 'UNION',
name: 'Pet',
fields: null,
interfaces: null,
interfaces: [],
possibleTypes: [{ name: 'Dog' }, { name: 'Cat' }],
enumValues: null,
inputFields: null,
Expand Down
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
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
25 changes: 25 additions & 0 deletions src/type/__tests__/schema-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
GraphQLList,
GraphQLObjectType,
GraphQLScalarType,
GraphQLUnionType,
} from '../definition';
import { GraphQLDirective } from '../directives';
import { GraphQLBoolean, GraphQLInt, GraphQLString } from '../scalars';
Expand Down Expand Up @@ -212,6 +213,30 @@ describe('Type System: Schema', () => {
expect(schema.getType('SomeSubtype')).to.equal(SomeSubtype);
});

it("includes unions's thunk subtypes in the type map", () => {
const SomeUnion = new GraphQLUnionType({
name: 'SomeUnion',
types: () => [SomeSubtype],
interfaces: () => [SomeInterface],
});

const SomeInterface = new GraphQLInterfaceType({
name: 'SomeInterface',
fields: {},
});

const SomeSubtype = new GraphQLObjectType({
name: 'SomeSubtype',
fields: {},
});

const schema = new GraphQLSchema({ types: [SomeUnion] });

expect(schema.getType('SomeUnion')).to.equal(SomeUnion);
expect(schema.getType('SomeInterface')).to.equal(SomeInterface);
expect(schema.getType('SomeSubtype')).to.equal(SomeSubtype);
});

it('includes nested input objects in the map', () => {
const NestedInputObject = new GraphQLInputObjectType({
name: 'NestedInputObject',
Expand Down
16 changes: 15 additions & 1 deletion src/type/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,9 @@ export class GraphQLObjectType<TSource = any, TContext = any> {

function defineInterfaces(
config: Readonly<
GraphQLObjectTypeConfig<any, any> | GraphQLInterfaceTypeConfig<any, any>
| GraphQLObjectTypeConfig<any, any>
| GraphQLInterfaceTypeConfig<any, any>
| GraphQLUnionTypeConfig<any, any>
>,
): ReadonlyArray<GraphQLInterfaceType> {
const interfaces = resolveReadonlyArrayThunk(config.interfaces ?? []);
Expand Down Expand Up @@ -1247,6 +1249,7 @@ export class GraphQLUnionType {
extensionASTNodes: ReadonlyArray<UnionTypeExtensionNode>;

private _types: ThunkReadonlyArray<GraphQLObjectType>;
private _interfaces: ThunkReadonlyArray<GraphQLInterfaceType>;

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

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

getInterfaces(): ReadonlyArray<GraphQLInterfaceType> {
if (typeof this._interfaces === 'function') {
this._interfaces = this._interfaces();
}
return this._interfaces;
}

toConfig(): GraphQLUnionTypeNormalizedConfig {
return {
name: this.name,
description: this.description,
interfaces: this.getInterfaces(),
types: this.getTypes(),
resolveType: this.resolveType,
extensions: this.extensions,
Expand Down Expand Up @@ -1310,6 +1322,7 @@ function defineTypes(
export interface GraphQLUnionTypeConfig<TSource, TContext> {
name: string;
description?: Maybe<string>;
interfaces?: ThunkReadonlyArray<GraphQLInterfaceType>;
types: ThunkReadonlyArray<GraphQLObjectType>;
/**
* Optionally provide a custom type resolver function. If one is not provided,
Expand All @@ -1324,6 +1337,7 @@ export interface GraphQLUnionTypeConfig<TSource, TContext> {

interface GraphQLUnionTypeNormalizedConfig
extends GraphQLUnionTypeConfig<any, any> {
interfaces: ReadonlyArray<GraphQLInterfaceType>;
types: ReadonlyArray<GraphQLObjectType>;
extensions: Readonly<GraphQLUnionTypeExtensions>;
extensionASTNodes: ReadonlyArray<UnionTypeExtensionNode>;
Expand Down
5 changes: 3 additions & 2 deletions src/type/introspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { print } from '../language/printer';

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

import type {
import {
GraphQLEnumValue,
GraphQLField,
GraphQLFieldConfigMap,
GraphQLInputField,
GraphQLNamedType,
GraphQLType,
isCompositeType,
} from './definition';
import {
GraphQLEnumType,
Expand Down Expand Up @@ -275,7 +276,7 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({
interfaces: {
type: new GraphQLList(new GraphQLNonNull(__Type)),
resolve(type) {
if (isObjectType(type) || isInterfaceType(type)) {
if (isCompositeType(type)) {
return type.getInterfaces();
}
},
Expand Down
Loading

0 comments on commit fcb08f8

Please sign in to comment.