Skip to content

Commit 3e224c8

Browse files
authored
fix: check for external on parent type (#167)
Adds support for `@external` on types. According to the spec (https://www.apollographql.com/docs/graphos/schema-design/federated-schemas/reference/directives#external) if `@external` is defined on the type, then all fields inside that type are considered external.
1 parent 8593e38 commit 3e224c8

File tree

3 files changed

+157
-40
lines changed

3 files changed

+157
-40
lines changed

.changeset/tiny-ways-add.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@theguild/federation-composition": patch
3+
---
4+
5+
Respect @external on parent type

__tests__/composition.spec.ts

Lines changed: 145 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4026,6 +4026,63 @@ testImplementations((api) => {
40264026
`);
40274027
});
40284028

4029+
test("@external + @tag", () => {
4030+
const subgraphs = [
4031+
{
4032+
name: "a",
4033+
typeDefs: parse(/* GraphQL */ `
4034+
extend schema @link(url: "https://specs.apollo.dev/federation/${version}", import: ["@key", "@tag"])
4035+
4036+
type Product @key(fields: "id") {
4037+
id: ID! @tag(name: "public")
4038+
name: String! @tag(name: "public")
4039+
inStock: Int! @tag(name: "public")
4040+
}
4041+
4042+
type Query {
4043+
products: [Product] @tag(name: "public")
4044+
}
4045+
`),
4046+
},
4047+
{
4048+
name: "b",
4049+
typeDefs: parse(/* GraphQL */ `
4050+
extend schema
4051+
@link(
4052+
url: "https://specs.apollo.dev/federation/${version}"
4053+
import: ["@key", "@external", "@requires", "@tag"]
4054+
)
4055+
4056+
type Product @key(fields: "id") {
4057+
id: ID! @tag(name: "public")
4058+
inStock: Int! @external
4059+
isAvailable: String! @requires(fields: "inStock") @tag(name: "public")
4060+
}
4061+
`),
4062+
},
4063+
];
4064+
4065+
const result = composeServices(subgraphs);
4066+
expect(result.errors).toEqual(undefined);
4067+
assertCompositionSuccess(result);
4068+
4069+
expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ `
4070+
type Product
4071+
@join__type(graph: A, key: "id")
4072+
@join__type(graph: B, key: "id") {
4073+
id: ID! @tag(name: "public")
4074+
name: String! @join__field(graph: A) @tag(name: "public")
4075+
inStock: Int!
4076+
@join__field(graph: A)
4077+
@join__field(graph: B, external: true)
4078+
@tag(name: "public")
4079+
isAvailable: String!
4080+
@join__field(graph: B, requires: "inStock")
4081+
@tag(name: "public")
4082+
}
4083+
`);
4084+
});
4085+
40294086
test("@tag", () => {
40304087
const result = composeServices([
40314088
{
@@ -7224,6 +7281,75 @@ testImplementations((api) => {
72247281
});
72257282

72267283
test("external on non-key field of an entity type", () => {
7284+
/** "apollo" errors on this schema definition */
7285+
api.runIf("guild", () => {
7286+
let result = api.composeServices([
7287+
{
7288+
name: "a",
7289+
typeDefs: parse(/* GraphQL */ `
7290+
extend schema
7291+
@link(url: "https://specs.apollo.dev/link/v1.0")
7292+
@link(
7293+
url: "https://specs.apollo.dev/federation/v2.8"
7294+
import: ["@key", "@external", "@requires", "@tag"]
7295+
)
7296+
7297+
type User @key(fields: "id") {
7298+
id: ID! @tag(name: "public")
7299+
creditScore: Int @requires(fields: "ssn") @tag(name: "public")
7300+
}
7301+
7302+
extend type User @external {
7303+
ssn: String
7304+
}
7305+
`),
7306+
},
7307+
{
7308+
name: "b",
7309+
typeDefs: parse(/* GraphQL */ `
7310+
schema
7311+
@link(url: "https://specs.apollo.dev/link/v1.0")
7312+
@link(
7313+
url: "https://specs.apollo.dev/federation/v2.8"
7314+
import: ["@key", "@tag"]
7315+
) {
7316+
query: Query
7317+
}
7318+
7319+
type Query {
7320+
user: User @tag(name: "public")
7321+
}
7322+
7323+
type User @key(fields: "id") {
7324+
id: ID! @tag(name: "public")
7325+
ssn: String
7326+
}
7327+
`),
7328+
},
7329+
]);
7330+
7331+
expect(result.errors).toEqual(undefined);
7332+
assertCompositionSuccess(result);
7333+
7334+
expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ `
7335+
type Query @join__type(graph: A) @join__type(graph: B) {
7336+
user: User @join__field(graph: B) @tag(name: "public")
7337+
}
7338+
7339+
type User
7340+
@join__type(graph: A, key: "id")
7341+
@join__type(graph: B, key: "id") {
7342+
id: ID! @tag(name: "public")
7343+
creditScore: Int
7344+
@join__field(graph: A, requires: "ssn")
7345+
@tag(name: "public")
7346+
ssn: String
7347+
@join__field(external: true, graph: A)
7348+
@join__field(graph: B)
7349+
}
7350+
`);
7351+
});
7352+
72277353
let result = api.composeServices([
72287354
{
72297355
name: "foo",
@@ -7233,22 +7359,18 @@ testImplementations((api) => {
72337359
url: "https://specs.apollo.dev/federation/v2.3"
72347360
import: ["@key", "@external", "@provides", "@shareable"]
72357361
)
7236-
72377362
type Note @key(fields: "id") @shareable {
72387363
id: ID!
72397364
name: String @external
72407365
author: User @external
72417366
}
7242-
72437367
type User @key(fields: "id", resolvable: false) {
72447368
id: ID!
72457369
}
7246-
72477370
type PrivateNote @key(fields: "id") @shareable {
72487371
id: ID!
72497372
note: Note @provides(fields: "name author { id }")
72507373
}
7251-
72527374
type Query {
72537375
note: Note @shareable
72547376
privateNote: PrivateNote @shareable
@@ -7260,29 +7382,25 @@ testImplementations((api) => {
72607382
typeDefs: parse(/* GraphQL */ `
72617383
extend schema
72627384
@link(
7263-
url: "https://specs.apollo.dev/federation/v2.3"
7264-
import: ["@key", "@shareable"]
7385+
url: "https://specs.apollo.dev/federation/v2.3"
7386+
import: ["@key", "@shareable"]
72657387
)
7266-
72677388
type Note @key(fields: "id") @shareable {
72687389
id: ID!
72697390
name: String
72707391
author: User
72717392
}
7272-
72737393
type User @key(fields: "id") {
7274-
id: ID!
7275-
name: String
7276-
}
7277-
7278-
type PrivateNote @key(fields: "id") @shareable {
7279-
id: ID!
7280-
note: Note
7394+
id: ID!
7395+
name: String
72817396
}
7282-
7283-
type Query {
7284-
note: Note @shareable
7285-
privateNote: PrivateNote @shareable
7397+
type PrivateNote @key(fields: "id") @shareable {
7398+
id: ID!
7399+
note: Note
7400+
}
7401+
type Query {
7402+
note: Note @shareable
7403+
privateNote: PrivateNote @shareable
72867404
}
72877405
`),
72887406
},
@@ -7307,29 +7425,25 @@ testImplementations((api) => {
73077425
result = api.composeServices([
73087426
{
73097427
name: "foo",
7310-
typeDefs: parse(/* GraphQL */ `
7428+
typeDefs: parse(/* GraphQL */ `
73117429
extend schema
7312-
@link(
7430+
@link(
73137431
url: "https://specs.apollo.dev/federation/v2.3"
73147432
import: ["@key", "@external", "@provides", "@shareable"]
73157433
)
7316-
73177434
type Note @key(fields: "id") @shareable {
73187435
id: ID!
73197436
name: String @external
73207437
author: User @external
73217438
}
7322-
73237439
type User @key(fields: "id", resolvable: false) {
73247440
id: ID!
73257441
}
7326-
73277442
type PrivateNote @key(fields: "id") @shareable {
73287443
id: ID!
73297444
note: Note @provides(fields: "name author { id }")
7330-
}
7331-
7332-
type Query {
7445+
}
7446+
type Query {
73337447
note: Note @shareable
73347448
privateNote: PrivateNote @shareable
73357449
}
@@ -7343,23 +7457,19 @@ testImplementations((api) => {
73437457
url: "https://specs.apollo.dev/federation/v2.3"
73447458
import: ["@key", "@shareable"]
73457459
)
7346-
73477460
type Note @key(fields: "id") @shareable {
73487461
id: ID!
73497462
name: String
73507463
author: User
7351-
}
7352-
7353-
type User @key(fields: "id") {
7464+
}
7465+
type User @key(fields: "id") {
73547466
id: ID!
73557467
name: String
73567468
}
7357-
73587469
type PrivateNote @key(fields: "id") @shareable {
73597470
id: ID!
73607471
note: Note
73617472
}
7362-
73637473
type Query {
73647474
note: Note @shareable
73657475
privateNote: PrivateNote @shareable
@@ -7374,15 +7484,13 @@ testImplementations((api) => {
73747484
url: "https://specs.apollo.dev/federation/v2.3"
73757485
import: ["@key", "@external", "@provides", "@shareable"]
73767486
)
7377-
73787487
type Query {
73797488
hello: String
73807489
}
7381-
73827490
type Note @key(fields: "id") @shareable {
73837491
id: ID!
73847492
tag: String
7385-
}
7493+
}
73867494
`),
73877495
},
73887496
]);
@@ -7402,7 +7510,7 @@ testImplementations((api) => {
74027510
@join__field(external: true, graph: FOO)
74037511
@join__field(graph: BAR)
74047512
tag: String @join__field(graph: BAZ)
7405-
}
7513+
}
74067514
`);
74077515
});
74087516

src/subgraph/helpers.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,9 +331,13 @@ export function visitFields({
331331
}
332332

333333
if (!isTypename && (interceptNonExternalField || interceptExternalField)) {
334-
const isExternal = selectionFieldDef.directives?.some((d) =>
335-
context.isAvailableFederationDirective("external", d),
336-
);
334+
const isExternal =
335+
selectionFieldDef.directives?.some((d) =>
336+
context.isAvailableFederationDirective("external", d),
337+
) ||
338+
typeDefinition.directives?.some((d) =>
339+
context.isAvailableFederationDirective("external", d),
340+
);
337341
const fieldName = selection.name.value;
338342

339343
// ignore if it's not a leaf

0 commit comments

Comments
 (0)