apollo_composition/
lib.rs1use 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#[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 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 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
182fn validate_overrides(schemas: HashMap<String, SubgraphSchema>) -> Vec<Issue> {
189 let mut override_errors = Vec::new();
190 for (subgraph_name, SubgraphSchema { schema, .. }) in &schemas {
191 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 ExtendedType::Scalar(_) | ExtendedType::Union(_) | ExtendedType::Enum(_) => {
218 Vec::new()
219 }
220 })
221 .filter(|(_, directive)| {
222 directive.name == "override" || directive.name == "federation__override"
225 });
226
227 for (field, directive) in override_directives {
229 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#[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}