Skip to content

Commit 1b98c17

Browse files
authored
fix: invalid public sdl for inaccessible interface type (#198)
The public schema SDL is wrong when it implements an inaccessible interface type, it retains the interface implementation even though it is not part of the public schema. See changeset for an example diff of what was fixed
1 parent d38ea11 commit 1b98c17

File tree

3 files changed

+215
-1
lines changed

3 files changed

+215
-1
lines changed

.changeset/huge-toes-mix.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
"@theguild/federation-composition": patch
3+
---
4+
5+
Fix public schema SDL in case a object type implements an inaccessible interface.
6+
7+
8+
Composing the following subgraph:
9+
10+
```graphql
11+
schema
12+
@link(
13+
url: "https://specs.apollo.dev/federation/v2.3"
14+
import: ["@inaccessible"]
15+
) {
16+
query: Query
17+
}
18+
19+
type Query {
20+
user: User!
21+
}
22+
23+
interface Node @inaccessible {
24+
id: ID!
25+
}
26+
27+
type User implements Node {
28+
id: ID!
29+
}
30+
```
31+
32+
now result in the following valid public SDL:
33+
34+
```diff
35+
type Query {
36+
user: User!
37+
}
38+
39+
- type User implements Node {
40+
+ type User {
41+
id: ID!
42+
}
43+
```

__tests__/composition.spec.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7925,4 +7925,132 @@ testImplementations((api) => {
79257925
`Invalid value for "@tag(name:)" of type "String!" in application of "@tag" to "Query.foo".`,
79267926
);
79277927
});
7928+
7929+
test("inaccessible interface implemented by accessible types", () => {
7930+
const sdl = /* GraphQL */ `
7931+
schema
7932+
@link(
7933+
url: "https://specs.apollo.dev/federation/v2.3"
7934+
import: ["@inaccessible"]
7935+
) {
7936+
query: Query
7937+
}
7938+
7939+
type Query {
7940+
public: PublicAccessTokenConnection!
7941+
private: PrivateAccessTokenConnection! @inaccessible
7942+
}
7943+
7944+
interface AccessToken @inaccessible {
7945+
id: ID!
7946+
}
7947+
7948+
interface AccessTokenEdge @inaccessible {
7949+
node: AccessToken!
7950+
}
7951+
7952+
interface AccessTokenConnection @inaccessible {
7953+
edges: [AccessTokenEdge!]!
7954+
}
7955+
7956+
type PrivateAccessToken implements AccessToken @inaccessible {
7957+
id: ID!
7958+
}
7959+
7960+
type PrivateAccessTokenEdge implements AccessTokenEdge @inaccessible {
7961+
node: PrivateAccessToken!
7962+
}
7963+
7964+
type PrivateAccessTokenConnection implements AccessTokenConnection
7965+
@inaccessible {
7966+
edges: [PrivateAccessTokenEdge!]!
7967+
}
7968+
7969+
type PublicAccessToken implements AccessToken {
7970+
id: ID!
7971+
}
7972+
7973+
type PublicAccessTokenEdge implements AccessTokenEdge {
7974+
node: PublicAccessToken!
7975+
}
7976+
7977+
type PublicAccessTokenConnection implements AccessTokenConnection {
7978+
edges: [PublicAccessTokenEdge!]!
7979+
}
7980+
`;
7981+
7982+
const result = api.composeServices([
7983+
{
7984+
typeDefs: parse(sdl),
7985+
name: "FOO_GRAPHQL",
7986+
url: "https://lol.de",
7987+
},
7988+
]);
7989+
7990+
expect(result.errors).toEqual(undefined);
7991+
expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ `
7992+
type PrivateAccessToken implements AccessToken
7993+
@join__implements(graph: FOO_GRAPHQL, interface: "AccessToken")
7994+
@join__type(graph: FOO_GRAPHQL)
7995+
@inaccessible {
7996+
id: ID!
7997+
}
7998+
7999+
type PrivateAccessTokenConnection implements AccessTokenConnection
8000+
@join__implements(
8001+
graph: FOO_GRAPHQL
8002+
interface: "AccessTokenConnection"
8003+
)
8004+
@join__type(graph: FOO_GRAPHQL)
8005+
@inaccessible {
8006+
edges: [PrivateAccessTokenEdge!]!
8007+
}
8008+
8009+
type PrivateAccessTokenEdge implements AccessTokenEdge
8010+
@join__implements(graph: FOO_GRAPHQL, interface: "AccessTokenEdge")
8011+
@join__type(graph: FOO_GRAPHQL)
8012+
@inaccessible {
8013+
node: PrivateAccessToken!
8014+
}
8015+
8016+
type PublicAccessToken implements AccessToken
8017+
@join__implements(graph: FOO_GRAPHQL, interface: "AccessToken")
8018+
@join__type(graph: FOO_GRAPHQL) {
8019+
id: ID!
8020+
}
8021+
8022+
type PublicAccessTokenConnection implements AccessTokenConnection
8023+
@join__implements(
8024+
graph: FOO_GRAPHQL
8025+
interface: "AccessTokenConnection"
8026+
)
8027+
@join__type(graph: FOO_GRAPHQL) {
8028+
edges: [PublicAccessTokenEdge!]!
8029+
}
8030+
8031+
type PublicAccessTokenEdge implements AccessTokenEdge
8032+
@join__implements(graph: FOO_GRAPHQL, interface: "AccessTokenEdge")
8033+
@join__type(graph: FOO_GRAPHQL) {
8034+
node: PublicAccessToken!
8035+
}
8036+
`);
8037+
assertCompositionSuccess(result);
8038+
expect(result.publicSdl).toContainGraphQL(`
8039+
type Query {
8040+
public: PublicAccessTokenConnection!
8041+
}
8042+
8043+
type PublicAccessToken {
8044+
id: ID!
8045+
}
8046+
8047+
type PublicAccessTokenEdge {
8048+
node: PublicAccessToken!
8049+
}
8050+
8051+
type PublicAccessTokenConnection {
8052+
edges: [PublicAccessTokenEdge!]!
8053+
}
8054+
`);
8055+
});
79288056
});

