apollo_subgraph/
lib.rs

1use crate::spec::{
2    AppliedFederationLink, FederationSpecDefinitions, FederationSpecError, LinkSpecDefinitions,
3    ANY_SCALAR_NAME, ENTITIES_QUERY, ENTITY_UNION_NAME, FEDERATION_V2_DIRECTIVE_NAMES,
4    KEY_DIRECTIVE_NAME, SERVICE_SDL_QUERY, SERVICE_TYPE,
5};
6use apollo_at_link::link::LinkError;
7use apollo_at_link::link::{self, DEFAULT_LINK_NAME};
8use apollo_at_link::spec::Identity;
9use apollo_compiler::ast::{Name, NamedType};
10use apollo_compiler::schema::{ComponentStr, ExtendedType, ObjectType};
11use apollo_compiler::{Node, Schema};
12use indexmap::map::Entry;
13use indexmap::{IndexMap, IndexSet};
14use std::collections::BTreeMap;
15use std::fmt::Formatter;
16use std::sync::Arc;
17
18pub mod database;
19mod spec;
20
21// TODO: we need a strategy for errors. All (or almost all) federation errors have a code in
22// particular (and the way we deal with this in typescript, having all errors declared in one place
23// with descriptions that allow to autogenerate doc is kind of useful (if not perfect)), so we
24// probably want some additional crate specific for errors and use that here.
25#[derive(Debug)]
26pub struct SubgraphError {
27    pub msg: String,
28}
29
30impl From<apollo_compiler::Diagnostics> for SubgraphError {
31    fn from(value: apollo_compiler::Diagnostics) -> Self {
32        SubgraphError {
33            msg: value.to_string_no_color(),
34        }
35    }
36}
37
38impl From<LinkError> for SubgraphError {
39    fn from(value: LinkError) -> Self {
40        SubgraphError {
41            msg: value.to_string(),
42        }
43    }
44}
45
46impl From<FederationSpecError> for SubgraphError {
47    fn from(value: FederationSpecError) -> Self {
48        SubgraphError {
49            msg: value.to_string(),
50        }
51    }
52}
53
54pub struct Subgraph {
55    pub name: String,
56    pub url: String,
57    pub schema: Schema,
58}
59
60impl Subgraph {
61    pub fn new(name: &str, url: &str, schema_str: &str) -> Self {
62        let schema = Schema::parse(schema_str, name);
63
64        // TODO: ideally, we'd want Subgraph to always represent a valid subgraph: we don't want
65        // every possible method that receive a subgraph to have to worry if the underlying schema
66        // is actually not a subgraph at all: we want the type-system to help carry known
67        // guarantees. This imply we should run validation here (both graphQL ones, but also
68        // subgraph specific ones).
69        // This also mean we would ideally want `schema` to not export any mutable methods
70        // but not sure what that entail/how doable that is currently.
71
72        Self {
73            name: name.to_string(),
74            url: url.to_string(),
75            schema,
76        }
77    }
78
79    pub fn parse_and_expand(
80        name: &str,
81        url: &str,
82        schema_str: &str,
83    ) -> Result<Self, SubgraphError> {
84        let mut schema = Schema::builder()
85            .adopt_orphan_extensions()
86            .parse(schema_str, name)
87            .build();
88
89        let mut imported_federation_definitions: Option<FederationSpecDefinitions> = None;
90        let mut imported_link_definitions: Option<LinkSpecDefinitions> = None;
91        let link_directives = schema
92            .schema_definition
93            .directives
94            .get_all(DEFAULT_LINK_NAME);
95
96        for directive in link_directives {
97            let link_directive = link::Link::from_directive_application(directive)?;
98            if link_directive
99                .url
100                .identity
101                .eq(&Identity::federation_identity())
102            {
103                if imported_federation_definitions.is_some() {
104                    return Err(SubgraphError { msg: "invalid graphql schema - multiple @link imports for the federation specification are not supported".to_owned() });
105                }
106
107                imported_federation_definitions =
108                    Some(FederationSpecDefinitions::from_link(link_directive)?);
109            } else if link_directive.url.identity.eq(&Identity::link_identity()) {
110                // user manually imported @link specification
111                if imported_link_definitions.is_some() {
112                    return Err(SubgraphError { msg: "invalid graphql schema - multiple @link imports for the link specification are not supported".to_owned() });
113                }
114
115                imported_link_definitions = Some(LinkSpecDefinitions::new(link_directive));
116            }
117        }
118
119        // generate additional schema definitions
120        Self::populate_missing_type_definitions(
121            &mut schema,
122            imported_federation_definitions,
123            imported_link_definitions,
124        )?;
125        schema.validate()?;
126        Ok(Self {
127            name: name.to_owned(),
128            url: url.to_owned(),
129            schema,
130        })
131    }
132
133    fn populate_missing_type_definitions(
134        schema: &mut Schema,
135        imported_federation_definitions: Option<FederationSpecDefinitions>,
136        imported_link_definitions: Option<LinkSpecDefinitions>,
137    ) -> Result<(), SubgraphError> {
138        // populate @link spec definitions
139        let link_spec_definitions = match imported_link_definitions {
140            Some(definitions) => definitions,
141            None => {
142                // need to apply default @link directive for link spec on schema
143                let defaults = LinkSpecDefinitions::default();
144                schema
145                    .schema_definition
146                    .make_mut()
147                    .directives
148                    .push(defaults.applied_link_directive().into());
149                defaults
150            }
151        };
152        Self::populate_missing_link_definitions(schema, link_spec_definitions)?;
153
154        // populate @link federation spec definitions
155        let fed_definitions = match imported_federation_definitions {
156            Some(definitions) => definitions,
157            None => {
158                // federation v1 schema or user does not import federation spec
159                // need to apply default @link directive for federation spec on schema
160                let defaults = FederationSpecDefinitions::default()?;
161                schema
162                    .schema_definition
163                    .make_mut()
164                    .directives
165                    .push(defaults.applied_link_directive().into());
166                defaults
167            }
168        };
169        Self::populate_missing_federation_directive_definitions(schema, &fed_definitions)?;
170        Self::populate_missing_federation_types(schema, &fed_definitions)
171    }
172
173    fn populate_missing_link_definitions(
174        schema: &mut Schema,
175        link_spec_definitions: LinkSpecDefinitions,
176    ) -> Result<(), SubgraphError> {
177        schema
178            .types
179            .entry(link_spec_definitions.purpose_enum_name.as_str().into())
180            .or_insert_with(|| link_spec_definitions.link_purpose_enum_definition().into());
181        schema
182            .types
183            .entry(link_spec_definitions.import_scalar_name.as_str().into())
184            .or_insert_with(|| link_spec_definitions.import_scalar_definition().into());
185        schema
186            .directive_definitions
187            .entry(DEFAULT_LINK_NAME.into())
188            .or_insert_with(|| link_spec_definitions.link_directive_definition().into());
189        Ok(())
190    }
191
192    fn populate_missing_federation_directive_definitions(
193        schema: &mut Schema,
194        fed_definitions: &FederationSpecDefinitions,
195    ) -> Result<(), SubgraphError> {
196        schema
197            .types
198            .entry(fed_definitions.fieldset_scalar_name.as_str().into())
199            .or_insert_with(|| fed_definitions.fieldset_scalar_definition().into());
200
201        for directive_name in FEDERATION_V2_DIRECTIVE_NAMES {
202            let namespaced_directive_name =
203                fed_definitions.namespaced_type_name(directive_name, true);
204            if let Entry::Vacant(entry) = schema
205                .directive_definitions
206                .entry(namespaced_directive_name.as_str().into())
207            {
208                let directive_definition = fed_definitions.directive_definition(
209                    directive_name,
210                    &Some(namespaced_directive_name.to_owned()),
211                )?;
212                entry.insert(directive_definition.into());
213            }
214        }
215        Ok(())
216    }
217
218    fn populate_missing_federation_types(
219        schema: &mut Schema,
220        fed_definitions: &FederationSpecDefinitions,
221    ) -> Result<(), SubgraphError> {
222        schema
223            .types
224            .entry(NamedType::new(SERVICE_TYPE))
225            .or_insert_with(|| fed_definitions.service_object_type_definition());
226
227        let entities = Self::locate_entities(schema, fed_definitions);
228        let entities_present = !entities.is_empty();
229        if entities_present {
230            schema
231                .types
232                .entry(NamedType::new(ENTITY_UNION_NAME))
233                .or_insert_with(|| fed_definitions.entity_union_definition(entities));
234            schema
235                .types
236                .entry(NamedType::new(ANY_SCALAR_NAME))
237                .or_insert_with(|| fed_definitions.any_scalar_definition());
238        }
239
240        let query_type_name = schema
241            .schema_definition
242            .make_mut()
243            .query
244            .get_or_insert(ComponentStr::new("Query"));
245        if let ExtendedType::Object(query_type) = schema
246            .types
247            .entry(NamedType::new(query_type_name.as_str()))
248            .or_insert(ExtendedType::Object(Node::new(ObjectType {
249                description: None,
250                directives: Default::default(),
251                fields: IndexMap::new(),
252                implements_interfaces: IndexSet::new(),
253            })))
254        {
255            let query_type = query_type.make_mut();
256            query_type
257                .fields
258                .entry(Name::new(SERVICE_SDL_QUERY))
259                .or_insert_with(|| fed_definitions.service_sdl_query_field());
260            if entities_present {
261                // _entities(representations: [_Any!]!): [_Entity]!
262                query_type
263                    .fields
264                    .entry(Name::new(ENTITIES_QUERY))
265                    .or_insert_with(|| fed_definitions.entities_query_field());
266            }
267        }
268        Ok(())
269    }
270
271    fn locate_entities(
272        schema: &mut Schema,
273        fed_definitions: &FederationSpecDefinitions,
274    ) -> IndexSet<ComponentStr> {
275        let mut entities = Vec::new();
276        let immutable_type_map = schema.types.to_owned();
277        for (named_type, extended_type) in immutable_type_map.iter() {
278            let is_entity = extended_type
279                .directives()
280                .iter()
281                .find(|d| {
282                    d.name.eq(&Name::new(
283                        fed_definitions
284                            .namespaced_type_name(KEY_DIRECTIVE_NAME, true)
285                            .as_str(),
286                    ))
287                })
288                .map(|_| true)
289                .unwrap_or(false);
290            if is_entity {
291                entities.push(named_type);
292            }
293        }
294        let entity_set: IndexSet<ComponentStr> = entities
295            .iter()
296            .map(|e| ComponentStr::new(e.as_str()))
297            .collect();
298        entity_set
299    }
300}
301
302impl std::fmt::Debug for Subgraph {
303    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
304        write!(f, r#"name: {}, urL: {}"#, self.name, self.url)
305    }
306}
307
308pub struct Subgraphs {
309    subgraphs: BTreeMap<String, Arc<Subgraph>>,
310}
311
312#[allow(clippy::new_without_default)]
313impl Subgraphs {
314    pub fn new() -> Self {
315        Subgraphs {
316            subgraphs: BTreeMap::new(),
317        }
318    }
319
320    pub fn add(&mut self, subgraph: Subgraph) -> Result<(), SubgraphError> {
321        if self.subgraphs.contains_key(&subgraph.name) {
322            return Err(SubgraphError {
323                msg: format!("A subgraph named {} already exists", subgraph.name),
324            });
325        }
326        self.subgraphs
327            .insert(subgraph.name.clone(), Arc::new(subgraph));
328        Ok(())
329    }
330
331    pub fn get(&self, name: &str) -> Option<Arc<Subgraph>> {
332        self.subgraphs.get(name).cloned()
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339    use crate::database::keys;
340
341    #[test]
342    fn can_inspect_a_type_key() {
343        // TODO: no schema expansion currently, so need to having the `@link` to `link` and the
344        // @link directive definition for @link-bootstrapping to work. Also, we should
345        // theoretically have the @key directive definition added too (but validation is not
346        // wired up yet, so we get away without). Point being, this is just some toy code at
347        // the moment.
348
349        let schema = r#"
350          extend schema
351            @link(url: "https://specs.apollo.dev/link/v1.0", import: ["Import"])
352            @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"])
353
354          type Query {
355            t: T
356          }
357
358          type T @key(fields: "id") {
359            id: ID!
360            x: Int
361          }
362
363          enum link__Purpose {
364            SECURITY
365            EXECUTION
366          }
367
368          scalar Import
369
370          directive @link(url: String, as: String, import: [Import], for: link__Purpose) repeatable on SCHEMA
371        "#;
372
373        let subgraph = Subgraph::new("S1", "http://s1", schema);
374        let keys = keys(&subgraph.schema, "T");
375        assert_eq!(keys.len(), 1);
376        assert_eq!(keys.get(0).unwrap().type_name, "T");
377
378        // TODO: no accessible selection yet.
379    }
380
381    #[test]
382    fn can_parse_and_expand() -> Result<(), String> {
383        let schema = r#"
384        extend schema
385          @link(url: "https://specs.apollo.dev/federation/v2.3", import: [ "@key" ])
386
387        type Query {
388            t: T
389        }
390
391        type T @key(fields: "id") {
392            id: ID!
393            x: Int
394        }
395        "#;
396
397        let subgraph = Subgraph::parse_and_expand("S1", "http://s1", schema).map_err(|e| {
398            println!("{}", e.msg);
399            String::from("failed to parse and expand the subgraph, see errors above for details")
400        })?;
401        assert!(subgraph.schema.types.contains_key("T"));
402        assert!(subgraph.schema.directive_definitions.contains_key("key"));
403        assert!(subgraph
404            .schema
405            .directive_definitions
406            .contains_key("federation__requires"));
407        Ok(())
408    }
409
410    #[test]
411    fn can_parse_and_expand_with_renames() -> Result<(), String> {
412        let schema = r#"
413        extend schema
414          @link(url: "https://specs.apollo.dev/federation/v2.3", import: [ { name: "@key", as: "@myKey" }, "@provides" ])
415
416        type Query {
417            t: T @provides(fields: "x")
418        }
419
420        type T @myKey(fields: "id") {
421            id: ID!
422            x: Int
423        }
424        "#;
425
426        let subgraph = Subgraph::parse_and_expand("S1", "http://s1", schema).map_err(|e| {
427            println!("{}", e.msg);
428            String::from("failed to parse and expand the subgraph, see errors above for details")
429        })?;
430        assert!(subgraph.schema.directive_definitions.contains_key("myKey"));
431        assert!(subgraph
432            .schema
433            .directive_definitions
434            .contains_key("provides"));
435        Ok(())
436    }
437
438    #[test]
439    fn can_parse_and_expand_with_namespace() -> Result<(), String> {
440        let schema = r#"
441        extend schema
442          @link(url: "https://specs.apollo.dev/federation/v2.3", import: [ "@key" ], as: "fed" )
443
444        type Query {
445            t: T
446        }
447
448        type T @key(fields: "id") {
449            id: ID!
450            x: Int
451        }
452        "#;
453
454        let subgraph = Subgraph::parse_and_expand("S1", "http://s1", schema).map_err(|e| {
455            println!("{}", e.msg);
456            String::from("failed to parse and expand the subgraph, see errors above for details")
457        })?;
458        assert!(subgraph.schema.directive_definitions.contains_key("key"));
459        assert!(subgraph
460            .schema
461            .directive_definitions
462            .contains_key("fed__requires"));
463        Ok(())
464    }
465
466    #[test]
467    fn can_parse_and_expand_preserves_user_definitions() -> Result<(), String> {
468        let schema = r#"
469        extend schema
470          @link(url: "https://specs.apollo.dev/link/v1.0", import: ["Import", "Purpose"])
471          @link(url: "https://specs.apollo.dev/federation/v2.3", import: [ "@key" ])
472
473        type Query {
474            t: T
475        }
476
477        type T @key(fields: "id") {
478            id: ID!
479            x: Int
480        }
481
482        enum Purpose {
483            SECURITY
484            EXECUTION
485        }
486
487        scalar Import
488
489        directive @link(url: String, as: String, import: [Import], for: Purpose) repeatable on SCHEMA
490        "#;
491
492        let subgraph = Subgraph::parse_and_expand("S1", "http://s1", schema).map_err(|e| {
493            println!("{}", e.msg);
494            String::from("failed to parse and expand the subgraph, see errors above for details")
495        })?;
496        assert!(subgraph.schema.types.contains_key("Purpose"));
497        Ok(())
498    }
499
500    #[test]
501    fn can_parse_and_expand_works_with_fed_v1() -> Result<(), String> {
502        let schema = r#"
503        type Query {
504            t: T
505        }
506
507        type T @key(fields: "id") {
508            id: ID!
509            x: Int
510        }
511        "#;
512
513        let subgraph = Subgraph::parse_and_expand("S1", "http://s1", schema).map_err(|e| {
514            println!("{}", e.msg);
515            String::from("failed to parse and expand the subgraph, see errors above for details")
516        })?;
517        assert!(subgraph.schema.types.contains_key("T"));
518        assert!(subgraph.schema.directive_definitions.contains_key("key"));
519        Ok(())
520    }
521
522    #[test]
523    fn can_parse_and_expand_will_fail_when_importing_same_spec_twice() {
524        let schema = r#"
525        extend schema
526          @link(url: "https://specs.apollo.dev/federation/v2.3", import: [ "@key" ] )
527          @link(url: "https://specs.apollo.dev/federation/v2.3", import: [ "@provides" ] )
528
529        type Query {
530            t: T
531        }
532
533        type T @key(fields: "id") {
534            id: ID!
535            x: Int
536        }
537        "#;
538
539        let result = Subgraph::parse_and_expand("S1", "http://s1", schema)
540            .expect_err("importing same specification twice should fail");
541        assert_eq!("invalid graphql schema - multiple @link imports for the federation specification are not supported", result.msg);
542    }
543}