apollo_composition/
lib.rs1use 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#[allow(async_fn_in_trait)]
22pub trait HybridComposition {
23 async fn compose_services_without_satisfiability(
26 &mut self,
27 subgraph_definitions: Vec<SubgraphDefinition>,
28 ) -> Option<SupergraphSdl>;
29
30 async fn validate_satisfiability(&mut self) -> Result<SatisfiabilityResult, Issue>;
44
45 fn update_supergraph_sdl(&mut self, supergraph_sdl: String);
48
49 fn add_issues<Source: Iterator<Item = Issue>>(&mut self, issues: Source);
54
55 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 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
171fn 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 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 ExtendedType::Scalar(_) | ExtendedType::Union(_) | ExtendedType::Enum(_) => {
208 Vec::new()
209 }
210 })
211 .filter(|(_, directive)| {
212 directive.name == "override" || directive.name == "federation__override"
215 });
216
217 for (field, directive) in override_directives {
219 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#[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}