diff --git a/composition-js/CHANGELOG.md b/composition-js/CHANGELOG.md index 6f49418b8..d61eaa57b 100644 --- a/composition-js/CHANGELOG.md +++ b/composition-js/CHANGELOG.md @@ -4,6 +4,8 @@ This CHANGELOG pertains only to Apollo Federation packages in the 2.x range. The ## vNext +- Preserves source of union members and enum values in supergraph [PR #2288](https://github.com/apollographql/federation/pull/2288). + ## 2.2.0 - __BREAKING__: composition now rejects `@shareable` on interface fields. The `@shareable` directive is about diff --git a/composition-js/src/__tests__/__snapshots__/compose.composeDirective.test.ts.snap b/composition-js/src/__tests__/__snapshots__/compose.composeDirective.test.ts.snap index 26d1b2e58..4d175f957 100644 --- a/composition-js/src/__tests__/__snapshots__/compose.composeDirective.test.ts.snap +++ b/composition-js/src/__tests__/__snapshots__/compose.composeDirective.test.ts.snap @@ -20,6 +20,10 @@ directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: j directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + directive @mytag(name: String!) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION directive @tag(name: String!, prop: String!) on FIELD_DEFINITION | OBJECT diff --git a/composition-js/src/__tests__/compose.composeDirective.test.ts b/composition-js/src/__tests__/compose.composeDirective.test.ts index f8df79ee8..6ab81079f 100644 --- a/composition-js/src/__tests__/compose.composeDirective.test.ts +++ b/composition-js/src/__tests__/compose.composeDirective.test.ts @@ -365,7 +365,7 @@ describe('composing custom core directives', () => { }); it.each([ - '@join__field', '@join__graph', '@join__implements', '@join__type', + '@join__field', '@join__graph', '@join__implements', '@join__type', '@join__unionMember', '@join__enumValue' ])('join spec directives should result in an error', (directive) => { const subgraphA = generateSubgraph({ name: 'subgraphA', @@ -376,6 +376,8 @@ describe('composing custom core directives', () => { directive @join__graph(name: String!, url: String!) on ENUM_VALUE directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE scalar join__FieldSet @@ -735,7 +737,7 @@ describe('composing custom core directives', () => { }); it.each([ - '@join__field', '@join__graph', '@join__implements', '@join__type' + '@join__field', '@join__graph', '@join__implements', '@join__type', '@join__unionMember', '@join__enumValue' ])('naming conflict with join spec directives', (directive) => { const subgraphA = generateSubgraph({ name: 'subgraphA', diff --git a/composition-js/src/__tests__/compose.test.ts b/composition-js/src/__tests__/compose.test.ts index 65bf69049..739af15e1 100644 --- a/composition-js/src/__tests__/compose.test.ts +++ b/composition-js/src/__tests__/compose.test.ts @@ -35,6 +35,12 @@ describe('composition', () => { type T @key(fields: "k") { k: ID } + + type S { + x: Int + } + + union U = S | T ` } @@ -47,6 +53,11 @@ describe('composition', () => { a: Int b: String } + + enum E { + V1 + V2 + } ` } @@ -61,6 +72,8 @@ describe('composition', () => { query: Query } + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -69,8 +82,17 @@ describe('composition', () => { directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + enum E + @join__type(graph: SUBGRAPH2) + { + V1 @join__enumValue(graph: SUBGRAPH2) + V2 @join__enumValue(graph: SUBGRAPH2) + } + scalar join__FieldSet enum join__Graph { @@ -99,6 +121,12 @@ describe('composition', () => { t: T @join__field(graph: SUBGRAPH1) } + type S + @join__type(graph: SUBGRAPH1) + { + x: Int + } + type T @join__type(graph: SUBGRAPH1, key: "k") @join__type(graph: SUBGRAPH2, key: "k") @@ -107,19 +135,36 @@ describe('composition', () => { a: Int @join__field(graph: SUBGRAPH2) b: String @join__field(graph: SUBGRAPH2) } + + union U + @join__type(graph: SUBGRAPH1) + @join__unionMember(graph: SUBGRAPH1, member: "S") + @join__unionMember(graph: SUBGRAPH1, member: "T") + = S | T `); const [_, api] = schemas(result); expect(printSchema(api)).toMatchString(` + enum E { + V1 + V2 + } + type Query { t: T } + type S { + x: Int + } + type T { k: ID a: Int b: String } + + union U = S | T `); }) @@ -2140,6 +2185,8 @@ describe('composition', () => { query: Query } + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -2148,6 +2195,8 @@ describe('composition', () => { directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA scalar join__FieldSet diff --git a/composition-js/src/__tests__/matchers/index.ts b/composition-js/src/__tests__/matchers/index.ts index 5ecc4f9dd..65b292ae4 100644 --- a/composition-js/src/__tests__/matchers/index.ts +++ b/composition-js/src/__tests__/matchers/index.ts @@ -1 +1,2 @@ import './toMatchString'; +import './toMatchSubgraph'; diff --git a/composition-js/src/__tests__/matchers/toMatchSubgraph.ts b/composition-js/src/__tests__/matchers/toMatchSubgraph.ts new file mode 100644 index 000000000..5732dda2d --- /dev/null +++ b/composition-js/src/__tests__/matchers/toMatchSubgraph.ts @@ -0,0 +1,30 @@ +import { defaultPrintOptions, orderPrintedDefinitions, Subgraph } from "@apollo/federation-internals"; + +// Make this file a module (See: https://github.com/microsoft/TypeScript/issues/17736) +export {}; + +declare global { + namespace jest { + interface Matchers { + toMatchSubgraph(actual: Subgraph): R; + } + } +} + +expect.extend({ + toMatchSubgraph(expected: Subgraph, actual: Subgraph) { + // Note: we use `Subgraph.toString`, not `printSchema()` because 1) it's simpler and 2) the former skips federation + // specific definitions, making errors diffs more readable. + const printOptions = orderPrintedDefinitions(defaultPrintOptions); + const expectedString = expected.toString(printOptions); + const actualString = actual.toString(printOptions); + const pass = this.equals(expectedString, actualString); + const msgBase = `For subgraph ${expected.name}\n` + + this.utils.matcherHint('toMatchSubgraph', undefined, undefined) + + '\n\n' + const message = pass + ? () => msgBase + `Expected: not ${this.printExpected(expectedString)}` + : () => msgBase + this.utils.printDiffOrStringify(expectedString, actualString, 'Expected', 'Received', true); + return {actual, expected, message, name: 'toMatchString', pass}; + } +}); diff --git a/composition-js/src/__tests__/supergraph_reversibility.test.ts b/composition-js/src/__tests__/supergraph_reversibility.test.ts new file mode 100644 index 000000000..2d0e2f88e --- /dev/null +++ b/composition-js/src/__tests__/supergraph_reversibility.test.ts @@ -0,0 +1,92 @@ +import { assertCompositionSuccess, composeAsFed2Subgraphs } from "./testHelper"; +import gql from 'graphql-tag'; +import { asFed2SubgraphDocument, buildSubgraph, buildSupergraphSchema, extractSubgraphsFromSupergraph, ServiceDefinition } from "@apollo/federation-internals"; +import './matchers'; + +function composeAndTestReversibility(subgraphs: ServiceDefinition[]) { + const result = composeAsFed2Subgraphs(subgraphs); + assertCompositionSuccess(result); + + const extracted = extractSubgraphsFromSupergraph(buildSupergraphSchema(result.supergraphSdl)[0]); + for (const expectedSubgraph of subgraphs) { + const actual = extracted.get(expectedSubgraph.name)!; + // Note: the subgraph extracted from the supergraph are created with their `@link` on the schema definition, not as an extension (no + // strong reason for that, it's how the code was written), so let's match that so our follwoing `toMatchSubgraph` don't fail for that. + const expected = buildSubgraph(expectedSubgraph.name, '', asFed2SubgraphDocument(expectedSubgraph.typeDefs, { addAsSchemaExtension: false })); + expect(actual).toMatchSubgraph(expected); + } +} + +it('preserves the source of union members', () => { + const s1 = { + typeDefs: gql` + type Query { + uFromS1: U + } + + union U = A | B + + type A { + a: Int + } + + type B { + b: Int @shareable + } + `, + name: 'S1', + }; + + const s2 = { + typeDefs: gql` + type Query { + uFromS2: U + } + + union U = B | C + + type B { + b: Int @shareable + } + + type C { + c: Int + } + `, + name: 'S2', + }; + + composeAndTestReversibility([s1, s2]); +}); + +it('preserves the source of enum values', () => { + const s1 = { + typeDefs: gql` + type Query { + eFromS1: E + } + + enum E { + A, + B + } + `, + name: 'S1', + }; + + const s2 = { + typeDefs: gql` + type Query { + eFromS2: E + } + + enum E { + B, + C + } + `, + name: 'S2', + }; + + composeAndTestReversibility([s1, s2]); +}); diff --git a/composition-js/src/merging/merge.ts b/composition-js/src/merging/merge.ts index d50de4038..18b946a09 100644 --- a/composition-js/src/merging/merge.ts +++ b/composition-js/src/merging/merge.ts @@ -1689,10 +1689,32 @@ class Merger { } } for (const type of dest.types()) { + this.addJoinUnionMember(sources, dest, type); this.hintOnInconsistentUnionMember(sources, dest, type.name); } } + private addJoinUnionMember(sources: (UnionType | undefined)[], dest: UnionType, member: ObjectType) { + const joinUnionMemberDirective = joinSpec.unionMemberDirective(this.merged); + // We should always be merging with the latest join spec, so this should exists, but well, in prior versions where + // the directive didn't existed, we simply did had any replacement so ... + if (!joinUnionMemberDirective) { + return; + } + + for (const [idx, source] of sources.entries()) { + if (!source?.hasTypeMember(member.name)) { + continue; + } + + const name = this.joinSpecName(idx); + dest.applyDirective(joinUnionMemberDirective, { + graph: name, + member: member.name, + }); + } + } + private hintOnInconsistentUnionMember( sources: (UnionType | undefined)[], dest: UnionType, @@ -1768,6 +1790,7 @@ class Merger { const valueSources = sources.map(s => s?.value(value.name)); this.mergeDescription(valueSources, value); this.recordAppliedDirectivesToMerge(valueSources, value); + this.addJoinEnumValue(valueSources, value); const inaccessibleInSupergraph = this.mergedFederationDirectiveInSupergraph.get(inaccessibleSpec.inaccessibleDirectiveSpec.name); const isInaccessible = inaccessibleInSupergraph && value.hasAppliedDirective(inaccessibleInSupergraph); @@ -1817,6 +1840,26 @@ class Merger { } } + private addJoinEnumValue(sources: (EnumValue | undefined)[], dest: EnumValue) { + const joinEnumValueDirective = joinSpec.enumValueDirective(this.merged); + // We should always be merging with the latest join spec, so this should exists, but well, in prior versions where + // the directive didn't existed, we simply did had any replacement so ... + if (!joinEnumValueDirective) { + return; + } + + for (const [idx, source] of sources.entries()) { + if (!source) { + continue; + } + + const name = this.joinSpecName(idx); + dest.applyDirective(joinEnumValueDirective, { + graph: name, + }); + } + } + private hintOnInconsistentOutputEnumValue( sources: (EnumType | undefined)[], dest: EnumType, diff --git a/gateway-js/CHANGELOG.md b/gateway-js/CHANGELOG.md index 2ef833153..37d6512c2 100644 --- a/gateway-js/CHANGELOG.md +++ b/gateway-js/CHANGELOG.md @@ -5,7 +5,7 @@ This CHANGELOG pertains only to Apollo Federation packages in the 2.x range. The ## vNext - Adds support for `@interfaceObject` and keys on interfaces [PR #2279](https://github.com/apollographql/federation/pull/2279). - +- Preserves source of union members and enum values in supergraph [PR #2288](https://github.com/apollographql/federation/pull/2288). ## 2.2.2 diff --git a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts index 7d81a4870..50be709aa 100644 --- a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts +++ b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts @@ -147,7 +147,7 @@ describe('lifecycle hooks', () => { // the supergraph (even just formatting differences), this ID will change // and this test will have to updated. expect(secondCall[0]!.compositionId).toEqual( - '97f11725be210d703ced94cd941ef00d72ab26a9e04bdb724207b9e89e87359e', + 'ed8cb418d55e7cd069f11d093b2ea29316e1a913a5757f383cc78ed399414104', ); // second call should have previous info in the second arg expect(secondCall[1]!.compositionId).toEqual(expectedFirstId); diff --git a/internals-js/src/__tests__/extractSubgraphsFromSupergraph.test.ts b/internals-js/src/__tests__/extractSubgraphsFromSupergraph.test.ts index 3138433d0..ea037232a 100644 --- a/internals-js/src/__tests__/extractSubgraphsFromSupergraph.test.ts +++ b/internals-js/src/__tests__/extractSubgraphsFromSupergraph.test.ts @@ -596,7 +596,6 @@ test('preserves default values of input object fields', () => { expect(inputFieldA?.defaultValue).toBe(1234) }) - test('throw meaningful error for invalid federation directive fieldSet', () => { const supergraph = ` schema diff --git a/internals-js/src/extractSubgraphsFromSupergraph.ts b/internals-js/src/extractSubgraphsFromSupergraph.ts index 915419720..1bfbfa107 100644 --- a/internals-js/src/extractSubgraphsFromSupergraph.ts +++ b/internals-js/src/extractSubgraphsFromSupergraph.ts @@ -201,6 +201,8 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema): Subgraphs { const implementsDirective = joinSpec.implementsDirective(supergraph); const ownerDirective = joinSpec.ownerDirective(supergraph); const fieldDirective = joinSpec.fieldDirective(supergraph); + const unionMemberDirective = joinSpec.unionMemberDirective(supergraph); + const enumValueDirective = joinSpec.enumValueDirective(supergraph); const getSubgraph = (application: Directive) => { const graph = application.arguments().graph; @@ -406,23 +408,40 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema): Subgraphs { continue; } assert(isEnumType(subgraphEnum), () => `${subgraphEnum} should be an enum but found a ${subgraphEnum.kind}`); + for (const value of type.values) { - subgraphEnum.addValue(value.name); + // Before version 0.3 of the join spec (before `enumValueDirective`), we were not recording which subgraph defined which values, + // and instead aded all values to all subgraphs (at least if the type existed there). + const addValue = !enumValueDirective + || value.appliedDirectivesOf(enumValueDirective).some((d) => + graphEnumNameToSubgraphName.get(d.arguments().graph) === subgraph.name + ); + if (addValue) { + subgraphEnum.addValue(value.name); + } } } break; case 'UnionType': - // TODO: Same as for enums. We need to know in which subgraph each member is defined. - // But for now, we also add every members to all subgraphs (as long as the subgraph has both the union type - // and the member in question). for (const subgraph of subgraphs) { const subgraphUnion = subgraph.schema.type(type.name); if (!subgraphUnion) { continue; } assert(isUnionType(subgraphUnion), () => `${subgraphUnion} should be an enum but found a ${subgraphUnion.kind}`); - for (const memberType of type.types()) { - const subgraphType = subgraph.schema.type(memberType.name); + let membersInSubgraph: string[]; + if (unionMemberDirective) { + membersInSubgraph = type + .appliedDirectivesOf(unionMemberDirective) + .filter((d) => graphEnumNameToSubgraphName.get(d.arguments().graph) === subgraph.name) + .map((d) => d.arguments().member); + } else { + // Before version 0.3 of the join spec, we were not recording which subgraph defined which members, + // and instead aded all members to all subgraphs (at least if the type existed there). + membersInSubgraph = type.types().map((t) => t.name); + } + for (const memberTypeName of membersInSubgraph) { + const subgraphType = subgraph.schema.type(memberTypeName); if (subgraphType) { subgraphUnion.addType(subgraphType as ObjectType); } diff --git a/internals-js/src/federation.ts b/internals-js/src/federation.ts index c40cfb28a..73495ad4a 100644 --- a/internals-js/src/federation.ts +++ b/internals-js/src/federation.ts @@ -40,8 +40,11 @@ import { PossibleTypeExtensionsRule, print as printAST, Source, - SchemaExtensionNode, GraphQLErrorOptions, + SchemaDefinitionNode, + OperationTypeNode, + OperationTypeDefinitionNode, + ConstDirectiveNode, } from "graphql"; import { KnownTypeNamesInFederationRule } from "./validation/KnownTypeNamesInFederationRule"; import { buildSchema, buildSchemaFromAST } from "./buildSchema"; @@ -1168,13 +1171,20 @@ export function setSchemaAsFed2Subgraph(schema: Schema) { // subgraph schema to avoid having to update 20+ tests every time we use a new directive or the order of import changes ... export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])'; -export function asFed2SubgraphDocument(document: DocumentNode): DocumentNode { - const fed2LinkExtension: SchemaExtensionNode = { - kind: Kind.SCHEMA_EXTENSION, - directives: [{ - kind: Kind.DIRECTIVE, - name: { kind: Kind.NAME, value: linkDirectiveDefaultName }, - arguments: [{ +/** + * Given a document that is assumed to _not_ be a fed2 schema (it does not have a `@link` to the federation spec), + * returns an equivalent document that `@link` to the last known federation spec. + * + * @param document - the document to "augment". + * @param options.addAsSchemaExtension - defines whethere the added `@link` is added as a schema extension (`extend schema`) or + * added to the schema definition. Defaults to `true` (added as an extension), as this mimics what we tends to write manually. + */ +export function asFed2SubgraphDocument(document: DocumentNode, options?: { addAsSchemaExtension: boolean }): DocumentNode { + const directiveToAdd: ConstDirectiveNode = ({ + kind: Kind.DIRECTIVE, + name: { kind: Kind.NAME, value: linkDirectiveDefaultName }, + arguments: [ + { kind: Kind.ARGUMENT, name: { kind: Kind.NAME, value: 'url' }, value: { kind: Kind.STRING, value: federationSpec.url.toString() } @@ -1183,13 +1193,54 @@ export function asFed2SubgraphDocument(document: DocumentNode): DocumentNode { kind: Kind.ARGUMENT, name: { kind: Kind.NAME, value: 'import' }, value: { kind: Kind.LIST, values: federationSpec.directiveSpecs().map((spec) => ({ kind: Kind.STRING, value: `@${spec.name}` })) } - }] - }] - }; - return { - kind: Kind.DOCUMENT, - loc: document.loc, - definitions: document.definitions.concat(fed2LinkExtension) + } + ] + }); + if (options?.addAsSchemaExtension ?? true) { + return { + kind: Kind.DOCUMENT, + loc: document.loc, + definitions: document.definitions.concat({ + kind: Kind.SCHEMA_EXTENSION, + directives: [directiveToAdd] + }), + } + } + + // We can't add a new schema definition if it already exists. If it doesn't we need to know if there is a mutation type or + // not. + const existingSchemaDefinition = document.definitions.find((d): d is SchemaDefinitionNode => d.kind == Kind.SCHEMA_DEFINITION); + if (existingSchemaDefinition) { + return { + kind: Kind.DOCUMENT, + loc: document.loc, + definitions: document.definitions.filter((d) => d !== existingSchemaDefinition).concat([{ + ...existingSchemaDefinition, + directives: [directiveToAdd].concat(existingSchemaDefinition.directives ?? []), + }]), + } + } else { + const hasMutation = document.definitions.some((d) => d.kind === Kind.OBJECT_TYPE_DEFINITION && d.name.value === 'Mutation'); + const makeOpType = (opType: OperationTypeNode, name: string): OperationTypeDefinitionNode => ({ + kind: Kind.OPERATION_TYPE_DEFINITION, + operation: opType, + type: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: name, + } + }, + }); + return { + kind: Kind.DOCUMENT, + loc: document.loc, + definitions: document.definitions.concat({ + kind: Kind.SCHEMA_DEFINITION, + directives: [directiveToAdd], + operationTypes: [ makeOpType(OperationTypeNode.QUERY, 'Query') ].concat(hasMutation ? makeOpType(OperationTypeNode.MUTATION, 'Mutation') : []), + }), + } } } diff --git a/internals-js/src/joinSpec.ts b/internals-js/src/joinSpec.ts index 192b47134..814a894c8 100644 --- a/internals-js/src/joinSpec.ts +++ b/internals-js/src/joinSpec.ts @@ -97,6 +97,17 @@ export class JoinSpecDefinition extends FeatureDefinition { joinImplements.addArgument('interface', new NonNullType(schema.stringType())); } + if (this.version >= (new FeatureVersion(0, 3))) { + const joinUnionMember = this.addDirective(schema, 'unionMember').addLocations(DirectiveLocation.UNION); + joinUnionMember.repeatable = true; + joinUnionMember.addArgument('graph', new NonNullType(graphEnum)); + joinUnionMember.addArgument('member', new NonNullType(schema.stringType())); + + const joinEnumValue = this.addDirective(schema, 'enumValue').addLocations(DirectiveLocation.ENUM_VALUE); + joinEnumValue.repeatable = true; + joinEnumValue.addArgument('graph', new NonNullType(graphEnum)); + } + if (this.isV01()) { const joinOwner = this.addDirective(schema, 'owner').addLocations(DirectiveLocation.OBJECT); joinOwner.addArgument('graph', new NonNullType(graphEnum)); @@ -183,6 +194,14 @@ export class JoinSpecDefinition extends FeatureDefinition { return this.directive(schema, 'field')!; } + unionMemberDirective(schema: Schema): DirectiveDefinition<{graph: string, member: string}> | undefined { + return this.directive(schema, 'unionMember'); + } + + enumValueDirective(schema: Schema): DirectiveDefinition<{graph: string}> | undefined { + return this.directive(schema, 'enumValue'); + } + ownerDirective(schema: Schema): DirectiveDefinition<{graph: string}> | undefined { return this.directive(schema, 'owner'); } diff --git a/query-planner-js/src/__tests__/buildPlan.test.ts b/query-planner-js/src/__tests__/buildPlan.test.ts index e35f7f6b8..99806d413 100644 --- a/query-planner-js/src/__tests__/buildPlan.test.ts +++ b/query-planner-js/src/__tests__/buildPlan.test.ts @@ -5170,10 +5170,7 @@ describe('merged abstract types handling', () => { `); }); - // TODO: this test currently doesn't work due to https://github.com/apollographql/federation/issues/2256 - // (it is not a direct test of that issue, but one of its consequence nonetheles). We should enable it - // with the fix of that issue. - test.skip('union/union interaction, but no need to type-explode', () => { + test('union/union interaction, but no need to type-explode', () => { const subgraph1 = { name: 'Subgraph1', typeDefs: gql` @@ -5240,3 +5237,79 @@ describe('merged abstract types handling', () => { `); }); }); + +test('handles spread unions correctly', () => { + const subgraph1 = { + name: 'Subgraph1', + typeDefs: gql` + type Query { + u: U + } + + union U = A | B + + type A @key(fields: "id") { + id: ID! + a1: Int + } + + type B { + id: ID! + b: Int + } + + type C @key(fields: "id") { + id: ID! + c1: Int + } + ` + } + + const subgraph2 = { + name: 'Subgraph2', + typeDefs: gql` + type Query { + otherQuery: U + } + + union U = A | C + + type A @key(fields: "id") { + id: ID! + a2: Int + } + + type C @key(fields: "id") { + id: ID! + c2: Int + } + ` + } + + const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); + const operation = operationFromDocument(api, gql` + { + u { + ... on C { + c1 + } + } + } + `); + + const plan = queryPlanner.buildQueryPlan(operation); + // Note: it's important that the query below DO NOT include the `... on C` part. Because in + // Subgraph 1, `C` is not a part of the union `U` and so a spread for `C` inside `u` is invalid + // GraphQL. + expect(plan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "Subgraph1") { + { + u { + __typename + } + } + }, + } + `); +})