1#![warn(
17 rustdoc::broken_intra_doc_links,
18 unreachable_pub,
19 unreachable_patterns,
20 unused,
21 unused_qualifications,
22 dead_code,
23 while_true,
24 unconditional_panic,
25 clippy::all
26)]
27
28mod api_schema;
29mod compat;
30pub mod composition;
31pub mod connectors;
32#[cfg(feature = "correctness")]
33pub mod correctness;
34mod display_helpers;
35pub mod error;
36pub mod link;
37pub mod merge;
38mod merger;
39pub(crate) mod operation;
40pub mod query_graph;
41pub mod query_plan;
42pub mod schema;
43pub mod subgraph;
44pub mod supergraph;
45
46pub mod utils;
47
48use apollo_compiler::Schema;
49use apollo_compiler::ast::NamedType;
50use apollo_compiler::collections::HashSet;
51use apollo_compiler::validation::Valid;
52use itertools::Itertools;
53use link::cache_tag_spec_definition::CACHE_TAG_VERSIONS;
54use link::join_spec_definition::JOIN_VERSIONS;
55use schema::FederationSchema;
56use strum::IntoEnumIterator;
57
58pub use crate::api_schema::ApiSchemaOptions;
59use crate::connectors::ConnectSpec;
60use crate::error::FederationError;
61use crate::error::MultiTryAll;
62use crate::error::MultipleFederationErrors;
63use crate::error::SingleFederationError;
64use crate::link::authenticated_spec_definition::AUTHENTICATED_VERSIONS;
65use crate::link::context_spec_definition::CONTEXT_VERSIONS;
66use crate::link::context_spec_definition::ContextSpecDefinition;
67use crate::link::cost_spec_definition::COST_VERSIONS;
68use crate::link::inaccessible_spec_definition::INACCESSIBLE_VERSIONS;
69use crate::link::join_spec_definition::JoinSpecDefinition;
70use crate::link::link_spec_definition::CORE_VERSIONS;
71use crate::link::link_spec_definition::LinkSpecDefinition;
72use crate::link::policy_spec_definition::POLICY_VERSIONS;
73use crate::link::requires_scopes_spec_definition::REQUIRES_SCOPES_VERSIONS;
74use crate::link::spec::Identity;
75use crate::link::spec::Url;
76use crate::link::spec::Version;
77use crate::link::spec_definition::SpecDefinition;
78use crate::link::spec_definition::SpecDefinitions;
79use crate::link::tag_spec_definition::TAG_VERSIONS;
80use crate::merge::MergeFailure;
81use crate::merge::merge_subgraphs;
82use crate::schema::ValidFederationSchema;
83use crate::subgraph::ValidSubgraph;
84pub use crate::supergraph::ValidFederationSubgraph;
85pub use crate::supergraph::ValidFederationSubgraphs;
86
87pub mod internal_lsp_api {
88 pub use crate::subgraph::schema_diff_expanded_from_initial;
89}
90
91pub mod internal_composition_api {
93 use super::*;
94 use crate::schema::validators::cache_tag;
95 use crate::subgraph::typestate;
96
97 #[derive(Default)]
98 pub struct ValidationResult {
99 pub errors: Vec<cache_tag::Message>,
101 }
102
103 pub fn validate_cache_tag_directives(
111 name: &str,
112 url: &str,
113 sdl: &str,
114 ) -> Result<ValidationResult, FederationError> {
115 let subgraph =
116 typestate::Subgraph::parse(name, url, sdl).map_err(|e| e.into_federation_error())?;
117 let subgraph = subgraph
118 .expand_links()
119 .map_err(|e| e.into_federation_error())?;
120 let mut result = ValidationResult::default();
121 cache_tag::validate_cache_tag_directives(subgraph.schema(), &mut result.errors)?;
122 Ok(result)
123 }
124}
125
126pub(crate) type SupergraphSpecs = (
127 &'static LinkSpecDefinition,
128 &'static JoinSpecDefinition,
129 Option<&'static ContextSpecDefinition>,
130);
131
132pub(crate) fn validate_supergraph_for_query_planning(
133 supergraph_schema: &FederationSchema,
134) -> Result<SupergraphSpecs, FederationError> {
135 validate_supergraph(supergraph_schema, &JOIN_VERSIONS, &CONTEXT_VERSIONS)
136}
137
138pub(crate) fn validate_supergraph(
140 supergraph_schema: &FederationSchema,
141 join_versions: &'static SpecDefinitions<JoinSpecDefinition>,
142 context_versions: &'static SpecDefinitions<ContextSpecDefinition>,
143) -> Result<SupergraphSpecs, FederationError> {
144 let Some(metadata) = supergraph_schema.metadata() else {
145 return Err(SingleFederationError::InvalidFederationSupergraph {
146 message: "Invalid supergraph: must be a core schema".to_owned(),
147 }
148 .into());
149 };
150 let link_spec_definition = metadata.link_spec_definition()?;
151 let Some(join_link) = metadata.for_identity(&Identity::join_identity()) else {
152 return Err(SingleFederationError::InvalidFederationSupergraph {
153 message: "Invalid supergraph: must use the join spec".to_owned(),
154 }
155 .into());
156 };
157 let Some(join_spec_definition) = join_versions.find(&join_link.url.version) else {
158 return Err(SingleFederationError::InvalidFederationSupergraph {
159 message: format!(
160 "Invalid supergraph: uses unsupported join spec version {} (supported versions: {})",
161 join_link.url.version,
162 join_versions.versions().map(|v| v.to_string()).collect::<Vec<_>>().join(", "),
163 ),
164 }.into());
165 };
166 let context_spec_definition = metadata.for_identity(&Identity::context_identity()).map(|context_link| {
167 context_versions.find(&context_link.url.version).ok_or_else(|| {
168 SingleFederationError::InvalidFederationSupergraph {
169 message: format!(
170 "Invalid supergraph: uses unsupported context spec version {} (supported versions: {})",
171 context_link.url.version,
172 context_versions.versions().join(", "),
173 ),
174 }
175 })
176 }).transpose()?;
177 if let Some(connect_link) = metadata.for_identity(&ConnectSpec::identity()) {
178 ConnectSpec::try_from(&connect_link.url.version)
179 .map_err(|message| SingleFederationError::UnknownLinkVersion { message })?;
180 }
181 Ok((
182 link_spec_definition,
183 join_spec_definition,
184 context_spec_definition,
185 ))
186}
187
188#[derive(Debug)]
189pub struct Supergraph {
190 pub schema: ValidFederationSchema,
191}
192
193impl Supergraph {
194 pub fn new_with_spec_check(
195 schema_str: &str,
196 supported_specs: &[Url],
197 ) -> Result<Self, FederationError> {
198 let schema = Schema::parse_and_validate(schema_str, "schema.graphql")?;
199 Self::from_schema(schema, Some(supported_specs))
200 }
201
202 pub fn new(schema_str: &str) -> Result<Self, FederationError> {
204 Self::new_with_spec_check(schema_str, &default_supported_supergraph_specs())
205 }
206
207 pub fn new_with_router_specs(schema_str: &str) -> Result<Self, FederationError> {
209 Self::new_with_spec_check(schema_str, &router_supported_supergraph_specs())
210 }
211
212 pub fn from_schema(
216 schema: Valid<Schema>,
217 supported_specs: Option<&[Url]>,
218 ) -> Result<Self, FederationError> {
219 let schema: Schema = schema.into_inner();
220 let schema = FederationSchema::new(schema)?;
221
222 let _ = validate_supergraph_for_query_planning(&schema)?;
223
224 if let Some(supported_specs) = supported_specs {
225 check_spec_support(&schema, supported_specs)?;
226 }
227
228 Ok(Self {
229 schema: schema.assume_valid()?,
231 })
232 }
233
234 pub fn compose(subgraphs: Vec<&ValidSubgraph>) -> Result<Self, MergeFailure> {
235 let schema = merge_subgraphs(subgraphs)?.schema;
236 Ok(Self {
237 schema: ValidFederationSchema::new(schema).map_err(Into::<MergeFailure>::into)?,
238 })
239 }
240
241 pub fn to_api_schema(
244 &self,
245 options: ApiSchemaOptions,
246 ) -> Result<ValidFederationSchema, FederationError> {
247 api_schema::to_api_schema(self.schema.clone(), options)
248 }
249
250 pub fn extract_subgraphs(&self) -> Result<ValidFederationSubgraphs, FederationError> {
251 supergraph::extract_subgraphs_from_supergraph(&self.schema, None)
252 }
253}
254
255const _: () = {
256 const fn assert_thread_safe<T: Sync + Send>() {}
257
258 assert_thread_safe::<Supergraph>();
259 assert_thread_safe::<query_plan::query_planner::QueryPlanner>();
260};
261
262pub(crate) fn is_leaf_type(schema: &Schema, ty: &NamedType) -> bool {
264 schema.get_scalar(ty).is_some() || schema.get_enum(ty).is_some()
265}
266
267pub fn default_supported_supergraph_specs() -> Vec<Url> {
268 fn urls(defs: &SpecDefinitions<impl SpecDefinition>) -> impl Iterator<Item = Url> {
269 defs.iter().map(|(_, def)| def.url()).cloned()
270 }
271
272 urls(&CORE_VERSIONS)
273 .chain(urls(&JOIN_VERSIONS))
274 .chain(urls(&TAG_VERSIONS))
275 .chain(urls(&INACCESSIBLE_VERSIONS))
276 .collect()
277}
278
279pub fn router_supported_supergraph_specs() -> Vec<Url> {
281 fn urls(defs: &SpecDefinitions<impl SpecDefinition>) -> impl Iterator<Item = Url> {
282 defs.iter().map(|(_, def)| def.url()).cloned()
283 }
284
285 default_supported_supergraph_specs()
288 .into_iter()
289 .chain(urls(&AUTHENTICATED_VERSIONS))
290 .chain(urls(&REQUIRES_SCOPES_VERSIONS))
291 .chain(urls(&POLICY_VERSIONS))
292 .chain(urls(&CONTEXT_VERSIONS))
293 .chain(urls(&COST_VERSIONS))
294 .chain(urls(&CACHE_TAG_VERSIONS))
295 .chain(ConnectSpec::iter().map(|s| s.url()))
296 .collect()
297}
298
299fn is_core_version_zero_dot_one(url: &Url) -> bool {
300 CORE_VERSIONS
301 .find(&Version { major: 0, minor: 1 })
302 .is_some_and(|v| *v.url() == *url)
303}
304
305fn check_spec_support(
306 schema: &FederationSchema,
307 supported_specs: &[Url],
308) -> Result<(), FederationError> {
309 let Some(metadata) = schema.metadata() else {
310 bail!("Schema must have metadata");
312 };
313 let mut errors = MultipleFederationErrors::new();
314 let link_spec = metadata.link_spec_definition()?;
315 if is_core_version_zero_dot_one(link_spec.url()) {
316 let has_link_with_purpose = metadata
317 .all_links()
318 .iter()
319 .any(|link| link.purpose.is_some());
320 if has_link_with_purpose {
321 errors.push(SingleFederationError::UnsupportedLinkedFeature {
327 message: format!(
328 "the `for:` argument is unsupported by version {version} of the core spec.\n\
329 Please upgrade to at least @core v0.2 (https://specs.apollo.dev/core/v0.2).",
330 version = link_spec.url().version),
331 }.into());
332 }
333 }
334
335 let supported_specs: HashSet<_> = supported_specs.iter().collect();
336 errors
337 .and_try(metadata.all_links().iter().try_for_all(|link| {
338 let Some(purpose) = link.purpose else {
339 return Ok(());
340 };
341 if !is_core_version_zero_dot_one(&link.url)
342 && purpose != link::Purpose::EXECUTION
343 && purpose != link::Purpose::SECURITY
344 {
345 return Ok(());
346 }
347
348 let link_url = &link.url;
349 if supported_specs.contains(link_url) {
350 Ok(())
351 } else {
352 Err(SingleFederationError::UnsupportedLinkedFeature {
353 message: format!("feature {link_url} is for: {purpose} but is unsupported"),
354 }
355 .into())
356 }
357 }))
358 .into_result()
359}
360
361#[cfg(test)]
362mod test_supergraph {
363 use pretty_assertions::assert_str_eq;
364
365 use super::*;
366 use crate::internal_composition_api::ValidationResult;
367 use crate::internal_composition_api::validate_cache_tag_directives;
368
369 #[test]
370 fn validates_connect_spec_is_known() {
371 let res = Supergraph::new(
372 r#"
373 extend schema @link(url: "https://specs.apollo.dev/connect/v99.99")
374
375 # Required stuff for the supergraph to parse at all, not what we're testing
376 extend schema
377 @link(url: "https://specs.apollo.dev/link/v1.0")
378 @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
379 directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
380 scalar link__Import
381 enum link__Purpose {
382 """
383 `SECURITY` features provide metadata necessary to securely resolve fields.
384 """
385 SECURITY
386
387 """
388 `EXECUTION` features provide metadata necessary for operation execution.
389 """
390 EXECUTION
391 }
392 type Query {required: ID!}
393 "#,
394 )
395 .expect_err("Unknown spec version did not cause error");
396 assert_str_eq!(res.to_string(), "Unknown connect version: 99.99");
397 }
398
399 #[track_caller]
400 fn build_and_validate(name: &str, url: &str, sdl: &str) -> ValidationResult {
401 validate_cache_tag_directives(name, url, sdl).unwrap()
402 }
403
404 #[test]
405 fn it_validates_cache_tag_directives() {
406 let res = build_and_validate(
408 "accounts",
409 "accounts.graphql",
410 r#"
411 extend schema
412 @link(
413 url: "https://specs.apollo.dev/federation/v2.11"
414 import: ["@key"]
415 )
416
417 type Query {
418 topProducts(first: Int = 5): [Product]
419 }
420
421 type Product
422 @key(fields: "upc")
423 @key(fields: "name") {
424 upc: String!
425 name: String!
426 price: Int
427 weight: Int
428 }
429 "#,
430 );
431
432 assert!(res.errors.is_empty());
433
434 let res = build_and_validate(
436 "accounts",
437 "https://accounts",
438 r#"
439 extend schema
440 @link(
441 url: "https://specs.apollo.dev/federation/v2.12"
442 import: ["@key", "@cacheTag"]
443 )
444
445 type Query {
446 topProducts(first: Int = 5): [Product]
447 @cacheTag(format: "topProducts")
448 @cacheTag(format: "topProducts-{$args.first}")
449 }
450
451 type Product
452 @key(fields: "upc")
453 @key(fields: "name")
454 @cacheTag(format: "product-{$key.upc}") {
455 upc: String!
456 name: String!
457 price: Int
458 weight: Int
459 }
460 "#,
461 );
462
463 assert_eq!(
464 res.errors
465 .into_iter()
466 .map(|err| err.to_string())
467 .collect::<Vec<String>>(),
468 vec![
469 "Each entity field referenced in a @cacheTag format (applied on entity type) must be a member of every @key field set. In other words, when there are multiple @key fields on the type, the referenced field(s) must be limited to their intersection. Bad cacheTag format \"product-{$key.upc}\" on type \"Product\"",
470 "@cacheTag format references a nullable argument \"first\""
471 ]
472 );
473
474 let res = build_and_validate(
476 "accounts",
477 "accounts.graphql",
478 r#"
479 extend schema
480 @link(
481 url: "https://specs.apollo.dev/federation/v2.12"
482 import: ["@key", "@cacheTag"]
483 )
484
485 type Query {
486 topProducts(first: Int! = 5): [Product]
487 @cacheTag(format: "topProducts")
488 @cacheTag(format: "topProducts-{$args.first}")
489 }
490
491 type Product
492 @key(fields: "upc")
493 @cacheTag(format: "product-{$key.upc}") {
494 upc: String!
495 name: String!
496 price: Int
497 weight: Int
498 }
499 "#,
500 );
501
502 assert!(res.errors.is_empty());
503 }
504}