Skip to content

Commit bc07e97

Browse files
authored
fix: handle @requires dependency on fields returned by @interfaceObject (#3318)
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.
1 parent 9cbdcb5 commit bc07e97

File tree

3 files changed

+158
-6
lines changed

3 files changed

+158
-6
lines changed

.changeset/ten-years-flash.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@apollo/composition": patch
3+
---
4+
5+
Fixed handling `@requires` dependency on fields returned by `@interfaceObject`
6+
7+
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.

composition-js/src/__tests__/compose.test.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4075,6 +4075,145 @@ describe('composition', () => {
40754075
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
40764076
assertCompositionSuccess(result);
40774077
});
4078+
4079+
it('composes @requires references to @interfaceObject', () => {
4080+
const subgraph1 = {
4081+
name: 'A',
4082+
url: 'https://Subgraph1',
4083+
typeDefs: gql`
4084+
4085+
type T implements I @key(fields: "id") {
4086+
id: ID!
4087+
i1: U! @external
4088+
specific: U! @requires(fields: "i1 { u1 }")
4089+
}
4090+
4091+
interface I @key(fields: "id") {
4092+
id: ID!
4093+
i1: U!
4094+
}
4095+
4096+
type U @shareable {
4097+
u1: String
4098+
}
4099+
4100+
type Query {
4101+
example: T!
4102+
}
4103+
`
4104+
}
4105+
4106+
const subgraph2 = {
4107+
name: 'B',
4108+
url: 'https://Subgraph2',
4109+
typeDefs: gql`
4110+
type I @key(fields: "id") @interfaceObject {
4111+
id: ID!
4112+
i1: U!
4113+
}
4114+
4115+
type U @shareable {
4116+
u1: String
4117+
}
4118+
`
4119+
}
4120+
4121+
let result = composeAsFed2Subgraphs([subgraph1, subgraph2]);
4122+
assertCompositionSuccess(result);
4123+
console.log(result.supergraphSdl);
4124+
4125+
expect(result.supergraphSdl).toMatchString(`
4126+
schema
4127+
@link(url: "https://specs.apollo.dev/link/v1.0")
4128+
@link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION)
4129+
{
4130+
query: Query
4131+
}
4132+
4133+
directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION
4134+
4135+
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
4136+
4137+
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
4138+
4139+
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
4140+
4141+
directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
4142+
4143+
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
4144+
4145+
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
4146+
4147+
directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
4148+
4149+
interface I
4150+
@join__type(graph: A, key: "id")
4151+
@join__type(graph: B, key: "id", isInterfaceObject: true)
4152+
{
4153+
id: ID!
4154+
i1: U!
4155+
}
4156+
4157+
input join__ContextArgument {
4158+
name: String!
4159+
type: String!
4160+
context: String!
4161+
selection: join__FieldValue!
4162+
}
4163+
4164+
scalar join__DirectiveArguments
4165+
4166+
scalar join__FieldSet
4167+
4168+
scalar join__FieldValue
4169+
4170+
enum join__Graph {
4171+
A @join__graph(name: "A", url: "https://Subgraph1")
4172+
B @join__graph(name: "B", url: "https://Subgraph2")
4173+
}
4174+
4175+
scalar link__Import
4176+
4177+
enum link__Purpose {
4178+
"""
4179+
\`SECURITY\` features provide metadata necessary to securely resolve fields.
4180+
"""
4181+
SECURITY
4182+
4183+
"""
4184+
\`EXECUTION\` features provide metadata necessary for operation execution.
4185+
"""
4186+
EXECUTION
4187+
}
4188+
4189+
type Query
4190+
@join__type(graph: A)
4191+
@join__type(graph: B)
4192+
{
4193+
example: T! @join__field(graph: A)
4194+
}
4195+
4196+
type T implements I
4197+
@join__implements(graph: A, interface: "I")
4198+
@join__type(graph: A, key: "id")
4199+
{
4200+
id: ID!
4201+
i1: U! @join__field(graph: A, external: true)
4202+
specific: U! @join__field(graph: A, requires: "i1 { u1 }")
4203+
}
4204+
4205+
type U
4206+
@join__type(graph: A)
4207+
@join__type(graph: B)
4208+
{
4209+
u1: String
4210+
}
4211+
`);
4212+
4213+
// composes regardless of the subgraph order
4214+
result = composeAsFed2Subgraphs([subgraph2, subgraph1]);
4215+
assertCompositionSuccess(result);
4216+
})
40784217
});
40794218

40804219
describe('@authenticated', () => {

composition-js/src/merging/merge.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ import {
8585
inaccessibleIdentity,
8686
FeatureDefinitions,
8787
CONNECT_VERSIONS,
88+
ScalarType
8889
} from "@apollo/federation-internals";
8990
import { ASTNode, GraphQLError, DirectiveLocation } from "graphql";
9091
import {
@@ -654,7 +655,8 @@ class Merger {
654655
const interfaceTypes: InterfaceType[] = [];
655656
const unionTypes: UnionType[] = [];
656657
const enumTypes: EnumType[] = [];
657-
const nonUnionEnumTypes: NamedType[] = [];
658+
const scalarTypes: ScalarType[] = [];
659+
const inputObjectTypes: InputObjectType[] = [];
658660

659661
this.merged.types().forEach(type => {
660662
if (
@@ -667,19 +669,23 @@ class Merger {
667669
switch (type.kind) {
668670
case 'UnionType':
669671
unionTypes.push(type);
670-
return;
672+
break;
671673
case 'EnumType':
672674
enumTypes.push(type);
673-
return;
675+
break;
674676
case 'ObjectType':
675677
objectTypes.push(type);
676678
break;
677679
case 'InterfaceType':
678680
interfaceTypes.push(type);
679681
break;
682+
case 'ScalarType':
683+
scalarTypes.push(type);
684+
break;
685+
case 'InputObjectType':
686+
inputObjectTypes.push(type);
687+
break;
680688
}
681-
682-
nonUnionEnumTypes.push(type);
683689
});
684690

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

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

0 commit comments

Comments
 (0)