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;
29pub mod 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::compat::coerce_and_validate_schema_values;
60use crate::connectors::ConnectSpec;
61use crate::error::FederationError;
62use crate::error::MultiTryAll;
63use crate::error::MultipleFederationErrors;
64use crate::error::SingleFederationError;
65use crate::link::authenticated_spec_definition::AUTHENTICATED_VERSIONS;
66use crate::link::context_spec_definition::CONTEXT_VERSIONS;
67use crate::link::context_spec_definition::ContextSpecDefinition;
68use crate::link::cost_spec_definition::COST_VERSIONS;
69use crate::link::inaccessible_spec_definition::INACCESSIBLE_VERSIONS;
70use crate::link::join_spec_definition::JoinSpecDefinition;
71use crate::link::link_spec_definition::CORE_VERSIONS;
72use crate::link::link_spec_definition::LinkSpecDefinition;
73use crate::link::policy_spec_definition::POLICY_VERSIONS;
74use crate::link::requires_scopes_spec_definition::REQUIRES_SCOPES_VERSIONS;
75use crate::link::spec::Identity;
76use crate::link::spec::Url;
77use crate::link::spec::Version;
78use crate::link::spec_definition::SpecDefinition;
79use crate::link::spec_definition::SpecDefinitions;
80use crate::link::tag_spec_definition::TAG_VERSIONS;
81use crate::merge::MergeFailure;
82use crate::merge::merge_subgraphs;
83use crate::schema::ValidFederationSchema;
84use crate::subgraph::ValidSubgraph;
85pub use crate::supergraph::ValidFederationSubgraph;
86pub use crate::supergraph::ValidFederationSubgraphs;
87
88pub mod internal_lsp_api {
89 pub use crate::subgraph::schema_diff_expanded_from_initial;
90}
91
92pub mod internal_composition_api {
94 use std::ops::Range;
95
96 use apollo_compiler::parser::LineColumn;
97
98 use super::*;
99 use crate::error::MultipleFederationErrors;
100 use crate::schema::validators::cache_tag;
101 use crate::subgraph::typestate;
102
103 #[derive(Clone, Debug)]
104 pub struct Message {
105 code: String,
106 message: String,
107 locations: Vec<Range<LineColumn>>,
108 }
109
110 impl Message {
111 pub fn code(&self) -> &str {
112 &self.code
113 }
114
115 pub fn message(&self) -> &str {
116 &self.message
117 }
118
119 pub fn locations(&self) -> &[Range<LineColumn>] {
120 &self.locations
121 }
122 }
123
124 #[derive(Default)]
125 pub struct ValidationResult {
126 pub errors: Vec<Message>,
128 }
129
130 pub fn validate_cache_tag_directives(
138 name: &str,
139 url: &str,
140 sdl: &str,
141 ) -> Result<ValidationResult, FederationError> {
142 let subgraph =
143 typestate::Subgraph::parse(name, url, sdl).map_err(|e| e.into_federation_error())?;
144 let subgraph = subgraph
145 .expand_links()
146 .map_err(|e| e.into_federation_error())?;
147
148 let mut errors = MultipleFederationErrors::new();
149 cache_tag::validate_cache_tag_directives(subgraph.schema(), &mut errors)?;
150
151 Ok(ValidationResult {
152 errors: errors
153 .errors
154 .into_iter()
155 .map(|error| Message {
156 code: error.code_string(),
157 message: error.to_string(),
158 locations: Vec::new(),
159 })
160 .collect(),
161 })
162 }
163}
164
165pub(crate) type SupergraphSpecs = (
166 &'static LinkSpecDefinition,
167 &'static JoinSpecDefinition,
168 Option<&'static ContextSpecDefinition>,
169);
170
171pub(crate) fn validate_supergraph_for_query_planning(
172 supergraph_schema: &FederationSchema,
173) -> Result<SupergraphSpecs, FederationError> {
174 validate_supergraph(supergraph_schema, &JOIN_VERSIONS, &CONTEXT_VERSIONS)
175}
176
177pub(crate) fn validate_supergraph(
179 supergraph_schema: &FederationSchema,
180 join_versions: &'static SpecDefinitions<JoinSpecDefinition>,
181 context_versions: &'static SpecDefinitions<ContextSpecDefinition>,
182) -> Result<SupergraphSpecs, FederationError> {
183 let Some(metadata) = supergraph_schema.metadata() else {
184 return Err(SingleFederationError::InvalidFederationSupergraph {
185 message: "Invalid supergraph: must be a core schema".to_owned(),
186 }
187 .into());
188 };
189 let link_spec_definition = metadata.link_spec_definition();
190 let Some(join_link) = metadata.for_identity(&Identity::join_identity()) else {
191 return Err(SingleFederationError::InvalidFederationSupergraph {
192 message: "Invalid supergraph: must use the join spec".to_owned(),
193 }
194 .into());
195 };
196 let Some(join_spec_definition) = join_versions.find(&join_link.url.version) else {
197 return Err(SingleFederationError::InvalidFederationSupergraph {
198 message: format!(
199 "Invalid supergraph: uses unsupported join spec version {} (supported versions: {})",
200 join_link.url.version,
201 join_versions.versions().map(|v| v.to_string()).collect::<Vec<_>>().join(", "),
202 ),
203 }.into());
204 };
205 let context_spec_definition = metadata.for_identity(&Identity::context_identity()).map(|context_link| {
206 context_versions.find(&context_link.url.version).ok_or_else(|| {
207 SingleFederationError::InvalidFederationSupergraph {
208 message: format!(
209 "Invalid supergraph: uses unsupported context spec version {} (supported versions: {})",
210 context_link.url.version,
211 context_versions.versions().join(", "),
212 ),
213 }
214 })
215 }).transpose()?;
216 if let Some(connect_link) = metadata.for_identity(&ConnectSpec::identity()) {
217 ConnectSpec::try_from(&connect_link.url.version)
218 .map_err(|message| SingleFederationError::UnknownLinkVersion { message })?;
219 }
220 Ok((
221 link_spec_definition,
222 join_spec_definition,
223 context_spec_definition,
224 ))
225}
226
227#[derive(Debug)]
228pub struct Supergraph {
229 pub schema: ValidFederationSchema,
230}
231
232impl Supergraph {
233 pub fn new_with_spec_check(
234 schema_str: &str,
235 supported_specs: &[Url],
236 ) -> Result<Self, FederationError> {
237 let mut schema = Schema::parse(schema_str, "schema.graphql")?;
238 coerce_and_validate_schema_values(&mut schema)?;
239 let schema = schema.validate()?;
240 Self::from_schema(schema, Some(supported_specs))
241 }
242
243 pub fn new(schema_str: &str) -> Result<Self, FederationError> {
245 Self::new_with_spec_check(schema_str, &default_supported_supergraph_specs())
246 }
247
248 pub fn new_with_router_specs(schema_str: &str) -> Result<Self, FederationError> {
250 Self::new_with_spec_check(schema_str, &router_supported_supergraph_specs())
251 }
252
253 pub fn from_schema(
257 schema: Valid<Schema>,
258 supported_specs: Option<&[Url]>,
259 ) -> Result<Self, FederationError> {
260 let schema: Schema = schema.into_inner();
261 let schema = FederationSchema::new(schema)?;
262
263 let _ = validate_supergraph_for_query_planning(&schema)?;
264
265 if let Some(supported_specs) = supported_specs {
266 check_spec_support(&schema, supported_specs)?;
267 }
268
269 Ok(Self {
270 schema: schema.assume_valid()?,
272 })
273 }
274
275 pub fn compose(subgraphs: Vec<&ValidSubgraph>) -> Result<Self, MergeFailure> {
276 let schema = merge_subgraphs(subgraphs)?.schema;
277 Ok(Self {
278 schema: ValidFederationSchema::new(schema).map_err(Into::<MergeFailure>::into)?,
279 })
280 }
281
282 pub fn to_api_schema(
285 &self,
286 options: ApiSchemaOptions,
287 ) -> Result<ValidFederationSchema, FederationError> {
288 api_schema::to_api_schema(self.schema.clone(), options)
289 }
290
291 pub fn extract_subgraphs(&self) -> Result<ValidFederationSubgraphs, FederationError> {
292 supergraph::extract_subgraphs_from_supergraph(&self.schema, None)
293 }
294}
295
296const _: () = {
297 const fn assert_thread_safe<T: Sync + Send>() {}
298
299 assert_thread_safe::<Supergraph>();
300 assert_thread_safe::<query_plan::query_planner::QueryPlanner>();
301};
302
303pub(crate) fn is_leaf_type(schema: &Schema, ty: &NamedType) -> bool {
305 schema.get_scalar(ty).is_some() || schema.get_enum(ty).is_some()
306}
307
308pub fn default_supported_supergraph_specs() -> Vec<Url> {
309 fn urls(defs: &SpecDefinitions<impl SpecDefinition>) -> impl Iterator<Item = Url> {
310 defs.iter().map(|(_, def)| def.url()).cloned()
311 }
312
313 urls(&CORE_VERSIONS)
314 .chain(urls(&JOIN_VERSIONS))
315 .chain(urls(&TAG_VERSIONS))
316 .chain(urls(&INACCESSIBLE_VERSIONS))
317 .collect()
318}
319
320pub fn router_supported_supergraph_specs() -> Vec<Url> {
322 fn urls(defs: &SpecDefinitions<impl SpecDefinition>) -> impl Iterator<Item = Url> {
323 defs.iter().map(|(_, def)| def.url()).cloned()
324 }
325
326 default_supported_supergraph_specs()
329 .into_iter()
330 .chain(urls(&AUTHENTICATED_VERSIONS))
331 .chain(urls(&REQUIRES_SCOPES_VERSIONS))
332 .chain(urls(&POLICY_VERSIONS))
333 .chain(urls(&CONTEXT_VERSIONS))
334 .chain(urls(&COST_VERSIONS))
335 .chain(urls(&CACHE_TAG_VERSIONS))
336 .chain(ConnectSpec::iter().map(|s| s.url()))
337 .collect()
338}
339
340fn is_core_version_zero_dot_one(url: &Url) -> bool {
341 CORE_VERSIONS
342 .find(&Version { major: 0, minor: 1 })
343 .is_some_and(|v| *v.url() == *url)
344}
345
346fn check_spec_support(
347 schema: &FederationSchema,
348 supported_specs: &[Url],
349) -> Result<(), FederationError> {
350 let Some(metadata) = schema.metadata() else {
351 bail!("Schema must have metadata");
353 };
354 let mut errors = MultipleFederationErrors::new();
355 let link_spec = metadata.link_spec_definition();
356 if is_core_version_zero_dot_one(link_spec.url()) {
357 let has_link_with_purpose = metadata.all_links().any(|link| link.purpose.is_some());
358 if has_link_with_purpose {
359 errors.push(SingleFederationError::UnsupportedLinkedFeature {
365 message: format!(
366 "the `for:` argument is unsupported by version {version} of the core spec.\n\
367 Please upgrade to at least @core v0.2 (https://specs.apollo.dev/core/v0.2).",
368 version = link_spec.url().version),
369 }.into());
370 }
371 }
372
373 let supported_specs: HashSet<_> = supported_specs.iter().collect();
374 errors
375 .and_try(metadata.all_links().try_for_all(|link| {
376 let Some(purpose) = link.purpose else {
377 return Ok(());
378 };
379 if !is_core_version_zero_dot_one(&link.url)
380 && purpose != link::Purpose::EXECUTION
381 && purpose != link::Purpose::SECURITY
382 {
383 return Ok(());
384 }
385
386 let link_url = &link.url;
387 if supported_specs.contains(link_url) {
388 Ok(())
389 } else {
390 Err(SingleFederationError::UnsupportedLinkedFeature {
391 message: format!("feature {link_url} is for: {purpose} but is unsupported"),
392 }
393 .into())
394 }
395 }))
396 .into_result()
397}
398
399#[cfg(test)]
400mod test_supergraph {
401 use pretty_assertions::assert_str_eq;
402
403 use super::*;
404 use crate::subgraph::SubgraphError;
405 use crate::subgraph::typestate;
406
407 #[test]
408 fn validates_connect_spec_is_known() {
409 let res = Supergraph::new(
410 r#"
411 extend schema @link(url: "https://specs.apollo.dev/connect/v99.99")
412
413 # Required stuff for the supergraph to parse at all, not what we're testing
414 extend schema
415 @link(url: "https://specs.apollo.dev/link/v1.0")
416 @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
417 directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
418 scalar link__Import
419 enum link__Purpose {
420 """
421 `SECURITY` features provide metadata necessary to securely resolve fields.
422 """
423 SECURITY
424
425 """
426 `EXECUTION` features provide metadata necessary for operation execution.
427 """
428 EXECUTION
429 }
430 type Query {required: ID!}
431 "#,
432 )
433 .expect_err("Unknown spec version did not cause error");
434 assert_str_eq!(res.to_string(), "Unknown connect version: 99.99");
435 }
436
437 #[track_caller]
438 fn build_and_validate(
439 name: &str,
440 url: &str,
441 sdl: &str,
442 ) -> Result<typestate::Subgraph<typestate::Expanded>, SubgraphError> {
443 typestate::Subgraph::parse(name, url, sdl)?.expand_links()
444 }
445
446 #[test]
447 fn it_validates_cache_tag_directives() {
448 let res = build_and_validate(
450 "accounts",
451 "accounts.graphql",
452 r#"
453 extend schema
454 @link(
455 url: "https://specs.apollo.dev/federation/v2.11"
456 import: ["@key"]
457 )
458
459 type Query {
460 topProducts(first: Int = 5): [Product]
461 }
462
463 type Product
464 @key(fields: "upc")
465 @key(fields: "name") {
466 upc: String!
467 name: String!
468 price: Int
469 weight: Int
470 }
471 "#,
472 );
473
474 assert!(res.is_ok());
475
476 let res = build_and_validate(
478 "accounts",
479 "https://accounts",
480 r#"
481 extend schema
482 @link(
483 url: "https://specs.apollo.dev/federation/v2.12"
484 import: ["@key", "@cacheTag"]
485 )
486
487 type Query {
488 topProducts(first: Int = 5): [Product]
489 @cacheTag(format: "topProducts")
490 @cacheTag(format: "topProducts-{$args.first}")
491 }
492
493 type Product
494 @key(fields: "upc")
495 @key(fields: "name")
496 @cacheTag(format: "product-{$key.upc}") {
497 upc: String!
498 name: String!
499 price: Int
500 weight: Int
501 }
502 "#,
503 );
504
505 let err = res.unwrap_err();
506 let errors: Vec<String> = err.to_composition_errors().map(|e| e.to_string()).collect();
507 assert!(
508 errors
509 .iter()
510 .any(|m| m.contains("cacheTag") && m.contains("$key")),
511 "expected cache tag validation error, got: {:?}",
512 errors
513 );
514
515 let res = build_and_validate(
517 "accounts",
518 "accounts.graphql",
519 r#"
520 extend schema
521 @link(
522 url: "https://specs.apollo.dev/federation/v2.12"
523 import: ["@key", "@cacheTag"]
524 )
525
526 type Query {
527 topProducts(first: Int! = 5): [Product]
528 @cacheTag(format: "topProducts")
529 @cacheTag(format: "topProducts-{$args.first}")
530 }
531
532 type Product
533 @key(fields: "upc")
534 @cacheTag(format: "product-{$key.upc}") {
535 upc: String!
536 name: String!
537 price: Int
538 weight: Int
539 }
540 "#,
541 );
542
543 assert!(res.is_ok());
544 }
545}