Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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, input objects 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
Loading