Skip to content

Commit e507fdd

Browse files
authored
Fix @join__field emission for Fed v1 @external and @override+ @requires edge cases (#282)
Reproduced issues caused by #267 and added a few test cases to showcase differences between Apollo's supergraph and ours.
1 parent 32f2b32 commit e507fdd

11 files changed

Lines changed: 824 additions & 343 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@theguild/federation-composition": patch
3+
---
4+
5+
Fix supergraph `@join__field` emission for Federation v1 `@external` fields and improve override/requires edge-case handling.
6+
7+
- Drop Federation v1 `@join__field(external: true)` fields based on key usage in the subgraph instead of aggregated field key usage across subgraphs.
8+
- Avoid emitting redundant `@join__field(external: true)` metadata when a field is only required through overridden paths.
9+
- Tighten `@join__field` emission around `@override` and interface type fields.

__tests__/composition.spec.ts

Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8271,4 +8271,380 @@ testImplementations((api) => {
82718271
assertCompositionSuccess(result);
82728272
expect(result.supergraphSdl).not.includes("scalar FieldSet");
82738273
});
8274+
8275+
test("keeps @join__field for object type extension with @key being @external", () => {
8276+
const result = api.composeServices([
8277+
{
8278+
name: "owner-service",
8279+
typeDefs: parse(/* GraphQL */ `
8280+
extend schema
8281+
@link(
8282+
url: "https://specs.apollo.dev/federation/v2.3"
8283+
import: ["@key", "@external"]
8284+
)
8285+
8286+
extend type Repository @key(fields: "id name") {
8287+
id: ID! @external
8288+
name: String! @external
8289+
}
8290+
`),
8291+
},
8292+
{
8293+
name: "team-service",
8294+
typeDefs: parse(/* GraphQL */ `
8295+
extend schema
8296+
@link(
8297+
url: "https://specs.apollo.dev/federation/v2.3"
8298+
import: ["@key", "@external"]
8299+
)
8300+
8301+
extend type Repository @key(fields: "id") {
8302+
id: ID! @external
8303+
}
8304+
`),
8305+
},
8306+
{
8307+
name: "repo-service",
8308+
typeDefs: parse(/* GraphQL */ `
8309+
extend schema
8310+
@link(
8311+
url: "https://specs.apollo.dev/federation/v2.3"
8312+
import: ["@key", "@shareable"]
8313+
)
8314+
8315+
type Query {
8316+
repo: Repository
8317+
}
8318+
8319+
type Repository @key(fields: "id") {
8320+
id: ID!
8321+
name: String! @shareable
8322+
}
8323+
`),
8324+
},
8325+
]);
8326+
8327+
assertCompositionSuccess(result);
8328+
8329+
expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ `
8330+
type Repository
8331+
@join__type(extension: true, graph: OWNER_SERVICE, key: "id name")
8332+
@join__type(extension: true, graph: TEAM_SERVICE, key: "id")
8333+
@join__type(graph: REPO_SERVICE, key: "id") {
8334+
id: ID!
8335+
name: String!
8336+
@join__field(graph: OWNER_SERVICE)
8337+
@join__field(graph: REPO_SERVICE)
8338+
}
8339+
`);
8340+
});
8341+
8342+
test("keep @join__field for object type extension where @key is @external and have other @external fields used with @requires", () => {
8343+
const result = api.composeServices([
8344+
{
8345+
name: "developer-service",
8346+
typeDefs: parse(/* GraphQL */ `
8347+
extend schema
8348+
@link(
8349+
url: "https://specs.apollo.dev/federation/v2.3"
8350+
import: ["@key", "@shareable"]
8351+
)
8352+
8353+
type Query {
8354+
me: Developer
8355+
}
8356+
8357+
type Developer @key(fields: "id") @key(fields: "guestGuid") {
8358+
id: ID!
8359+
guestGuid: ID! @shareable
8360+
isMe: Boolean! @shareable
8361+
myFollowingStatus: String! @shareable
8362+
userVisibility: String! @shareable
8363+
}
8364+
`),
8365+
},
8366+
{
8367+
name: "team-service",
8368+
typeDefs: parse(/* GraphQL */ `
8369+
extend schema
8370+
@link(
8371+
url: "https://specs.apollo.dev/federation/v2.3"
8372+
import: ["@key", "@external", "@requires"]
8373+
)
8374+
8375+
extend type Developer @key(fields: "guestGuid") {
8376+
guestGuid: ID! @external
8377+
myFollowingStatus: String! @external
8378+
userVisibility: String! @external
8379+
guestSavedLists: [ID!]!
8380+
@requires(fields: "myFollowingStatus userVisibility")
8381+
}
8382+
`),
8383+
},
8384+
{
8385+
name: "commit-service",
8386+
typeDefs: parse(/* GraphQL */ `
8387+
extend schema
8388+
@link(
8389+
url: "https://specs.apollo.dev/federation/v2.3"
8390+
import: ["@key", "@external", "@requires"]
8391+
)
8392+
8393+
extend type Developer @key(fields: "id") {
8394+
id: ID! @external
8395+
isMe: Boolean! @external
8396+
userVisibility: String! @external
8397+
commits: [ID!]! @requires(fields: "isMe userVisibility")
8398+
}
8399+
`),
8400+
},
8401+
{
8402+
name: "reaction-service",
8403+
typeDefs: parse(/* GraphQL */ `
8404+
extend schema
8405+
@link(
8406+
url: "https://specs.apollo.dev/federation/v2.3"
8407+
import: ["@key", "@external"]
8408+
)
8409+
8410+
extend type Developer @key(fields: "id") {
8411+
id: ID! @external
8412+
}
8413+
`),
8414+
},
8415+
]);
8416+
8417+
assertCompositionSuccess(result);
8418+
8419+
expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ `
8420+
type Developer
8421+
@join__type(extension: true, graph: TEAM_SERVICE, key: "guestGuid")
8422+
@join__type(extension: true, graph: REACTION_SERVICE, key: "id")
8423+
@join__type(extension: true, graph: COMMIT_SERVICE, key: "id")
8424+
@join__type(graph: DEVELOPER_SERVICE, key: "guestGuid")
8425+
@join__type(graph: DEVELOPER_SERVICE, key: "id") {
8426+
guestGuid: ID!
8427+
@join__field(graph: TEAM_SERVICE)
8428+
@join__field(graph: DEVELOPER_SERVICE)
8429+
guestSavedLists: [ID!]!
8430+
@join__field(
8431+
graph: TEAM_SERVICE
8432+
requires: "myFollowingStatus userVisibility"
8433+
)
8434+
id: ID!
8435+
@join__field(graph: REACTION_SERVICE)
8436+
@join__field(graph: COMMIT_SERVICE)
8437+
@join__field(graph: DEVELOPER_SERVICE)
8438+
isMe: Boolean!
8439+
@join__field(external: true, graph: COMMIT_SERVICE)
8440+
@join__field(graph: DEVELOPER_SERVICE)
8441+
myFollowingStatus: String!
8442+
@join__field(external: true, graph: TEAM_SERVICE)
8443+
@join__field(graph: DEVELOPER_SERVICE)
8444+
commits: [ID!]!
8445+
@join__field(graph: COMMIT_SERVICE, requires: "isMe userVisibility")
8446+
userVisibility: String!
8447+
@join__field(external: true, graph: TEAM_SERVICE)
8448+
@join__field(external: true, graph: COMMIT_SERVICE)
8449+
@join__field(graph: DEVELOPER_SERVICE)
8450+
}
8451+
`);
8452+
});
8453+
8454+
test("@external on non-key field in Federation v1", () => {
8455+
let result = api.composeServices([
8456+
{
8457+
name: "users",
8458+
typeDefs: parse(/* GraphQL */ `
8459+
type Query {
8460+
users: [User]
8461+
}
8462+
8463+
type User @key(fields: "id") {
8464+
id: ID!
8465+
name: String! @external
8466+
}
8467+
`),
8468+
},
8469+
{
8470+
name: "extra",
8471+
typeDefs: parse(/* GraphQL */ `
8472+
type User @key(fields: "id") {
8473+
id: ID!
8474+
name: String!
8475+
}
8476+
`),
8477+
},
8478+
]);
8479+
8480+
assertCompositionSuccess(result);
8481+
expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ `
8482+
type User
8483+
@join__type(graph: EXTRA, key: "id")
8484+
@join__type(graph: USERS, key: "id") {
8485+
id: ID!
8486+
name: String! @join__field(graph: EXTRA)
8487+
}
8488+
`);
8489+
});
8490+
8491+
test("@external on non-key field in Federation v1", () => {
8492+
let result = api.composeServices([
8493+
{
8494+
name: "users",
8495+
typeDefs: parse(/* GraphQL */ `
8496+
type Query {
8497+
users: [User]
8498+
}
8499+
8500+
type User @key(fields: "id") {
8501+
id: ID!
8502+
name: String! @external
8503+
}
8504+
`),
8505+
},
8506+
{
8507+
name: "extra",
8508+
typeDefs: parse(/* GraphQL */ `
8509+
type User @key(fields: "id") {
8510+
id: ID!
8511+
name: String!
8512+
}
8513+
`),
8514+
},
8515+
]);
8516+
8517+
assertCompositionSuccess(result);
8518+
expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ `
8519+
type User
8520+
@join__type(graph: EXTRA, key: "id")
8521+
@join__type(graph: USERS, key: "id") {
8522+
id: ID!
8523+
name: String! @join__field(graph: EXTRA)
8524+
}
8525+
`);
8526+
});
8527+
8528+
test("@external on non-key field in Federation v2", () => {
8529+
let result = api.composeServices([
8530+
{
8531+
name: "users",
8532+
typeDefs: parse(/* GraphQL */ `
8533+
extend schema
8534+
@link(
8535+
url: "https://specs.apollo.dev/federation/v2.3"
8536+
import: ["@key", "@external"]
8537+
)
8538+
8539+
type Query {
8540+
users: [User]
8541+
}
8542+
8543+
type User @key(fields: "id") {
8544+
id: ID!
8545+
name: String! @external
8546+
}
8547+
`),
8548+
},
8549+
{
8550+
name: "extra",
8551+
typeDefs: parse(/* GraphQL */ `
8552+
extend schema
8553+
@link(
8554+
url: "https://specs.apollo.dev/federation/v2.3"
8555+
import: ["@key", "@external"]
8556+
)
8557+
8558+
type User @key(fields: "id") {
8559+
id: ID!
8560+
name: String!
8561+
}
8562+
`),
8563+
},
8564+
]);
8565+
8566+
assertCompositionFailure(result);
8567+
});
8568+
8569+
test("@external object type field implementing interface field", () => {
8570+
const result = api.composeServices([
8571+
{
8572+
name: "github_core",
8573+
typeDefs: parse(/* GraphQL */ `
8574+
extend schema
8575+
@link(
8576+
url: "https://specs.apollo.dev/federation/v2.3"
8577+
import: ["@key"]
8578+
)
8579+
8580+
type Query {
8581+
checks: QueuedCheckRunInfo
8582+
}
8583+
8584+
interface CheckRunInfo {
8585+
id: ID!
8586+
status: String!
8587+
}
8588+
8589+
type QueuedCheckRunInfo implements CheckRunInfo @key(fields: "id") {
8590+
id: ID!
8591+
status: String!
8592+
commitOid: String!
8593+
}
8594+
`),
8595+
},
8596+
{
8597+
name: "github_action",
8598+
typeDefs: parse(/* GraphQL */ `
8599+
extend schema
8600+
@link(
8601+
url: "https://specs.apollo.dev/federation/v2.3"
8602+
import: ["@key", "@external", "@requires"]
8603+
)
8604+
8605+
extend type QueuedCheckRunInfo implements CheckRunInfo
8606+
@key(fields: "id") {
8607+
id: ID! @external
8608+
status: String! @external
8609+
commitOid: String! @external
8610+
workflowRunId: ID! @requires(fields: "commitOid")
8611+
}
8612+
8613+
interface CheckRunInfo {
8614+
id: ID!
8615+
status: String!
8616+
}
8617+
`),
8618+
},
8619+
]);
8620+
8621+
assertCompositionSuccess(result);
8622+
8623+
expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ `
8624+
interface CheckRunInfo
8625+
@join__type(graph: GITHUB_ACTION)
8626+
@join__type(graph: GITHUB_CORE) {
8627+
id: ID!
8628+
status: String!
8629+
}
8630+
`);
8631+
8632+
expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ `
8633+
type QueuedCheckRunInfo implements CheckRunInfo
8634+
@join__implements(graph: GITHUB_ACTION, interface: "CheckRunInfo")
8635+
@join__implements(graph: GITHUB_CORE, interface: "CheckRunInfo")
8636+
@join__type(graph: GITHUB_CORE, key: "id")
8637+
@join__type(graph: GITHUB_ACTION, key: "id", extension: true) {
8638+
id: ID!
8639+
status: String!
8640+
@join__field(graph: GITHUB_CORE)
8641+
@join__field(graph: GITHUB_ACTION, external: true)
8642+
commitOid: String!
8643+
@join__field(graph: GITHUB_CORE)
8644+
@join__field(graph: GITHUB_ACTION, external: true)
8645+
workflowRunId: ID!
8646+
@join__field(graph: GITHUB_ACTION, requires: "commitOid")
8647+
}
8648+
`);
8649+
});
82748650
});

0 commit comments

Comments
 (0)