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 [Router documentation](https://www.apollographql.com/docs/router/federation-version-support/)
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 super::*;
94    use crate::schema::validators::cache_tag;
95    use crate::subgraph::typestate;
96
97    #[derive(Default)]
98    pub struct ValidationResult {
99        /// If `errors` is empty, validation was successful.
100        pub errors: Vec<cache_tag::Message>,
101    }
102
103    /// Validates `@cacheTag` directives in the original (unexpanded) subgraph schema.
104    /// * name: Subgraph name
105    /// * url: Subgraph URL
106    /// * sdl: Subgraph schema
107    /// * Returns a `ValidationResult` if validation finished (either successfully or with
108    ///   validation errors).
109    /// * Or, a `FederationError` if validation stopped due to an internal error.
110    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
138/// Checks that required supergraph directives are in the schema, and returns which ones were used.
139pub(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    /// Same as `new_with_spec_check(...)` with the default set of supported specs.
203    pub fn new(schema_str: &str) -> Result<Self, FederationError> {
204        Self::new_with_spec_check(schema_str, &default_supported_supergraph_specs())
205    }
206
207    /// Same as `new_with_spec_check(...)` with the specs supported by Router.
208    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    /// Construct from a pre-validation supergraph schema, which will be validated.
213    /// * `supported_specs`: (optional) If provided, checks if all EXECUTION/SECURITY specs are
214    ///   supported.
215    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            // We know it's valid because the input was.
230            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    /// Generates an API Schema from this supergraph schema. The API Schema represents the combined
242    /// API of the supergraph that's visible to end users.
243    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
262/// Returns if the type of the node is a scalar or enum.
263pub(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
279/// default_supported_supergraph_specs() + additional specs supported by Router
280pub 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    // PORT_NOTE: "https://specs.apollo.dev/source/v0.1" is listed in the JS version. But, it is
286    //            not ported here, since it has been fully deprecated.
287    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        // This can't happen since `validate_supergraph_for_query_planning` already checked.
311        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            // PORT_NOTE: This is unreachable since the schema is validated before this check in
322            //            Rust and a apollo-compiler error will have been raised already. This is
323            //            still kept for historic reasons and potential fix in the future. However,
324            //            it didn't seem worth changing the router's workflow so this specialized
325            //            error message can be displayed.
326            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        // Ok with older federation versions without @cacheTag directive.
407        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        // validation error test
435        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        // valid usage test
475        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}