diff --git a/benchmark/github-schema.json b/benchmark/github-schema.json index 7352a87fa3..d5e9e59471 100644 --- a/benchmark/github-schema.json +++ b/benchmark/github-schema.json @@ -56058,7 +56058,7 @@ }, { "name": "UNION", - "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "description": "Indicates this type is a union. `memberTypes` and `possibleTypes` are valid fields.", "isDeprecated": false, "deprecationReason": null }, diff --git a/docs-old/APIReference-TypeSystem.md b/docs-old/APIReference-TypeSystem.md index 5b5047c349..1873c403d4 100644 --- a/docs-old/APIReference-TypeSystem.md +++ b/docs-old/APIReference-TypeSystem.md @@ -378,12 +378,12 @@ class GraphQLUnionType { type GraphQLUnionTypeConfig = { name: string, - types: GraphQLObjectsThunk | Array, + types: GraphQLCompositesThunk | Array, resolveType?: (value: any, info?: GraphQLResolveInfo) => ?GraphQLObjectType; description?: ?string; }; -type GraphQLObjectsThunk = () => Array; +type GraphQLCompositesThunk = () => Array; ``` When a field can return one of a heterogeneous set of types, a Union type diff --git a/src/type/__tests__/definition-test.ts b/src/type/__tests__/definition-test.ts index 7e8914449a..d16773f2b7 100644 --- a/src/type/__tests__/definition-test.ts +++ b/src/type/__tests__/definition-test.ts @@ -375,7 +375,8 @@ describe('Type System: Unions', () => { name: 'SomeUnion', types: [ObjectType], }); - expect(unionType.getTypes()).to.deep.equal([ObjectType]); + expect(unionType.getMemberTypes()).to.deep.equal([ObjectType]); + expect(unionType.getPossibleTypes()).to.deep.equal([ObjectType]); }); it('accepts a Union type with function returning an array of types', () => { @@ -383,15 +384,15 @@ describe('Type System: Unions', () => { name: 'SomeUnion', types: () => [ObjectType], }); - expect(unionType.getTypes()).to.deep.equal([ObjectType]); + expect(unionType.getMemberTypes()).to.deep.equal([ObjectType]); }); - it('accepts a Union type without types', () => { - const unionType = new GraphQLUnionType({ + it('accepts a recursive Union type', () => { + const unionType: GraphQLUnionType = new GraphQLUnionType({ name: 'SomeUnion', - types: [], + types: () => [unionType], }); - expect(unionType.getTypes()).to.deep.equal([]); + expect(unionType.getMemberTypes()).to.deep.equal([unionType]); }); it('rejects an Union type with invalid name', () => { diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index df431dafd3..aef1a16c5e 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -26,6 +26,7 @@ describe('Introspection', () => { descriptions: false, specifiedByUrl: true, directiveIsRepeatable: true, + memberTypes: true, }); const result = graphqlSync({ schema, source }); @@ -56,6 +57,7 @@ describe('Introspection', () => { inputFields: null, interfaces: [], enumValues: null, + memberTypes: null, possibleTypes: null, }, { @@ -66,6 +68,7 @@ describe('Introspection', () => { inputFields: null, interfaces: null, enumValues: null, + memberTypes: null, possibleTypes: null, }, { @@ -76,6 +79,7 @@ describe('Introspection', () => { inputFields: null, interfaces: null, enumValues: null, + memberTypes: null, possibleTypes: null, }, { @@ -181,6 +185,7 @@ describe('Introspection', () => { inputFields: null, interfaces: [], enumValues: null, + memberTypes: null, possibleTypes: null, }, { @@ -284,6 +289,25 @@ describe('Introspection', () => { isDeprecated: false, deprecationReason: null, }, + { + name: 'memberTypes', + args: [], + type: { + kind: 'LIST', + name: null, + ofType: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'OBJECT', + name: '__Type', + ofType: null, + }, + }, + }, + isDeprecated: false, + deprecationReason: null, + }, { name: 'possibleTypes', args: [], @@ -376,6 +400,7 @@ describe('Introspection', () => { inputFields: null, interfaces: [], enumValues: null, + memberTypes: null, possibleTypes: null, }, { @@ -427,6 +452,7 @@ describe('Introspection', () => { deprecationReason: null, }, ], + memberTypes: null, possibleTypes: null, }, { @@ -538,6 +564,7 @@ describe('Introspection', () => { inputFields: null, interfaces: [], enumValues: null, + memberTypes: null, possibleTypes: null, }, { @@ -627,6 +654,7 @@ describe('Introspection', () => { inputFields: null, interfaces: [], enumValues: null, + memberTypes: null, possibleTypes: null, }, { @@ -690,6 +718,7 @@ describe('Introspection', () => { inputFields: null, interfaces: [], enumValues: null, + memberTypes: null, possibleTypes: null, }, { @@ -798,6 +827,7 @@ describe('Introspection', () => { inputFields: null, interfaces: [], enumValues: null, + memberTypes: null, possibleTypes: null, }, { @@ -904,6 +934,7 @@ describe('Introspection', () => { deprecationReason: null, }, ], + memberTypes: null, possibleTypes: null, }, ], @@ -1612,6 +1643,68 @@ describe('Introspection', () => { }); }); + it('exposes memberTypes for Union types', () => { + const schema = buildSchema(` + union SomeUnion = SomeObject + + union AnotherUnion = SomeUnion | SomeObject + + type SomeObject { + someField(arg: String): String + } + + schema { + query: SomeObject + } + `); + + const source = ` + { + SomeObject: __type(name: "SomeObject") { + memberTypes { + name + } + possibleTypes { + name + } + } + SomeUnion: __type(name: "SomeUnion") { + memberTypes { + name + } + possibleTypes { + name + } + } + AnotherUnion: __type(name: "AnotherUnion") { + memberTypes { + name + } + possibleTypes { + name + } + } + } + `; + + expect(graphqlSync({ schema, source })).to.deep.equal({ + data: { + SomeObject: { + memberTypes: null, + possibleTypes: null, + }, + SomeUnion: { + memberTypes: [{ name: 'SomeObject' }], + possibleTypes: [{ name: 'SomeObject' }], + }, + AnotherUnion: { + memberTypes: [{ name: 'SomeUnion' }, { name: 'SomeObject' }], + possibleTypes: [{ name: 'SomeObject' }], + }, + }, + }); + }); + it('executes an introspection query without calling global resolvers', () => { const schema = buildSchema(` type Query { @@ -1623,6 +1716,7 @@ describe('Introspection', () => { specifiedByUrl: true, directiveIsRepeatable: true, schemaDescription: true, + memberTypes: true, }); /* c8 ignore start */ diff --git a/src/type/__tests__/validation-test.ts b/src/type/__tests__/validation-test.ts index af82d30fbb..a3102657b1 100644 --- a/src/type/__tests__/validation-test.ts +++ b/src/type/__tests__/validation-test.ts @@ -730,7 +730,7 @@ describe('Type System: Union types must be valid', () => { ]); }); - it('rejects a Union type with non-Object members types', () => { + it('rejects a Union type with non-composite member types', () => { let schema = buildSchema(` type Query { test: BadUnion @@ -755,12 +755,12 @@ describe('Type System: Union types must be valid', () => { expectJSON(validateSchema(schema)).toDeepEqual([ { message: - 'Union type BadUnion can only include Object types, it cannot include String.', + 'Union type BadUnion can only include Object, Interface, or Union types, it cannot include String.', locations: [{ line: 16, column: 11 }], }, { message: - 'Union type BadUnion can only include Object types, it cannot include Int.', + 'Union type BadUnion can only include Object, Interface, or Union types, it cannot include Int.', locations: [{ line: 1, column: 25 }], }, ]); @@ -769,8 +769,6 @@ describe('Type System: Union types must be valid', () => { GraphQLString, new GraphQLNonNull(SomeObjectType), new GraphQLList(SomeObjectType), - SomeInterfaceType, - SomeUnionType, SomeEnumType, SomeInputObjectType, ]; @@ -784,12 +782,68 @@ describe('Type System: Union types must be valid', () => { expectJSON(validateSchema(badSchema)).toDeepEqual([ { message: - 'Union type BadUnion can only include Object types, ' + + 'Union type BadUnion can only include Object, Interface, or Union types, ' + `it cannot include ${inspect(memberType)}.`, }, ]); } }); + + it('rejects a Union type that does not include transitive member union type members', () => { + const schema = buildSchema(` + type Query { + test: BadUnion + } + + type TypeA { + field: String + } + + union SomeUnion = TypeA + + union BadUnion = SomeUnion + `); + + expectJSON(validateSchema(schema)).toDeepEqual([ + { + message: + 'Union type BadUnion must include TypeA because BadUnion includes SomeUnion and SomeUnion includes TypeA.', + locations: [ + { line: 10, column: 25 }, + { line: 12, column: 24 }, + ], + }, + ]); + }); + + it('rejects a Union type that does not include member interface type implementations', () => { + const schema = buildSchema(` + type Query { + test: BadUnion + } + + type TypeA implements SomeInterface { + field: String + } + + interface SomeInterface { + field: String + } + + union BadUnion = SomeInterface + `); + + expectJSON(validateSchema(schema)).toDeepEqual([ + { + message: + 'Union type BadUnion must include TypeA because BadUnion includes SomeInterface and TypeA implements SomeInterface.', + locations: [ + { line: 6, column: 29 }, + { line: 14, column: 24 }, + ], + }, + ]); + }); }); describe('Type System: Input Objects must have fields', () => { @@ -1900,7 +1954,7 @@ describe('Objects must adhere to Interface they implement', () => { expectJSON(validateSchema(schema)).toDeepEqual([]); }); - it('accepts an Object with a subtyped Interface field (union)', () => { + it('accepts an Object with a subtyped Interface field (union subtyped by object)', () => { const schema = buildSchema(` type Query { test: AnotherObject @@ -1923,6 +1977,58 @@ describe('Objects must adhere to Interface they implement', () => { expectJSON(validateSchema(schema)).toDeepEqual([]); }); + it('accepts an Object with a subtyped Interface field (union subtyped by union)', () => { + const schema = buildSchema(` + type Query { + test: AnotherObject + } + + type SomeObject { + field: String + } + + union SomeUnionType = ChildUnionType | SomeObject + + union ChildUnionType = SomeObject + + interface AnotherInterface { + field: SomeUnionType + } + + type AnotherObject implements AnotherInterface { + field: ChildUnionType + } + `); + expectJSON(validateSchema(schema)).toDeepEqual([]); + }); + + it('accepts an Object with a subtyped Interface field (union subtyped by interface)', () => { + const schema = buildSchema(` + type Query { + test: AnotherObject + } + + type SomeObject { + field: String + } + + union SomeUnionType = SomeInterface | SomeObject + + interface SomeInterface { + field: String + } + + interface AnotherInterface { + field: SomeUnionType + } + + type AnotherObject implements AnotherInterface { + field: SomeInterface + } + `); + expectJSON(validateSchema(schema)).toDeepEqual([]); + }); + it('rejects an Object missing an Interface argument', () => { const schema = buildSchema(` type Query { @@ -2334,7 +2440,7 @@ describe('Interfaces must adhere to Interface they implement', () => { expectJSON(validateSchema(schema)).toDeepEqual([]); }); - it('accepts an Interface with a subtyped Interface field (union)', () => { + it('accepts an Interface with a subtyped Interface field (union subtyped by object)', () => { const schema = buildSchema(` type Query { test: ChildInterface @@ -2357,6 +2463,58 @@ describe('Interfaces must adhere to Interface they implement', () => { expectJSON(validateSchema(schema)).toDeepEqual([]); }); + it('accepts an Interface with a subtyped Interface field (union subtyped by union)', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + type SomeObject { + field: String + } + + union SomeUnionType = ChildUnionType | SomeObject + + union ChildUnionType = SomeObject + + interface ParentInterface { + field: SomeUnionType + } + + interface ChildInterface implements ParentInterface { + field: ChildUnionType + } + `); + expectJSON(validateSchema(schema)).toDeepEqual([]); + }); + + it('accepts an Interface with a subtyped Interface field (union subtyped by interface)', () => { + const schema = buildSchema(` + type Query { + test: ChildInterface + } + + type SomeObject { + field: String + } + + union SomeUnionType = AnotherChildInterface | SomeObject + + interface AnotherChildInterface { + field: String + } + + interface ParentInterface { + field: SomeUnionType + } + + interface ChildInterface implements ParentInterface { + field: AnotherChildInterface + } + `); + expectJSON(validateSchema(schema)).toDeepEqual([]); + }); + it('rejects an Interface implementing a non-Interface type', () => { const schema = buildSchema(` type Query { diff --git a/src/type/definition.ts b/src/type/definition.ts index f4cb1a90fe..2deda7df15 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -1152,7 +1152,8 @@ export class GraphQLUnionType { astNode: Maybe; extensionASTNodes: ReadonlyArray; - private _types: ThunkReadonlyArray; + private _memberTypes: ThunkReadonlyArray; + private _possibleTypes: ThunkReadonlyArray; constructor(config: Readonly>) { this.name = assertName(config.name); @@ -1162,25 +1163,33 @@ export class GraphQLUnionType { this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes ?? []; - this._types = defineTypes.bind(undefined, config); + this._memberTypes = defineMemberTypes.bind(undefined, config); + this._possibleTypes = definePossibleTypes.bind(undefined, config); } get [Symbol.toStringTag]() { return 'GraphQLUnionType'; } - getTypes(): ReadonlyArray { - if (typeof this._types === 'function') { - this._types = this._types(); + getMemberTypes(): ReadonlyArray { + if (typeof this._memberTypes === 'function') { + this._memberTypes = this._memberTypes(); } - return this._types; + return this._memberTypes; + } + + getPossibleTypes(): ReadonlyArray { + if (typeof this._possibleTypes === 'function') { + this._possibleTypes = this._possibleTypes(); + } + return this._possibleTypes; } toConfig(): GraphQLUnionTypeNormalizedConfig { return { name: this.name, description: this.description, - types: this.getTypes(), + types: this.getMemberTypes(), resolveType: this.resolveType, extensions: this.extensions, astNode: this.astNode, @@ -1197,16 +1206,29 @@ export class GraphQLUnionType { } } -function defineTypes( +function defineMemberTypes( config: Readonly>, -): ReadonlyArray { +): ReadonlyArray { return resolveReadonlyArrayThunk(config.types); } +function definePossibleTypes( + config: Readonly>, +): ReadonlyArray { + const memberTypes = resolveReadonlyArrayThunk(config.types); + const possibleTypes: Array = []; + for (const memberType of memberTypes) { + if (isObjectType(memberType)) { + possibleTypes.push(memberType); + } + } + return possibleTypes; +} + export interface GraphQLUnionTypeConfig { name: string; description?: Maybe; - types: ThunkReadonlyArray; + types: ThunkReadonlyArray; /** * Optionally provide a custom type resolver function. If one is not provided, * the default implementation will call `isTypeOf` on each implementing @@ -1220,7 +1242,7 @@ export interface GraphQLUnionTypeConfig { interface GraphQLUnionTypeNormalizedConfig extends GraphQLUnionTypeConfig { - types: ReadonlyArray; + types: ReadonlyArray; extensions: Readonly; extensionASTNodes: ReadonlyArray; } diff --git a/src/type/introspection.ts b/src/type/introspection.ts index e5fce6f241..ce6e8bab87 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -280,6 +280,14 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({ } }, }, + memberTypes: { + type: new GraphQLList(new GraphQLNonNull(__Type)), + resolve(type, _args, _context) { + if (isUnionType(type)) { + return type.getMemberTypes(); + } + }, + }, possibleTypes: { type: new GraphQLList(new GraphQLNonNull(__Type)), resolve(type, _args, _context, { schema }) { @@ -467,7 +475,7 @@ export const __TypeKind: GraphQLEnumType = new GraphQLEnumType({ UNION: { value: TypeKind.UNION, description: - 'Indicates this type is a union. `possibleTypes` is a valid field.', + 'Indicates this type is a union. `memberTypes` and `possibleTypes` are valid fields.', }, ENUM: { value: TypeKind.ENUM, diff --git a/src/type/schema.ts b/src/type/schema.ts index b8bc8935e7..1d062cd32e 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -292,7 +292,7 @@ export class GraphQLSchema { abstractType: GraphQLAbstractType, ): ReadonlyArray { return isUnionType(abstractType) - ? abstractType.getTypes() + ? abstractType.getPossibleTypes() : this.getImplementations(abstractType).objects; } @@ -306,14 +306,14 @@ export class GraphQLSchema { isSubType( abstractType: GraphQLAbstractType, - maybeSubType: GraphQLObjectType | GraphQLInterfaceType, + maybeSubType: GraphQLCompositeType, ): boolean { let map = this._subTypeMap[abstractType.name]; if (map === undefined) { map = Object.create(null); if (isUnionType(abstractType)) { - for (const type of abstractType.getTypes()) { + for (const type of abstractType.getMemberTypes()) { map[type.name] = true; } } else { @@ -437,7 +437,7 @@ function collectReferencedTypes( if (!typeSet.has(namedType)) { typeSet.add(namedType); if (isUnionType(namedType)) { - for (const memberType of namedType.getTypes()) { + for (const memberType of namedType.getMemberTypes()) { collectReferencedTypes(memberType, typeSet); } } else if (isObjectType(namedType) || isInterfaceType(namedType)) { diff --git a/src/type/validate.ts b/src/type/validate.ts index c6385d27fb..80c08f59b4 100644 --- a/src/type/validate.ts +++ b/src/type/validate.ts @@ -30,6 +30,7 @@ import type { GraphQLUnionType, } from './definition'; import { + isCompositeType, isEnumType, isInputObjectType, isInputType, @@ -461,7 +462,7 @@ function validateUnionMembers( context: SchemaValidationContext, union: GraphQLUnionType, ): void { - const memberTypes = union.getTypes(); + const memberTypes = union.getMemberTypes(); if (memberTypes.length === 0) { context.reportError( @@ -480,12 +481,55 @@ function validateUnionMembers( continue; } includedTypeNames[memberType.name] = true; - if (!isObjectType(memberType)) { + if (!isCompositeType(memberType)) { context.reportError( - `Union type ${union.name} can only include Object types, ` + + `Union type ${union.name} can only include Object, Interface, or Union types, ` + `it cannot include ${inspect(memberType)}.`, getUnionMemberTypeNodes(union, String(memberType)), ); + } else if (isUnionType(memberType)) { + validateUnionIncludesUnionMembers(context, union, memberType); + } else if (isInterfaceType(memberType)) { + validateUnionIncludesInterfaceImplementations(context, union, memberType); + } + } +} + +function validateUnionIncludesUnionMembers( + context: SchemaValidationContext, + union: GraphQLUnionType, + memberUnion: GraphQLUnionType, +): void { + const unionMemberTypes = union.getMemberTypes(); + for (const transitive of memberUnion.getMemberTypes()) { + if (!unionMemberTypes.includes(transitive)) { + context.reportError( + `Union type ${union.name} must include ${transitive.name} because ${union.name} includes ${memberUnion.name} and ${memberUnion.name} includes ${transitive.name}.`, + [ + ...getUnionMemberTypeNodes(memberUnion, transitive.name), + ...getUnionMemberTypeNodes(union, memberUnion.name), + ], + ); + } + } +} + +function validateUnionIncludesInterfaceImplementations( + context: SchemaValidationContext, + union: GraphQLUnionType, + memberInterface: GraphQLInterfaceType, +): void { + const unionMemberTypes = union.getMemberTypes(); + for (const transitive of context.schema.getImplementations(memberInterface) + .objects) { + if (!unionMemberTypes.includes(transitive)) { + context.reportError( + `Union type ${union.name} must include ${transitive.name} because ${union.name} includes ${memberInterface.name} and ${transitive.name} implements ${memberInterface.name}.`, + [ + ...getAllImplementsInterfaceNodes(transitive, memberInterface), + ...getUnionMemberTypeNodes(union, memberInterface.name), + ], + ); } } } @@ -618,7 +662,7 @@ function getAllImplementsInterfaceNodes( function getUnionMemberTypeNodes( union: GraphQLUnionType, typeName: string, -): Maybe> { +): ReadonlyArray { const { astNode, extensionASTNodes } = union; const nodes: ReadonlyArray = astNode != null ? [astNode, ...extensionASTNodes] : extensionASTNodes; diff --git a/src/utilities/__tests__/buildASTSchema-test.ts b/src/utilities/__tests__/buildASTSchema-test.ts index 435abc2d7a..bbd7a82228 100644 --- a/src/utilities/__tests__/buildASTSchema-test.ts +++ b/src/utilities/__tests__/buildASTSchema-test.ts @@ -469,7 +469,7 @@ describe('Schema Builder', () => { } `); const errors = validateSchema(schema); - expect(errors).to.have.lengthOf.above(0); + expect(errors).to.have.lengthOf(0); }); it('Custom Scalar', () => { diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts index 8c043f0e77..7c95ca55b4 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -5,6 +5,7 @@ import { dedent } from '../../__testUtils__/dedent'; import { assertEnumType, + assertUnionType, GraphQLEnumType, GraphQLObjectType, } from '../../type/definition'; @@ -958,12 +959,10 @@ describe('Type System: build schema from introspection', () => { ); expect(fooIntrospection).to.deep.include({ name: 'Foo', - possibleTypes: [{ kind: 'UNION', name: 'Foo', ofType: null }], + possibleTypes: [], }); - expect(() => buildClientSchema(introspection)).to.throw( - 'Expected Foo to be a GraphQL Object type.', - ); + assertUnionType(buildClientSchema(introspection).getType('Foo')); }); }); }); diff --git a/src/utilities/__tests__/getIntrospectionQuery-test.ts b/src/utilities/__tests__/getIntrospectionQuery-test.ts index e2f5595b3c..9befa02022 100644 --- a/src/utilities/__tests__/getIntrospectionQuery-test.ts +++ b/src/utilities/__tests__/getIntrospectionQuery-test.ts @@ -130,4 +130,12 @@ describe('getIntrospectionQuery', () => { 2, ); }); + + it('include "memberTypes" field on types', () => { + expectIntrospectionQuery().toNotMatch('memberTypes'); + + expectIntrospectionQuery({ memberTypes: true }).toMatch('memberTypes'); + + expectIntrospectionQuery({ memberTypes: false }).toNotMatch('memberTypes'); + }); }); diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index d09153a2e6..007541542a 100644 --- a/src/utilities/__tests__/printSchema-test.ts +++ b/src/utilities/__tests__/printSchema-test.ts @@ -710,6 +710,7 @@ describe('Type System Printer', () => { specifiedByURL: String fields(includeDeprecated: Boolean = false): [__Field!] interfaces: [__Type!] + memberTypes: [__Type!] possibleTypes: [__Type!] enumValues(includeDeprecated: Boolean = false): [__EnumValue!] inputFields(includeDeprecated: Boolean = false): [__InputValue!] @@ -731,7 +732,9 @@ describe('Type System Printer', () => { """ INTERFACE - """Indicates this type is a union. \`possibleTypes\` is a valid field.""" + """ + Indicates this type is a union. \`memberTypes\` and \`possibleTypes\` are valid fields. + """ UNION """Indicates this type is an enum. \`enumValues\` is a valid field.""" diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index e69b24bef9..22faa40420 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -36,6 +36,7 @@ import { import type { GraphQLArgumentConfig, + GraphQLCompositeType, GraphQLEnumValueConfigMap, GraphQLFieldConfig, GraphQLFieldConfigArgumentMap, @@ -365,7 +366,7 @@ export function extendSchemaImpl( return new GraphQLUnionType({ ...config, types: () => [ - ...type.getTypes().map(replaceNamedType), + ...type.getMemberTypes().map(replaceNamedType), ...buildUnionTypes(extensions), ], extensionASTNodes: config.extensionASTNodes.concat(extensions), @@ -566,7 +567,7 @@ export function extendSchemaImpl( function buildUnionTypes( nodes: ReadonlyArray, - ): Array { + ): Array { // 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. diff --git a/src/utilities/findBreakingChanges.ts b/src/utilities/findBreakingChanges.ts index 0bf0d453b4..6f70a8408d 100644 --- a/src/utilities/findBreakingChanges.ts +++ b/src/utilities/findBreakingChanges.ts @@ -271,19 +271,22 @@ function findUnionTypeChanges( newType: GraphQLUnionType, ): Array { const schemaChanges = []; - const possibleTypesDiff = diff(oldType.getTypes(), newType.getTypes()); + const memberTypesDiff = diff( + oldType.getMemberTypes(), + newType.getMemberTypes(), + ); - for (const newPossibleType of possibleTypesDiff.added) { + for (const newMemberType of memberTypesDiff.added) { schemaChanges.push({ type: DangerousChangeType.TYPE_ADDED_TO_UNION, - description: `${newPossibleType.name} was added to union type ${oldType.name}.`, + description: `${newMemberType.name} was added to union type ${oldType.name}.`, }); } - for (const oldPossibleType of possibleTypesDiff.removed) { + for (const oldMemberType of memberTypesDiff.removed) { schemaChanges.push({ type: BreakingChangeType.TYPE_REMOVED_FROM_UNION, - description: `${oldPossibleType.name} was removed from union type ${oldType.name}.`, + description: `${oldMemberType.name} was removed from union type ${oldType.name}.`, }); } diff --git a/src/utilities/getIntrospectionQuery.ts b/src/utilities/getIntrospectionQuery.ts index c21fe9a1bb..a0211c3bb5 100644 --- a/src/utilities/getIntrospectionQuery.ts +++ b/src/utilities/getIntrospectionQuery.ts @@ -32,6 +32,12 @@ export interface IntrospectionOptions { * Default: false */ inputValueDeprecation?: boolean; + + /** + * Whether to include 'memberTypes' field on types. + * Default: false + */ + memberTypes?: boolean; } /** @@ -45,6 +51,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { directiveIsRepeatable: false, schemaDescription: false, inputValueDeprecation: false, + memberTypes: false, ...options, }; @@ -63,6 +70,12 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { return optionsWithDefault.inputValueDeprecation ? str : ''; } + const memberTypes = optionsWithDefault.memberTypes + ? `memberTypes { + ...TypeRef + }` + : ''; + return ` query IntrospectionQuery { __schema { @@ -114,6 +127,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { isDeprecated deprecationReason } + ${memberTypes} possibleTypes { ...TypeRef } @@ -234,6 +248,13 @@ export interface IntrospectionUnionType { readonly kind: 'UNION'; readonly name: string; readonly description?: Maybe; + readonly memberTypes: ReadonlyArray< + IntrospectionNamedTypeRef< + | IntrospectionObjectType + | IntrospectionInterfaceType + | IntrospectionUnionType + > + >; readonly possibleTypes: ReadonlyArray< IntrospectionNamedTypeRef >; diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts index 83859feee8..108a7c0f3b 100644 --- a/src/utilities/printSchema.ts +++ b/src/utilities/printSchema.ts @@ -186,9 +186,9 @@ function printInterface(type: GraphQLInterfaceType): string { } function printUnion(type: GraphQLUnionType): string { - const types = type.getTypes(); - const possibleTypes = types.length ? ' = ' + types.join(' | ') : ''; - return printDescription(type) + 'union ' + type.name + possibleTypes; + const types = type.getMemberTypes(); + const memberTypes = types.length ? ' = ' + types.join(' | ') : ''; + return printDescription(type) + 'union ' + type.name + memberTypes; } function printEnum(type: GraphQLEnumType): string { diff --git a/src/utilities/typeComparators.ts b/src/utilities/typeComparators.ts index 287be40bfe..025dd210cc 100644 --- a/src/utilities/typeComparators.ts +++ b/src/utilities/typeComparators.ts @@ -1,10 +1,9 @@ import type { GraphQLCompositeType, GraphQLType } from '../type/definition'; import { isAbstractType, - isInterfaceType, + isCompositeType, isListType, isNonNullType, - isObjectType, } from '../type/definition'; import type { GraphQLSchema } from '../type/schema'; @@ -73,7 +72,7 @@ export function isTypeSubTypeOf( // Otherwise, the child type is not a valid subtype of the parent type. return ( isAbstractType(superType) && - (isInterfaceType(maybeSubType) || isObjectType(maybeSubType)) && + isCompositeType(maybeSubType) && schema.isSubType(superType, maybeSubType) ); }