Skip to content

Commit 443283e

Browse files
authored
feat: public schema from supergraph (#21)
1 parent b3365d0 commit 443283e

File tree

5 files changed

+420
-69
lines changed

5 files changed

+420
-69
lines changed

.changeset/curly-ants-march.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@theguild/federation-composition': minor
3+
---
4+
5+
Remove `stripFederationFromSupergraph` in favor of `transformSupergraphToPublicSchema`.
6+
7+
Instead of stripping only federation specific types, `transformSupergraphToPublicSchema` yields the
8+
public api schema as served by the gateway.
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
import { parse, print } from 'graphql';
2+
import { describe, expect, test } from 'vitest';
3+
import { transformSupergraphToPublicSchema } from '../../src/graphql/transform-supergraph-to-public-schema';
4+
5+
describe('transformSupergraphToPublicSchema', () => {
6+
describe('@inaccessible', () => {
7+
test('scalar removal', () => {
8+
const sdl = parse(/* GraphQL */ `
9+
scalar Scalar1 @inaccessible
10+
scalar Scalar2
11+
`);
12+
const resultSdl = transformSupergraphToPublicSchema(sdl);
13+
expect(print(resultSdl)).toMatchInlineSnapshot('"scalar Scalar2"');
14+
});
15+
test('enum removal', () => {
16+
const sdl = parse(/* GraphQL */ `
17+
enum Enum1 @inaccessible {
18+
VALUE1
19+
VALUE2
20+
}
21+
enum Enum2 {
22+
VALUE1
23+
VALUE2
24+
}
25+
`);
26+
const resultSdl = transformSupergraphToPublicSchema(sdl);
27+
expect(print(resultSdl)).toMatchInlineSnapshot(`
28+
"enum Enum2 {
29+
VALUE1
30+
VALUE2
31+
}"
32+
`);
33+
});
34+
test('object type removal', () => {
35+
const sdl = parse(/* GraphQL */ `
36+
type Object1 @inaccessible {
37+
field1: String
38+
}
39+
type Object2 {
40+
field1: String
41+
}
42+
`);
43+
const resultSdl = transformSupergraphToPublicSchema(sdl);
44+
expect(print(resultSdl)).toMatchInlineSnapshot(`
45+
"type Object2 {
46+
field1: String
47+
}"
48+
`);
49+
});
50+
test('object field removal', () => {
51+
const sdl = parse(/* GraphQL */ `
52+
type Object {
53+
field1: String @inaccessible
54+
field2: String
55+
}
56+
`);
57+
const resultSdl = transformSupergraphToPublicSchema(sdl);
58+
expect(print(resultSdl)).toMatchInlineSnapshot(`
59+
"type Object {
60+
field2: String
61+
}"
62+
`);
63+
});
64+
test('interface type removal', () => {
65+
const sdl = parse(/* GraphQL */ `
66+
interface Interface1 @inaccessible {
67+
field1: String
68+
}
69+
interface Interface2 {
70+
field1: String
71+
}
72+
`);
73+
const resultSdl = transformSupergraphToPublicSchema(sdl);
74+
expect(print(resultSdl)).toMatchInlineSnapshot(`
75+
"interface Interface2 {
76+
field1: String
77+
}"
78+
`);
79+
});
80+
test('interface field removal', () => {
81+
const sdl = parse(/* GraphQL */ `
82+
interface Interface {
83+
field1: String @inaccessible
84+
field2: String
85+
}
86+
`);
87+
const resultSdl = transformSupergraphToPublicSchema(sdl);
88+
expect(print(resultSdl)).toMatchInlineSnapshot(`
89+
"interface Interface {
90+
field2: String
91+
}"
92+
`);
93+
});
94+
test('union type removal', () => {
95+
const sdl = parse(/* GraphQL */ `
96+
union Union1 @inaccessible = Type1 | Type2
97+
union Union2 = Type1 | Type2
98+
`);
99+
const resultSdl = transformSupergraphToPublicSchema(sdl);
100+
expect(print(resultSdl)).toMatchInlineSnapshot('"union Union2 = Type1 | Type2"');
101+
});
102+
test('object field argument removal', () => {
103+
const sdl = parse(/* GraphQL */ `
104+
type Object {
105+
field1(arg1: String @inaccessible): String
106+
field2(arg1: String): String
107+
}
108+
`);
109+
const resultSdl = transformSupergraphToPublicSchema(sdl);
110+
expect(print(resultSdl)).toMatchInlineSnapshot(`
111+
"type Object {
112+
field1: String
113+
field2(arg1: String): String
114+
}"
115+
`);
116+
});
117+
test('interface field argument removal', () => {
118+
const sdl = parse(/* GraphQL */ `
119+
interface Object {
120+
field1(arg1: String @inaccessible): String
121+
field2(arg1: String): String
122+
}
123+
`);
124+
const resultSdl = transformSupergraphToPublicSchema(sdl);
125+
expect(print(resultSdl)).toMatchInlineSnapshot(`
126+
"interface Object {
127+
field1: String
128+
field2(arg1: String): String
129+
}"
130+
`);
131+
});
132+
test('input object type removal', () => {
133+
const sdl = parse(/* GraphQL */ `
134+
input Input1 @inaccessible {
135+
field1: String
136+
}
137+
input Input2 {
138+
field1: String
139+
}
140+
`);
141+
const resultSdl = transformSupergraphToPublicSchema(sdl);
142+
expect(print(resultSdl)).toMatchInlineSnapshot(`
143+
"input Input2 {
144+
field1: String
145+
}"
146+
`);
147+
});
148+
test('input object field removal', () => {
149+
const sdl = parse(/* GraphQL */ `
150+
input Input {
151+
field1: String @inaccessible
152+
field2: String
153+
}
154+
`);
155+
const resultSdl = transformSupergraphToPublicSchema(sdl);
156+
expect(print(resultSdl)).toMatchInlineSnapshot(`
157+
"input Input {
158+
field2: String
159+
}"
160+
`);
161+
});
162+
});
163+
test('strips out all federation directives and types', () => {
164+
const sdl = parse(/* GraphQL */ `
165+
schema
166+
@link(url: "https://specs.apollo.dev/link/v1.0")
167+
@link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
168+
@link(
169+
url: "https://specs.apollo.dev/inaccessible/v0.2"
170+
as: "federation__inaccessible"
171+
for: SECURITY
172+
) {
173+
query: Query
174+
}
175+
176+
directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
177+
178+
directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
179+
180+
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
181+
182+
directive @join__field(
183+
graph: join__Graph
184+
requires: join__FieldSet
185+
provides: join__FieldSet
186+
type: String
187+
external: Boolean
188+
override: String
189+
usedOverridden: Boolean
190+
) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
191+
192+
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
193+
194+
directive @join__implements(
195+
graph: join__Graph!
196+
interface: String!
197+
) repeatable on OBJECT | INTERFACE
198+
199+
directive @join__type(
200+
graph: join__Graph!
201+
key: join__FieldSet
202+
extension: Boolean! = false
203+
resolvable: Boolean! = true
204+
isInterfaceObject: Boolean! = false
205+
) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
206+
207+
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
208+
209+
directive @link(
210+
url: String
211+
as: String
212+
for: link__Purpose
213+
import: [link__Import]
214+
) repeatable on SCHEMA
215+
216+
scalar join__FieldSet
217+
218+
enum join__Graph {
219+
BRRT @join__graph(name: "brrt", url: "http://localhost/graphql")
220+
BUBUBU @join__graph(name: "bububu", url: "http://localhost:1/graphql")
221+
}
222+
223+
scalar link__Import
224+
225+
enum link__Purpose {
226+
SECURITY
227+
228+
EXECUTION
229+
}
230+
231+
type Query @join__type(graph: BRRT) @join__type(graph: BUBUBU) {
232+
foo: Int! @join__field(graph: BRRT)
233+
ok1: Int! @join__field(graph: BRRT)
234+
a: String! @join__field(graph: BUBUBU)
235+
oi: Type2 @federation__inaccessible @join__field(graph: BUBUBU)
236+
}
237+
238+
type Type2
239+
@join__type(graph: BRRT, key: "id", extension: true)
240+
@join__type(graph: BUBUBU, key: "id") {
241+
id: ID! @federation__inaccessible
242+
inStock: Boolean! @join__field(graph: BRRT)
243+
field1: String! @federation__inaccessible @join__field(graph: BUBUBU)
244+
}
245+
`);
246+
247+
const resultSdl = transformSupergraphToPublicSchema(sdl);
248+
expect(print(resultSdl)).toMatchInlineSnapshot(`
249+
"type Query {
250+
foo: Int!
251+
ok1: Int!
252+
a: String!
253+
}
254+
255+
type Type2 {
256+
inStock: Boolean!
257+
}"
258+
`);
259+
});
260+
test('graphql specification directives are omitted from the SDL', () => {
261+
const sdl = parse(/* GraphQL */ `
262+
directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
263+
directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
264+
directive @deprecated(reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE
265+
`);
266+
const resultSdl = transformSupergraphToPublicSchema(sdl);
267+
expect(print(resultSdl)).toMatchInlineSnapshot('""');
268+
});
269+
test('does not omit @deprecated directive', () => {
270+
const sdl = parse(/* GraphQL */ `
271+
type Query {
272+
foo: String @deprecated(reason: "jooo")
273+
}
274+
`);
275+
const resultSdl = transformSupergraphToPublicSchema(sdl);
276+
expect(print(resultSdl)).toMatchInlineSnapshot(`
277+
"type Query {
278+
foo: String @deprecated(reason: \\"jooo\\")
279+
}"
280+
`);
281+
});
282+
});

src/graphql/helpers.ts

Lines changed: 1 addition & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,5 @@
1-
import {
2-
DefinitionNode,
3-
DirectiveDefinitionNode,
4-
DocumentNode,
5-
Kind,
6-
specifiedDirectives,
7-
visit,
8-
} from 'graphql';
1+
import { Kind, type DefinitionNode, type DirectiveDefinitionNode } from 'graphql';
92

103
export function isDirectiveDefinition(node: DefinitionNode): node is DirectiveDefinitionNode {
114
return node.kind === Kind.DIRECTIVE_DEFINITION;
125
}
13-
14-
export function stripFederationFromSupergraph(supergraph: DocumentNode) {
15-
function removeDirective(node: {
16-
name: {
17-
value: string;
18-
};
19-
}) {
20-
const directiveName = node.name.value;
21-
const isSpecifiedDirective = specifiedDirectives.some(d => d.name === directiveName);
22-
if (!isSpecifiedDirective) {
23-
const isFederationDirective =
24-
directiveName === 'link' ||
25-
directiveName === 'inaccessible' ||
26-
directiveName === 'tag' ||
27-
directiveName === 'join__graph' ||
28-
directiveName === 'join__type' ||
29-
directiveName === 'join__implements' ||
30-
directiveName === 'join__unionMember' ||
31-
directiveName === 'join__enumValue' ||
32-
directiveName === 'join__field';
33-
34-
if (isFederationDirective) {
35-
return null;
36-
}
37-
}
38-
}
39-
40-
return visit(supergraph, {
41-
DirectiveDefinition: removeDirective,
42-
Directive: removeDirective,
43-
SchemaDefinition() {
44-
return null;
45-
},
46-
SchemaExtension() {
47-
return null;
48-
},
49-
EnumTypeDefinition: node => {
50-
if (
51-
node.name.value === 'core__Purpose' ||
52-
node.name.value === 'join__Graph' ||
53-
node.name.value === 'link__Purpose'
54-
) {
55-
return null;
56-
}
57-
58-
return node;
59-
},
60-
ScalarTypeDefinition: node => {
61-
if (
62-
node.name.value === '_FieldSet' ||
63-
node.name.value === 'link__Import' ||
64-
node.name.value === 'join__FieldSet'
65-
) {
66-
return null;
67-
}
68-
69-
return node;
70-
},
71-
});
72-
}

0 commit comments

Comments
 (0)