apollo_supergraph/
lib.rs

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// TODO: Same remark as in other crates: we need to define this more cleanly, and probably need
13// some "federation errors" crate.
14#[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        // TODO: like for subgraphs, it would nice if `Supergraph` was always representing
28        // a valid supergraph (which is simpler than for subgraph, but still at least means
29        // that it's valid graphQL in the first place, and that it has the `join` spec).
30
31        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            // TODO handle errors
38            Err(_) => Err("failed to compose"),
39        };
40        merge_result
41    }
42
43    /// Generates API schema from the supergraph schema.
44    pub fn to_api_schema(&self) -> Schema {
45        let mut api_schema = self.schema.clone();
46
47        // remove schema directives
48        api_schema.schema_definition.make_mut().directives.clear();
49
50        // remove known internal types
51        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        // remove directive applications
59        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        // remove directives
120        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        // TODO: not actually implemented; just here to give a sense of the API.
160        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        // TODO: actual assertions on the subgraph once it's actually implemented.
243    }
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        // TODO currently printer does not respect multi line comments
472        // TODO printer also adds extra comma after arguments
473        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}