Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/ten-years-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@apollo/composition": patch
---

Fixed handling `@requires` dependency on fields returned by `@interfaceObject`

Depending on the merge order of the types, we could fail composition if a type that `@requires` data from an `@interfaceObject` is merged before the interface. Updated merge logic to use explicit merge order of scalars, input objects, interfaces, and finally objects.
139 changes: 139 additions & 0 deletions composition-js/src/__tests__/compose.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4075,6 +4075,145 @@ describe('composition', () => {
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
assertCompositionSuccess(result);
});

it('composes @requires references to @interfaceObject', () => {
const subgraph1 = {
name: 'A',
url: 'https://Subgraph1',
typeDefs: gql`

type T implements I @key(fields: "id") {
id: ID!
i1: U! @external
specific: U! @requires(fields: "i1 { u1 }")
}

interface I @key(fields: "id") {
id: ID!
i1: U!
}

type U @shareable {
u1: String
}

type Query {
example: T!
}
`
}

const subgraph2 = {
name: 'B',
url: 'https://Subgraph2',
typeDefs: gql`
type I @key(fields: "id") @interfaceObject {
id: ID!
i1: U!
}

type U @shareable {
u1: String
}
`
}

let result = composeAsFed2Subgraphs([subgraph1, subgraph2]);
assertCompositionSuccess(result);
console.log(result.supergraphSdl);

expect(result.supergraphSdl).toMatchString(`
schema
@link(url: "https://specs.apollo.dev/link/v1.0")
@link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION)
{
query: Query
}

directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION

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, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION

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, 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

interface I
@join__type(graph: A, key: "id")
@join__type(graph: B, key: "id", isInterfaceObject: true)
{
id: ID!
i1: U!
}

input join__ContextArgument {
name: String!
type: String!
context: String!
selection: join__FieldValue!
}

scalar join__DirectiveArguments

scalar join__FieldSet

scalar join__FieldValue

enum join__Graph {
A @join__graph(name: "A", url: "https://Subgraph1")
B @join__graph(name: "B", url: "https://Subgraph2")
}

scalar link__Import

enum link__Purpose {
"""
\`SECURITY\` features provide metadata necessary to securely resolve fields.
"""
SECURITY

"""
\`EXECUTION\` features provide metadata necessary for operation execution.
"""
EXECUTION
}

type Query
@join__type(graph: A)
@join__type(graph: B)
{
example: T! @join__field(graph: A)
}

type T implements I
@join__implements(graph: A, interface: "I")
@join__type(graph: A, key: "id")
{
id: ID!
i1: U! @join__field(graph: A, external: true)
specific: U! @join__field(graph: A, requires: "i1 { u1 }")
}

type U
@join__type(graph: A)
@join__type(graph: B)
{
u1: String
}
`);

// composes regardless of the subgraph order
result = composeAsFed2Subgraphs([subgraph2, subgraph1]);
assertCompositionSuccess(result);
})
});

describe('@authenticated', () => {
Expand Down
18 changes: 12 additions & 6 deletions composition-js/src/merging/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import {
inaccessibleIdentity,
FeatureDefinitions,
CONNECT_VERSIONS,
ScalarType
} from "@apollo/federation-internals";
import { ASTNode, GraphQLError, DirectiveLocation } from "graphql";
import {
Expand Down Expand Up @@ -654,7 +655,8 @@ class Merger {
const interfaceTypes: InterfaceType[] = [];
const unionTypes: UnionType[] = [];
const enumTypes: EnumType[] = [];
const nonUnionEnumTypes: NamedType[] = [];
const scalarTypes: ScalarType[] = [];
const inputObjectTypes: InputObjectType[] = [];

this.merged.types().forEach(type => {
if (
Expand All @@ -667,19 +669,23 @@ class Merger {
switch (type.kind) {
case 'UnionType':
unionTypes.push(type);
return;
break;
case 'EnumType':
enumTypes.push(type);
return;
break;
case 'ObjectType':
objectTypes.push(type);
break;
case 'InterfaceType':
interfaceTypes.push(type);
break;
case 'ScalarType':
scalarTypes.push(type);
break;
case 'InputObjectType':
inputObjectTypes.push(type);
break;
}

nonUnionEnumTypes.push(type);
});

// Then, for object and interface types, we merge the 'implements' relationship, and we merge the unions.
Expand All @@ -705,7 +711,7 @@ class Merger {
);

// We've already merged unions above and we've going to merge enums last
for (const type of nonUnionEnumTypes) {
for (const type of [...scalarTypes, ...inputObjectTypes, ...interfaceTypes, ...objectTypes]) {
this.mergeType(this.subgraphsTypes(type), type);
}

Expand Down