Skip to main content

apollo_federation/
lib.rs

1//! ## Usage
2//!
3//! This crate is internal to [Apollo Router](https://www.apollographql.com/docs/router/)
4//! and not intended to be used directly.
5//!
6//! ## Crate versioning
7//!
8//! The  `apollo-federation` crate does **not** adhere to [Semantic Versioning](https://semver.org/).
9//! Any version may have breaking API changes, as this APIĀ is expected to only be used by `apollo-router`.
10//! Instead, the version number matches exactly that of the `apollo-router` crate version using it.
11//!
12//! This version number is **not** that of the Apollo Federation specification being implemented.
13//! See the [Apollo Federation Changelog](https://www.apollographql.com/docs/graphos/schema-design/federated-schemas/reference/versions)
14//! for which Federation versions are supported by which Router versions.
15
16#![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
91/// Internal API for the apollo-composition crate.
92pub mod internal_composition_api {
93    use std::ops::Range;
94
95    use apollo_compiler::parser::LineColumn;
96
97    use super::*;
98    use crate::error::MultipleFederationErrors;
99    use crate::schema::validators::cache_tag;
100    use crate::subgraph::typestate;
101
102    #[derive(Clone, Debug)]
103    pub struct Message {
104        code: String,
105        message: String,
106        locations: Vec<Range<LineColumn>>,
107    }
108
109    impl Message {
110        pub fn code(&self) -> &str {
111            &self.code
112        }
113
114        pub fn message(&self) -> &str {
115            &self.message
116        }
117
118        pub fn locations(&self) -> &[Range<LineColumn>] {
119            &self.locations
120        }
121    }
122
123    #[derive(Default)]
124    pub struct ValidationResult {
125        /// If `errors` is empty, validation was successful.
126        pub errors: Vec<Message>,
127    }
128
129    /// Validates `@cacheTag` directives in the original (unexpanded) subgraph schema.
130    /// * name: Subgraph name
131    /// * url: Subgraph URL
132    /// * sdl: Subgraph schema
133    /// * Returns a `ValidationResult` if validation finished (either successfully or with
134    ///   validation errors).
135    /// * Or, a `FederationError` if validation stopped due to an internal error.
136    pub fn validate_cache_tag_directives(
137        name: &str,
138        url: &str,
139        sdl: &str,
140    ) -> Result<ValidationResult, FederationError> {
141        let subgraph =
142            typestate::Subgraph::parse(name, url, sdl).map_err(|e| e.into_federation_error())?;
143        let subgraph = subgraph
144            .expand_links()
145            .map_err(|e| e.into_federation_error())?;
146
147        let mut errors = MultipleFederationErrors::new();
148        cache_tag::validate_cache_tag_directives(subgraph.schema(), &mut errors)?;
149
150        Ok(ValidationResult {
151            errors: errors
152                .errors
153                .into_iter()
154                .map(|error| Message {
155                    code: error.code_string(),
156                    message: error.to_string(),
157                    locations: Vec::new(),
158                })
159                .collect(),
160        })
161    }
162}
163
164pub(crate) type SupergraphSpecs = (
165    &'static LinkSpecDefinition,
166    &'static JoinSpecDefinition,
167    Option<&'static ContextSpecDefinition>,
168);
169
170pub(crate) fn validate_supergraph_for_query_planning(
171    supergraph_schema: &FederationSchema,
172) -> Result<SupergraphSpecs, FederationError> {
173    validate_supergraph(supergraph_schema, &JOIN_VERSIONS, &CONTEXT_VERSIONS)
174}
175
176/// Checks that required supergraph directives are in the schema, and returns which ones were used.
177pub(crate) fn validate_supergraph(
178    supergraph_schema: &FederationSchema,
179    join_versions: &'static SpecDefinitions<JoinSpecDefinition>,
180    context_versions: &'static SpecDefinitions<ContextSpecDefinition>,
181) -> Result<SupergraphSpecs, FederationError> {
182    let Some(metadata) = supergraph_schema.metadata() else {
183        return Err(SingleFederationError::InvalidFederationSupergraph {
184            message: "Invalid supergraph: must be a core schema".to_owned(),
185        }
186        .into());
187    };
188    let link_spec_definition = metadata.link_spec_definition()?;
189    let Some(join_link) = metadata.for_identity(&Identity::join_identity()) else {
190        return Err(SingleFederationError::InvalidFederationSupergraph {
191            message: "Invalid supergraph: must use the join spec".to_owned(),
192        }
193        .into());
194    };
195    let Some(join_spec_definition) = join_versions.find(&join_link.url.version) else {
196        return Err(SingleFederationError::InvalidFederationSupergraph {
197            message: format!(
198                "Invalid supergraph: uses unsupported join spec version {} (supported versions: {})",
199                join_link.url.version,
200                join_versions.versions().map(|v| v.to_string()).collect::<Vec<_>>().join(", "),
201            ),
202        }.into());
203    };
204    let context_spec_definition = metadata.for_identity(&Identity::context_identity()).map(|context_link| {
205        context_versions.find(&context_link.url.version).ok_or_else(|| {
206            SingleFederationError::InvalidFederationSupergraph {
207                message: format!(
208                    "Invalid supergraph: uses unsupported context spec version {} (supported versions: {})",
209                    context_link.url.version,
210                    context_versions.versions().join(", "),
211                ),
212            }
213        })
214    }).transpose()?;
215    if let Some(connect_link) = metadata.for_identity(&ConnectSpec::identity()) {
216        ConnectSpec::try_from(&connect_link.url.version)
217            .map_err(|message| SingleFederationError::UnknownLinkVersion { message })?;
218    }
219    Ok((
220        link_spec_definition,
221        join_spec_definition,
222        context_spec_definition,
223    ))
224}
225
226#[derive(Debug)]
227pub struct Supergraph {
228    pub schema: ValidFederationSchema,
229}
230
231impl Supergraph {
232    pub fn new_with_spec_check(
233        schema_str: &str,
234        supported_specs: &[Url],
235    ) -> Result<Self, FederationError> {
236        let schema = Schema::parse_and_validate(schema_str, "schema.graphql")?;
237        Self::from_schema(schema, Some(supported_specs))
238    }
239
240    /// Same as `new_with_spec_check(...)` with the default set of supported specs.
241    pub fn new(schema_str: &str) -> Result<Self, FederationError> {
242        Self::new_with_spec_check(schema_str, &default_supported_supergraph_specs())
243    }
244
245    /// Same as `new_with_spec_check(...)` with the specs supported by Router.
246    pub fn new_with_router_specs(schema_str: &str) -> Result<Self, FederationError> {
247        Self::new_with_spec_check(schema_str, &router_supported_supergraph_specs())
248    }
249
250    /// Construct from a pre-validation supergraph schema, which will be validated.
251    /// * `supported_specs`: (optional) If provided, checks if all EXECUTION/SECURITY specs are
252    ///   supported.
253    pub fn from_schema(
254        schema: Valid<Schema>,
255        supported_specs: Option<&[Url]>,
256    ) -> Result<Self, FederationError> {
257        let schema: Schema = schema.into_inner();
258        let schema = FederationSchema::new(schema)?;
259
260        let _ = validate_supergraph_for_query_planning(&schema)?;
261
262        if let Some(supported_specs) = supported_specs {
263            check_spec_support(&schema, supported_specs)?;
264        }
265
266        Ok(Self {
267            // We know it's valid because the input was.
268            schema: schema.assume_valid()?,
269        })
270    }
271
272    pub fn compose(subgraphs: Vec<&ValidSubgraph>) -> Result<Self, MergeFailure> {
273        let schema = merge_subgraphs(subgraphs)?.schema;
274        Ok(Self {
275            schema: ValidFederationSchema::new(schema).map_err(Into::<MergeFailure>::into)?,
276        })
277    }
278
279    /// Generates an API Schema from this supergraph schema. The API Schema represents the combined
280    /// API of the supergraph that's visible to end users.
281    pub fn to_api_schema(
282        &self,
283        options: ApiSchemaOptions,
284    ) -> Result<ValidFederationSchema, FederationError> {
285        api_schema::to_api_schema(self.schema.clone(), options)
286    }
287
288    pub fn extract_subgraphs(&self) -> Result<ValidFederationSubgraphs, FederationError> {
289        supergraph::extract_subgraphs_from_supergraph(&self.schema, None)
290    }
291}
292
293const _: () = {
294    const fn assert_thread_safe<T: Sync + Send>() {}
295
296    assert_thread_safe::<Supergraph>();
297    assert_thread_safe::<query_plan::query_planner::QueryPlanner>();
298};
299
300/// Returns if the type of the node is a scalar or enum.
301pub(crate) fn is_leaf_type(schema: &Schema, ty: &NamedType) -> bool {
302    schema.get_scalar(ty).is_some() || schema.get_enum(ty).is_some()
303}
304
305pub fn default_supported_supergraph_specs() -> Vec<Url> {
306    fn urls(defs: &SpecDefinitions<impl SpecDefinition>) -> impl Iterator<Item = Url> {
307        defs.iter().map(|(_, def)| def.url()).cloned()
308    }
309
310    urls(&CORE_VERSIONS)
311        .chain(urls(&JOIN_VERSIONS))
312        .chain(urls(&TAG_VERSIONS))
313        .chain(urls(&INACCESSIBLE_VERSIONS))
314        .collect()
315}
316
317/// default_supported_supergraph_specs() + additional specs supported by Router
318pub fn router_supported_supergraph_specs() -> Vec<Url> {
319    fn urls(defs: &SpecDefinitions<impl SpecDefinition>) -> impl Iterator<Item = Url> {
320        defs.iter().map(|(_, def)| def.url()).cloned()
321    }
322
323    // PORT_NOTE: "https://specs.apollo.dev/source/v0.1" is listed in the JS version. But, it is
324    //            not ported here, since it has been fully deprecated.
325    default_supported_supergraph_specs()
326        .into_iter()
327        .chain(urls(&AUTHENTICATED_VERSIONS))
328        .chain(urls(&REQUIRES_SCOPES_VERSIONS))
329        .chain(urls(&POLICY_VERSIONS))
330        .chain(urls(&CONTEXT_VERSIONS))
331        .chain(urls(&COST_VERSIONS))
332        .chain(urls(&CACHE_TAG_VERSIONS))
333        .chain(ConnectSpec::iter().map(|s| s.url()))
334        .collect()
335}
336
337fn is_core_version_zero_dot_one(url: &Url) -> bool {
338    CORE_VERSIONS
339        .find(&Version { major: 0, minor: 1 })
340        .is_some_and(|v| *v.url() == *url)
341}
342
343fn check_spec_support(
344    schema: &FederationSchema,
345    supported_specs: &[Url],
346) -> Result<(), FederationError> {
347    let Some(metadata) = schema.metadata() else {
348        // This can't happen since `validate_supergraph_for_query_planning` already checked.
349        bail!("Schema must have metadata");
350    };
351    let mut errors = MultipleFederationErrors::new();
352    let link_spec = metadata.link_spec_definition()?;
353    if is_core_version_zero_dot_one(link_spec.url()) {
354        let has_link_with_purpose = metadata
355            .all_links()
356            .iter()
357            .any(|link| link.purpose.is_some());
358        if has_link_with_purpose {
359            // PORT_NOTE: This is unreachable since the schema is validated before this check in
360            //            Rust and a apollo-compiler error will have been raised already. This is
361            //            still kept for historic reasons and potential fix in the future. However,
362            //            it didn't seem worth changing the router's workflow so this specialized
363            //            error message can be displayed.
364            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().iter().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        // Ok with older federation versions without @cacheTag directive.
449        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        // validation error test
477        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        // valid usage test
516        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}