1use crate::merge::merge_subgraphs;
2use apollo_compiler::ast::Directives;
3use apollo_compiler::schema::ExtendedType;
4use apollo_compiler::Schema;
5use apollo_subgraph::Subgraph;
6
7pub mod database;
8pub mod merge;
9
10type MergeError = &'static str;
11
12#[derive(Debug)]
15pub struct SupergraphError {
16 pub msg: String,
17}
18
19pub struct Supergraph {
20 pub schema: Schema,
21}
22
23impl Supergraph {
24 pub fn new(schema_str: &str) -> Self {
25 let schema = Schema::parse(schema_str, "schema.graphql");
26
27 Self { schema }
32 }
33
34 pub fn compose(subgraphs: Vec<&Subgraph>) -> Result<Self, MergeError> {
35 let merge_result = match merge_subgraphs(subgraphs) {
36 Ok(success) => Ok(Self::new(success.schema.to_string().as_str())),
37 Err(_) => Err("failed to compose"),
39 };
40 merge_result
41 }
42
43 pub fn to_api_schema(&self) -> Schema {
45 let mut api_schema = self.schema.clone();
46
47 api_schema.schema_definition.make_mut().directives.clear();
49
50 api_schema.types.retain(|type_name, graphql_type| {
52 !is_join_type(type_name.as_str())
53 && !graphql_type
54 .directives()
55 .iter()
56 .any(|d| d.name.eq("inaccessible"))
57 });
58 for (_, graphql_type) in api_schema.types.iter_mut() {
60 match graphql_type {
61 ExtendedType::Scalar(scalar) => {
62 scalar.make_mut().directives.clear();
63 }
64 ExtendedType::Object(object) => {
65 let object = object.make_mut();
66 object.directives.clear();
67 object
68 .fields
69 .retain(|_, field| !is_inaccessible_applied(&field.directives));
70 for (_, field) in object.fields.iter_mut() {
71 let field = field.make_mut();
72 field.directives.clear();
73 field
74 .arguments
75 .retain(|arg| !is_inaccessible_applied(&arg.directives));
76 for arg in field.arguments.iter_mut() {
77 arg.make_mut().directives.clear();
78 }
79 }
80 }
81 ExtendedType::Interface(intf) => {
82 let intf = intf.make_mut();
83 intf.directives.clear();
84 intf.fields
85 .retain(|_, field| !is_inaccessible_applied(&field.directives));
86 for (_, field) in intf.fields.iter_mut() {
87 let field = field.make_mut();
88 field.directives.clear();
89 for arg in field.arguments.iter_mut() {
90 arg.make_mut().directives.clear();
91 }
92 }
93 }
94 ExtendedType::Union(union) => {
95 union.make_mut().directives.clear();
96 }
97 ExtendedType::Enum(enum_type) => {
98 let enum_type = enum_type.make_mut();
99 enum_type.directives.clear();
100 enum_type
101 .values
102 .retain(|_, enum_value| !is_inaccessible_applied(&enum_value.directives));
103 for (_, enum_value) in enum_type.values.iter_mut() {
104 enum_value.make_mut().directives.clear();
105 }
106 }
107 ExtendedType::InputObject(input_object) => {
108 let input_object = input_object.make_mut();
109 input_object.directives.clear();
110 input_object
111 .fields
112 .retain(|_, input_field| !is_inaccessible_applied(&input_field.directives));
113 for (_, input_field) in input_object.fields.iter_mut() {
114 input_field.make_mut().directives.clear();
115 }
116 }
117 }
118 }
119 api_schema.directive_definitions.clear();
121
122 api_schema
123 }
124}
125
126impl From<Schema> for Supergraph {
127 fn from(schema: Schema) -> Self {
128 Self { schema }
129 }
130}
131
132const JOIN_TYPES: [&str; 4] = [
133 "join__Graph",
134 "link__Purpose",
135 "join__FieldSet",
136 "link__Import",
137];
138fn is_join_type(type_name: &str) -> bool {
139 JOIN_TYPES.contains(&type_name)
140}
141
142fn is_inaccessible_applied(directives: &Directives) -> bool {
143 directives.iter().any(|d| d.name.eq("inaccessible"))
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 fn print_sdl(schema: &Schema) -> String {
151 let mut schema = schema.clone();
152 schema.types.sort_keys();
153 schema.directive_definitions.sort_keys();
154 schema.to_string()
155 }
156
157 #[test]
158 fn can_extract_subgraph() {
159 let schema = r#"
161 schema
162 @link(url: "https://specs.apollo.dev/link/v1.0")
163 @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
164 {
165 query: Query
166 }
167
168 directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
169
170 directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
171
172 directive @join__graph(name: String!, url: String!) on ENUM_VALUE
173
174 directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
175
176 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
177
178 directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
179
180 directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
181
182 enum E
183 @join__type(graph: SUBGRAPH2)
184 {
185 V1 @join__enumValue(graph: SUBGRAPH2)
186 V2 @join__enumValue(graph: SUBGRAPH2)
187 }
188
189 scalar join__FieldSet
190
191 enum join__Graph {
192 SUBGRAPH1 @join__graph(name: "Subgraph1", url: "https://Subgraph1")
193 SUBGRAPH2 @join__graph(name: "Subgraph2", url: "https://Subgraph2")
194 }
195
196 scalar link__Import
197
198 enum link__Purpose {
199 """
200 \`SECURITY\` features provide metadata necessary to securely resolve fields.
201 """
202 SECURITY
203
204 """
205 \`EXECUTION\` features provide metadata necessary for operation execution.
206 """
207 EXECUTION
208 }
209
210 type Query
211 @join__type(graph: SUBGRAPH1)
212 @join__type(graph: SUBGRAPH2)
213 {
214 t: T @join__field(graph: SUBGRAPH1)
215 }
216
217 type S
218 @join__type(graph: SUBGRAPH1)
219 {
220 x: Int
221 }
222
223 type T
224 @join__type(graph: SUBGRAPH1, key: "k")
225 @join__type(graph: SUBGRAPH2, key: "k")
226 {
227 k: ID
228 a: Int @join__field(graph: SUBGRAPH2)
229 b: String @join__field(graph: SUBGRAPH2)
230 }
231
232 union U
233 @join__type(graph: SUBGRAPH1)
234 @join__unionMember(graph: SUBGRAPH1, member: "S")
235 @join__unionMember(graph: SUBGRAPH1, member: "T")
236 = S | T
237 "#;
238
239 let supergraph = Supergraph::new(schema);
240 let _subgraphs = database::extract_subgraphs(&supergraph)
241 .expect("Should have been able to extract subgraphs");
242 }
244
245 #[test]
246 fn can_compose_supergraph() {
247 let s1 = Subgraph::parse_and_expand(
248 "Subgraph1",
249 "https://subgraph1",
250 r#"
251 type Query {
252 t: T
253 }
254
255 type T @key(fields: "k") {
256 k: ID
257 }
258
259 type S {
260 x: Int
261 }
262
263 union U = S | T
264 "#,
265 )
266 .unwrap();
267 let s2 = Subgraph::parse_and_expand(
268 "Subgraph2",
269 "https://subgraph2",
270 r#"
271 type T @key(fields: "k") {
272 k: ID
273 a: Int
274 b: String
275 }
276
277 enum E {
278 V1
279 V2
280 }
281 "#,
282 )
283 .unwrap();
284
285 let supergraph = Supergraph::compose(vec![&s1, &s2]).unwrap();
286 let expected_supergraph_sdl = r#"schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) {
287 query: Query
288}
289
290directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
291
292directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
293
294directive @join__graph(name: String!, url: String!) on ENUM_VALUE
295
296directive @join__implements(graph: join__Graph!, interface: String!) repeatable on INTERFACE | OBJECT
297
298directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on ENUM | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION
299
300directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
301
302directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
303
304enum E @join__type(graph: SUBGRAPH2) {
305 V1 @join__enumValue(graph: SUBGRAPH2)
306 V2 @join__enumValue(graph: SUBGRAPH2)
307}
308
309type Query @join__type(graph: SUBGRAPH1) @join__type(graph: SUBGRAPH2) {
310 t: T @join__field(graph: SUBGRAPH1)
311}
312
313type S @join__type(graph: SUBGRAPH1) {
314 x: Int
315}
316
317type T @join__type(graph: SUBGRAPH1, key: "k") @join__type(graph: SUBGRAPH2, key: "k") {
318 k: ID
319 a: Int @join__field(graph: SUBGRAPH2)
320 b: String @join__field(graph: SUBGRAPH2)
321}
322
323union U @join__type(graph: SUBGRAPH1) @join__unionMember(graph: SUBGRAPH1, member: "S") @join__unionMember(graph: SUBGRAPH1, member: "T") = S | T
324
325scalar join__FieldSet
326
327enum join__Graph {
328 SUBGRAPH1 @join__graph(name: "Subgraph1", url: "https://subgraph1")
329 SUBGRAPH2 @join__graph(name: "Subgraph2", url: "https://subgraph2")
330}
331
332scalar link__Import
333
334enum link__Purpose {
335 "SECURITY features provide metadata necessary to securely resolve fields."
336 SECURITY
337 "EXECUTION features provide metadata necessary for operation execution."
338 EXECUTION
339}
340"#;
341 assert_eq!(print_sdl(&supergraph.schema), expected_supergraph_sdl);
342
343 let expected_api_schema = r#"enum E {
344 V1
345 V2
346}
347
348type Query {
349 t: T
350}
351
352type S {
353 x: Int
354}
355
356type T {
357 k: ID
358 a: Int
359 b: String
360}
361
362union U = S | T
363"#;
364
365 assert_eq!(print_sdl(&supergraph.to_api_schema()), expected_api_schema);
366 }
367
368 #[test]
369 fn can_compose_with_descriptions() {
370 let s1 = Subgraph::parse_and_expand(
371 "Subgraph1",
372 "https://subgraph1",
373 r#"
374 "The foo directive description"
375 directive @foo(url: String) on FIELD
376
377 "A cool schema"
378 schema {
379 query: Query
380 }
381
382 """
383 Available queries
384 Not much yet
385 """
386 type Query {
387 "Returns tea"
388 t(
389 "An argument that is very important"
390 x: String!
391 ): String
392 }
393 "#,
394 )
395 .unwrap();
396
397 let s2 = Subgraph::parse_and_expand(
398 "Subgraph2",
399 "https://subgraph2",
400 r#"
401 "The foo directive description"
402 directive @foo(url: String) on FIELD
403
404 "An enum"
405 enum E {
406 "The A value"
407 A
408 "The B value"
409 B
410 }
411 "#,
412 )
413 .unwrap();
414
415 let expected_supergraph_sdl = r#""A cool schema"
416schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) {
417 query: Query
418}
419
420"The foo directive description"
421directive @foo(url: String) on FIELD
422
423directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
424
425directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
426
427directive @join__graph(name: String!, url: String!) on ENUM_VALUE
428
429directive @join__implements(graph: join__Graph!, interface: String!) repeatable on INTERFACE | OBJECT
430
431directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on ENUM | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION
432
433directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
434
435directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
436
437"An enum"
438enum E @join__type(graph: SUBGRAPH2) {
439 "The A value"
440 A @join__enumValue(graph: SUBGRAPH2)
441 "The B value"
442 B @join__enumValue(graph: SUBGRAPH2)
443}
444
445"Available queries\nNot much yet"
446type Query @join__type(graph: SUBGRAPH1) @join__type(graph: SUBGRAPH2) {
447 "Returns tea"
448 t(
449 "An argument that is very important"
450 x: String!,
451 ): String @join__field(graph: SUBGRAPH1)
452}
453
454scalar join__FieldSet
455
456enum join__Graph {
457 SUBGRAPH1 @join__graph(name: "Subgraph1", url: "https://subgraph1")
458 SUBGRAPH2 @join__graph(name: "Subgraph2", url: "https://subgraph2")
459}
460
461scalar link__Import
462
463enum link__Purpose {
464 "SECURITY features provide metadata necessary to securely resolve fields."
465 SECURITY
466 "EXECUTION features provide metadata necessary for operation execution."
467 EXECUTION
468}
469"#;
470 let supergraph = Supergraph::compose(vec![&s1, &s2]).unwrap();
471 assert_eq!(print_sdl(&supergraph.schema), expected_supergraph_sdl);
474
475 let expected_api_schema = r#""A cool schema"
476schema {
477 query: Query
478}
479
480"An enum"
481enum E {
482 "The A value"
483 A
484 "The B value"
485 B
486}
487
488"Available queries\nNot much yet"
489type Query {
490 "Returns tea"
491 t(
492 "An argument that is very important"
493 x: String!,
494 ): String
495}
496"#;
497 assert_eq!(print_sdl(&supergraph.to_api_schema()), expected_api_schema);
498 }
499
500 #[test]
501 fn can_compose_types_from_different_subgraphs() {
502 let s1 = Subgraph::parse_and_expand(
503 "SubgraphA",
504 "https://subgraphA",
505 r#"
506 type Query {
507 products: [Product!]
508 }
509
510 type Product {
511 sku: String!
512 name: String!
513 }
514 "#,
515 )
516 .unwrap();
517
518 let s2 = Subgraph::parse_and_expand(
519 "SubgraphB",
520 "https://subgraphB",
521 r#"
522 type User {
523 name: String
524 email: String!
525 }
526 "#,
527 )
528 .unwrap();
529
530 let expected_supergraph_sdl = r#"schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) {
531 query: Query
532}
533
534directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
535
536directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
537
538directive @join__graph(name: String!, url: String!) on ENUM_VALUE
539
540directive @join__implements(graph: join__Graph!, interface: String!) repeatable on INTERFACE | OBJECT
541
542directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on ENUM | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION
543
544directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
545
546directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
547
548type Product @join__type(graph: SUBGRAPHA) {
549 sku: String!
550 name: String!
551}
552
553type Query @join__type(graph: SUBGRAPHA) @join__type(graph: SUBGRAPHB) {
554 products: [Product!] @join__field(graph: SUBGRAPHA)
555}
556
557type User @join__type(graph: SUBGRAPHB) {
558 name: String
559 email: String!
560}
561
562scalar join__FieldSet
563
564enum join__Graph {
565 SUBGRAPHA @join__graph(name: "SubgraphA", url: "https://subgraphA")
566 SUBGRAPHB @join__graph(name: "SubgraphB", url: "https://subgraphB")
567}
568
569scalar link__Import
570
571enum link__Purpose {
572 "SECURITY features provide metadata necessary to securely resolve fields."
573 SECURITY
574 "EXECUTION features provide metadata necessary for operation execution."
575 EXECUTION
576}
577"#;
578 let supergraph = Supergraph::compose(vec![&s1, &s2]).unwrap();
579 assert_eq!(print_sdl(&supergraph.schema), expected_supergraph_sdl);
580
581 let expected_api_schema = r#"type Product {
582 sku: String!
583 name: String!
584}
585
586type Query {
587 products: [Product!]
588}
589
590type User {
591 name: String
592 email: String!
593}
594"#;
595
596 assert_eq!(print_sdl(&supergraph.to_api_schema()), expected_api_schema);
597 }
598
599 #[test]
600 fn compose_removes_federation_directives() {
601 let s1 = Subgraph::parse_and_expand(
602 "SubgraphA",
603 "https://subgraphA",
604 r#"
605 extend schema @link(url: "https://specs.apollo.dev/federation/v2.5", import: [ "@key", "@provides", "@external" ])
606
607 type Query {
608 products: [Product!] @provides(fields: "name")
609 }
610
611 type Product @key(fields: "sku") {
612 sku: String!
613 name: String! @external
614 }
615 "#,
616 )
617 .unwrap();
618
619 let s2 = Subgraph::parse_and_expand(
620 "SubgraphB",
621 "https://subgraphB",
622 r#"
623 extend schema @link(url: "https://specs.apollo.dev/federation/v2.5", import: [ "@key", "@shareable" ])
624
625 type Product @key(fields: "sku") {
626 sku: String!
627 name: String! @shareable
628 }
629 "#,
630 )
631 .unwrap();
632
633 let expected_supergraph_sdl = r#"schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) {
634 query: Query
635}
636
637directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
638
639directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
640
641directive @join__graph(name: String!, url: String!) on ENUM_VALUE
642
643directive @join__implements(graph: join__Graph!, interface: String!) repeatable on INTERFACE | OBJECT
644
645directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on ENUM | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION
646
647directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
648
649directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
650
651type Product @join__type(graph: SUBGRAPHA, key: "sku") @join__type(graph: SUBGRAPHB, key: "sku") {
652 sku: String!
653 name: String! @join__field(graph: SUBGRAPHA, external: true) @join__field(graph: SUBGRAPHB)
654}
655
656type Query @join__type(graph: SUBGRAPHA) @join__type(graph: SUBGRAPHB) {
657 products: [Product!] @join__field(graph: SUBGRAPHA, provides: "name")
658}
659
660scalar join__FieldSet
661
662enum join__Graph {
663 SUBGRAPHA @join__graph(name: "SubgraphA", url: "https://subgraphA")
664 SUBGRAPHB @join__graph(name: "SubgraphB", url: "https://subgraphB")
665}
666
667scalar link__Import
668
669enum link__Purpose {
670 "SECURITY features provide metadata necessary to securely resolve fields."
671 SECURITY
672 "EXECUTION features provide metadata necessary for operation execution."
673 EXECUTION
674}
675"#;
676
677 let supergraph = Supergraph::compose(vec![&s1, &s2]).unwrap();
678 assert_eq!(print_sdl(&supergraph.schema), expected_supergraph_sdl);
679
680 let expected_api_schema = r#"type Product {
681 sku: String!
682 name: String!
683}
684
685type Query {
686 products: [Product!]
687}
688"#;
689
690 assert_eq!(print_sdl(&supergraph.to_api_schema()), expected_api_schema);
691 }
692}