apollo_composition/
lib.rs

1use std::collections::HashMap;
2use std::iter::once;
3
4use apollo_compiler::schema::ExtendedType;
5use apollo_federation::sources::connect::{
6    expand::{expand_connectors, Connectors, ExpansionResult},
7    validation::{validate, Severity as ValidationSeverity, ValidationResult},
8};
9use apollo_federation_types::composition::SubgraphLocation;
10use apollo_federation_types::{
11    composition::{Issue, Severity},
12    javascript::{SatisfiabilityResult, SubgraphDefinition},
13};
14use either::Either;
15
16/// This trait includes all the Rust-side composition logic, plus hooks for the JavaScript side.
17/// If you implement the functions in this trait to build your own JavaScript interface, then you
18/// can call [`HybridComposition::compose`] to run the complete composition process.
19///
20/// JavaScript should be implemented using `@apollo/composition@2.9.0-connectors.0`.
21#[allow(async_fn_in_trait)]
22pub trait HybridComposition {
23    /// Call the JavaScript `composeServices` function from `@apollo/composition` plus whatever
24    /// extra logic you need. Make sure to disable satisfiability, like `composeServices(definitions, {runSatisfiability: false})`
25    async fn compose_services_without_satisfiability(
26        &mut self,
27        subgraph_definitions: Vec<SubgraphDefinition>,
28    ) -> Option<SupergraphSdl>;
29
30    /// Call the JavaScript `validateSatisfiability` function from `@apollo/composition` plus whatever
31    /// extra logic you need.
32    ///
33    /// # Input
34    ///
35    /// The `validateSatisfiability` function wants an argument like `{ supergraphSdl }`. That field
36    /// should be the value that's updated when [`update_supergraph_sdl`] is called.
37    ///
38    /// # Output
39    ///
40    /// If satisfiability completes from JavaScript, the [`SatisfiabilityResult`] (matching the shape
41    /// of that function) should be returned. If Satisfiability _can't_ be run, you can return an
42    /// `Err(Issue)` instead indicating what went wrong.
43    async fn validate_satisfiability(&mut self) -> Result<SatisfiabilityResult, Issue>;
44
45    /// Allows the Rust composition code to modify the stored supergraph SDL
46    /// (for example, to expand connectors).
47    fn update_supergraph_sdl(&mut self, supergraph_sdl: String);
48
49    /// When the Rust composition/validation code finds issues, it will call this method to add
50    /// them to the list of issues that will be returned to the user.
51    ///
52    /// It's on the implementor of this trait to convert `From<Issue>`
53    fn add_issues<Source: Iterator<Item = Issue>>(&mut self, issues: Source);
54
55    /// Runs the complete composition process, hooking into both the Rust and JavaScript implementations.
56    ///
57    /// # Asyncness
58    ///
59    /// While this function is async to allow for flexible JavaScript execution, it is a CPU-heavy task.
60    /// Take care when consuming this in an async context, as it may block longer than desired.
61    ///
62    /// # Algorithm
63    ///
64    /// 1. Run Rust-based validation on the subgraphs
65    /// 2. Call [`compose_services_without_satisfiability`] to run JavaScript-based composition
66    /// 3. Run Rust-based validation on the supergraph
67    /// 4. Call [`validate_satisfiability`] to run JavaScript-based validation on the supergraph
68    async fn compose(&mut self, subgraph_definitions: Vec<SubgraphDefinition>) {
69        let validation_results = subgraph_definitions
70            .iter()
71            .map(|subgraph| {
72                (
73                    subgraph.name.clone(),
74                    validate(&subgraph.sdl, &subgraph.name),
75                )
76            })
77            .collect::<Vec<_>>();
78        let subgraph_validation_errors = validation_results
79            .iter()
80            .flat_map(|(name, validation_result)| {
81                validation_result
82                    .errors
83                    .iter()
84                    .cloned()
85                    .map(|validation_error| Issue {
86                        code: validation_error.code.to_string(),
87                        message: validation_error.message,
88                        locations: validation_error
89                            .locations
90                            .into_iter()
91                            .map(|range| SubgraphLocation {
92                                subgraph: Some(name.clone()),
93                                range: Some(range),
94                            })
95                            .collect(),
96                        severity: convert_severity(validation_error.code.severity()),
97                    })
98            })
99            .collect::<Vec<_>>();
100
101        let run_composition = subgraph_validation_errors
102            .iter()
103            .all(|issue| issue.severity != Severity::Error);
104        self.add_issues(subgraph_validation_errors.into_iter());
105        if !run_composition {
106            return;
107        }
108
109        let Some(supergraph_sdl) = self
110            .compose_services_without_satisfiability(subgraph_definitions)
111            .await
112        else {
113            return;
114        };
115
116        // Any issues with overrides are fatal since they'll cause errors in expansion,
117        // so we return early if we see any.
118        let override_errors = validate_overrides(validation_results);
119        if !override_errors.is_empty() {
120            self.add_issues(override_errors.into_iter());
121            return;
122        }
123
124        let expansion_result = match expand_connectors(supergraph_sdl, &Default::default()) {
125            Ok(result) => result,
126            Err(err) => {
127                self.add_issues(once(Issue {
128                    code: "INTERNAL_ERROR".to_string(),
129                    message: format!(
130                        "Composition failed due to an internal error, please report this: {}",
131                        err
132                    ),
133                    locations: vec![],
134                    severity: Severity::Error,
135                }));
136                return;
137            }
138        };
139        match expansion_result {
140            ExpansionResult::Expanded {
141                raw_sdl,
142                connectors: Connectors {
143                    by_service_name, ..
144                },
145                ..
146            } => {
147                let original_supergraph_sdl = supergraph_sdl.to_string();
148                self.update_supergraph_sdl(raw_sdl);
149                let satisfiability_result = self.validate_satisfiability().await;
150                self.add_issues(
151                    satisfiability_result_into_issues(satisfiability_result).map(|mut issue| {
152                        for (service_name, connector) in by_service_name.iter() {
153                            issue.message = issue
154                                .message
155                                .replace(&**service_name, connector.id.subgraph_name.as_str());
156                        }
157                        issue
158                    }),
159                );
160
161                self.update_supergraph_sdl(original_supergraph_sdl);
162            }
163            ExpansionResult::Unchanged => {
164                let satisfiability_result = self.validate_satisfiability().await;
165                self.add_issues(satisfiability_result_into_issues(satisfiability_result));
166            }
167        }
168    }
169}
170
171/// Validate overrides for connector-related subgraphs
172///
173/// Overrides mess with the supergraph in ways that can be difficult to detect when
174/// expanding connectors; the supergraph may omit overridden fields and other shenanigans.
175/// To allow for a better developer experience, we check here if any connector-enabled subgraphs
176/// have fields overridden.
177fn validate_overrides(schemas: impl IntoIterator<Item = (String, ValidationResult)>) -> Vec<Issue> {
178    let validations_by_subgraph_name = HashMap::<_, _>::from_iter(schemas);
179    let mut override_errors = Vec::new();
180    for (subgraph_name, ValidationResult { schema, .. }) in validations_by_subgraph_name.iter() {
181        // We need to grab all fields in the schema since only fields can have the @override
182        // directive attached
183        macro_rules! extract_directives {
184            ($node:ident) => {
185                $node
186                    .fields
187                    .iter()
188                    .flat_map(|(name, field)| {
189                        field
190                            .directives
191                            .iter()
192                            .map(move |d| (format!("{}.{}", $node.name, name), d))
193                    })
194                    .collect::<Vec<_>>()
195            };
196        }
197
198        let override_directives = schema
199            .types
200            .values()
201            .flat_map(|v| match v {
202                ExtendedType::Object(node) => extract_directives!(node),
203                ExtendedType::Interface(node) => extract_directives!(node),
204                ExtendedType::InputObject(node) => extract_directives!(node),
205
206                // These types do not have fields
207                ExtendedType::Scalar(_) | ExtendedType::Union(_) | ExtendedType::Enum(_) => {
208                    Vec::new()
209                }
210            })
211            .filter(|(_, directive)| {
212                // TODO: The directive name for @override could have been aliased
213                // at the SDL level, so we'll need to extract the aliased name here instead
214                directive.name == "override" || directive.name == "federation__override"
215            });
216
217        // Now see if we have any overrides that try to reference connector subgraphs
218        for (field, directive) in override_directives {
219            // If the override directive does not have a valid `from` field, then there is
220            // no point trying to validate it, as later steps will validate the entire schema.
221            let Ok(Some(overriden_subgraph_name)) = directive
222                .argument_by_name("from", schema)
223                .map(|node| node.as_str())
224            else {
225                continue;
226            };
227
228            if let Some(overriden_subgraph) =
229                validations_by_subgraph_name.get(overriden_subgraph_name)
230            {
231                if overriden_subgraph.has_connectors {
232                    override_errors.push(Issue {
233                        code: "OVERRIDE_ON_CONNECTOR".to_string(),
234                        message: format!(
235                            r#"Field "{}" on subgraph "{}" is trying to override connector-enabled subgraph "{}", which is not yet supported. See https://go.apollo.dev/connectors/limitations#override-is-partially-unsupported"#,
236                            field,
237                            subgraph_name,
238                            overriden_subgraph_name,
239                        ),
240                        locations: vec![SubgraphLocation {
241                            subgraph: Some(subgraph_name.clone()),
242                            range: directive.line_column_range(&schema.sources),
243                        }],
244                        severity: Severity::Error,
245                    });
246                }
247            }
248        }
249    }
250
251    override_errors
252}
253
254pub type SupergraphSdl<'a> = &'a str;
255
256/// A successfully composed supergraph, optionally with some issues that should be addressed.
257#[derive(Clone, Debug)]
258pub struct PartialSuccess {
259    pub supergraph_sdl: String,
260    pub issues: Vec<Issue>,
261}
262
263fn convert_severity(severity: ValidationSeverity) -> Severity {
264    match severity {
265        ValidationSeverity::Error => Severity::Error,
266        ValidationSeverity::Warning => Severity::Warning,
267    }
268}
269
270fn satisfiability_result_into_issues(
271    satisfiability_result: Result<SatisfiabilityResult, Issue>,
272) -> Either<impl Iterator<Item = Issue>, impl Iterator<Item = Issue>> {
273    match satisfiability_result {
274        Ok(satisfiability_result) => Either::Left(
275            satisfiability_result
276                .errors
277                .into_iter()
278                .flatten()
279                .map(Issue::from)
280                .chain(
281                    satisfiability_result
282                        .hints
283                        .into_iter()
284                        .flatten()
285                        .map(Issue::from),
286                ),
287        ),
288        Err(issue) => Either::Right(once(issue)),
289    }
290}