apollo_composition/
lib.rs

1use std::collections::HashMap;
2use std::iter::once;
3
4use apollo_compiler::{schema::ExtendedType, Schema};
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 mut subgraph_validation_errors = Vec::new();
70        let mut parsed_schemas = HashMap::new();
71        let subgraph_definitions = subgraph_definitions
72            .into_iter()
73            .map(|mut subgraph| {
74                let ValidationResult {
75                    errors,
76                    has_connectors,
77                    schema,
78                    transformed,
79                } = validate(subgraph.sdl, &subgraph.name);
80                subgraph.sdl = transformed;
81                for error in errors {
82                    subgraph_validation_errors.push(Issue {
83                        code: error.code.to_string(),
84                        message: error.message,
85                        locations: error
86                            .locations
87                            .into_iter()
88                            .map(|range| SubgraphLocation {
89                                subgraph: Some(subgraph.name.clone()),
90                                range: Some(range),
91                            })
92                            .collect(),
93                        severity: convert_severity(error.code.severity()),
94                    })
95                }
96                parsed_schemas.insert(
97                    subgraph.name.clone(),
98                    SubgraphSchema {
99                        schema,
100                        has_connectors,
101                    },
102                );
103                subgraph
104            })
105            .collect();
106
107        let run_composition = subgraph_validation_errors
108            .iter()
109            .all(|issue| issue.severity != Severity::Error);
110        self.add_issues(subgraph_validation_errors.into_iter());
111        if !run_composition {
112            return;
113        }
114
115        let Some(supergraph_sdl) = self
116            .compose_services_without_satisfiability(subgraph_definitions)
117            .await
118        else {
119            return;
120        };
121
122        // Any issues with overrides are fatal since they'll cause errors in expansion,
123        // so we return early if we see any.
124        let override_errors = validate_overrides(parsed_schemas);
125        if !override_errors.is_empty() {
126            self.add_issues(override_errors.into_iter());
127            return;
128        }
129
130        let expansion_result = match expand_connectors(supergraph_sdl, &Default::default()) {
131            Ok(result) => result,
132            Err(err) => {
133                self.add_issues(once(Issue {
134                    code: "INTERNAL_ERROR".to_string(),
135                    message: format!(
136                        "Composition failed due to an internal error, please report this: {}",
137                        err
138                    ),
139                    locations: vec![],
140                    severity: Severity::Error,
141                }));
142                return;
143            }
144        };
145        match expansion_result {
146            ExpansionResult::Expanded {
147                raw_sdl,
148                connectors: Connectors {
149                    by_service_name, ..
150                },
151                ..
152            } => {
153                let original_supergraph_sdl = supergraph_sdl.to_string();
154                self.update_supergraph_sdl(raw_sdl);
155                let satisfiability_result = self.validate_satisfiability().await;
156                self.add_issues(
157                    satisfiability_result_into_issues(satisfiability_result).map(|mut issue| {
158                        for (service_name, connector) in by_service_name.iter() {
159                            issue.message = issue
160                                .message
161                                .replace(&**service_name, connector.id.subgraph_name.as_str());
162                        }
163                        issue
164                    }),
165                );
166
167                self.update_supergraph_sdl(original_supergraph_sdl);
168            }
169            ExpansionResult::Unchanged => {
170                let satisfiability_result = self.validate_satisfiability().await;
171                self.add_issues(satisfiability_result_into_issues(satisfiability_result));
172            }
173        }
174    }
175}
176
177struct SubgraphSchema {
178    schema: Schema,
179    has_connectors: bool,
180}
181
182/// Validate overrides for connector-related subgraphs
183///
184/// Overrides mess with the supergraph in ways that can be difficult to detect when
185/// expanding connectors; the supergraph may omit overridden fields and other shenanigans.
186/// To allow for a better developer experience, we check here if any connector-enabled subgraphs
187/// have fields overridden.
188fn validate_overrides(schemas: HashMap<String, SubgraphSchema>) -> Vec<Issue> {
189    let mut override_errors = Vec::new();
190    for (subgraph_name, SubgraphSchema { schema, .. }) in &schemas {
191        // We need to grab all fields in the schema since only fields can have the @override
192        // directive attached
193        macro_rules! extract_directives {
194            ($node:ident) => {
195                $node
196                    .fields
197                    .iter()
198                    .flat_map(|(name, field)| {
199                        field
200                            .directives
201                            .iter()
202                            .map(move |d| (format!("{}.{}", $node.name, name), d))
203                    })
204                    .collect::<Vec<_>>()
205            };
206        }
207
208        let override_directives = schema
209            .types
210            .values()
211            .flat_map(|v| match v {
212                ExtendedType::Object(node) => extract_directives!(node),
213                ExtendedType::Interface(node) => extract_directives!(node),
214                ExtendedType::InputObject(node) => extract_directives!(node),
215
216                // These types do not have fields
217                ExtendedType::Scalar(_) | ExtendedType::Union(_) | ExtendedType::Enum(_) => {
218                    Vec::new()
219                }
220            })
221            .filter(|(_, directive)| {
222                // TODO: The directive name for @override could have been aliased
223                // at the SDL level, so we'll need to extract the aliased name here instead
224                directive.name == "override" || directive.name == "federation__override"
225            });
226
227        // Now see if we have any overrides that try to reference connector subgraphs
228        for (field, directive) in override_directives {
229            // If the override directive does not have a valid `from` field, then there is
230            // no point trying to validate it, as later steps will validate the entire schema.
231            let Ok(Some(overridden_subgraph_name)) = directive
232                .argument_by_name("from", schema)
233                .map(|node| node.as_str())
234            else {
235                continue;
236            };
237
238            if schemas
239                .get(overridden_subgraph_name)
240                .is_some_and(|schema| schema.has_connectors)
241            {
242                override_errors.push(Issue {
243                        code: "OVERRIDE_ON_CONNECTOR".to_string(),
244                        message: format!(
245                            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"#,
246                            field,
247                            subgraph_name,
248                            overridden_subgraph_name,
249                        ),
250                        locations: vec![SubgraphLocation {
251                            subgraph: Some(String::from(overridden_subgraph_name)),
252                            range: directive.line_column_range(&schema.sources),
253                        }],
254                        severity: Severity::Error,
255                    });
256            }
257        }
258    }
259
260    override_errors
261}
262
263pub type SupergraphSdl<'a> = &'a str;
264
265/// A successfully composed supergraph, optionally with some issues that should be addressed.
266#[derive(Clone, Debug)]
267pub struct PartialSuccess {
268    pub supergraph_sdl: String,
269    pub issues: Vec<Issue>,
270}
271
272fn convert_severity(severity: ValidationSeverity) -> Severity {
273    match severity {
274        ValidationSeverity::Error => Severity::Error,
275        ValidationSeverity::Warning => Severity::Warning,
276    }
277}
278
279fn satisfiability_result_into_issues(
280    satisfiability_result: Result<SatisfiabilityResult, Issue>,
281) -> Either<impl Iterator<Item = Issue>, impl Iterator<Item = Issue>> {
282    match satisfiability_result {
283        Ok(satisfiability_result) => Either::Left(
284            satisfiability_result
285                .errors
286                .into_iter()
287                .flatten()
288                .map(Issue::from)
289                .chain(
290                    satisfiability_result
291                        .hints
292                        .into_iter()
293                        .flatten()
294                        .map(Issue::from),
295                ),
296        ),
297        Err(issue) => Either::Right(once(issue)),
298    }
299}