apollo_federation/subgraph/
mod.rs

1use std::collections::BTreeMap;
2use std::fmt::Formatter;
3use std::sync::Arc;
4
5use apollo_compiler::Node;
6use apollo_compiler::Schema;
7use apollo_compiler::collections::IndexMap;
8use apollo_compiler::collections::IndexSet;
9use apollo_compiler::name;
10use apollo_compiler::schema::ComponentName;
11use apollo_compiler::schema::ExtendedType;
12use apollo_compiler::schema::ObjectType;
13use apollo_compiler::validation::Valid;
14use indexmap::map::Entry;
15
16use crate::ValidFederationSubgraph;
17use crate::error::FederationError;
18use crate::link::DEFAULT_LINK_NAME;
19use crate::link::Link;
20use crate::link::LinkError;
21use crate::link::spec::Identity;
22use crate::subgraph::spec::ANY_SCALAR_NAME;
23use crate::subgraph::spec::AppliedFederationLink;
24use crate::subgraph::spec::CONTEXTFIELDVALUE_SCALAR_NAME;
25use crate::subgraph::spec::ENTITIES_QUERY;
26use crate::subgraph::spec::ENTITY_UNION_NAME;
27use crate::subgraph::spec::FEDERATION_V2_DIRECTIVE_NAMES;
28use crate::subgraph::spec::FederationSpecDefinitions;
29use crate::subgraph::spec::KEY_DIRECTIVE_NAME;
30use crate::subgraph::spec::LinkSpecDefinitions;
31use crate::subgraph::spec::SERVICE_SDL_QUERY;
32use crate::subgraph::spec::SERVICE_TYPE;
33
34mod database;
35pub mod spec;
36
37pub struct Subgraph {
38    pub name: String,
39    pub url: String,
40    pub schema: Schema,
41}
42
43impl Subgraph {
44    pub fn new(name: &str, url: &str, schema_str: &str) -> Result<Self, FederationError> {
45        let schema = Schema::parse(schema_str, name)?;
46        // TODO: federation-specific validation
47        Ok(Self {
48            name: name.to_string(),
49            url: url.to_string(),
50            schema,
51        })
52    }
53
54    pub fn parse_and_expand(
55        name: &str,
56        url: &str,
57        schema_str: &str,
58    ) -> Result<ValidSubgraph, FederationError> {
59        let mut schema = Schema::builder()
60            .adopt_orphan_extensions()
61            .parse(schema_str, name)
62            .build()?;
63
64        let mut imported_federation_definitions: Option<FederationSpecDefinitions> = None;
65        let mut imported_link_definitions: Option<LinkSpecDefinitions> = None;
66        let default_link_name = DEFAULT_LINK_NAME;
67        let link_directives = schema
68            .schema_definition
69            .directives
70            .get_all(&default_link_name);
71
72        for directive in link_directives {
73            let link_directive = Link::from_directive_application(directive)?;
74            if link_directive.url.identity == Identity::federation_identity() {
75                if imported_federation_definitions.is_some() {
76                    let msg = "invalid graphql schema - multiple @link imports for the federation specification are not supported";
77                    return Err(LinkError::BootstrapError(msg.to_owned()).into());
78                }
79
80                imported_federation_definitions =
81                    Some(FederationSpecDefinitions::from_link(link_directive)?);
82            } else if link_directive.url.identity == Identity::link_identity() {
83                // user manually imported @link specification
84                if imported_link_definitions.is_some() {
85                    let msg = "invalid graphql schema - multiple @link imports for the link specification are not supported";
86                    return Err(LinkError::BootstrapError(msg.to_owned()).into());
87                }
88
89                imported_link_definitions = Some(LinkSpecDefinitions::new(link_directive));
90            }
91        }
92
93        // generate additional schema definitions
94        Self::populate_missing_type_definitions(
95            &mut schema,
96            imported_federation_definitions,
97            imported_link_definitions,
98        )?;
99        let schema = schema.validate()?;
100        Ok(ValidSubgraph {
101            name: name.to_owned(),
102            url: url.to_owned(),
103            schema,
104        })
105    }
106
107    fn populate_missing_type_definitions(
108        schema: &mut Schema,
109        imported_federation_definitions: Option<FederationSpecDefinitions>,
110        imported_link_definitions: Option<LinkSpecDefinitions>,
111    ) -> Result<(), FederationError> {
112        // populate @link spec definitions
113        let link_spec_definitions = match imported_link_definitions {
114            Some(definitions) => definitions,
115            None => {
116                // need to apply default @link directive for link spec on schema
117                let defaults = LinkSpecDefinitions::default();
118                schema
119                    .schema_definition
120                    .make_mut()
121                    .directives
122                    .push(defaults.applied_link_directive());
123                defaults
124            }
125        };
126        Self::populate_missing_link_definitions(schema, link_spec_definitions)?;
127
128        // populate @link federation spec definitions
129        let fed_definitions = match imported_federation_definitions {
130            Some(definitions) => definitions,
131            None => {
132                // federation v1 schema or user does not import federation spec
133                // need to apply default @link directive for federation spec on schema
134                let defaults = FederationSpecDefinitions::default()?;
135                schema
136                    .schema_definition
137                    .make_mut()
138                    .directives
139                    .push(defaults.applied_link_directive());
140                defaults
141            }
142        };
143        Self::populate_missing_federation_directive_definitions(schema, &fed_definitions)?;
144        Self::populate_missing_federation_types(schema, &fed_definitions)
145    }
146
147    fn populate_missing_link_definitions(
148        schema: &mut Schema,
149        link_spec_definitions: LinkSpecDefinitions,
150    ) -> Result<(), FederationError> {
151        let purpose_enum_name = &link_spec_definitions.purpose_enum_name;
152        schema
153            .types
154            .entry(purpose_enum_name.clone())
155            .or_insert_with(|| {
156                link_spec_definitions
157                    .link_purpose_enum_definition(purpose_enum_name.clone())
158                    .into()
159            });
160        let import_scalar_name = &link_spec_definitions.import_scalar_name;
161        schema
162            .types
163            .entry(import_scalar_name.clone())
164            .or_insert_with(|| {
165                link_spec_definitions
166                    .import_scalar_definition(import_scalar_name.clone())
167                    .into()
168            });
169        if let Entry::Vacant(entry) = schema.directive_definitions.entry(DEFAULT_LINK_NAME) {
170            entry.insert(link_spec_definitions.link_directive_definition()?.into());
171        }
172        Ok(())
173    }
174
175    fn populate_missing_federation_directive_definitions(
176        schema: &mut Schema,
177        fed_definitions: &FederationSpecDefinitions,
178    ) -> Result<(), FederationError> {
179        // scalar FieldSet
180        let fieldset_scalar_name = &fed_definitions.fieldset_scalar_name;
181        schema
182            .types
183            .entry(fieldset_scalar_name.clone())
184            .or_insert_with(|| {
185                fed_definitions
186                    .fieldset_scalar_definition(fieldset_scalar_name.clone())
187                    .into()
188            });
189
190        // scalar ContextFieldValue
191        let namespaced_contextfieldvalue_scalar_name =
192            fed_definitions.namespaced_type_name(&CONTEXTFIELDVALUE_SCALAR_NAME, false);
193        if let Entry::Vacant(entry) = schema
194            .types
195            .entry(namespaced_contextfieldvalue_scalar_name.clone())
196        {
197            let type_definition = fed_definitions.contextfieldvalue_scalar_definition(&Some(
198                namespaced_contextfieldvalue_scalar_name,
199            ));
200            entry.insert(type_definition.into());
201        }
202
203        for directive_name in &FEDERATION_V2_DIRECTIVE_NAMES {
204            let namespaced_directive_name =
205                fed_definitions.namespaced_type_name(directive_name, true);
206            if let Entry::Vacant(entry) = schema
207                .directive_definitions
208                .entry(namespaced_directive_name.clone())
209            {
210                let directive_definition = fed_definitions.directive_definition(
211                    directive_name,
212                    &Some(namespaced_directive_name.to_owned()),
213                )?;
214                entry.insert(directive_definition.into());
215            }
216        }
217        Ok(())
218    }
219
220    fn populate_missing_federation_types(
221        schema: &mut Schema,
222        fed_definitions: &FederationSpecDefinitions,
223    ) -> Result<(), FederationError> {
224        schema
225            .types
226            .entry(SERVICE_TYPE)
227            .or_insert_with(|| fed_definitions.service_object_type_definition());
228
229        let entities = Self::locate_entities(schema, fed_definitions);
230        let entities_present = !entities.is_empty();
231        if entities_present {
232            schema
233                .types
234                .entry(ENTITY_UNION_NAME)
235                .or_insert_with(|| fed_definitions.entity_union_definition(entities));
236            schema
237                .types
238                .entry(ANY_SCALAR_NAME)
239                .or_insert_with(|| fed_definitions.any_scalar_definition());
240        }
241
242        let query_type_name = schema
243            .schema_definition
244            .make_mut()
245            .query
246            .get_or_insert(ComponentName::from(name!("Query")));
247        if let ExtendedType::Object(query_type) = schema
248            .types
249            .entry(query_type_name.name.clone())
250            .or_insert(ExtendedType::Object(Node::new(ObjectType {
251                description: None,
252                name: query_type_name.name.clone(),
253                directives: Default::default(),
254                fields: IndexMap::default(),
255                implements_interfaces: IndexSet::default(),
256            })))
257        {
258            let query_type = query_type.make_mut();
259            query_type
260                .fields
261                .entry(SERVICE_SDL_QUERY)
262                .or_insert_with(|| fed_definitions.service_sdl_query_field());
263            if entities_present {
264                // _entities(representations: [_Any!]!): [_Entity]!
265                query_type
266                    .fields
267                    .entry(ENTITIES_QUERY)
268                    .or_insert_with(|| fed_definitions.entities_query_field());
269            }
270        }
271        Ok(())
272    }
273
274    fn locate_entities(
275        schema: &mut Schema,
276        fed_definitions: &FederationSpecDefinitions,
277    ) -> IndexSet<ComponentName> {
278        let mut entities = Vec::new();
279        let immutable_type_map = schema.types.to_owned();
280        for (named_type, extended_type) in immutable_type_map.iter() {
281            let is_entity = extended_type
282                .directives()
283                .iter()
284                .find(|d| {
285                    d.name
286                        == fed_definitions
287                            .namespaced_type_name(&KEY_DIRECTIVE_NAME, true)
288                            .as_str()
289                })
290                .map(|_| true)
291                .unwrap_or(false);
292            if is_entity {
293                entities.push(named_type);
294            }
295        }
296        let entity_set: IndexSet<ComponentName> =
297            entities.iter().map(|e| ComponentName::from(*e)).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<(), String> {
321        if self.subgraphs.contains_key(&subgraph.name) {
322            return Err(format!("A subgraph named {} already exists", subgraph.name));
323        }
324        self.subgraphs
325            .insert(subgraph.name.clone(), Arc::new(subgraph));
326        Ok(())
327    }
328
329    pub fn get(&self, name: &str) -> Option<Arc<Subgraph>> {
330        self.subgraphs.get(name).cloned()
331    }
332}
333
334pub struct ValidSubgraph {
335    pub name: String,
336    pub url: String,
337    pub schema: Valid<Schema>,
338}
339
340impl std::fmt::Debug for ValidSubgraph {
341    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
342        write!(f, r#"name: {}, url: {}"#, self.name, self.url)
343    }
344}
345
346impl From<ValidFederationSubgraph> for ValidSubgraph {
347    fn from(value: ValidFederationSubgraph) -> Self {
348        Self {
349            name: value.name,
350            url: value.url,
351            schema: value.schema.schema().clone(),
352        }
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359    use crate::subgraph::database::keys;
360
361    #[test]
362    fn can_inspect_a_type_key() {
363        // TODO: no schema expansion currently, so need to having the `@link` to `link` and the
364        // @link directive definition for @link-bootstrapping to work. Also, we should
365        // theoretically have the @key directive definition added too (but validation is not
366        // wired up yet, so we get away without). Point being, this is just some toy code at
367        // the moment.
368
369        let schema = r#"
370          extend schema
371            @link(url: "https://specs.apollo.dev/link/v1.0", import: ["Import"])
372            @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"])
373
374          type Query {
375            t: T
376          }
377
378          type T @key(fields: "id") {
379            id: ID!
380            x: Int
381          }
382
383          enum link__Purpose {
384            SECURITY
385            EXECUTION
386          }
387
388          scalar Import
389
390          directive @link(url: String, as: String, import: [Import], for: link__Purpose) repeatable on SCHEMA
391        "#;
392
393        let subgraph = Subgraph::new("S1", "http://s1", schema).unwrap();
394        let keys = keys(&subgraph.schema, &name!("T"));
395        assert_eq!(keys.len(), 1);
396        assert_eq!(keys.first().unwrap().type_name, name!("T"));
397
398        // TODO: no accessible selection yet.
399    }
400}