Skip to main content

apollo_federation/composition/
mod.rs

1mod satisfiability;
2
3use std::sync::Arc;
4use std::vec;
5
6use tracing::instrument;
7
8pub use crate::composition::satisfiability::validate_satisfiability;
9use crate::connectors::Connector;
10use crate::connectors::expand::Connectors;
11use crate::connectors::expand::ExpansionResult;
12use crate::connectors::expand::expand_connectors;
13use crate::error::CompositionError;
14use crate::merger::merge::Merger;
15pub use crate::schema::schema_upgrader::upgrade_subgraphs_if_necessary;
16use crate::schema::validators::root_fields::validate_consistent_root_fields;
17use crate::subgraph::typestate::Expanded;
18use crate::subgraph::typestate::Initial;
19use crate::subgraph::typestate::Subgraph;
20use crate::subgraph::typestate::Validated;
21pub use crate::supergraph::CompositionHint;
22pub use crate::supergraph::Merged;
23pub use crate::supergraph::Satisfiable;
24pub use crate::supergraph::Supergraph;
25
26#[derive(Debug)]
27pub struct CompositionFailure {
28    pub errors: Vec<CompositionError>,
29    pub hints: Vec<CompositionHint>,
30}
31
32impl CompositionFailure {
33    pub fn from_errors(errors: Vec<CompositionError>) -> Self {
34        Self {
35            errors,
36            hints: Vec::new(),
37        }
38    }
39}
40
41impl From<Vec<CompositionError>> for CompositionFailure {
42    fn from(errors: Vec<CompositionError>) -> Self {
43        Self::from_errors(errors)
44    }
45}
46
47/// Options that configure composition. Mirrors the JS `CompositionOptions` interface
48/// (see `composition-js/src/compose.ts`). Most fields are not yet ported — add them here
49/// as needed.
50#[derive(Debug, Default, Clone)]
51pub struct CompositionOptions {
52    /// Maximum allowable number of outstanding subgraph paths to validate during satisfiability.
53    pub max_validation_subgraph_paths: Option<usize>,
54}
55
56/// Mirrors the JS `compose` function.
57#[instrument(skip(subgraphs, options))]
58pub fn compose(
59    subgraphs: Vec<Subgraph<Initial>>,
60    options: CompositionOptions,
61) -> Result<Supergraph<Satisfiable>, CompositionFailure> {
62    // explicitly sort subgraphs by their names
63    // this was done automatically in JS as Subgraphs class stored subgraphs in OrderedMap (by name)
64    let mut subgraphs = subgraphs;
65    subgraphs.sort_by(|s1, s2| s1.name.cmp(&s2.name));
66
67    tracing::debug!("Expanding subgraphs...");
68    let expanded_subgraphs = expand_subgraphs(subgraphs)?;
69    tracing::debug!("Upgrading subgraphs...");
70    let validated_subgraphs = upgrade_subgraphs_if_necessary(expanded_subgraphs)
71        .map_err(CompositionFailure::from_errors)?;
72
73    tracing::debug!("Pre-merge validations...");
74    pre_merge_validations(&validated_subgraphs)?;
75    tracing::debug!("Merging subgraphs...");
76    let supergraph = merge_subgraphs(validated_subgraphs, &options)?;
77    tracing::debug!("Post-merge validations...");
78    post_merge_validations(&supergraph)?;
79    tracing::debug!("Validating satisfiability...");
80    validate_satisfiability(supergraph, &options)
81}
82
83/// Mirrors the `HybridComposition::compose` from the apollo-composition crate.
84pub fn compose_with_connectors(
85    subgraphs: Vec<Subgraph<Initial>>,
86    options: CompositionOptions,
87) -> Result<Supergraph<Satisfiable>, CompositionFailure> {
88    // Pre-expand validation
89    // - These were supposed to be pre-merge validations, but historically FBP performed these
90    //   Rust-based validation, before JS composition.
91    // - Once JS-to-Rust migration is done, we can move these to pre-merge validations.
92    // TODO: (FED-855) Call `connectors::validation`, which may change the subgraphs before upgrading.
93
94    tracing::debug!("Expanding subgraphs...");
95    let expanded_subgraphs = expand_subgraphs(subgraphs)?;
96
97    tracing::debug!("Upgrading subgraphs...");
98    let validated_subgraphs = upgrade_subgraphs_if_necessary(expanded_subgraphs)
99        .map_err(CompositionFailure::from_errors)?;
100
101    tracing::debug!("Pre-merge validations...");
102    pre_merge_validations(&validated_subgraphs)?;
103
104    tracing::debug!("Merging subgraphs...");
105    let supergraph = merge_subgraphs(validated_subgraphs, &options)?;
106
107    tracing::debug!("Post-merge validations...");
108    post_merge_validations(&supergraph)?;
109    // TODO: (FED-855) Call `validate_overrides`, which validates the original subgraphs for connectors after merging.
110    // - Once JS-to-Rust migration is done, we may consider to move that to the pre-merge validation step.
111
112    tracing::debug!("Validating satisfiability...");
113    validate_satisfiability_with_connectors(supergraph, &options)
114}
115
116/// Apollo Federation allow subgraphs to specify partial schemas (i.e. "import" directives through
117/// `@link`). This function will update subgraph schemas with all missing federation definitions.
118#[instrument(skip(subgraphs))]
119pub fn expand_subgraphs(
120    subgraphs: Vec<Subgraph<Initial>>,
121) -> Result<Vec<Subgraph<Expanded>>, CompositionFailure> {
122    let mut errors: Vec<CompositionError> = vec![];
123    let expanded: Vec<Subgraph<Expanded>> = subgraphs
124        .into_iter()
125        .map(|s| s.expand_links())
126        .filter_map(|r| r.map_err(|e| errors.extend(e.to_composition_errors())).ok())
127        .collect();
128    if errors.is_empty() {
129        Ok(expanded)
130    } else {
131        Err(CompositionFailure::from_errors(errors))
132    }
133}
134
135/// Perform validations that require information about all available subgraphs.
136#[instrument(skip(subgraphs))]
137pub fn pre_merge_validations(subgraphs: &[Subgraph<Validated>]) -> Result<(), CompositionFailure> {
138    validate_consistent_root_fields(subgraphs).map_err(CompositionFailure::from_errors)?;
139    // TODO: (FED-713) Implement any pre-merge validations that require knowledge of all subgraphs.
140    Ok(())
141}
142
143#[instrument(skip(subgraphs, options))]
144pub fn merge_subgraphs(
145    subgraphs: Vec<Subgraph<Validated>>,
146    options: &CompositionOptions,
147) -> Result<Supergraph<Merged>, CompositionFailure> {
148    let merger = Merger::new(subgraphs, options.clone()).map_err(|e| {
149        CompositionFailure::from_errors(vec![CompositionError::InternalError {
150            message: e.to_string(),
151        }])
152    })?;
153    let result = merger.merge().map_err(|e| {
154        CompositionFailure::from_errors(vec![CompositionError::InternalError {
155            message: e.to_string(),
156        }])
157    })?;
158    tracing::trace!(
159        "Merge has {} errors and {} hints",
160        result.errors.len(),
161        result.hints.len()
162    );
163    if result.errors.is_empty() {
164        let Some(supergraph_schema) = result.supergraph else {
165            return Err(CompositionFailure::from_errors(vec![
166                CompositionError::InternalError {
167                    message: "Merge completed with no supergraph schema".to_string(),
168                },
169            ]));
170        };
171        let supergraph = Supergraph::with_hints(supergraph_schema, result.hints);
172        Ok(supergraph)
173    } else {
174        Err(CompositionFailure {
175            errors: result.errors,
176            hints: result.hints,
177        })
178    }
179}
180
181#[instrument(skip(_supergraph))]
182pub fn post_merge_validations(_supergraph: &Supergraph<Merged>) -> Result<(), CompositionFailure> {
183    // TODO: (FED-714) Implement any post-merge validations other than satisfiability, which is
184    // checked separately.
185    Ok(())
186}
187
188/// Mirroring HybridComposition in the apollo-composition crate.
189/// - Expand connectors as needed.
190pub fn validate_satisfiability_with_connectors(
191    supergraph: Supergraph<Merged>,
192    options: &CompositionOptions,
193) -> Result<Supergraph<Satisfiable>, CompositionFailure> {
194    let merge_hints = supergraph.hints().to_vec();
195    let supergraph_str = supergraph.schema().schema().to_string();
196    let expansion_result = match expand_connectors(&supergraph_str, &Default::default()) {
197        Ok(result) => result,
198        Err(e) => {
199            return Err(CompositionFailure {
200                errors: vec![CompositionError::InternalError {
201                    message: format!(
202                        "Composition failed due to an internal error when expanding connectors, please report this: {e}"
203                    ),
204                }],
205                hints: merge_hints,
206            });
207        }
208    };
209
210    match expansion_result {
211        ExpansionResult::Expanded {
212            raw_sdl,
213            connectors: Connectors {
214                by_service_name, ..
215            },
216            ..
217        } => {
218            let expanded_supergraph = match Supergraph::parse(&raw_sdl) {
219                Ok(s) => s,
220                Err(e) => {
221                    return Err(CompositionFailure {
222                        errors: vec![CompositionError::InternalError {
223                            message: e.to_string(),
224                        }],
225                        hints: merge_hints,
226                    });
227                }
228            };
229            match validate_satisfiability(expanded_supergraph, options) {
230                Ok(mut supergraph) => {
231                    for hint in supergraph.hints_mut() {
232                        sanitize_connectors_message(&mut hint.message, by_service_name.iter());
233                    }
234                    Ok(supergraph)
235                }
236                Err(mut failure) => {
237                    failure.hints.extend(merge_hints);
238                    for error in failure.errors.iter_mut() {
239                        sanitize_connectors_error(error, by_service_name.iter());
240                    }
241                    for hint in failure.hints.iter_mut() {
242                        sanitize_connectors_message(&mut hint.message, by_service_name.iter());
243                    }
244                    Err(failure)
245                }
246            }
247        }
248        ExpansionResult::Unchanged => validate_satisfiability(supergraph, options),
249    }
250}
251
252fn sanitize_connectors_error<'a>(
253    issue: &mut CompositionError,
254    connector_subgraphs: impl Iterator<Item = (&'a Arc<str>, &'a Connector)>,
255) {
256    match issue {
257        CompositionError::SatisfiabilityError { message } => {
258            sanitize_connectors_message(message, connector_subgraphs);
259        }
260        CompositionError::ShareableHasMismatchedRuntimeTypes { message } => {
261            sanitize_connectors_message(message, connector_subgraphs);
262        }
263        _ => {}
264    }
265}
266
267fn sanitize_connectors_message<'a>(
268    message: &mut String,
269    connector_subgraphs: impl Iterator<Item = (&'a Arc<str>, &'a Connector)>,
270) {
271    for (service_name, connector) in connector_subgraphs {
272        *message = message.replace(&**service_name, connector.id.subgraph_name.as_str());
273    }
274}