src/graphql/transform-supergraph-to-public-schema.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
Kind,
3+
type NamedTypeNode,
34
specifiedDirectives as specifiedDirectivesArray,
45
visit,
56
type ConstDirectiveNode,
@@ -157,18 +158,24 @@ export function transformSupergraphToPublicSchema(
157158
);
158159
}
159160

161+
const inaccessibleInterfaceTypes = new Set<string>();
162+
160163
function removeInaccessibleNode(node: {
164+
kind: Kind;
161165
directives?: readonly ConstDirectiveNode[];
162166
name: {
163167
value: string;
164168
};
165169
}) {
166170
if (hasInaccessibleDirective(node) || belongsToLinkedSpec(node)) {
171+
if (node.kind === Kind.INTERFACE_TYPE_DEFINITION) {
172+
inaccessibleInterfaceTypes.add(node.name.value);
173+
}
167174
return null;
168175
}
169176
}
170177

171-
return visit(documentNode, {
178+
const newDocumentNode = visit(documentNode, {
172179
[Kind.DIRECTIVE_DEFINITION]: removeFederationOrSpecifiedDirectives,
173180
[Kind.DIRECTIVE]: removeFederationOrSpecifiedDirectives,
174181
[Kind.SCHEMA_EXTENSION]: () => null,
@@ -212,4 +219,40 @@ export function transformSupergraphToPublicSchema(
212219
},
213220
[Kind.INPUT_VALUE_DEFINITION]: removeInaccessibleNode,
214221
});
222+
223+
if (!inaccessibleInterfaceTypes.size) {
224+
return newDocumentNode;
225+
}
226+
227+
// If there are inaccessible interface types we need to visit the document a second time and filter these out.
228+
229+
function removeInaccessibleInterfaces(node: {
230+
directives?: readonly ConstDirectiveNode[];
231+
name: {
232+
value: string;
233+
};
234+
interfaces?: readonly NamedTypeNode[] | undefined;
235+
}) {
236+
if (!node.interfaces) {
237+
return;
238+
}
239+
240+
const newInterfaces = node.interfaces.filter(
241+
(value) => !inaccessibleInterfaceTypes.has(value.name.value),
242+
);
243+
244+
if (newInterfaces.length === node.interfaces.length) {
245+
return;
246+
}
247+
248+
return {
249+
...node,
250+
interfaces: newInterfaces,
251+
};
252+
}
253+
254+
return visit(newDocumentNode, {
255+
[Kind.OBJECT_TYPE_DEFINITION]: removeInaccessibleInterfaces,
256+
[Kind.INTERFACE_TYPE_DEFINITION]: removeInaccessibleInterfaces,
257+
});
215258
}

0 commit comments

Comments
 (0)