From 74cef634a68ee016e6dd30a8e5647b5e178abebb Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 20 Sep 2024 17:17:56 +0300 Subject: [PATCH 01/23] feat: allow users to load apollo key from file (#5982) Co-authored-by: Lucas Leadbetter <5595530+lleadbet@users.noreply.github.com> Co-authored-by: Shane Myrick Co-authored-by: Gary Pennington --- .changesets/feat_feat_key_from_file.md | 9 +++ .circleci/config.yml | 1 - apollo-router/src/executable.rs | 86 ++++++++++++++++++++++-- apollo-router/tests/integration_tests.rs | 34 ++++++++++ docs/source/configuration/overview.mdx | 34 +++++++++- 5 files changed, 155 insertions(+), 9 deletions(-) create mode 100644 .changesets/feat_feat_key_from_file.md diff --git a/.changesets/feat_feat_key_from_file.md b/.changesets/feat_feat_key_from_file.md new file mode 100644 index 0000000000..afa3090192 --- /dev/null +++ b/.changesets/feat_feat_key_from_file.md @@ -0,0 +1,9 @@ +### feat: allow users to load apollo key from file ([PR #5917](https://github.com/apollographql/router/pull/5917)) + +Users sometimes would rather not pass sensitive keys to the router through environment variables out of an abundance of caution. To help address this, you can now pass an argument `--apollo-key-path` or env var `APOLLO_KEY_PATH`, that takes a file location as an argument which is read and then used as the Apollo key for use with Uplink and usage reporting. + +This addresses a portion of #3264, specifically the APOLLO_KEY. + +Note: This feature is not available on Windows. + +By [@lleadbet](https://github.com/lleadbet) in https://github.com/apollographql/router/pull/5917 diff --git a/.circleci/config.yml b/.circleci/config.yml index 1d0e41f62c..588e0aedca 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,5 +1,4 @@ version: 2.1 - # These "CircleCI Orbs" are reusable bits of configuration that can be shared # across projects. See https://circleci.com/orbs/ for more information. orbs: diff --git a/apollo-router/src/executable.rs b/apollo-router/src/executable.rs index 4d826b6554..86bdee162f 100644 --- a/apollo-router/src/executable.rs +++ b/apollo-router/src/executable.rs @@ -4,6 +4,8 @@ use std::cell::Cell; use std::env; use std::fmt::Debug; use std::net::SocketAddr; +#[cfg(unix)] +use std::os::unix::fs::MetadataExt; use std::path::PathBuf; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; @@ -211,6 +213,11 @@ pub struct Opt { #[clap(skip = std::env::var("APOLLO_KEY").ok())] apollo_key: Option, + /// Key file location relative to the current directory. + #[cfg(unix)] + #[clap(long = "apollo-key-path", env = "APOLLO_KEY_PATH")] + apollo_key_path: Option, + /// Your Apollo graph reference. #[clap(skip = std::env::var("APOLLO_GRAPH_REF").ok())] apollo_graph_ref: Option, @@ -513,14 +520,19 @@ impl Executable { // 2. Env APOLLO_ROUTER_SUPERGRAPH_PATH // 3. Env APOLLO_ROUTER_SUPERGRAPH_URLS // 4. Env APOLLO_KEY and APOLLO_GRAPH_REF - let schema_source = match (schema, &opt.supergraph_path, &opt.supergraph_urls, &opt.apollo_key) { - (Some(_), Some(_), _, _) | (Some(_), _, Some(_), _) => { + #[cfg(unix)] + let akp = &opt.apollo_key_path; + #[cfg(not(unix))] + let akp: &Option = &None; + + let schema_source = match (schema, &opt.supergraph_path, &opt.supergraph_urls, &opt.apollo_key, akp) { + (Some(_), Some(_), _, _, _) | (Some(_), _, Some(_), _, _) => { return Err(anyhow!( "--supergraph and APOLLO_ROUTER_SUPERGRAPH_PATH cannot be used when a custom schema source is in use" )) } - (Some(source), None, None,_) => source, - (_, Some(supergraph_path), _, _) => { + (Some(source), None, None,_,_) => source, + (_, Some(supergraph_path), _, _, _) => { tracing::info!("{apollo_router_msg}"); tracing::info!("{apollo_telemetry_msg}"); @@ -535,7 +547,7 @@ impl Executable { delay: None, } } - (_, _, Some(supergraph_urls), _) => { + (_, _, Some(supergraph_urls), _, _) => { tracing::info!("{apollo_router_msg}"); tracing::info!("{apollo_telemetry_msg}"); @@ -545,7 +557,67 @@ impl Executable { period: opt.apollo_uplink_poll_interval } } - (_, None, None, Some(_apollo_key)) => { + (_, None, None, _, Some(apollo_key_path)) => { + let apollo_key_path = if apollo_key_path.is_relative() { + current_directory.join(apollo_key_path) + } else { + apollo_key_path.clone() + }; + + if !apollo_key_path.exists() { + tracing::error!( + "Apollo key at path '{}' does not exist.", + apollo_key_path.to_string_lossy() + ); + return Err(anyhow!( + "Apollo key at path '{}' does not exist.", + apollo_key_path.to_string_lossy() + )); + } else { + // On unix systems, Check that the executing user is the only user who may + // read the key file. + // Note: We could, in future, add support for Windows. + #[cfg(unix)] + { + let meta = std::fs::metadata(apollo_key_path.clone()).map_err(|err| + anyhow!( + "Failed to read Apollo key file: {}", + err + ))?; + let mode = meta.mode(); + // If our mode isn't "safe", fail... + // safe == none of the "group" or "other" bits set. + if mode & 0o077 != 0 { + return Err( + anyhow!( + "Apollo key file permissions ({:#o}) are too permissive", mode & 0o000777 + )); + } + let euid = unsafe { libc::geteuid() }; + let owner = meta.uid(); + if euid != owner { + return Err( + anyhow!( + "Apollo key file owner id ({owner}) does not match effective user id ({euid})" + )); + } + } + //The key file exists try and load it + match std::fs::read_to_string(&apollo_key_path) { + Ok(apollo_key) => { + opt.apollo_key = Some(apollo_key.trim().to_string()); + } + Err(err) => { + return Err(anyhow!( + "Failed to read Apollo key file: {}", + err + )); + } + }; + SchemaSource::Registry(opt.uplink_config()?) + } + } + (_, None, None, Some(_apollo_key), None) => { tracing::info!("{apollo_router_msg}"); tracing::info!("{apollo_telemetry_msg}"); SchemaSource::Registry(opt.uplink_config()?) @@ -585,7 +657,7 @@ impl Executable { } }; - // Order of precedence: + // Order of precedence for licenses: // 1. explicit path from cli // 2. env APOLLO_ROUTER_LICENSE // 3. uplink diff --git a/apollo-router/tests/integration_tests.rs b/apollo-router/tests/integration_tests.rs index 4f99c0602d..378a20ddd5 100644 --- a/apollo-router/tests/integration_tests.rs +++ b/apollo-router/tests/integration_tests.rs @@ -1386,3 +1386,37 @@ async fn test_telemetry_doesnt_hang_with_invalid_schema() { ) .await; } + +// Ensure that, on unix, the router won't start with wrong file permissions +#[cfg(unix)] +#[test] +fn it_will_not_start_with_loose_file_permissions() { + use std::os::fd::AsRawFd; + use std::process::Command; + + use crate::integration::IntegrationTest; + + let mut router = Command::new(IntegrationTest::router_location()); + + let tester = tempfile::NamedTempFile::new().expect("it created a temporary test file"); + let fd = tester.as_file().as_raw_fd(); + let path = tester.path().to_str().expect("got the tempfile path"); + + // Modify our temporary file permissions so that they are definitely too loose. + unsafe { + libc::fchmod(fd, 0o777); + } + + let output = router + .args(["--apollo-key-path", path]) + .output() + .expect("router could not start"); + + // Assert that our router executed unsuccessfully + assert!(!output.status.success()); + // It may have been unsuccessful for a variety of reasons, is it the right reason? + assert_eq!( + std::str::from_utf8(&output.stderr).expect("output is a string"), + "Apollo key file permissions (0o777) are too permissive\n" + ) +} diff --git a/docs/source/configuration/overview.mdx b/docs/source/configuration/overview.mdx index 13e6991a6a..0caa139fe9 100644 --- a/docs/source/configuration/overview.mdx +++ b/docs/source/configuration/overview.mdx @@ -49,11 +49,26 @@ The graph ref for the GraphOS graph and variant that the router fetches its supe The [graph API key](/graphos/api-keys/#graph-api-keys) that the router should use to authenticate with GraphOS when fetching its supergraph schema. -**Required** when using [managed federation](/federation/managed-federation/overview/), except when using an [offline license](#--license) to run the router. +**Required** when using [managed federation](/federation/managed-federation/overview/), except when using an [offline license](#--license) to run the router or when using `APOLLO_KEY_PATH`. + + + +##### `APOLLO_KEY_PATH` + + + +⚠️ **This is not available on Windows.** + +A path to a file containing the [graph API key](/graphos/api-keys/#graph-api-keys) that the router should use to authenticate with GraphOS when fetching its supergraph schema. + +**Required** when using [managed federation](/federation/managed-federation/overview/), except when using an [offline license](#--license) to run the router or when using `APOLLO_KEY`. + + + @@ -114,6 +129,23 @@ To learn how to compose your supergraph schema with the Rover CLI, see the [Fede The absolute or relative path to the router's optional [YAML configuration file](#yaml-config-file). + + + + + + +##### `--apollo-key-path` + +`APOLLO_KEY_PATH` + + + + +⚠️ **This is not available on Windows.** + +The absolute or relative path to a file containing the Apollo graph API key for use with managed federation. + From 98b647afb871424a8419a3887a82bf163fd03864 Mon Sep 17 00:00:00 2001 From: Iryna Shestak Date: Mon, 23 Sep 2024 12:44:40 +0200 Subject: [PATCH 02/23] adjust log levels for introspection and query planner mismatches (#6037) At this point majority of the comparison cases in query planner semantic diffing match, we can move the trace for matched case to `trace`. And in order to get a better insight on introspection mismatches, the log level for introspection comparison diff is moved up to `debug`. --- apollo-router/src/query_planner/dual_introspection.rs | 4 ++-- apollo-router/src/query_planner/dual_query_planner.rs | 2 +- apollo-router/tests/integration/introspection.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apollo-router/src/query_planner/dual_introspection.rs b/apollo-router/src/query_planner/dual_introspection.rs index a6c51d7f63..d8a72d34b2 100644 --- a/apollo-router/src/query_planner/dual_introspection.rs +++ b/apollo-router/src/query_planner/dual_introspection.rs @@ -32,11 +32,11 @@ pub(crate) fn compare_introspection_responses( } is_matched = js_response.data == rust_response.data; if is_matched { - tracing::debug!("Introspection match! πŸŽ‰") + tracing::trace!("Introspection match! πŸŽ‰") } else { tracing::debug!("Introspection mismatch"); tracing::trace!("Introspection query:\n{query}"); - tracing::trace!("Introspection diff:\n{}", { + tracing::debug!("Introspection diff:\n{}", { let rust = rust_response .data .as_ref() diff --git a/apollo-router/src/query_planner/dual_query_planner.rs b/apollo-router/src/query_planner/dual_query_planner.rs index 9952eb9782..7e84ade311 100644 --- a/apollo-router/src/query_planner/dual_query_planner.rs +++ b/apollo-router/src/query_planner/dual_query_planner.rs @@ -153,7 +153,7 @@ impl BothModeComparisonJob { let match_result = opt_plan_node_matches(js_root_node, &rust_root_node); is_matched = match_result.is_ok(); match match_result { - Ok(_) => tracing::debug!("JS and Rust query plans match{operation_desc}! πŸŽ‰"), + Ok(_) => tracing::trace!("JS and Rust query plans match{operation_desc}! πŸŽ‰"), Err(err) => { tracing::debug!("JS v.s. Rust query plan mismatch{operation_desc}"); tracing::debug!("{}", err.full_description()); diff --git a/apollo-router/tests/integration/introspection.rs b/apollo-router/tests/integration/introspection.rs index 64aea564d5..fc66cd231e 100644 --- a/apollo-router/tests/integration/introspection.rs +++ b/apollo-router/tests/integration/introspection.rs @@ -239,7 +239,7 @@ async fn both_mode_integration() { ", ) .supergraph("../examples/graphql/local.graphql") - .log("error,apollo_router=info,apollo_router::query_planner=debug") + .log("error,apollo_router=info,apollo_router::query_planner=trace") .build() .await; router.start().await; From 33097a886e582994164020185d2d1b61d757cf4d Mon Sep 17 00:00:00 2001 From: Duckki Oe Date: Mon, 23 Sep 2024 20:42:19 -0700 Subject: [PATCH 03/23] updated `non_trivial_followup_edges` to use `Vec`, instead of IndexSet (#6042) - Also changed `precompute_non_trivial_followup_edges` to use `QueryGraph::out_edges` method to iterate edges. - This matches the JS QP's behaviors more closely. --- .../src/query_graph/build_query_graph.rs | 12 ++++-------- apollo-federation/src/query_graph/mod.rs | 4 ++-- .../build_query_plan_tests/interface_object.rs | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/apollo-federation/src/query_graph/build_query_graph.rs b/apollo-federation/src/query_graph/build_query_graph.rs index d4d2acf609..1bb2faa41d 100644 --- a/apollo-federation/src/query_graph/build_query_graph.rs +++ b/apollo-federation/src/query_graph/build_query_graph.rs @@ -1968,13 +1968,9 @@ impl FederatedQueryGraphBuilder { for edge in self.base.query_graph.graph.edge_indices() { let edge_weight = self.base.query_graph.edge_weight(edge)?; let (_, tail) = self.base.query_graph.edge_endpoints(edge)?; - let mut non_trivial_followups = IndexSet::default(); - for followup_edge_ref in self - .base - .query_graph - .graph - .edges_directed(tail, Direction::Outgoing) - { + let out_edges = self.base.query_graph.out_edges(tail); + let mut non_trivial_followups = Vec::with_capacity(out_edges.len()); + for followup_edge_ref in out_edges { let followup_edge_weight = followup_edge_ref.weight(); match edge_weight.transition { QueryGraphEdgeTransition::KeyResolution => { @@ -2035,7 +2031,7 @@ impl FederatedQueryGraphBuilder { } _ => {} } - non_trivial_followups.insert(followup_edge_ref.id()); + non_trivial_followups.push(followup_edge_ref.id()); } self.base .query_graph diff --git a/apollo-federation/src/query_graph/mod.rs b/apollo-federation/src/query_graph/mod.rs index 4060ffdbe6..1b00776e12 100644 --- a/apollo-federation/src/query_graph/mod.rs +++ b/apollo-federation/src/query_graph/mod.rs @@ -355,7 +355,7 @@ pub struct QueryGraph { /// significantly faster (and pretty easy). FWIW, when originally introduced, this optimization /// lowered composition validation on a big composition (100+ subgraphs) from ~4 minutes to /// ~10 seconds. - non_trivial_followup_edges: IndexMap>, + non_trivial_followup_edges: IndexMap>, } impl QueryGraph { @@ -528,7 +528,7 @@ impl QueryGraph { }) } - pub(crate) fn non_trivial_followup_edges(&self) -> &IndexMap> { + pub(crate) fn non_trivial_followup_edges(&self) -> &IndexMap> { &self.non_trivial_followup_edges } diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs index 0cd50ce299..d15e05bf6f 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs @@ -790,7 +790,7 @@ fn it_handles_interface_object_input_rewrites_when_cloning_dependency_graph() { }, Parallel { Flatten(path: "i.i2") { - Fetch(service: "S4") { + Fetch(service: "S3") { { ... on T { __typename From 207324cead92994bd8c23f6a20e17d24190bffe4 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Tue, 24 Sep 2024 18:00:35 +0200 Subject: [PATCH 04/23] Temporarily pin CI to Rust nightly-2024-09-22 (#6048) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We use Rust nightly on CI in order to check that fuzz targets can still compile. With current latest nightly we’re running into a compiler panic: https://github.com/rust-lang/rust/issues/130769 To unblock merging PRs, this pins the nightly version used on CI to a slightly older one that does not have this panic. This should be reverted after the panic is fixed in a new Rust nightly. --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 588e0aedca..221f6ab0ef 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -355,9 +355,9 @@ commands: equal: [ *arm_linux_test_executor, << parameters.platform >> ] steps: - run: - name: Install nightly Rust to build the fuzzers + name: Install nightly-2024-09-22 Rust to build the fuzzers command: | - rustup install nightly + rustup install nightly-2024-09-22 install_extra_tools: steps: @@ -523,7 +523,7 @@ commands: path: ./target/nextest/ci/junit.xml fuzz_build: steps: - - run: cargo +nightly fuzz build + - run: cargo +nightly-2024-09-22 fuzz build jobs: lint: From 21606c1f1eee86bb59a9b63b5a58fbf4a137e3f8 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Tue, 24 Sep 2024 18:38:46 +0200 Subject: [PATCH 05/23] Fix introspection `deprecationReason` field (#6046) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … to correctly return the default string instead of `null` for argument-less `@deprecated`. https://github.com/apollographql/apollo-rs/pull/914 also renames `argument_for_name` to `specified_argument_for_name`, leaving the previous name for a new method that accounts for default value and nullability. Additionally, introspection response comparison in "both" mode now tolerates differences in the ordering of input object fields in `defaultValue` strings. Together, these two changes fix the remaining known sources of mismatch. --- Cargo.lock | 8 +- Cargo.toml | 4 +- apollo-federation/src/compat.rs | 10 +- apollo-federation/src/link/argument.rs | 8 +- apollo-federation/src/link/database.rs | 12 +- apollo-federation/src/link/mod.rs | 10 +- .../src/query_graph/graph_path.rs | 2 +- apollo-federation/src/query_graph/mod.rs | 5 +- .../src/query_plan/conditions.rs | 4 +- apollo-federation/src/supergraph/mod.rs | 6 +- .../src/plugins/authorization/policy.rs | 4 +- .../src/plugins/authorization/scopes.rs | 4 +- .../cost_calculator/directives.rs | 20 +- .../src/plugins/progressive_override/mod.rs | 2 +- .../src/query_planner/dual_introspection.rs | 62 +- apollo-router/src/spec/query/change.rs | 7 +- apollo-router/src/spec/schema.rs | 16 +- apollo-router/src/spec/selection.rs | 4 +- .../src/uplink/license_enforcement.rs | 8 +- .../fixtures/introspect_full_schema.graphql | 15 + .../fixtures/schema_to_introspect.graphql | 89 + .../tests/integration/introspection.rs | 35 +- ..._introspection__both_mode_integration.snap | 1728 +++++++++++++++++ ...tegration__introspection__integration.snap | 1728 +++++++++++++++++ examples/supergraph-sdl/rust/Cargo.toml | 2 +- 25 files changed, 3720 insertions(+), 73 deletions(-) create mode 100644 apollo-router/tests/fixtures/schema_to_introspect.graphql create mode 100644 apollo-router/tests/integration/snapshots/integration_tests__integration__introspection__both_mode_integration.snap create mode 100644 apollo-router/tests/integration/snapshots/integration_tests__integration__introspection__integration.snap diff --git a/Cargo.lock b/Cargo.lock index 280f936fd2..6010e36413 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,9 +159,9 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "apollo-compiler" -version = "1.0.0-beta.23" +version = "1.0.0-beta.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875f39060728ac3e775fc3fe5421225d6df92c4d5155a9524cdb198f05006d36" +checksum = "71153ad85c85f7aa63f0e0a5868912c220bb48e4c764556f5841d37fc17b0103" dependencies = [ "ahash", "apollo-parser", @@ -448,9 +448,9 @@ dependencies = [ [[package]] name = "apollo-smith" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40cff1a5989a471714cfdf53f24d0948b7f77631ab3dbd25b2f6eacbf58e5261" +checksum = "d89479524886fdbe62b124d3825879778680e0147304d1a6d32164418f8089a2" dependencies = [ "apollo-compiler", "apollo-parser", diff --git a/Cargo.toml b/Cargo.toml index 0352205ebb..e48b91c854 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,9 +49,9 @@ debug = 1 # Dependencies used in more than one place are specified here in order to keep versions in sync: # https://doc.rust-lang.org/cargo/reference/workspaces.html#the-dependencies-table [workspace.dependencies] -apollo-compiler = "=1.0.0-beta.23" +apollo-compiler = "=1.0.0-beta.24" apollo-parser = "0.8.0" -apollo-smith = "0.13.0" +apollo-smith = "0.14.0" async-trait = "0.1.77" hex = { version = "0.4.3", features = ["serde"] } http = "0.2.11" diff --git a/apollo-federation/src/compat.rs b/apollo-federation/src/compat.rs index 701337714c..d4faa04880 100644 --- a/apollo-federation/src/compat.rs +++ b/apollo-federation/src/compat.rs @@ -24,11 +24,13 @@ use apollo_compiler::Schema; fn is_semantic_directive_application(directive: &Directive) -> bool { match directive.name.as_str() { "specifiedBy" => true, - // For @deprecated, explicitly writing `reason: null` disables the directive, - // as `null` overrides the default string value. + // graphql-js’ intropection returns `isDeprecated: false` for `@deprecated(reason: null)`, + // which is arguably a bug. Do the same here for now. + // TODO: remove this and allow `isDeprecated: true`, `deprecatedReason: null` + // after we fully move to Rust introspection? "deprecated" if directive - .argument_by_name("reason") + .specified_argument_by_name("reason") .is_some_and(|value| value.is_null()) => { false @@ -42,7 +44,7 @@ fn is_semantic_directive_application(directive: &Directive) -> bool { fn standardize_deprecated(directive: &mut Directive) { if directive.name == "deprecated" && directive - .argument_by_name("reason") + .specified_argument_by_name("reason") .and_then(|value| value.as_str()) .is_some_and(|reason| reason == "No longer supported") { diff --git a/apollo-federation/src/link/argument.rs b/apollo-federation/src/link/argument.rs index 662cd0a08d..12702dadb2 100644 --- a/apollo-federation/src/link/argument.rs +++ b/apollo-federation/src/link/argument.rs @@ -13,7 +13,7 @@ pub(crate) fn directive_optional_enum_argument( application: &Node, name: &Name, ) -> Result, FederationError> { - match application.argument_by_name(name) { + match application.specified_argument_by_name(name) { Some(value) => match value.deref() { Value::Enum(name) => Ok(Some(name.clone())), Value::Null => Ok(None), @@ -48,7 +48,7 @@ pub(crate) fn directive_optional_string_argument<'doc>( application: &'doc Node, name: &Name, ) -> Result, FederationError> { - match application.argument_by_name(name) { + match application.specified_argument_by_name(name) { Some(value) => match value.deref() { Value::String(name) => Ok(Some(name)), Value::Null => Ok(None), @@ -83,7 +83,7 @@ pub(crate) fn directive_optional_boolean_argument( application: &Node, name: &Name, ) -> Result, FederationError> { - match application.argument_by_name(name) { + match application.specified_argument_by_name(name) { Some(value) => match value.deref() { Value::Boolean(value) => Ok(Some(*value)), Value::Null => Ok(None), @@ -119,7 +119,7 @@ pub(crate) fn directive_optional_variable_boolean_argument( application: &Node, name: &Name, ) -> Result, FederationError> { - match application.argument_by_name(name) { + match application.specified_argument_by_name(name) { Some(value) => match value.deref() { Value::Variable(name) => Ok(Some(BooleanOrVariable::Variable(name.clone()))), Value::Boolean(value) => Ok(Some(BooleanOrVariable::Boolean(*value))), diff --git a/apollo-federation/src/link/database.rs b/apollo-federation/src/link/database.rs index 94ea7ba0ff..ced0dc7b07 100644 --- a/apollo-federation/src/link/database.rs +++ b/apollo-federation/src/link/database.rs @@ -33,10 +33,10 @@ pub fn links_metadata(schema: &Schema) -> Result, LinkErro return Err(LinkError::BootstrapError(format!( "the @link specification itself (\"{}\") is applied multiple times", extraneous_directive - .argument_by_name("url") + .specified_argument_by_name("url") // XXX(@goto-bus-stop): @core compatibility is primarily to support old tests in other projects, // and should be removed when those are updated. - .or(extraneous_directive.argument_by_name("feature")) + .or(extraneous_directive.specified_argument_by_name("feature")) .and_then(|value| value.as_str().map(Cow::Borrowed)) .unwrap_or_else(|| Cow::Owned(Identity::link_identity().to_string())) ))); @@ -184,13 +184,13 @@ fn is_bootstrap_directive(schema: &Schema, directive: &Directive) -> bool { }; if is_link_directive_definition(definition) { if let Some(url) = directive - .argument_by_name("url") + .specified_argument_by_name("url") .and_then(|value| value.as_str()) { let url = url.parse::(); let default_link_name = DEFAULT_LINK_NAME; let expected_name = directive - .argument_by_name("as") + .specified_argument_by_name("as") .and_then(|value| value.as_str()) .unwrap_or(default_link_name.as_str()); return url.map_or(false, |url| { @@ -201,12 +201,12 @@ fn is_bootstrap_directive(schema: &Schema, directive: &Directive) -> bool { // XXX(@goto-bus-stop): @core compatibility is primarily to support old tests--should be // removed when those are updated. if let Some(url) = directive - .argument_by_name("feature") + .specified_argument_by_name("feature") .and_then(|value| value.as_str()) { let url = url.parse::(); let expected_name = directive - .argument_by_name("as") + .specified_argument_by_name("as") .and_then(|value| value.as_str()) .unwrap_or("core"); return url.map_or(false, |url| { diff --git a/apollo-federation/src/link/mod.rs b/apollo-federation/src/link/mod.rs index 96473e59db..76a59da2ea 100644 --- a/apollo-federation/src/link/mod.rs +++ b/apollo-federation/src/link/mod.rs @@ -275,9 +275,9 @@ impl Link { } pub fn from_directive_application(directive: &Node) -> Result { - let (url, is_link) = if let Some(value) = directive.argument_by_name("url") { + let (url, is_link) = if let Some(value) = directive.specified_argument_by_name("url") { (value, true) - } else if let Some(value) = directive.argument_by_name("feature") { + } else if let Some(value) = directive.specified_argument_by_name("feature") { // XXX(@goto-bus-stop): @core compatibility is primarily to support old tests--should be // removed when those are updated. (value, false) @@ -303,11 +303,11 @@ impl Link { })?; let spec_alias = directive - .argument_by_name("as") + .specified_argument_by_name("as") .and_then(|arg| arg.as_str()) .map(Name::new) .transpose()?; - let purpose = if let Some(value) = directive.argument_by_name("for") { + let purpose = if let Some(value) = directive.specified_argument_by_name("for") { Some(Purpose::from_value(value)?) } else { None @@ -315,7 +315,7 @@ impl Link { let imports = if is_link { directive - .argument_by_name("import") + .specified_argument_by_name("import") .and_then(|arg| arg.as_list()) .unwrap_or(&[]) .iter() diff --git a/apollo-federation/src/query_graph/graph_path.rs b/apollo-federation/src/query_graph/graph_path.rs index 88bba9e8a0..84ae1bac7d 100644 --- a/apollo-federation/src/query_graph/graph_path.rs +++ b/apollo-federation/src/query_graph/graph_path.rs @@ -367,7 +367,7 @@ impl OpPathElement { ] { let directive_name: &'static str = (&kind).into(); if let Some(application) = self.directives().get(directive_name) { - let Some(arg) = application.argument_by_name("if") else { + let Some(arg) = application.specified_argument_by_name("if") else { return Err(FederationError::internal(format!( "@{} missing required argument \"if\"", directive_name diff --git a/apollo-federation/src/query_graph/mod.rs b/apollo-federation/src/query_graph/mod.rs index 1b00776e12..a69ba3cabf 100644 --- a/apollo-federation/src/query_graph/mod.rs +++ b/apollo-federation/src/query_graph/mod.rs @@ -906,7 +906,10 @@ impl QueryGraph { ty.directives() .get_all(&key_directive_definition.name) - .filter_map(|key| key.argument_by_name("fields").and_then(|arg| arg.as_str())) + .filter_map(|key| { + key.specified_argument_by_name("fields") + .and_then(|arg| arg.as_str()) + }) .map(|value| parse_field_set(schema, ty.name().clone(), value)) .find_ok(|selection| { !metadata diff --git a/apollo-federation/src/query_plan/conditions.rs b/apollo-federation/src/query_plan/conditions.rs index a91681de68..d1fd31a319 100644 --- a/apollo-federation/src/query_plan/conditions.rs +++ b/apollo-federation/src/query_plan/conditions.rs @@ -99,7 +99,7 @@ impl Conditions { "skip" => true, _ => continue, }; - let value = directive.argument_by_name("if").ok_or_else(|| { + let value = directive.specified_argument_by_name("if").ok_or_else(|| { FederationError::internal(format!( "missing if argument on @{}", if negated { "skip" } else { "include" }, @@ -346,7 +346,7 @@ fn matches_condition_for_kind( return false; } - let value = directive.argument_by_name("if"); + let value = directive.specified_argument_by_name("if"); let matches_if_negated = match kind { ConditionKind::Include => false, diff --git a/apollo-federation/src/supergraph/mod.rs b/apollo-federation/src/supergraph/mod.rs index 01210155d9..bc8893f3d5 100644 --- a/apollo-federation/src/supergraph/mod.rs +++ b/apollo-federation/src/supergraph/mod.rs @@ -2242,7 +2242,7 @@ fn extract_join_directives( fn join_directive_to_real_directive(directive: &Node) -> (Directive, Vec) { let subgraph_enum_values = directive - .argument_by_name("graphs") + .specified_argument_by_name("graphs") .and_then(|arg| arg.as_list()) .map(|list| { list.iter() @@ -2259,13 +2259,13 @@ fn join_directive_to_real_directive(directive: &Node) -> (Directive, .expect("join__directive(graphs:) missing"); let name = directive - .argument_by_name("name") + .specified_argument_by_name("name") .expect("join__directive(name:) is present") .as_str() .expect("join__directive(name:) is a string"); let arguments = directive - .argument_by_name("args") + .specified_argument_by_name("args") .and_then(|a| a.as_object()) .map(|args| { args.iter() diff --git a/apollo-router/src/plugins/authorization/policy.rs b/apollo-router/src/plugins/authorization/policy.rs index 821546a01c..e317b7eb97 100644 --- a/apollo-router/src/plugins/authorization/policy.rs +++ b/apollo-router/src/plugins/authorization/policy.rs @@ -111,7 +111,7 @@ fn policy_argument( opt_directive: Option<&impl AsRef>, ) -> impl Iterator + '_ { opt_directive - .and_then(|directive| directive.as_ref().argument_by_name("policies")) + .and_then(|directive| directive.as_ref().specified_argument_by_name("policies")) // outer array .and_then(|value| value.as_list()) .into_iter() @@ -205,7 +205,7 @@ fn policies_sets_argument( directive: &ast::Directive, ) -> impl Iterator> + '_ { directive - .argument_by_name("policies") + .specified_argument_by_name("policies") // outer array .and_then(|value| value.as_list()) .into_iter() diff --git a/apollo-router/src/plugins/authorization/scopes.rs b/apollo-router/src/plugins/authorization/scopes.rs index 55a4d0a0c4..6dcccfc0a0 100644 --- a/apollo-router/src/plugins/authorization/scopes.rs +++ b/apollo-router/src/plugins/authorization/scopes.rs @@ -111,7 +111,7 @@ fn scopes_argument( opt_directive: Option<&impl AsRef>, ) -> impl Iterator + '_ { opt_directive - .and_then(|directive| directive.as_ref().argument_by_name("scopes")) + .and_then(|directive| directive.as_ref().specified_argument_by_name("scopes")) // outer array .and_then(|value| value.as_list()) .into_iter() @@ -188,7 +188,7 @@ impl<'a> traverse::Visitor for ScopeExtractionVisitor<'a> { fn scopes_sets_argument(directive: &ast::Directive) -> impl Iterator> + '_ { directive - .argument_by_name("scopes") + .specified_argument_by_name("scopes") // outer array .and_then(|value| value.as_list()) .into_iter() diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs b/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs index 6f23fcdc00..cf819478e1 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs @@ -90,7 +90,7 @@ impl CostDirective { .get(&COST_DIRECTIVE_NAME) .and_then(|name| directives.get(name)) .or(directives.get(&COST_DIRECTIVE_DEFAULT_NAME)) - .and_then(|cost| cost.argument_by_name(&COST_DIRECTIVE_WEIGHT_ARGUMENT_NAME)) + .and_then(|cost| cost.specified_argument_by_name(&COST_DIRECTIVE_WEIGHT_ARGUMENT_NAME)) .and_then(|weight| weight.to_i32()) .map(|weight| Self { weight }) } @@ -103,7 +103,7 @@ impl CostDirective { .get(&COST_DIRECTIVE_NAME) .and_then(|name| directives.get(name)) .or(directives.get(&COST_DIRECTIVE_DEFAULT_NAME)) - .and_then(|cost| cost.argument_by_name(&COST_DIRECTIVE_WEIGHT_ARGUMENT_NAME)) + .and_then(|cost| cost.specified_argument_by_name(&COST_DIRECTIVE_WEIGHT_ARGUMENT_NAME)) .and_then(|weight| weight.to_i32()) .map(|weight| Self { weight }) } @@ -120,7 +120,7 @@ impl IncludeDirective { let directive = field .directives .get("include") - .and_then(|skip| skip.argument_by_name("if")) + .and_then(|skip| skip.specified_argument_by_name("if")) .and_then(|arg| arg.to_bool()) .map(|cond| Self { is_included: cond }); @@ -167,10 +167,10 @@ impl DefinitionListSizeDirective { .or(definition.directives.get(&LIST_SIZE_DIRECTIVE_DEFAULT_NAME)); if let Some(directive) = directive { let assumed_size = directive - .argument_by_name(&LIST_SIZE_DIRECTIVE_ASSUMED_SIZE_ARGUMENT_NAME) + .specified_argument_by_name(&LIST_SIZE_DIRECTIVE_ASSUMED_SIZE_ARGUMENT_NAME) .and_then(|arg| arg.to_i32()); let slicing_argument_names = directive - .argument_by_name(&LIST_SIZE_DIRECTIVE_SLICING_ARGUMENTS_ARGUMENT_NAME) + .specified_argument_by_name(&LIST_SIZE_DIRECTIVE_SLICING_ARGUMENTS_ARGUMENT_NAME) .and_then(|arg| arg.as_list()) .map(|arg_list| { arg_list @@ -180,7 +180,7 @@ impl DefinitionListSizeDirective { .collect() }); let sized_fields = directive - .argument_by_name(&LIST_SIZE_DIRECTIVE_SIZED_FIELDS_ARGUMENT_NAME) + .specified_argument_by_name(&LIST_SIZE_DIRECTIVE_SIZED_FIELDS_ARGUMENT_NAME) .and_then(|arg| arg.as_list()) .map(|arg_list| { arg_list @@ -190,7 +190,9 @@ impl DefinitionListSizeDirective { .collect() }); let require_one_slicing_argument = directive - .argument_by_name(&LIST_SIZE_DIRECTIVE_REQUIRE_ONE_SLICING_ARGUMENT_ARGUMENT_NAME) + .specified_argument_by_name( + &LIST_SIZE_DIRECTIVE_REQUIRE_ONE_SLICING_ARGUMENT_ARGUMENT_NAME, + ) .and_then(|arg| arg.to_bool()) .unwrap_or(true); @@ -275,7 +277,7 @@ impl RequiresDirective { let requires_arg = definition .directives .get("join__field") - .and_then(|requires| requires.argument_by_name("requires")) + .and_then(|requires| requires.specified_argument_by_name("requires")) .and_then(|arg| arg.as_str()); if let Some(arg) = requires_arg { @@ -302,7 +304,7 @@ impl SkipDirective { let directive = field .directives .get("skip") - .and_then(|skip| skip.argument_by_name("if")) + .and_then(|skip| skip.specified_argument_by_name("if")) .and_then(|arg| arg.to_bool()) .map(|cond| Self { is_skipped: cond }); diff --git a/apollo-router/src/plugins/progressive_override/mod.rs b/apollo-router/src/plugins/progressive_override/mod.rs index 542b0d0722..d4e1adb9b5 100644 --- a/apollo-router/src/plugins/progressive_override/mod.rs +++ b/apollo-router/src/plugins/progressive_override/mod.rs @@ -91,7 +91,7 @@ fn collect_labels_from_schema(schema: &Schema) -> LabelsFromSchema { .flatten() .filter_map(|join_directive| { if let Some(override_label_arg) = - join_directive.argument_by_name(OVERRIDE_LABEL_ARG_NAME) + join_directive.specified_argument_by_name(OVERRIDE_LABEL_ARG_NAME) { override_label_arg .as_str() diff --git a/apollo-router/src/query_planner/dual_introspection.rs b/apollo-router/src/query_planner/dual_introspection.rs index d8a72d34b2..400de5dbb6 100644 --- a/apollo-router/src/query_planner/dual_introspection.rs +++ b/apollo-router/src/query_planner/dual_introspection.rs @@ -1,5 +1,6 @@ use std::cmp::Ordering; +use apollo_compiler::ast; use serde_json_bytes::Value; use crate::error::QueryPlannerError; @@ -27,8 +28,8 @@ pub(crate) fn compare_introspection_responses( if let (Some(js_data), Some(rust_data)) = (&mut js_response.data, &mut rust_response.data) { - json_sort_arrays(js_data); - json_sort_arrays(rust_data); + normalize_response(js_data); + normalize_response(rust_data); } is_matched = js_response.data == rust_response.data; if is_matched { @@ -67,23 +68,72 @@ pub(crate) fn compare_introspection_responses( ); } -fn json_sort_arrays(value: &mut Value) { +fn normalize_response(value: &mut Value) { match value { Value::Array(array) => { for item in array.iter_mut() { - json_sort_arrays(item) + normalize_response(item) } array.sort_by(json_compare) } Value::Object(object) => { - for (_key, value) in object { - json_sort_arrays(value) + for (key, value) in object { + if let Some(new_value) = normalize_default_value(key.as_str(), value) { + *value = new_value + } else { + normalize_response(value) + } } } Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {} } } +/// When a default value is an input object, graphql-js seems to sort its fields by name +fn normalize_default_value(key: &str, value: &Value) -> Option { + if key != "defaultValue" { + return None; + } + let default_value = value.as_str()?; + // We don’t have a parser entry point for a standalone GraphQL `Value`, + // so mint a document that contains that value. + let doc = format!("{{ field(arg: {default_value}) }}"); + let doc = ast::Document::parse(doc, "").ok()?; + let parsed_default_value = &doc + .definitions + .first()? + .as_operation_definition()? + .selection_set + .first()? + .as_field()? + .arguments + .first()? + .value; + parsed_default_value.as_object()?; + let normalized = normalize_parsed_default_value(parsed_default_value); + Some(normalized.serialize().no_indent().to_string().into()) +} + +fn normalize_parsed_default_value(value: &ast::Value) -> ast::Value { + match value { + ast::Value::List(items) => ast::Value::List( + items + .iter() + .map(|item| normalize_parsed_default_value(item).into()) + .collect(), + ), + ast::Value::Object(fields) => { + let mut new_fields: Vec<_> = fields + .iter() + .map(|(name, value)| (name.clone(), normalize_parsed_default_value(value).into())) + .collect(); + new_fields.sort_by(|(name_1, _value_1), (name_2, _value_2)| name_1.cmp(name_2)); + ast::Value::Object(new_fields) + } + v => v.clone(), + } +} + fn json_compare(a: &Value, b: &Value) -> Ordering { match (a, b) { (Value::Null, Value::Null) => Ordering::Equal, diff --git a/apollo-router/src/spec/query/change.rs b/apollo-router/src/spec/query/change.rs index 9db9554a04..8bca0e025b 100644 --- a/apollo-router/src/spec/query/change.rs +++ b/apollo-router/src/spec/query/change.rs @@ -286,7 +286,10 @@ impl<'a> QueryHashVisitor<'a> { fn hash_join_type(&mut self, name: &Name, directives: &DirectiveList) -> Result<(), BoxError> { if let Some(dir_name) = self.join_type_directive_name.as_deref() { if let Some(dir) = directives.get(dir_name) { - if let Some(key) = dir.argument_by_name("key").and_then(|arg| arg.as_str()) { + if let Some(key) = dir + .specified_argument_by_name("key") + .and_then(|arg| arg.as_str()) + { let mut parser = Parser::new(); if let Ok(field_set) = parser.parse_field_set( Valid::assume_valid_ref(self.schema), @@ -315,7 +318,7 @@ impl<'a> QueryHashVisitor<'a> { if let Some(dir_name) = self.join_field_directive_name.as_deref() { if let Some(dir) = directives.get(dir_name) { if let Some(requires) = dir - .argument_by_name("requires") + .specified_argument_by_name("requires") .and_then(|arg| arg.as_str()) { if let Ok(parent_type) = Name::new(parent_type) { diff --git a/apollo-router/src/spec/schema.rs b/apollo-router/src/spec/schema.rs index edb6c3bbac..2208a5863e 100644 --- a/apollo-router/src/spec/schema.rs +++ b/apollo-router/src/spec/schema.rs @@ -67,8 +67,10 @@ impl Schema { if let Some(join_enum) = definitions.get_enum("join__Graph") { for (name, url) in join_enum.values.iter().filter_map(|(_name, value)| { let join_directive = value.directives.get("join__graph")?; - let name = join_directive.argument_by_name("name")?.as_str()?; - let url = join_directive.argument_by_name("url")?.as_str()?; + let name = join_directive + .specified_argument_by_name("name")? + .as_str()?; + let url = join_directive.specified_argument_by_name("url")?.as_str()?; Some((name, url)) }) { if url.is_empty() { @@ -220,7 +222,7 @@ impl Schema { for directive in &self.supergraph_schema().schema_definition.directives { let join_url = if directive.name == "core" { let Some(feature) = directive - .argument_by_name("feature") + .specified_argument_by_name("feature") .and_then(|value| value.as_str()) else { continue; @@ -229,7 +231,7 @@ impl Schema { feature } else if directive.name == "link" { let Some(url) = directive - .argument_by_name("url") + .specified_argument_by_name("url") .and_then(|value| value.as_str()) else { continue; @@ -257,7 +259,7 @@ impl Schema { .filter(|dir| dir.name.as_str() == "link") .any(|link| { if let Some(url_in_link) = link - .argument_by_name("url") + .specified_argument_by_name("url") .and_then(|value| value.as_str()) { let Some((base_url_in_link, version_in_link)) = url_in_link.rsplit_once("/v") @@ -295,7 +297,7 @@ impl Schema { .filter(|dir| dir.name.as_str() == "link") .find(|link| { if let Some(url_in_link) = link - .argument_by_name("url") + .specified_argument_by_name("url") .and_then(|value| value.as_str()) { let Some((base_url_in_link, version_in_link)) = url_in_link.rsplit_once("/v") @@ -319,7 +321,7 @@ impl Schema { } }) .map(|link| { - link.argument_by_name("as") + link.specified_argument_by_name("as") .and_then(|value| value.as_str().map(|s| s.to_string())) .unwrap_or_else(|| default.to_string()) }) diff --git a/apollo-router/src/spec/selection.rs b/apollo-router/src/spec/selection.rs index 0c0cd545b8..f9ac7e42b9 100644 --- a/apollo-router/src/spec/selection.rs +++ b/apollo-router/src/spec/selection.rs @@ -304,7 +304,7 @@ fn parse_defer( } let label = if condition != Condition::No { directive - .argument_by_name("label") + .specified_argument_by_name("label") .and_then(|value| value.as_str()) .map(|str| str.to_owned()) } else { @@ -355,7 +355,7 @@ impl IncludeSkip { impl Condition { pub(crate) fn parse(directive: &executable::Directive) -> Option { - match directive.argument_by_name("if")?.as_ref() { + match directive.specified_argument_by_name("if")?.as_ref() { executable::Value::Boolean(true) => Some(Condition::Yes), executable::Value::Boolean(false) => Some(Condition::No), executable::Value::Variable(variable) => { diff --git a/apollo-router/src/uplink/license_enforcement.rs b/apollo-router/src/uplink/license_enforcement.rs index 1f20719436..1d23f9cc6c 100644 --- a/apollo-router/src/uplink/license_enforcement.rs +++ b/apollo-router/src/uplink/license_enforcement.rs @@ -104,7 +104,7 @@ impl ParsedLinkSpec { link_directive: &Directive, ) -> Option> { link_directive - .argument_by_name(LINK_URL_ARGUMENT) + .specified_argument_by_name(LINK_URL_ARGUMENT) .and_then(|value| { let url_string = value.as_str(); let parsed_url = Url::parse(url_string.unwrap_or_default()).ok()?; @@ -122,7 +122,7 @@ impl ParsedLinkSpec { semver::Version::parse(format!("{}.0", &version_string).as_str()).ok()?; let imported_as = link_directive - .argument_by_name(LINK_AS_ARGUMENT) + .specified_argument_by_name(LINK_AS_ARGUMENT) .map(|as_arg| as_arg.as_str().unwrap_or_default().to_string()); Some(Ok(ParsedLinkSpec { @@ -274,7 +274,9 @@ impl LicenseEnforcementReport { } _ => vec![], }) - .any(|directive| directive.argument_by_name(argument).is_some()) + .any(|directive| { + directive.specified_argument_by_name(argument).is_some() + }) { schema_violations.push(SchemaViolation::DirectiveArgument { url: link_spec.url.to_string(), diff --git a/apollo-router/tests/fixtures/introspect_full_schema.graphql b/apollo-router/tests/fixtures/introspect_full_schema.graphql index a0a3100c4f..85d1b5e72f 100644 --- a/apollo-router/tests/fixtures/introspect_full_schema.graphql +++ b/apollo-router/tests/fixtures/introspect_full_schema.graphql @@ -19,6 +19,9 @@ query IntrospectionQuery { args(includeDeprecated: true) { ...InputValue } + nonDeprecatedArgs: args { + name + } } } } @@ -32,15 +35,24 @@ fragment FullType on __Type { args(includeDeprecated: true) { ...InputValue } + nonDeprecatedArgs: args { + name + } type { ...TypeRef } isDeprecated deprecationReason } + nonDeprecatedFields: fields { + name + } inputFields(includeDeprecated: true) { ...InputValue } + nonDeprecatedInputFields: inputFields { + name + } interfaces { ...TypeRef } @@ -50,6 +62,9 @@ fragment FullType on __Type { isDeprecated deprecationReason } + nonDeprecatedEnumValues: enumValues { + name + } possibleTypes { ...TypeRef } diff --git a/apollo-router/tests/fixtures/schema_to_introspect.graphql b/apollo-router/tests/fixtures/schema_to_introspect.graphql new file mode 100644 index 0000000000..f45dfa4335 --- /dev/null +++ b/apollo-router/tests/fixtures/schema_to_introspect.graphql @@ -0,0 +1,89 @@ +"The schema" +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: TheQuery +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +scalar join__FieldSet +scalar link__Import + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "subgraph1", url: "http://localhost:4001/graphql") +} + +enum link__Purpose { + SECURITY + EXECUTION +} + +""" +Root query type +""" +type TheQuery implements I @join__type(graph: SUBGRAPH1) { + id: ID! + ints: [[Int!]]! @deprecated(reason: "…") + url(arg: In = { b: 4, a: 2 }): Url + union: U @deprecated(reason: null) +} + +interface I @join__type(graph: SUBGRAPH1) { + id: ID! +} + +input In @join__type(graph: SUBGRAPH1) { + a: Int! = 0 @deprecated(reason: null) + b: Int @deprecated +} + +scalar Url @specifiedBy(url: "https://url.spec.whatwg.org/") @join__type(graph: SUBGRAPH1) + +union U @join__type(graph: SUBGRAPH1) = TheQuery | T + +type T @join__type(graph: SUBGRAPH1) { + enum: E @deprecated +} + +enum E @join__type(graph: SUBGRAPH1) { + NEW + OLD @deprecated +} diff --git a/apollo-router/tests/integration/introspection.rs b/apollo-router/tests/integration/introspection.rs index fc66cd231e..ac5ae33915 100644 --- a/apollo-router/tests/integration/introspection.rs +++ b/apollo-router/tests/integration/introspection.rs @@ -238,17 +238,40 @@ async fn both_mode_integration() { introspection: true ", ) - .supergraph("../examples/graphql/local.graphql") + .supergraph("tests/fixtures/schema_to_introspect.graphql") .log("error,apollo_router=info,apollo_router::query_planner=trace") .build() .await; router.start().await; router.assert_started().await; - router - .execute_query(&json!({ - "query": include_str!("../fixtures/introspect_full_schema.graphql"), - })) - .await; + let query = json!({ + "query": include_str!("../fixtures/introspect_full_schema.graphql"), + }); + let (_trace_id, response) = router.execute_query(&query).await; + insta::assert_json_snapshot!(response.json::().await.unwrap()); router.assert_log_contains("Introspection match! πŸŽ‰").await; router.graceful_shutdown().await; } + +#[tokio::test] +async fn integration() { + let mut router = IntegrationTest::builder() + .config( + " + experimental_introspection_mode: new + supergraph: + introspection: true + ", + ) + .supergraph("tests/fixtures/schema_to_introspect.graphql") + .build() + .await; + router.start().await; + router.assert_started().await; + let query = json!({ + "query": include_str!("../fixtures/introspect_full_schema.graphql"), + }); + let (_trace_id, response) = router.execute_query(&query).await; + insta::assert_json_snapshot!(response.json::().await.unwrap()); + router.graceful_shutdown().await; +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__introspection__both_mode_integration.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__introspection__both_mode_integration.snap new file mode 100644 index 0000000000..1bad697545 --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__introspection__both_mode_integration.snap @@ -0,0 +1,1728 @@ +--- +source: apollo-router/tests/integration/introspection.rs +expression: "response.json::().await.unwrap()" +--- +{ + "data": { + "__schema": { + "queryType": { + "name": "TheQuery" + }, + "mutationType": null, + "subscriptionType": null, + "types": [ + { + "kind": "OBJECT", + "name": "TheQuery", + "description": "Root query type", + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ints", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + } + } + } + }, + "isDeprecated": true, + "deprecationReason": "…" + }, + { + "name": "url", + "description": null, + "args": [ + { + "name": "arg", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "In", + "ofType": null + }, + "defaultValue": "{a: 2, b: 4}", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "arg" + } + ], + "type": { + "kind": "SCALAR", + "name": "Url", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "union", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "UNION", + "name": "U", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "id" + }, + { + "name": "url" + }, + { + "name": "union" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "I", + "ofType": null + } + ], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Int", + "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "INTERFACE", + "name": "I", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "id" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "TheQuery", + "ofType": null + } + ] + }, + { + "kind": "INPUT_OBJECT", + "name": "In", + "description": null, + "fields": null, + "nonDeprecatedFields": null, + "inputFields": [ + { + "name": "a", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": "0", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "b", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": true, + "deprecationReason": "No longer supported" + } + ], + "nonDeprecatedInputFields": [ + { + "name": "a" + } + ], + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Url", + "description": null, + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "UNION", + "name": "U", + "description": null, + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "TheQuery", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "T", + "ofType": null + } + ] + }, + { + "kind": "OBJECT", + "name": "T", + "description": null, + "fields": [ + { + "name": "enum", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "ENUM", + "name": "E", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "No longer supported" + } + ], + "nonDeprecatedFields": [], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "E", + "description": null, + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "NEW", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OLD", + "description": null, + "isDeprecated": true, + "deprecationReason": "No longer supported" + } + ], + "nonDeprecatedEnumValues": [ + { + "name": "NEW" + } + ], + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "The `Boolean` scalar type represents `true` or `false`.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Schema", + "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", + "fields": [ + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": "If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "description": "A list of all directives supported by this server.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "description" + }, + { + "name": "types" + }, + { + "name": "queryType" + }, + { + "name": "mutationType" + }, + { + "name": "subscriptionType" + }, + { + "name": "directives" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByURL`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", + "fields": [ + { + "name": "kind", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "specifiedByURL", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "kind" + }, + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "specifiedByURL" + }, + { + "name": "fields" + }, + { + "name": "interfaces" + }, + { + "name": "possibleTypes" + }, + { + "name": "enumValues" + }, + { + "name": "inputFields" + }, + { + "name": "ofType" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given `__Type` is.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedEnumValues": [ + { + "name": "SCALAR" + }, + { + "name": "OBJECT" + }, + { + "name": "INTERFACE" + }, + { + "name": "UNION" + }, + { + "name": "ENUM" + }, + { + "name": "INPUT_OBJECT" + }, + { + "name": "LIST" + }, + { + "name": "NON_NULL" + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "args" + }, + { + "name": "type" + }, + { + "name": "isDeprecated" + }, + { + "name": "deprecationReason" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultValue", + "description": "A GraphQL-formatted string representing the default value for this input value.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "type" + }, + { + "name": "defaultValue" + }, + { + "name": "isDeprecated" + }, + { + "name": "deprecationReason" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "isDeprecated" + }, + { + "name": "deprecationReason" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isRepeatable", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__DirectiveLocation", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "isRepeatable" + }, + { + "name": "locations" + }, + { + "name": "args" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "description": "Location adjacent to a query operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "description": "Location adjacent to a mutation operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBSCRIPTION", + "description": "Location adjacent to a subscription operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "description": "Location adjacent to a field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "description": "Location adjacent to a fragment definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "description": "Location adjacent to a fragment spread.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "description": "Location adjacent to an inline fragment.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VARIABLE_DEFINITION", + "description": "Location adjacent to a variable definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "description": "Location adjacent to a schema definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "description": "Location adjacent to a scalar definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Location adjacent to an object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "description": "Location adjacent to a field definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "description": "Location adjacent to an argument definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Location adjacent to an interface definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Location adjacent to a union definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Location adjacent to an enum definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "description": "Location adjacent to an enum value definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Location adjacent to an input object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "description": "Location adjacent to an input object field definition.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedEnumValues": [ + { + "name": "QUERY" + }, + { + "name": "MUTATION" + }, + { + "name": "SUBSCRIPTION" + }, + { + "name": "FIELD" + }, + { + "name": "FRAGMENT_DEFINITION" + }, + { + "name": "FRAGMENT_SPREAD" + }, + { + "name": "INLINE_FRAGMENT" + }, + { + "name": "VARIABLE_DEFINITION" + }, + { + "name": "SCHEMA" + }, + { + "name": "SCALAR" + }, + { + "name": "OBJECT" + }, + { + "name": "FIELD_DEFINITION" + }, + { + "name": "ARGUMENT_DEFINITION" + }, + { + "name": "INTERFACE" + }, + { + "name": "UNION" + }, + { + "name": "ENUM" + }, + { + "name": "ENUM_VALUE" + }, + { + "name": "INPUT_OBJECT" + }, + { + "name": "INPUT_FIELD_DEFINITION" + } + ], + "possibleTypes": null + } + ], + "directives": [ + { + "name": "defer", + "description": null, + "locations": [ + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "label", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "if", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": "true", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "label" + }, + { + "name": "if" + } + ] + }, + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "if" + } + ] + }, + { + "name": "skip", + "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "if" + } + ] + }, + { + "name": "deprecated", + "description": "Marks an element of a GraphQL schema as no longer supported.", + "locations": [ + "FIELD_DEFINITION", + "ARGUMENT_DEFINITION", + "INPUT_FIELD_DEFINITION", + "ENUM_VALUE" + ], + "args": [ + { + "name": "reason", + "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": "\"No longer supported\"", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "reason" + } + ] + }, + { + "name": "specifiedBy", + "description": "Exposes a URL that specifies the behavior of this scalar.", + "locations": [ + "SCALAR" + ], + "args": [ + { + "name": "url", + "description": "The URL that specifies the behavior of this scalar.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "url" + } + ] + } + ] + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__introspection__integration.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__introspection__integration.snap new file mode 100644 index 0000000000..40e9d17440 --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__introspection__integration.snap @@ -0,0 +1,1728 @@ +--- +source: apollo-router/tests/integration/introspection.rs +expression: "response.json::().await.unwrap()" +--- +{ + "data": { + "__schema": { + "queryType": { + "name": "TheQuery" + }, + "mutationType": null, + "subscriptionType": null, + "types": [ + { + "kind": "OBJECT", + "name": "__Schema", + "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", + "fields": [ + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": "If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "description": "A list of all directives supported by this server.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "description" + }, + { + "name": "types" + }, + { + "name": "queryType" + }, + { + "name": "mutationType" + }, + { + "name": "subscriptionType" + }, + { + "name": "directives" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByURL`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", + "fields": [ + { + "name": "kind", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "specifiedByURL", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "kind" + }, + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "fields" + }, + { + "name": "interfaces" + }, + { + "name": "possibleTypes" + }, + { + "name": "enumValues" + }, + { + "name": "inputFields" + }, + { + "name": "ofType" + }, + { + "name": "specifiedByURL" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given `__Type` is.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedEnumValues": [ + { + "name": "SCALAR" + }, + { + "name": "OBJECT" + }, + { + "name": "INTERFACE" + }, + { + "name": "UNION" + }, + { + "name": "ENUM" + }, + { + "name": "INPUT_OBJECT" + }, + { + "name": "LIST" + }, + { + "name": "NON_NULL" + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "args" + }, + { + "name": "type" + }, + { + "name": "isDeprecated" + }, + { + "name": "deprecationReason" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultValue", + "description": "A GraphQL-formatted string representing the default value for this input value.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "type" + }, + { + "name": "defaultValue" + }, + { + "name": "isDeprecated" + }, + { + "name": "deprecationReason" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "isDeprecated" + }, + { + "name": "deprecationReason" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__DirectiveLocation", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isRepeatable", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "locations" + }, + { + "name": "args" + }, + { + "name": "isRepeatable" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "description": "Location adjacent to a query operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "description": "Location adjacent to a mutation operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBSCRIPTION", + "description": "Location adjacent to a subscription operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "description": "Location adjacent to a field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "description": "Location adjacent to a fragment definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "description": "Location adjacent to a fragment spread.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "description": "Location adjacent to an inline fragment.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VARIABLE_DEFINITION", + "description": "Location adjacent to a variable definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "description": "Location adjacent to a schema definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "description": "Location adjacent to a scalar definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Location adjacent to an object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "description": "Location adjacent to a field definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "description": "Location adjacent to an argument definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Location adjacent to an interface definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Location adjacent to a union definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Location adjacent to an enum definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "description": "Location adjacent to an enum value definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Location adjacent to an input object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "description": "Location adjacent to an input object field definition.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedEnumValues": [ + { + "name": "QUERY" + }, + { + "name": "MUTATION" + }, + { + "name": "SUBSCRIPTION" + }, + { + "name": "FIELD" + }, + { + "name": "FRAGMENT_DEFINITION" + }, + { + "name": "FRAGMENT_SPREAD" + }, + { + "name": "INLINE_FRAGMENT" + }, + { + "name": "VARIABLE_DEFINITION" + }, + { + "name": "SCHEMA" + }, + { + "name": "SCALAR" + }, + { + "name": "OBJECT" + }, + { + "name": "FIELD_DEFINITION" + }, + { + "name": "ARGUMENT_DEFINITION" + }, + { + "name": "INTERFACE" + }, + { + "name": "UNION" + }, + { + "name": "ENUM" + }, + { + "name": "ENUM_VALUE" + }, + { + "name": "INPUT_OBJECT" + }, + { + "name": "INPUT_FIELD_DEFINITION" + } + ], + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Int", + "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "The `Boolean` scalar type represents `true` or `false`.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TheQuery", + "description": "Root query type", + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ints", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + } + } + } + }, + "isDeprecated": true, + "deprecationReason": "…" + }, + { + "name": "url", + "description": null, + "args": [ + { + "name": "arg", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "In", + "ofType": null + }, + "defaultValue": "{b: 4, a: 2}", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "arg" + } + ], + "type": { + "kind": "SCALAR", + "name": "Url", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "union", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "UNION", + "name": "U", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "id" + }, + { + "name": "url" + }, + { + "name": "union" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "I", + "ofType": null + } + ], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "INTERFACE", + "name": "I", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "id" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "TheQuery", + "ofType": null + } + ] + }, + { + "kind": "INPUT_OBJECT", + "name": "In", + "description": null, + "fields": null, + "nonDeprecatedFields": null, + "inputFields": [ + { + "name": "a", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": "0", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "b", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": true, + "deprecationReason": "No longer supported" + } + ], + "nonDeprecatedInputFields": [ + { + "name": "a" + } + ], + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Url", + "description": null, + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "UNION", + "name": "U", + "description": null, + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "TheQuery", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "T", + "ofType": null + } + ] + }, + { + "kind": "OBJECT", + "name": "T", + "description": null, + "fields": [ + { + "name": "enum", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "ENUM", + "name": "E", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "No longer supported" + } + ], + "nonDeprecatedFields": [], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "E", + "description": null, + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "NEW", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OLD", + "description": null, + "isDeprecated": true, + "deprecationReason": "No longer supported" + } + ], + "nonDeprecatedEnumValues": [ + { + "name": "NEW" + } + ], + "possibleTypes": null + } + ], + "directives": [ + { + "name": "skip", + "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "if" + } + ] + }, + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "if" + } + ] + }, + { + "name": "deprecated", + "description": "Marks an element of a GraphQL schema as no longer supported.", + "locations": [ + "FIELD_DEFINITION", + "ARGUMENT_DEFINITION", + "INPUT_FIELD_DEFINITION", + "ENUM_VALUE" + ], + "args": [ + { + "name": "reason", + "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": "\"No longer supported\"", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "reason" + } + ] + }, + { + "name": "specifiedBy", + "description": "Exposes a URL that specifies the behavior of this scalar.", + "locations": [ + "SCALAR" + ], + "args": [ + { + "name": "url", + "description": "The URL that specifies the behavior of this scalar.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "url" + } + ] + }, + { + "name": "defer", + "description": null, + "locations": [ + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "label", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "if", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": "true", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "label" + }, + { + "name": "if" + } + ] + } + ] + } + } +} diff --git a/examples/supergraph-sdl/rust/Cargo.toml b/examples/supergraph-sdl/rust/Cargo.toml index ead321ca05..827e44ed5d 100644 --- a/examples/supergraph-sdl/rust/Cargo.toml +++ b/examples/supergraph-sdl/rust/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] anyhow = "1" -apollo-compiler = "=1.0.0-beta.23" +apollo-compiler = "=1.0.0-beta.24" apollo-router = { path = "../../../apollo-router" } async-trait = "0.1" tower = { version = "0.4", features = ["full"] } From 5cf75eb4ef57d44d73db070490689bbedad81d3e Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Tue, 24 Sep 2024 09:45:56 -0700 Subject: [PATCH 06/23] Fix bugs identified by `@skip`/`@include` tests (#6043) This PR: - Reverts most of #5919: - The changes to selection set merge logic have been reversed, as it turns out the performance impact can be noticed in some rare cases. - The changes to `Selection::from_element()` have been reversed, as it deviated from JS QP. The bugs causing the failure of test `it_handles_a_simple_at_requires_triggered_within_a_conditional()` have been identified, and fixed in this PR. - The followup PR #5963 with fixes to `Selection::from_element()`-related changes has also been reverted. - The only changes kept are the ones to `remove_unneeded_top_level_fragment_directives()` (and the added function `with_updated_directives_and_selection_set()`), as those fixed deviations from JS QP. - The tests from #5963 are also kept (they still pass). - Fixes a bug in `OpGraphPathContext::with_context_of()` where the conditionals were pushed in the wrong order (this can result in `@skip`/`@include`s being applied in inverse order, causing spurious differences in generated QPs). - This caused a change in the snapshot for test `handles_multiple_conditions_on_abstract_types()`. - Fixes bugs in `SelectionSet::without_unnecessary_fragments()` where: - It did not recurse into the selections of removed inline fragments. - It checked whether the parent type and fragment's type conditions were strictly equal and forgot about subtyping. - Fixes bugs in `SelectionSet::is_useless_followup_element()` where: - It flipped a boolean for checking whether directives were useless. - It flipped the order of arguments passed to `Schema::is_subtype()`. Test-wise, this PR causes the following tests to pass: - `it_handles_a_simple_at_requires_triggered_within_a_conditional()` - As mentioned above, the previous PRs had caused this test to pass but deviated from JS QP behavior to do so. This PR fixes the bugs in the ported JS QP logic that caused the test failure, and reverses the introduced deviations. - `it_handles_an_at_requires_where_multiple_conditional_are_involved()` --- apollo-federation/src/operation/merging.rs | 259 ++++++++++-------- apollo-federation/src/operation/mod.rs | 152 +++++----- apollo-federation/src/operation/optimize.rs | 165 ++++++----- apollo-federation/src/operation/simplify.rs | 2 +- apollo-federation/src/operation/tests/mod.rs | 4 +- .../src/query_graph/graph_path.rs | 21 +- .../src/query_plan/conditions.rs | 10 +- .../src/query_plan/fetch_dependency_graph.rs | 4 +- .../query_plan/build_query_plan_tests.rs | 4 +- .../requires/include_skip.rs | 18 +- ...res_triggered_within_a_conditional.graphql | 2 +- ...t_requires_triggered_conditionally.graphql | 2 +- ..._multiple_conditional_are_involved.graphql | 2 +- ...rwritten_after_removing_directives.graphql | 2 +- ...include_is_stripped_from_fragments.graphql | 2 +- 15 files changed, 325 insertions(+), 324 deletions(-) diff --git a/apollo-federation/src/operation/merging.rs b/apollo-federation/src/operation/merging.rs index c938f2e564..4c2b31cbd3 100644 --- a/apollo-federation/src/operation/merging.rs +++ b/apollo-federation/src/operation/merging.rs @@ -1,13 +1,14 @@ //! Provides methods for recursively merging selections and selection sets. use std::sync::Arc; -use selection_map::SelectionMap; +use apollo_compiler::collections::IndexMap; use super::selection_map; use super::FieldSelection; use super::FieldSelectionValue; use super::FragmentSpreadSelection; use super::FragmentSpreadSelectionValue; +use super::HasSelectionKey as _; use super::InlineFragmentSelection; use super::InlineFragmentSelectionValue; use super::NamedFragments; @@ -15,8 +16,6 @@ use super::Selection; use super::SelectionSet; use super::SelectionValue; use crate::error::FederationError; -use crate::operation::HasSelectionKey; -use crate::schema::position::CompositeTypeDefinitionPosition; impl<'a> FieldSelectionValue<'a> { /// Merges the given field selections into this one. @@ -29,38 +28,43 @@ impl<'a> FieldSelectionValue<'a> { /// Returns an error if: /// - The parent type or schema of any selection does not match `self`'s. /// - Any selection does not select the same field position as `self`. - fn merge_into(&mut self, other: &FieldSelection) -> Result<(), FederationError> { + fn merge_into<'op>( + &mut self, + others: impl Iterator, + ) -> Result<(), FederationError> { let self_field = &self.get().field; - let mut selection_set = None; - let other_field = &other.field; - if other_field.schema != self_field.schema { - return Err(FederationError::internal( - "Cannot merge field selections from different schemas", - )); - } - if other_field.field_position != self_field.field_position { - return Err(FederationError::internal(format!( + let mut selection_sets = vec![]; + for other in others { + let other_field = &other.field; + if other_field.schema != self_field.schema { + return Err(FederationError::internal( + "Cannot merge field selections from different schemas", + )); + } + if other_field.field_position != self_field.field_position { + return Err(FederationError::internal(format!( "Cannot merge field selection for field \"{}\" into a field selection for field \"{}\"", other_field.field_position, self_field.field_position, ))); - } - if self.get().selection_set.is_some() { - let Some(other_selection_set) = &other.selection_set else { + } + if self.get().selection_set.is_some() { + let Some(other_selection_set) = &other.selection_set else { + return Err(FederationError::internal(format!( + "Field \"{}\" has composite type but not a selection set", + other_field.field_position, + ))); + }; + selection_sets.push(other_selection_set); + } else if other.selection_set.is_some() { return Err(FederationError::internal(format!( - "Field \"{}\" has composite type but not a selection set", + "Field \"{}\" has non-composite type but also has a selection set", other_field.field_position, ))); - }; - selection_set = Some(other_selection_set); - } else if other.selection_set.is_some() { - return Err(FederationError::internal(format!( - "Field \"{}\" has non-composite type but also has a selection set", - other_field.field_position, - ))); + } } if let Some(self_selection_set) = self.get_selection_set_mut() { - self_selection_set.merge_into(selection_set.into_iter())?; + self_selection_set.merge_into(selection_sets.into_iter())?; } Ok(()) } @@ -75,26 +79,35 @@ impl<'a> InlineFragmentSelectionValue<'a> { /// /// # Errors /// Returns an error if the parent type or schema of any selection does not match `self`'s. - fn merge_into(&mut self, other: &InlineFragmentSelection) -> Result<(), FederationError> { + fn merge_into<'op>( + &mut self, + others: impl Iterator, + ) -> Result<(), FederationError> { let self_inline_fragment = &self.get().inline_fragment; - let other_inline_fragment = &other.inline_fragment; - if other_inline_fragment.schema != self_inline_fragment.schema { - return Err(FederationError::internal( - "Cannot merge inline fragment from different schemas", - )); - } - if other_inline_fragment.parent_type_position != self_inline_fragment.parent_type_position { - return Err(FederationError::internal( - format!( - "Cannot merge inline fragment of parent type \"{}\" into an inline fragment of parent type \"{}\"", - other_inline_fragment.parent_type_position, - self_inline_fragment.parent_type_position, - ), - )); + let mut selection_sets = vec![]; + for other in others { + let other_inline_fragment = &other.inline_fragment; + if other_inline_fragment.schema != self_inline_fragment.schema { + return Err(FederationError::internal( + "Cannot merge inline fragment from different schemas", + )); + } + if other_inline_fragment.parent_type_position + != self_inline_fragment.parent_type_position + { + return Err(FederationError::internal( + format!( + "Cannot merge inline fragment of parent type \"{}\" into an inline fragment of parent type \"{}\"", + other_inline_fragment.parent_type_position, + self_inline_fragment.parent_type_position, + ), + )); + } + selection_sets.push(&other.selection_set); } - self.get_selection_set_mut() - .merge_into(std::iter::once(&other.selection_set)) + .merge_into(selection_sets.into_iter())?; + Ok(()) } } @@ -107,19 +120,24 @@ impl<'a> FragmentSpreadSelectionValue<'a> { /// /// # Errors /// Returns an error if the parent type or schema of any selection does not match `self`'s. - fn merge_into(&mut self, other: &FragmentSpreadSelection) -> Result<(), FederationError> { + fn merge_into<'op>( + &mut self, + others: impl Iterator, + ) -> Result<(), FederationError> { let self_fragment_spread = &self.get().spread; - let other_fragment_spread = &other.spread; - if other_fragment_spread.schema != self_fragment_spread.schema { - return Err(FederationError::internal( - "Cannot merge fragment spread from different schemas", - )); + for other in others { + let other_fragment_spread = &other.spread; + if other_fragment_spread.schema != self_fragment_spread.schema { + return Err(FederationError::internal( + "Cannot merge fragment spread from different schemas", + )); + } + // Nothing to do since the fragment spread is already part of the selection set. + // Fragment spreads are uniquely identified by fragment name and applied directives. + // Since there is already an entry for the same fragment spread, there is no point + // in attempting to merge its sub-selections, as the underlying entry should be + // exactly the same as the currently processed one. } - // Nothing to do since the fragment spread is already part of the selection set. - // Fragment spreads are uniquely identified by fragment name and applied directives. - // Since there is already an entry for the same fragment spread, there is no point - // in attempting to merge its sub-selections, as the underlying entry should be - // exactly the same as the currently processed one. Ok(()) } } @@ -155,65 +173,68 @@ impl SelectionSet { } selections_to_merge.extend(other.selections.values()); } - self.merge_selections_into(selections_to_merge.into_iter(), false) + self.merge_selections_into(selections_to_merge.into_iter()) } /// NOTE: This is a private API and should be used with care, use `add_selection` instead. /// /// A helper function for merging the given selections into this one. /// - /// The `do_fragment_inlining` flag enables a check to see if any inline fragments yielded from - /// `others` can be recursively merged into the selection set instead of just merging in the - /// fragment. This requires that the fragment has no directives and either has no type - /// condition or the type condition matches this selection set's type position. - /// /// # Errors /// Returns an error if the parent type or schema of any selection does not match `self`'s. /// /// Returns an error if any selection contains invalid GraphQL that prevents the merge. - #[allow(unreachable_code)] pub(super) fn merge_selections_into<'op>( &mut self, - mut others: impl Iterator, - do_fragment_inlining: bool, + others: impl Iterator, ) -> Result<(), FederationError> { - fn insert_selection( - target: &mut SelectionMap, - selection: &Selection, - ) -> Result<(), FederationError> { - match target.entry(selection.key()) { - selection_map::Entry::Vacant(vacant) => { - vacant.insert(selection.clone())?; - Ok(()) - } - selection_map::Entry::Occupied(mut entry) => match entry.get_mut() { - SelectionValue::Field(mut field) => { - let Selection::Field(other_field) = selection else { - return Err(FederationError::internal(format!( - "Field selection key for field \"{}\" references non-field selection", - field.get().field.field_position, - ))); + let mut fields = IndexMap::default(); + let mut fragment_spreads = IndexMap::default(); + let mut inline_fragments = IndexMap::default(); + let target = Arc::make_mut(&mut self.selections); + for other_selection in others { + let other_key = other_selection.key(); + match target.entry(other_key.clone()) { + selection_map::Entry::Occupied(existing) => match existing.get() { + Selection::Field(self_field_selection) => { + let Selection::Field(other_field_selection) = other_selection else { + return Err(FederationError::internal( + format!( + "Field selection key for field \"{}\" references non-field selection", + self_field_selection.field.field_position, + ), + )); }; - field.merge_into(other_field) + fields + .entry(other_key) + .or_insert_with(Vec::new) + .push(other_field_selection); } - SelectionValue::FragmentSpread(mut spread) => { - let Selection::FragmentSpread(other_spread) = selection else { + Selection::FragmentSpread(self_fragment_spread_selection) => { + let Selection::FragmentSpread(other_fragment_spread_selection) = + other_selection + else { return Err(FederationError::internal( format!( "Fragment spread selection key for fragment \"{}\" references non-field selection", - spread.get().spread.fragment_name, + self_fragment_spread_selection.spread.fragment_name, ), )); }; - spread.merge_into(other_spread) + fragment_spreads + .entry(other_key) + .or_insert_with(Vec::new) + .push(other_fragment_spread_selection); } - SelectionValue::InlineFragment(mut inline) => { - let Selection::InlineFragment(other_inline) = selection else { + Selection::InlineFragment(self_inline_fragment_selection) => { + let Selection::InlineFragment(other_inline_fragment_selection) = + other_selection + else { return Err(FederationError::internal( format!( "Inline fragment selection key under parent type \"{}\" {}references non-field selection", - inline.get().inline_fragment.parent_type_position, - inline.get().inline_fragment.type_condition_position.clone() + self_inline_fragment_selection.inline_fragment.parent_type_position, + self_inline_fragment_selection.inline_fragment.type_condition_position.clone() .map_or_else( String::new, |cond| format!("(type condition: {}) ", cond), @@ -221,36 +242,53 @@ impl SelectionSet { ), )); }; - inline.merge_into(other_inline) + inline_fragments + .entry(other_key) + .or_insert_with(Vec::new) + .push(other_inline_fragment_selection); } }, + selection_map::Entry::Vacant(vacant) => { + vacant.insert(other_selection.clone())?; + } } } - let target = Arc::make_mut(&mut self.selections); - - if do_fragment_inlining { - fn recurse_on_inline_fragment<'a>( - target: &mut SelectionMap, - type_pos: &CompositeTypeDefinitionPosition, - mut others: impl Iterator, - ) -> Result<(), FederationError> { - others.try_for_each(|selection| match selection { - Selection::InlineFragment(inline) if inline.is_unnecessary(type_pos) => { - recurse_on_inline_fragment( - target, - type_pos, - inline.selection_set.selections.values(), - ) + for (key, self_selection) in target.iter_mut() { + match self_selection { + SelectionValue::Field(mut self_field_selection) => { + if let Some(other_field_selections) = fields.shift_remove(key) { + self_field_selection.merge_into( + other_field_selections.iter().map(|selection| &***selection), + )?; + } + } + SelectionValue::FragmentSpread(mut self_fragment_spread_selection) => { + if let Some(other_fragment_spread_selections) = + fragment_spreads.shift_remove(key) + { + self_fragment_spread_selection.merge_into( + other_fragment_spread_selections + .iter() + .map(|selection| &***selection), + )?; } - selection => insert_selection(target, selection), - }) + } + SelectionValue::InlineFragment(mut self_inline_fragment_selection) => { + if let Some(other_inline_fragment_selections) = + inline_fragments.shift_remove(key) + { + self_inline_fragment_selection.merge_into( + other_inline_fragment_selections + .iter() + .map(|selection| &***selection), + )?; + } + } } - - recurse_on_inline_fragment(target, &self.type_position, others) - } else { - others.try_for_each(|selection| insert_selection(target, selection)) } + + Ok(()) } /// Inserts a `Selection` into the inner map. Should a selection with the same key already @@ -267,14 +305,13 @@ impl SelectionSet { pub(crate) fn add_local_selection( &mut self, selection: &Selection, - do_fragment_inlining: bool, ) -> Result<(), FederationError> { debug_assert_eq!( &self.schema, selection.schema(), "In order to add selection it needs to point to the same schema" ); - self.merge_selections_into(std::iter::once(selection), do_fragment_inlining) + self.merge_selections_into(std::iter::once(selection)) } /// Inserts a `SelectionSet` into the inner map. Should any sub selection with the same key already diff --git a/apollo-federation/src/operation/mod.rs b/apollo-federation/src/operation/mod.rs index fdbfff205b..a66644dd2b 100644 --- a/apollo-federation/src/operation/mod.rs +++ b/apollo-federation/src/operation/mod.rs @@ -20,7 +20,6 @@ use std::ops::Deref; use std::sync::atomic; use std::sync::Arc; -use apollo_compiler::collections::HashSet; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; use apollo_compiler::executable; @@ -673,8 +672,8 @@ mod selection_map { if *self.key() != value.key() { return Err(Internal { message: format!( - "Key mismatch when inserting selection `{value}` into vacant entry. Expected {:?}, found {:?}", - self.key(), value.key() + "Key mismatch when inserting selection {} into vacant entry ", + value ), } .into()); @@ -789,7 +788,6 @@ impl Selection { pub(crate) fn from_element( element: OpPathElement, sub_selections: Option, - unnecessary_directives: Option<&HashSet>>, ) -> Result { // PORT_NOTE: This is TODO item is copied from the JS `selectionOfElement` function. // TODO: validate that the subSelection is ok for the element @@ -801,21 +799,7 @@ impl Selection { "unexpected inline fragment without sub-selections", )); }; - if let Some(unnecessary_directives) = unnecessary_directives { - let directives = inline_fragment - .directives - .iter() - .filter(|dir| !unnecessary_directives.contains(dir.as_ref())) - .cloned() - .collect::(); - Ok(InlineFragmentSelection::new( - inline_fragment.with_updated_directives(directives), - sub_selections, - ) - .into()) - } else { - Ok(InlineFragmentSelection::new(inline_fragment, sub_selections).into()) - } + Ok(InlineFragmentSelection::new(inline_fragment, sub_selections).into()) } } } @@ -1686,6 +1670,7 @@ mod inline_fragment_selection { selection_set: self.selection_set.clone(), } } + pub(crate) fn with_updated_directives_and_selection_set( &self, directives: impl Into, @@ -1931,7 +1916,7 @@ impl SelectionSet { .flat_map(SelectionSet::split_top_level_fields) .filter_map(move |set| { let parent_type = ele.parent_type_position(); - Selection::from_element(ele.clone(), Some(set), None) + Selection::from_element(ele.clone(), Some(set)) .ok() .map(|sel| SelectionSet::from_selection(parent_type, sel)) }), @@ -2051,7 +2036,7 @@ impl SelectionSet { type_position, selections: Arc::new(SelectionMap::new()), }; - merged.merge_selections_into(normalized_selections.iter(), false)?; + merged.merge_selections_into(normalized_selections.iter())?; Ok(merged) } @@ -2148,7 +2133,7 @@ impl SelectionSet { type_position: self.type_position.clone(), selections: Arc::new(SelectionMap::new()), }; - expanded.merge_selections_into(expanded_selections.iter(), false)?; + expanded.merge_selections_into(expanded_selections.iter())?; Ok(expanded) } @@ -2522,7 +2507,7 @@ impl SelectionSet { sibling_typename.alias().cloned(), ); let typename_selection = - Selection::from_element(field_element.into(), /*subselection*/ None, None)?; + Selection::from_element(field_element.into(), /*subselection*/ None)?; Ok([typename_selection, updated].into_iter().collect()) }) } @@ -2626,16 +2611,6 @@ impl SelectionSet { &mut self, path: &[Arc], selection_set: Option<&Arc>, - ) -> Result<(), FederationError> { - let mut unnecessary_directives = HashSet::default(); - self.add_at_path_inner(path, selection_set, &mut unnecessary_directives) - } - - fn add_at_path_inner( - &mut self, - path: &[Arc], - selection_set: Option<&Arc>, - unnecessary_directives: &mut HashSet>, ) -> Result<(), FederationError> { // PORT_NOTE: This method was ported from the JS class `SelectionSetUpdates`. Unlike the // JS code, this mutates the selection set map in-place. @@ -2646,39 +2621,28 @@ impl SelectionSet { let Some(sub_selection_type) = element.sub_selection_type_position()? else { return Err(FederationError::internal("unexpected error: add_at_path encountered a field that is not of a composite type".to_string())); }; - let target = Arc::make_mut(&mut self.selections); - let mut selection = match target.get_mut(&ele.key()) { - Some(selection) => selection, - None => { - let selection = Selection::from_element( + let mut selection = Arc::make_mut(&mut self.selections) + .entry(ele.key()) + .or_insert(|| { + Selection::from_element( element, // We immediately add a selection afterward to make this selection set // valid. Some(SelectionSet::empty(self.schema.clone(), sub_selection_type)), - Some(&*unnecessary_directives), - )?; - target.entry(selection.key()).or_insert(|| Ok(selection))? - } - }; - unnecessary_directives.extend( - selection - .get_directives_mut() - .iter() - .filter(|d| d.name == "include" || d.name == "skip") - .cloned(), - ); + ) + })?; match &mut selection { SelectionValue::Field(field) => match field.get_selection_set_mut() { - Some(sub_selection) => sub_selection.add_at_path_inner(path, selection_set, unnecessary_directives), + Some(sub_selection) => sub_selection.add_at_path(path, selection_set)?, None => return Err(FederationError::internal("add_at_path encountered a field without a subselection which should never happen".to_string())), }, SelectionValue::InlineFragment(fragment) => fragment .get_selection_set_mut() - .add_at_path_inner(path, selection_set, unnecessary_directives), + .add_at_path(path, selection_set)?, SelectionValue::FragmentSpread(_fragment) => { - return Err(FederationError::internal("add_at_path encountered a named fragment spread which should never happen".to_string())) + return Err(FederationError::internal("add_at_path encountered a named fragment spread which should never happen".to_string())); } - }?; + }; } // If we have no sub-path, we can add the selection. Some((ele, &[])) => { @@ -2697,9 +2661,8 @@ impl SelectionSet { return Ok(()); } else { // add leaf - let selection = - Selection::from_element(element, None, Some(&*unnecessary_directives))?; - self.add_local_selection(&selection, true)? + let selection = Selection::from_element(element, None)?; + self.add_local_selection(&selection)? } } else { let selection_set = selection_set @@ -2714,12 +2677,8 @@ impl SelectionSet { }) .transpose()? .map(|selection_set| selection_set.without_unnecessary_fragments()); - let selection = Selection::from_element( - element, - selection_set, - Some(&*unnecessary_directives), - )?; - self.add_local_selection(&selection, true)?; + let selection = Selection::from_element(element, selection_set)?; + self.add_local_selection(&selection)? } } // If we don't have any path, we rebase and merge in the given sub selections at the root. @@ -2931,34 +2890,45 @@ impl SelectionSet { } } + /// Using path-based updates along with selection sets may result in some inefficiencies. + /// Specifically, we may end up with some unnecessary top-level inline fragment selections, i.e. + /// fragments without any directives and with the type condition equal to (or a supertype of) + /// the parent type of the fragment. This method inlines those unnecessary top-level fragments. + /// /// JS PORT NOTE: In Rust implementation we are doing the selection set updates in-place whereas /// JS code was pooling the updates and only apply those when building the final selection set. /// See `makeSelectionSet` method for details. - /// - /// Manipulating selection sets may result in some inefficiencies. As a result we may end up with - /// some unnecessary top level inline fragment selections, i.e. fragments without any directives - /// and with the type condition same as the parent type that should be inlined. - /// - /// This method inlines those unnecessary top level fragments only. While the JS code was applying - /// this logic recursively, since we are manipulating selections sets in-place we only need to - /// apply this normalization at the top level. fn without_unnecessary_fragments(&self) -> SelectionSet { let parent_type = &self.type_position; let mut final_selections = SelectionMap::new(); - for selection in self.selections.values() { - match selection { - Selection::InlineFragment(inline_fragment) => { - if inline_fragment.is_unnecessary(parent_type) { - final_selections.extend_ref(&inline_fragment.selection_set.selections); - } else { + fn process_selection_set( + selection_set: &SelectionSet, + final_selections: &mut SelectionMap, + parent_type: &CompositeTypeDefinitionPosition, + schema: &ValidFederationSchema, + ) { + for selection in selection_set.selections.values() { + match selection { + Selection::InlineFragment(inline_fragment) => { + if inline_fragment.is_unnecessary(parent_type, schema) { + process_selection_set( + &inline_fragment.selection_set, + final_selections, + parent_type, + schema, + ); + } else { + final_selections.insert(selection.clone()); + } + } + _ => { final_selections.insert(selection.clone()); } } - _ => { - final_selections.insert(selection.clone()); - } } } + process_selection_set(self, &mut final_selections, parent_type, &self.schema); + SelectionSet { schema: self.schema.clone(), type_position: parent_type.clone(), @@ -3468,12 +3438,22 @@ impl InlineFragmentSelection { /// /// Fragment is unnecessary if following are true: /// * it has no applied directives - /// * has no type condition OR type condition is same as passed in `maybe_parent` - fn is_unnecessary(&self, maybe_parent: &CompositeTypeDefinitionPosition) -> bool { - let inline_fragment_type_condition = self.inline_fragment.type_condition_position.clone(); - self.inline_fragment.directives.is_empty() - && (inline_fragment_type_condition.is_none() - || inline_fragment_type_condition.is_some_and(|t| t == *maybe_parent)) + /// * has no type condition OR type condition is equal to (or a supertype of) `parent` + fn is_unnecessary( + &self, + parent: &CompositeTypeDefinitionPosition, + schema: &ValidFederationSchema, + ) -> bool { + if !self.inline_fragment.directives.is_empty() { + return false; + } + let Some(type_condition) = &self.inline_fragment.type_condition_position else { + return true; + }; + type_condition == parent + || schema + .schema() + .is_subtype(type_condition.type_name(), parent.type_name()) } pub(crate) fn any_element( diff --git a/apollo-federation/src/operation/optimize.rs b/apollo-federation/src/operation/optimize.rs index ce2802bf4e..79fe71c8ef 100644 --- a/apollo-federation/src/operation/optimize.rs +++ b/apollo-federation/src/operation/optimize.rs @@ -1007,7 +1007,7 @@ impl SelectionSet { &fragment, /*directives*/ &Default::default(), ); - optimized.add_local_selection(&fragment_selection.into(), false)?; + optimized.add_local_selection(&fragment_selection.into())?; } optimized.add_local_selection_set(¬_covered_so_far)?; @@ -1697,21 +1697,17 @@ impl FragmentGenerator { self.visit_selection_set(selection_set)?; } new_selection_set - .add_local_selection(&Selection::Field(Arc::clone(field.get())), false)?; + .add_local_selection(&Selection::Field(Arc::clone(field.get())))?; } SelectionValue::FragmentSpread(frag) => { - new_selection_set.add_local_selection( - &Selection::FragmentSpread(Arc::clone(frag.get())), - false, - )?; + new_selection_set + .add_local_selection(&Selection::FragmentSpread(Arc::clone(frag.get())))?; } SelectionValue::InlineFragment(frag) if !Self::is_worth_using(&frag.get().selection_set) => { - new_selection_set.add_local_selection( - &Selection::InlineFragment(Arc::clone(frag.get())), - false, - )?; + new_selection_set + .add_local_selection(&Selection::InlineFragment(Arc::clone(frag.get())))?; } SelectionValue::InlineFragment(mut candidate) => { self.visit_selection_set(candidate.get_selection_set_mut())?; @@ -1729,10 +1725,9 @@ impl FragmentGenerator { // we can't just transfer them to the generated fragment spread, // so we have to keep this inline fragment. let Ok(skip_include) = skip_include else { - new_selection_set.add_local_selection( - &Selection::InlineFragment(Arc::clone(candidate.get())), - false, - )?; + new_selection_set.add_local_selection(&Selection::InlineFragment( + Arc::clone(candidate.get()), + ))?; continue; }; @@ -1741,10 +1736,9 @@ impl FragmentGenerator { // there's any directives on it. This code duplicates the body from the // previous condition so it's very easy to remove when we're ready :) if !skip_include.is_empty() { - new_selection_set.add_local_selection( - &Selection::InlineFragment(Arc::clone(candidate.get())), - false, - )?; + new_selection_set.add_local_selection(&Selection::InlineFragment( + Arc::clone(candidate.get()), + ))?; continue; } @@ -1769,8 +1763,8 @@ impl FragmentGenerator { }); self.fragments.get(&name).unwrap() }; - new_selection_set.add_local_selection( - &Selection::from(FragmentSpreadSelection { + new_selection_set.add_local_selection(&Selection::from( + FragmentSpreadSelection { spread: FragmentSpread::new(FragmentSpreadData { schema: selection_set.schema.clone(), fragment_name: existing.name.clone(), @@ -1780,9 +1774,8 @@ impl FragmentGenerator { selection_id: crate::operation::SelectionId::new(), }), selection_set: existing.selection_set.clone(), - }), - false, - )?; + }, + ))?; } } } @@ -2288,12 +2281,12 @@ mod tests { type Query { t: T } - + type T { a: A b: Int } - + type A { x: String y: String @@ -2319,14 +2312,14 @@ mod tests { x y } - + fragment FT on T { a { __typename ...FA } } - + query { t { ...FT @@ -2353,19 +2346,19 @@ mod tests { type Query { t: T } - + type T { a: String b: B c: Int d: D } - + type B { x: String y: String } - + type D { m: String n: String @@ -2383,7 +2376,7 @@ mod tests { m } } - + { t { ...FragT @@ -2422,23 +2415,23 @@ mod tests { type Query { i: I } - + interface I { a: String } - + type T implements I { a: String b: B c: Int d: D } - + type B { x: String y: String } - + type D { m: String n: String @@ -2456,7 +2449,7 @@ mod tests { m } } - + { i { ... on T { @@ -2496,19 +2489,19 @@ mod tests { type Query { t: T } - + type T { a: String b: B c: Int d: D } - + type B { x: String y: String } - + type D { m: String n: String @@ -2534,7 +2527,7 @@ mod tests { m } } - + fragment Frag2 on T { a b { @@ -2546,7 +2539,7 @@ mod tests { n } } - + { t { ...Frag1 @@ -2580,11 +2573,11 @@ mod tests { type Query { t: T } - + interface I { x: String } - + type T implements I { x: String a: String @@ -2598,7 +2591,7 @@ mod tests { a } } - + { t { ...FragI @@ -2622,12 +2615,12 @@ mod tests { type Query { t: T } - + type T { a: String u: U } - + type U { x: String y: String @@ -2638,7 +2631,7 @@ mod tests { fragment Frag1 on T { a } - + fragment Frag2 on T { u { x @@ -2646,13 +2639,13 @@ mod tests { } ...Frag1 } - + fragment Frag3 on Query { t { ...Frag2 } } - + { ...Frag3 } @@ -2677,16 +2670,16 @@ mod tests { type Query { t1: T1 } - + interface I { x: Int } - + type T1 implements I { x: Int y: Int } - + type T2 implements I { x: Int z: Int @@ -2702,7 +2695,7 @@ mod tests { z } } - + { t1 { ...FragOnI @@ -2725,24 +2718,24 @@ mod tests { type Query { i2: I2 } - + interface I1 { x: Int } - + interface I2 { y: Int } - + interface I3 { z: Int } - + type T1 implements I1 & I2 { x: Int y: Int } - + type T2 implements I1 & I3 { x: Int z: Int @@ -2758,7 +2751,7 @@ mod tests { z } } - + { i2 { ...FragOnI1 @@ -2788,13 +2781,13 @@ mod tests { type Query { t1: T1 } - + union U = T1 | T2 - + type T1 { x: Int } - + type T2 { y: Int } @@ -2809,7 +2802,7 @@ mod tests { y } } - + { t1 { ...OnU @@ -2919,18 +2912,18 @@ mod tests { type Query { t1: T1 } - + union U1 = T1 | T2 | T3 union U2 = T2 | T3 - + type T1 { x: Int } - + type T2 { y: Int } - + type T3 { z: Int } @@ -2942,7 +2935,7 @@ mod tests { ...Outer } } - + fragment Outer on U1 { ... on T1 { x @@ -2954,7 +2947,7 @@ mod tests { ... Inner } } - + fragment Inner on U2 { ... on T2 { y @@ -3013,23 +3006,23 @@ mod tests { type Query { t1: T1 } - + union U1 = T1 | T2 | T3 union U2 = T2 | T3 - + type T1 { x: Int } - + type T2 { y1: Y y2: Y } - + type T3 { z: Int } - + type Y { v: Int } @@ -3041,7 +3034,7 @@ mod tests { ...Outer } } - + fragment Outer on U1 { ... on T1 { x @@ -3053,7 +3046,7 @@ mod tests { ... Inner } } - + fragment Inner on U2 { ... on T2 { y1 { @@ -3064,7 +3057,7 @@ mod tests { } } } - + fragment WillBeUnused on Y { v } @@ -3099,14 +3092,14 @@ mod tests { t1: T t2: T } - + type T { a1: Int a2: Int b1: B b2: B } - + type B { x: Int y: Int @@ -3122,7 +3115,7 @@ mod tests { ...TFields } } - + fragment TFields on T { ...DirectFieldsOfT b1 { @@ -3132,12 +3125,12 @@ mod tests { ...BFields } } - + fragment DirectFieldsOfT on T { a1 a2 } - + fragment BFields on B { x y @@ -3220,7 +3213,7 @@ mod tests { t2: T t3: T } - + type T { a: Int b: Int @@ -3233,7 +3226,7 @@ mod tests { fragment DirectiveInDef on T { a @include(if: $cond1) } - + query myQuery($cond1: Boolean!, $cond2: Boolean!) { t1 { a @@ -3270,7 +3263,7 @@ mod tests { t2: T t3: T } - + type T { a: Int b: Int @@ -3283,7 +3276,7 @@ mod tests { fragment NoDirectiveDef on T { a } - + query myQuery($cond1: Boolean!) { t1 { ...NoDirectiveDef diff --git a/apollo-federation/src/operation/simplify.rs b/apollo-federation/src/operation/simplify.rs index 802bbfcab1..8555b241a2 100644 --- a/apollo-federation/src/operation/simplify.rs +++ b/apollo-federation/src/operation/simplify.rs @@ -460,7 +460,7 @@ impl SelectionSet { { match selection_or_set { SelectionOrSet::Selection(normalized_selection) => { - normalized_selections.add_local_selection(&normalized_selection, false)?; + normalized_selections.add_local_selection(&normalized_selection)?; } SelectionOrSet::SelectionSet(normalized_set) => { // Since the `selection` has been expanded/lifted, we use diff --git a/apollo-federation/src/operation/tests/mod.rs b/apollo-federation/src/operation/tests/mod.rs index 276b0a579f..89c9817587 100644 --- a/apollo-federation/src/operation/tests/mod.rs +++ b/apollo-federation/src/operation/tests/mod.rs @@ -1204,7 +1204,7 @@ mod make_selection_tests { base_selection_set.type_position.clone(), selection.clone(), ); - Selection::from_element(base.element().unwrap(), Some(subselections), None).unwrap() + Selection::from_element(base.element().unwrap(), Some(subselections)).unwrap() }; let foo_with_a = clone_selection_at_path(foo, &[name!("a")]); @@ -1331,7 +1331,7 @@ mod lazy_map_tests { let field_element = Field::new_introspection_typename(s.schema(), &parent_type_pos, None); let typename_selection = - Selection::from_element(field_element.into(), /*subselection*/ None, None)?; + Selection::from_element(field_element.into(), /*subselection*/ None)?; // return `updated` and `typename_selection` Ok([updated, typename_selection].into_iter().collect()) }) diff --git a/apollo-federation/src/query_graph/graph_path.rs b/apollo-federation/src/query_graph/graph_path.rs index 84ae1bac7d..7a3eb2b31f 100644 --- a/apollo-federation/src/query_graph/graph_path.rs +++ b/apollo-federation/src/query_graph/graph_path.rs @@ -501,16 +501,18 @@ impl OpGraphPathContext { &self, operation_element: &OpPathElement, ) -> Result { - let mut new_context = self.clone(); if operation_element.directives().is_empty() { - return Ok(new_context); + return Ok(self.clone()); } - let new_conditionals = operation_element.extract_operation_conditionals()?; - if !new_conditionals.is_empty() { - Arc::make_mut(&mut new_context.conditionals).extend(new_conditionals); + let mut new_conditionals = operation_element.extract_operation_conditionals()?; + if new_conditionals.is_empty() { + return Ok(self.clone()); } - Ok(new_context) + new_conditionals.extend(self.iter().cloned()); + Ok(OpGraphPathContext { + conditionals: Arc::new(new_conditionals), + }) } pub(crate) fn is_empty(&self) -> bool { @@ -3867,15 +3869,12 @@ fn is_useless_followup_element( }; let are_useless_directives = fragment.directives.is_empty() - || fragment - .directives - .iter() - .any(|d| !conditionals.contains(d)); + || fragment.directives.iter().all(|d| conditionals.contains(d)); let is_same_type = type_of_first.type_name() == type_of_second.type_name(); let is_subtype = first .schema() .schema() - .is_subtype(type_of_first.type_name(), type_of_second.type_name()); + .is_subtype(type_of_second.type_name(), type_of_first.type_name()); Ok(are_useless_directives && (is_same_type || is_subtype)) } }; diff --git a/apollo-federation/src/query_plan/conditions.rs b/apollo-federation/src/query_plan/conditions.rs index d1fd31a319..07388b7366 100644 --- a/apollo-federation/src/query_plan/conditions.rs +++ b/apollo-federation/src/query_plan/conditions.rs @@ -223,12 +223,12 @@ pub(crate) fn remove_conditions_from_selection_set( selection.with_updated_selection_set(Some(updated_selection_set))? } } else { - Selection::from_element(updated_element, Some(updated_selection_set), None)? + Selection::from_element(updated_element, Some(updated_selection_set))? } } else if updated_element == element { selection.clone() } else { - Selection::from_element(updated_element, None, None)? + Selection::from_element(updated_element, None)? }; selection_map.insert(new_selection); } @@ -247,7 +247,7 @@ pub(crate) fn remove_conditions_from_selection_set( /// "starting" fragments having the unneeded condition/directives removed. pub(crate) fn remove_unneeded_top_level_fragment_directives( selection_set: &SelectionSet, - unneded_directives: &DirectiveList, + unneeded_directives: &DirectiveList, ) -> Result { let mut selection_map = SelectionMap::new(); @@ -265,7 +265,7 @@ pub(crate) fn remove_unneeded_top_level_fragment_directives( let needed_directives: Vec> = fragment .directives .iter() - .filter(|directive| !unneded_directives.contains(directive)) + .filter(|directive| !unneeded_directives.contains(directive)) .cloned() .collect(); @@ -273,7 +273,7 @@ pub(crate) fn remove_unneeded_top_level_fragment_directives( // at the "top-level" of the set. let updated_selections = remove_unneeded_top_level_fragment_directives( &inline_fragment.selection_set, - unneded_directives, + unneeded_directives, )?; if needed_directives.len() == fragment.directives.len() { // We need all the directives that the fragment has. Return it unchanged. diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index ad2134aaff..e5aeba3b69 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -2938,7 +2938,6 @@ fn operation_for_entities_fetch( sibling_typename: None, })), Some(selection_set), - None, )?; let type_position: CompositeTypeDefinitionPosition = subgraph_schema @@ -3050,8 +3049,7 @@ impl FetchSelectionSet { path_in_node: &OpPath, selection_set: Option<&Arc>, ) -> Result<(), FederationError> { - let target = Arc::make_mut(&mut self.selection_set); - target.add_at_path(path_in_node, selection_set)?; + Arc::make_mut(&mut self.selection_set).add_at_path(path_in_node, selection_set)?; // TODO: when calling this multiple times, maybe only re-compute conditions at the end? // Or make it lazily-initialized and computed on demand? self.conditions = self.selection_set.conditions()?; diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests.rs b/apollo-federation/tests/query_plan/build_query_plan_tests.rs index acef092c2c..cf6b3009e7 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests.rs @@ -1259,8 +1259,8 @@ fn handles_multiple_conditions_on_abstract_types() { } } => { - ... on Book @skip(if: $title) { - ... on Book @include(if: $title) { + ... on Book @include(if: $title) { + ... on Book @skip(if: $title) { sku } } diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/requires/include_skip.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/requires/include_skip.rs index ebb98c6653..b5dad429d8 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/requires/include_skip.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/requires/include_skip.rs @@ -5,7 +5,7 @@ fn it_handles_a_simple_at_requires_triggered_within_a_conditional() { type Query { t: T } - + type T @key(fields: "id") { id: ID! a: Int @@ -71,7 +71,7 @@ fn it_handles_an_at_requires_triggered_conditionally() { type Query { t: T } - + type T @key(fields: "id") { id: ID! a: Int @@ -133,15 +133,13 @@ fn it_handles_an_at_requires_triggered_conditionally() { } #[test] -#[should_panic(expected = "snapshot assertion")] -// TODO: investigate this failure (redundant inline spread) fn it_handles_an_at_requires_where_multiple_conditional_are_involved() { let planner = planner!( Subgraph1: r#" type Query { a: A } - + type A @key(fields: "idA") { idA: ID! } @@ -151,7 +149,7 @@ fn it_handles_an_at_requires_where_multiple_conditional_are_involved() { idA: ID! b: [B] } - + type B @key(fields: "idB") { idB: ID! required: Int @@ -230,9 +228,9 @@ fn it_handles_an_at_requires_where_multiple_conditional_are_involved() { } }, }, - } + }, }, - } + }, }, } "### @@ -246,12 +244,10 @@ fn unnecessary_include_is_stripped_from_fragments() { type Query { foo: Foo, } - type Foo @key(fields: "id") { id: ID, bar: Bar, } - type Bar @key(fields: "id") { id: ID, } @@ -336,13 +332,11 @@ fn selections_are_not_overwritten_after_removing_directives() { type Query { foo: Foo, } - type Foo @key(fields: "id") { id: ID, foo: Foo, bar: Bar, } - type Bar @key(fields: "id") { id: ID, } diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql index c7102b37a9..7cd49ac90f 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql @@ -1,4 +1,4 @@ -# Composed from subgraphs with hash: a95f4fdeb4fa54d3e87c7a5eb71952eba3efdf28 +# Composed from subgraphs with hash: 88de1465b4ef08a76a910cff26136f931b69eca4 schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql index c7102b37a9..7cd49ac90f 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql @@ -1,4 +1,4 @@ -# Composed from subgraphs with hash: a95f4fdeb4fa54d3e87c7a5eb71952eba3efdf28 +# Composed from subgraphs with hash: 88de1465b4ef08a76a910cff26136f931b69eca4 schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql index 1b13a0feb0..9442a08abf 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql @@ -1,4 +1,4 @@ -# Composed from subgraphs with hash: b43c835356ccf4afa94c6ad48206c70f22f39ffc +# Composed from subgraphs with hash: 213ee593775b6e4a22a852a35da688bc2e85d710 schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) diff --git a/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql b/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql index 6c750cf146..6d2137b561 100644 --- a/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql @@ -1,4 +1,4 @@ -# Composed from subgraphs with hash: a3e748206fa553b8f317912a3a411ee68b55db5d +# Composed from subgraphs with hash: 56ab651bf30bcb3ac7a9fb826fa6b03a57288859 schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) diff --git a/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql b/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql index a3668cb67b..33f5dd6f05 100644 --- a/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql @@ -1,4 +1,4 @@ -# Composed from subgraphs with hash: 133c359820bd42cb8afd64ab1e02bb912b5d1746 +# Composed from subgraphs with hash: e6c72fb53e93abe8f8aed4982aca6f6109fe1171 schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) From d96e0e2b7e98dfa470d6ce4d614c5d7c88551424 Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Wed, 25 Sep 2024 02:32:23 -0700 Subject: [PATCH 07/23] Remove pruning when computing best plan (#6053) This PR removes option pruning in `compute_best_plan_from_closed_branches()`, as it has been removed from JS QP. (The reasoning was that it was observed pruning could save time in some cases, but could be substantially worse in a few others.) Note that we should also be able to get rid of overriding IDs, although that requires changing more code (and the JS QP hasn't done it yet either), so I'm leaving it to the future/later PRs. --- .../query_plan/query_planning_traversal.rs | 45 ------------------- 1 file changed, 45 deletions(-) diff --git a/apollo-federation/src/query_plan/query_planning_traversal.rs b/apollo-federation/src/query_plan/query_planning_traversal.rs index cb5e6bb9f1..6142cd908f 100644 --- a/apollo-federation/src/query_plan/query_planning_traversal.rs +++ b/apollo-federation/src/query_plan/query_planning_traversal.rs @@ -615,7 +615,6 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { if self.closed_branches.is_empty() { return Ok(()); } - self.prune_closed_branches(); self.sort_options_in_closed_branches()?; self.reduce_options_if_needed(); @@ -732,50 +731,6 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { Ok(()) } - /// Remove closed branches that are known to be overridden by others. - /// - /// We've computed all branches and need to compare all the possible plans to pick the best. - /// Note however that "all the possible plans" is essentially a cartesian product of all - /// the closed branches options, and if a lot of branches have multiple options, this can - /// exponentially explode. - /// So first, we check if we can preemptively prune some branches based on - /// those branches having options that are known to be overriden by other ones. - fn prune_closed_branches(&mut self) { - for branch in &mut self.closed_branches { - if branch.0.len() <= 1 { - continue; - } - - let mut pruned = ClosedBranch(Vec::new()); - for (i, to_check) in branch.0.iter().enumerate() { - if !Self::option_is_overriden(i, &to_check.paths, branch) { - pruned.0.push(to_check.clone()); - } - } - - *branch = pruned - } - } - - fn option_is_overriden( - index: usize, - to_check: &SimultaneousPaths, - all_options: &ClosedBranch, - ) -> bool { - all_options - .0 - .iter() - .enumerate() - // Don’t compare `to_check` with itself - .filter(|&(i, _)| i != index) - .any(|(_i, option)| { - to_check - .0 - .iter() - .all(|p| option.paths.0.iter().any(|o| p.is_overridden_by(o))) - }) - } - /// We now sort the options within each branch, /// putting those with the least amount of subgraph jumps first. /// The idea is that for each branch taken individually, From cef981547f360b4a2e6c7c3c55790b01912c02d5 Mon Sep 17 00:00:00 2001 From: Duckki Oe Date: Wed, 25 Sep 2024 02:55:00 -0700 Subject: [PATCH 08/23] fix(dual-query-planner): improved semantic diff to handle default value differences (#6052) Summary: - case 1: JS QP may convert an enum value into string. Compare JS string value and Rust enum value by the content. - case 2: Rust QP expands an empty object value by filling in its default field values. Consider any JS empty object values are equal to any Rust object values (assuming Rust field values are all default values). Co-authored-by: Iryna Shestak --- .../src/query_planner/dual_query_planner.rs | 85 +++++++++++++++++-- 1 file changed, 79 insertions(+), 6 deletions(-) diff --git a/apollo-router/src/query_planner/dual_query_planner.rs b/apollo-router/src/query_planner/dual_query_planner.rs index 7e84ade311..91d273fb26 100644 --- a/apollo-router/src/query_planner/dual_query_planner.rs +++ b/apollo-router/src/query_planner/dual_query_planner.rs @@ -386,7 +386,7 @@ fn vec_matches_result( item_matches(this, other) .map_err(|err| err.add_description(&format!("under item[{}]", index))) })?; - assert!(vec_matches(this, other, |a, b| item_matches(a, b).is_ok())); + assert!(vec_matches(this, other, |a, b| item_matches(a, b).is_ok())); // Note: looks redundant Ok(()) } @@ -402,12 +402,16 @@ fn vec_matches_sorted_by( this: &[T], other: &[T], compare: impl Fn(&T, &T) -> std::cmp::Ordering, -) -> bool { + item_matches: impl Fn(&T, &T) -> Result<(), MatchFailure>, +) -> Result<(), MatchFailure> { + check_match_eq!(this.len(), other.len()); let mut this_sorted = this.to_owned(); let mut other_sorted = other.to_owned(); this_sorted.sort_by(&compare); other_sorted.sort_by(&compare); - vec_matches(&this_sorted, &other_sorted, T::eq) + std::iter::zip(&this_sorted, &other_sorted) + .try_fold((), |_acc, (this, other)| item_matches(this, other))?; + Ok(()) } // performs a set comparison, ignoring order @@ -701,9 +705,13 @@ fn same_ast_operation_definition( ) -> Result<(), MatchFailure> { // Note: Operation names are ignored, since parallel fetches may have different names. check_match_eq!(x.operation_type, y.operation_type); - check_match!(vec_matches_sorted_by(&x.variables, &y.variables, |x, y| x - .name - .cmp(&y.name))); + vec_matches_sorted_by( + &x.variables, + &y.variables, + |a, b| a.name.cmp(&b.name), + |a, b| same_variable_definition(a, b), + ) + .map_err(|err| err.add_description("under Variable definition"))?; check_match_eq!(x.directives, y.directives); check_match!(same_ast_selection_set_sorted( &x.selection_set, @@ -712,6 +720,49 @@ fn same_ast_operation_definition( Ok(()) } +// Use this function, instead of `VariableDefinition`'s `PartialEq` implementation, +// due to known differences. +fn same_variable_definition( + x: &ast::VariableDefinition, + y: &ast::VariableDefinition, +) -> Result<(), MatchFailure> { + check_match_eq!(x.name, y.name); + check_match_eq!(x.ty, y.ty); + if x.default_value != y.default_value { + if let (Some(x), Some(y)) = (&x.default_value, &y.default_value) { + match (x.as_ref(), y.as_ref()) { + // Special case 1: JS QP may convert an enum value into string. + // - In this case, compare them as strings. + (ast::Value::String(ref x), ast::Value::Enum(ref y)) => { + if x == y.as_str() { + return Ok(()); + } + } + + // Special case 2: Rust QP expands an empty object value by filling in its + // default field values. + // - If the JS QP value is an empty object, consider any object is a match. + // - Assuming the Rust QP object value has only default field values. + // - Warning: This is an unsound heuristic. + (ast::Value::Object(ref x), ast::Value::Object(_)) => { + if x.is_empty() { + return Ok(()); + } + } + + _ => {} // otherwise, fall through + } + } + + return Err(MatchFailure::new(format!( + "mismatch between default values:\nleft: {:?}\nright: {:?}", + x.default_value, y.default_value + ))); + } + check_match_eq!(x.directives, y.directives); + Ok(()) +} + fn same_ast_fragment_definition( x: &ast::FragmentDefinition, y: &ast::FragmentDefinition, @@ -806,6 +857,28 @@ mod ast_comparison_tests { assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); } + #[test] + fn test_query_variable_decl_enum_value_coercion() { + // Note: JS QP converts enum default values into strings. + let op_x = r#"query($qv1: E! = "default_value") { x(arg1: $qv1) }"#; + let op_y = r#"query($qv1: E! = default_value) { x(arg1: $qv1) }"#; + let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); + let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); + assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); + } + + #[test] + fn test_query_variable_decl_object_value_coercion() { + // Note: Rust QP expands empty object default values by filling in its default field + // values. + let op_x = r#"query($qv1: T! = {}) { x(arg1: $qv1) }"#; + let op_y = + r#"query($qv1: T! = { field1: true, field2: "default_value" }) { x(arg1: $qv1) }"#; + let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); + let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); + assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); + } + #[test] fn test_entities_selection_order() { let op_x = r#" From 20fcc055bdc57b4f00529419aecf836bb16181df Mon Sep 17 00:00:00 2001 From: Tyler Bloom Date: Wed, 25 Sep 2024 06:19:15 -0400 Subject: [PATCH 09/23] Finalize defer support in Rust QP (#6038) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: RenΓ©e Kooi --- apollo-federation/src/operation/mod.rs | 597 ++- .../src/operation/tests/defer.rs | 194 + apollo-federation/src/operation/tests/mod.rs | 2 + .../src/query_graph/graph_path.rs | 26 +- apollo-federation/src/query_plan/display.rs | 28 +- .../src/query_plan/fetch_dependency_graph.rs | 186 +- .../fetch_dependency_graph_processor.rs | 30 +- .../src/query_plan/query_planner.rs | 111 +- .../query_plan/query_planning_traversal.rs | 20 +- .../query_plan/build_query_plan_tests.rs | 1 + .../build_query_plan_tests/defer.rs | 3826 +++++++++++++++++ .../build_query_plan_tests/subscriptions.rs | 41 +- ...t_can_request_typename_in_fragment.graphql | 62 + ...est_defer_everything_within_entity.graphql | 62 + ...iple_fields_in_different_subgraphs.graphql | 67 + ...fer_on_enity_but_with_unuseful_key.graphql | 62 + ...r_test_defer_on_everything_queried.graphql | 62 + ..._multi_dependency_deferred_section.graphql | 74 + ...defer_on_mutation_in_same_subgraph.graphql | 71 + ...on_mutation_on_different_subgraphs.graphql | 72 + ...efer_test_defer_on_query_root_type.graphql | 64 + .../defer_test_defer_on_value_types.graphql | 76 + ...st_defer_only_the_key_of_an_entity.graphql | 58 + ...efer_resuming_in_the_same_subgraph.graphql | 59 + ..._with_condition_on_single_subgraph.graphql | 59 + ...t_defer_with_conditions_and_labels.graphql | 62 + ...ith_mutliple_conditions_and_labels.graphql | 74 + ...efer_test_direct_nesting_on_entity.graphql | 63 + ..._test_direct_nesting_on_value_type.graphql | 60 + ...ot_merge_query_branches_with_defer.graphql | 63 + ...nto_same_field_regardless_of_defer.graphql | 63 + ...es_simple_defer_with_defer_enabled.graphql | 62 + ...simple_defer_without_defer_enabled.graphql | 62 + ...rent_definitions_between_subgraphs.graphql | 74 + ...n_nested_defer_plus_label_handling.graphql | 75 + .../defer_test_named_fragments_simple.graphql | 62 + ...efer_test_nested_defer_on_entities.graphql | 70 + ...st_non_router_based_defer_case_one.graphql | 68 + ..._non_router_based_defer_case_three.graphql | 76 + ...st_non_router_based_defer_case_two.graphql | 62 + .../defer_test_normalizes_if_false.graphql | 62 + .../defer_test_normalizes_if_true.graphql | 62 + ...es_are_ignored_for_deferred_fields.graphql | 62 + ...ts_of_deferred_fields_are_deferred.graphql | 66 + ...defer_includes_traversed_fragments.graphql | 73 + 45 files changed, 6932 insertions(+), 299 deletions(-) create mode 100644 apollo-federation/src/operation/tests/defer.rs create mode 100644 apollo-federation/tests/query_plan/build_query_plan_tests/defer.rs create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_can_request_typename_in_fragment.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_defer_everything_within_entity.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_defer_multiple_fields_in_different_subgraphs.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_enity_but_with_unuseful_key.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_everything_queried.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_multi_dependency_deferred_section.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_in_same_subgraph.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_on_different_subgraphs.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_query_root_type.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_value_types.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_defer_only_the_key_of_an_entity.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_defer_resuming_in_the_same_subgraph.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_condition_on_single_subgraph.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_conditions_and_labels.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_mutliple_conditions_and_labels.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_entity.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_value_type.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_do_not_merge_query_branches_with_defer.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_fragments_expand_into_same_field_regardless_of_defer.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_with_defer_enabled.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_without_defer_enabled.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_interface_has_different_definitions_between_subgraphs.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_multiple_non_nested_defer_plus_label_handling.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_named_fragments_simple.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_nested_defer_on_entities.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_one.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_three.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_two.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_false.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_true.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_provides_are_ignored_for_deferred_fields.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_requirements_of_deferred_fields_are_deferred.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/defer_test_the_path_in_defer_includes_traversed_fragments.graphql diff --git a/apollo-federation/src/operation/mod.rs b/apollo-federation/src/operation/mod.rs index a66644dd2b..bfab878313 100644 --- a/apollo-federation/src/operation/mod.rs +++ b/apollo-federation/src/operation/mod.rs @@ -20,10 +20,12 @@ use std::ops::Deref; use std::sync::atomic; use std::sync::Arc; +use apollo_compiler::collections::HashSet; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; use apollo_compiler::executable; use apollo_compiler::name; +use apollo_compiler::schema::Directive; use apollo_compiler::validation::Valid; use apollo_compiler::Name; use apollo_compiler::Node; @@ -34,6 +36,8 @@ use crate::compat::coerce_executable_values; use crate::error::FederationError; use crate::error::SingleFederationError; use crate::error::SingleFederationError::Internal; +use crate::link::graphql_definition::BooleanOrVariable; +use crate::link::graphql_definition::DeferDirectiveArguments; use crate::query_graph::graph_path::OpPathElement; use crate::query_plan::conditions::Conditions; use crate::query_plan::FetchDataKeyRenamer; @@ -196,13 +200,6 @@ pub struct Operation { pub(crate) named_fragments: NamedFragments, } -pub(crate) struct NormalizedDefer { - pub operation: Operation, - pub has_defers: bool, - pub assigned_defer_labels: IndexSet, - pub defer_conditions: IndexMap>, -} - impl Operation { /// Parse an operation from a source string. #[cfg(any(test, doc))] @@ -241,49 +238,6 @@ impl Operation { named_fragments, }) } - - // PORT_NOTE(@goto-bus-stop): It might make sense for the returned data structure to *be* the - // `DeferNormalizer` from the JS side - pub(crate) fn with_normalized_defer(self) -> NormalizedDefer { - NormalizedDefer { - operation: self, - has_defers: false, - assigned_defer_labels: IndexSet::default(), - defer_conditions: IndexMap::default(), - } - // TODO(@TylerBloom): Once defer is implement, the above statement needs to be replaced - // with the commented-out one below. This is part of FED-95 - /* - if self.has_defer() { - todo!("@defer not implemented"); - } else { - NormalizedDefer { - operation: self, - has_defers: false, - assigned_defer_labels: IndexSet::default(), - defer_conditions: IndexMap::default(), - } - } - */ - } - - fn has_defer(&self) -> bool { - self.selection_set.has_defer() - || self - .named_fragments - .fragments - .values() - .any(|f| f.has_defer()) - } - - /// Removes the @defer directive from all selections without removing that selection. - pub(crate) fn without_defer(mut self) -> Self { - if self.has_defer() { - self.selection_set.without_defer(); - } - debug_assert!(!self.has_defer()); - self - } } /// An analogue of the apollo-compiler type `SelectionSet` with these changes: @@ -532,11 +486,11 @@ mod selection_map { } } - pub(super) fn get_directives_mut(&mut self) -> &mut DirectiveList { + pub(super) fn directives(&self) -> &'_ DirectiveList { match self { - Self::Field(field) => field.get_directives_mut(), - Self::FragmentSpread(spread) => spread.get_directives_mut(), - Self::InlineFragment(inline) => inline.get_directives_mut(), + Self::Field(field) => &field.get().field.directives, + Self::FragmentSpread(frag) => &frag.get().spread.directives, + Self::InlineFragment(frag) => &frag.get().inline_fragment.directives, } } @@ -565,10 +519,6 @@ mod selection_map { Arc::make_mut(self.0).field.sibling_typename_mut() } - pub(super) fn get_directives_mut(&mut self) -> &mut DirectiveList { - Arc::make_mut(self.0).field.directives_mut() - } - pub(crate) fn get_selection_set_mut(&mut self) -> &mut Option { &mut Arc::make_mut(self.0).selection_set } @@ -582,10 +532,6 @@ mod selection_map { Self(fragment_spread_selection) } - pub(super) fn get_directives_mut(&mut self) -> &mut DirectiveList { - Arc::make_mut(self.0).spread.directives_mut() - } - pub(crate) fn get_selection_set_mut(&mut self) -> &mut SelectionSet { &mut Arc::make_mut(self.0).selection_set } @@ -607,10 +553,6 @@ mod selection_map { self.0 } - pub(super) fn get_directives_mut(&mut self) -> &mut DirectiveList { - Arc::make_mut(self.0).inline_fragment.directives_mut() - } - pub(crate) fn get_selection_set_mut(&mut self) -> &mut SelectionSet { &mut Arc::make_mut(self.0).selection_set } @@ -690,6 +632,15 @@ mod selection_map { as IntoIterator>::into_iter(self.0) } } + + impl<'a> IntoIterator for &'a SelectionMap { + type Item = <&'a IndexMap as IntoIterator>::Item; + type IntoIter = <&'a IndexMap as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } + } } pub(crate) use selection_map::FieldSelectionValue; @@ -922,18 +873,6 @@ impl Selection { } } - pub(crate) fn has_defer(&self) -> bool { - match self { - Selection::Field(field_selection) => field_selection.has_defer(), - Selection::FragmentSpread(fragment_spread_selection) => { - fragment_spread_selection.has_defer() - } - Selection::InlineFragment(inline_fragment_selection) => { - inline_fragment_selection.has_defer() - } - } - } - pub(crate) fn with_updated_selection_set( &self, selection_set: Option, @@ -1085,10 +1024,6 @@ impl Fragment { )?, }) } - - fn has_defer(&self) -> bool { - self.selection_set.has_defer() - } } mod field_selection { @@ -1529,10 +1464,6 @@ pub(crate) use fragment_spread_selection::FragmentSpreadData; pub(crate) use fragment_spread_selection::FragmentSpreadSelection; impl FragmentSpreadSelection { - pub(crate) fn has_defer(&self) -> bool { - self.spread.directives.has("defer") || self.selection_set.has_defer() - } - /// Copies fragment spread selection and assigns it a new unique selection ID. pub(crate) fn with_unique_id(&self) -> Self { let mut data = self.spread.data().clone(); @@ -2691,23 +2622,6 @@ impl SelectionSet { Ok(()) } - /// Removes the @defer directive from all selections without removing that selection. - fn without_defer(&mut self) { - for (_key, mut selection) in Arc::make_mut(&mut self.selections).iter_mut() { - // TODO(@goto-bus-stop): doing this changes the key of the selection! - // We have to rebuild the selection map. - selection.get_directives_mut().remove_one("defer"); - if let Some(set) = selection.get_selection_set_mut() { - set.without_defer(); - } - } - debug_assert!(!self.has_defer()); - } - - fn has_defer(&self) -> bool { - self.selections.values().any(|s| s.has_defer()) - } - // - `self` must be fragment-spread-free. pub(crate) fn add_aliases_for_non_merging_fields( &self, @@ -2964,6 +2878,15 @@ impl IntoIterator for SelectionSet { } } +impl<'a> IntoIterator for &'a SelectionSet { + type Item = <&'a IndexMap as IntoIterator>::Item; + type IntoIter = <&'a IndexMap as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.selections.as_ref().into_iter() + } +} + pub(crate) struct FieldSelectionsIter<'sel> { stack: Vec>, } @@ -3255,10 +3178,6 @@ impl FieldSelection { } } - pub(crate) fn has_defer(&self) -> bool { - self.field.has_defer() || self.selection_set.as_ref().is_some_and(|s| s.has_defer()) - } - pub(crate) fn any_element( &self, predicate: &mut impl FnMut(OpPathElement) -> Result, @@ -3276,11 +3195,6 @@ impl FieldSelection { } impl Field { - pub(crate) fn has_defer(&self) -> bool { - // @defer cannot be on field at the moment - false - } - pub(crate) fn parent_type_position(&self) -> CompositeTypeDefinitionPosition { self.field_position.parent() } @@ -3425,15 +3339,6 @@ impl InlineFragmentSelection { .unwrap_or(&self.inline_fragment.parent_type_position) } - pub(crate) fn has_defer(&self) -> bool { - self.inline_fragment.directives.has("defer") - || self - .selection_set - .selections - .values() - .any(|s| s.has_defer()) - } - /// Returns true if this inline fragment selection is "unnecessary" and should be inlined. /// /// Fragment is unnecessary if following are true: @@ -3648,6 +3553,456 @@ impl NamedFragments { } } +// @defer handling: removing and normalization + +const DEFER_DIRECTIVE_NAME: Name = name!("defer"); +const DEFER_LABEL_ARGUMENT_NAME: Name = name!("label"); +const DEFER_IF_ARGUMENT_NAME: Name = name!("if"); + +pub(crate) struct NormalizedDefer { + /// The operation modified to normalize @defer applications. + pub operation: Operation, + /// True if the operation contains any @defer applications. + pub has_defers: bool, + /// `@defer(label:)` values assigned by normalization. + pub assigned_defer_labels: IndexSet, + /// Map of variable conditions to the @defer labels depending on those conditions. + pub defer_conditions: IndexMap>, +} + +struct DeferNormalizer { + used_labels: HashSet, + assigned_labels: IndexSet, + conditions: IndexMap>, + label_offset: usize, +} + +impl DeferNormalizer { + fn new(selection_set: &SelectionSet) -> Result { + let mut digest = Self { + used_labels: HashSet::default(), + label_offset: 0, + assigned_labels: IndexSet::default(), + conditions: IndexMap::default(), + }; + let mut stack = selection_set + .into_iter() + .map(|(_, sel)| sel) + .collect::>(); + while let Some(selection) = stack.pop() { + if let Selection::InlineFragment(inline) = selection { + if let Some(args) = inline.inline_fragment.data().defer_directive_arguments()? { + let DeferDirectiveArguments { label, if_: _ } = args; + if let Some(label) = label { + digest.used_labels.insert(label); + } + } + } + stack.extend( + selection + .selection_set() + .into_iter() + .flatten() + .map(|(_, sel)| sel), + ); + } + Ok(digest) + } + + fn get_label(&mut self) -> String { + loop { + let digest = format!("qp__{}", self.label_offset); + self.label_offset += 1; + if !self.used_labels.contains(&digest) { + self.assigned_labels.insert(digest.clone()); + return digest; + } + } + } + + fn register_condition(&mut self, label: String, cond: Name) { + self.conditions.entry(cond).or_default().insert(label); + } +} + +#[derive(Debug, Clone, Copy)] +enum DeferFilter<'a> { + All, + Labels(&'a IndexSet), +} + +impl DeferFilter<'_> { + fn remove_defer(&self, directive_list: &mut DirectiveList, schema: &apollo_compiler::Schema) { + match self { + Self::All => { + directive_list.remove_one(&DEFER_DIRECTIVE_NAME); + } + Self::Labels(set) => { + let label = directive_list + .get(&DEFER_DIRECTIVE_NAME) + .and_then(|directive| { + directive + .argument_by_name(&DEFER_LABEL_ARGUMENT_NAME, schema) + .ok() + }) + .and_then(|arg| arg.as_str()); + if label.is_some_and(|label| set.contains(label)) { + directive_list.remove_one(&DEFER_DIRECTIVE_NAME); + } + } + } + } +} + +impl Fragment { + /// Returns true if the fragment's selection set contains the @defer directive. + fn has_defer(&self) -> bool { + self.selection_set.has_defer() + } + + fn without_defer( + &self, + filter: DeferFilter<'_>, + named_fragments: &NamedFragments, + ) -> Result { + let selection_set = self.selection_set.without_defer(filter, named_fragments)?; + Ok(Fragment { + schema: self.schema.clone(), + name: self.name.clone(), + type_condition_position: self.type_condition_position.clone(), + directives: self.directives.clone(), + selection_set, + }) + } +} + +impl NamedFragments { + /// Returns true if any fragment uses the @defer directive. + fn has_defer(&self) -> bool { + self.iter().any(|fragment| fragment.has_defer()) + } + + /// Creates new fragment definitions with the @defer directive removed. + fn without_defer(&self, filter: DeferFilter<'_>) -> Result { + let mut new_fragments = NamedFragments { + fragments: Default::default(), + }; + // The iteration is in dependency order: when we iterate a fragment A that depends on + // fragment B, we know that we have already processed fragment B. + // This implies that all references to other fragments will already be part of + // `new_fragments`. Note that we must process all fragments that depend on each other, even + // if a fragment doesn't actually use @defer itself, to make sure that the `.selection_set` + // values on each selection are up to date. + for fragment in self.iter() { + let fragment = fragment.without_defer(filter, &new_fragments)?; + new_fragments.insert(fragment); + } + Ok(new_fragments) + } +} + +impl FieldSelection { + /// Returns true if the selection or any of its subselections uses the @defer directive. + fn has_defer(&self) -> bool { + // Fields don't have @defer, so we only check the subselection. + self.selection_set.as_ref().is_some_and(|s| s.has_defer()) + } +} + +impl FragmentSpread { + /// Returns true if the fragment spread has a @defer directive. + fn has_defer(&self) -> bool { + self.directives.has(&DEFER_DIRECTIVE_NAME) + } + + fn without_defer(&self, filter: DeferFilter<'_>) -> Result { + let mut data = self.data().clone(); + filter.remove_defer(&mut data.directives, data.schema.schema()); + Ok(Self::new(data)) + } +} + +impl FragmentSpreadSelection { + fn has_defer(&self) -> bool { + self.spread.has_defer() || self.selection_set.has_defer() + } +} + +impl InlineFragment { + /// Returns true if the fragment has a @defer directive. + fn has_defer(&self) -> bool { + self.directives.has(&DEFER_DIRECTIVE_NAME) + } + + fn without_defer(&self, filter: DeferFilter<'_>) -> Result { + let mut data = self.data().clone(); + filter.remove_defer(&mut data.directives, data.schema.schema()); + Ok(Self::new(data)) + } +} + +impl InlineFragmentSelection { + /// Returns true if the selection or any of its subselections uses the @defer directive. + fn has_defer(&self) -> bool { + self.inline_fragment.has_defer() + || self + .selection_set + .selections + .values() + .any(|s| s.has_defer()) + } + + fn normalize_defer(self, normalizer: &mut DeferNormalizer) -> Result { + // This should always be `Some` + let Some(args) = self.inline_fragment.defer_directive_arguments()? else { + return Ok(self); + }; + + let mut remove_defer = false; + let mut args_copy = args.clone(); + if let Some(BooleanOrVariable::Boolean(b)) = &args.if_ { + if *b { + args_copy.if_ = None; + } else { + remove_defer = true; + } + } + + if args_copy.label.is_none() { + args_copy.label = Some(normalizer.get_label()); + } + + if remove_defer { + let directives: DirectiveList = self + .inline_fragment + .directives + .iter() + .filter(|dir| dir.name != "defer") + .cloned() + .collect(); + return Ok(self.with_updated_directives(directives)); + } + + // NOTE: If this is `Some`, it will be a variable. + if let Some(BooleanOrVariable::Variable(cond)) = args_copy.if_.clone() { + normalizer.register_condition(args_copy.label.clone().unwrap(), cond); + } + + if args_copy == args { + Ok(self) + } else { + let directives: DirectiveList = self + .inline_fragment + .directives + .iter() + .map(|dir| { + if dir.name == "defer" { + let mut dir: Directive = (**dir).clone(); + dir.arguments.retain(|arg| { + ![DEFER_LABEL_ARGUMENT_NAME, DEFER_IF_ARGUMENT_NAME].contains(&arg.name) + }); + dir.arguments.push( + (DEFER_LABEL_ARGUMENT_NAME, args_copy.label.clone().unwrap()).into(), + ); + if let Some(cond) = args_copy.if_.clone() { + dir.arguments.push((DEFER_IF_ARGUMENT_NAME, cond).into()); + } + Node::new(dir) + } else { + dir.clone() + } + }) + .collect(); + Ok(self.with_updated_directives(directives)) + } + } +} + +impl Selection { + /// Returns true if the selection or any of its subselections uses the @defer directive. + pub(crate) fn has_defer(&self) -> bool { + match self { + Selection::Field(field_selection) => field_selection.has_defer(), + Selection::FragmentSpread(fragment_spread_selection) => { + fragment_spread_selection.has_defer() + } + Selection::InlineFragment(inline_fragment_selection) => { + inline_fragment_selection.has_defer() + } + } + } + + fn without_defer( + &self, + filter: DeferFilter<'_>, + named_fragments: &NamedFragments, + ) -> Result { + match self { + Selection::Field(field) => { + let Some(selection_set) = field + .selection_set + .as_ref() + .filter(|selection_set| selection_set.has_defer()) + else { + return Ok(Selection::Field(Arc::clone(field))); + }; + + Ok(field + .with_updated_selection_set(Some( + selection_set.without_defer(filter, named_fragments)?, + )) + .into()) + } + Selection::FragmentSpread(frag) => { + let spread = frag.spread.without_defer(filter)?; + Ok(FragmentSpreadSelection::new(spread, named_fragments)?.into()) + } + Selection::InlineFragment(frag) => { + let inline_fragment = frag.inline_fragment.without_defer(filter)?; + let selection_set = frag.selection_set.without_defer(filter, named_fragments)?; + Ok(InlineFragmentSelection::new(inline_fragment, selection_set).into()) + } + } + } + + fn normalize_defer(self, normalizer: &mut DeferNormalizer) -> Result { + match self { + Selection::Field(field) => Ok(Self::Field(Arc::new( + field.with_updated_selection_set( + field + .selection_set + .clone() + .map(|set| set.normalize_defer(normalizer)) + .transpose()?, + ), + ))), + Selection::FragmentSpread(_spread) => { + Err(FederationError::internal("unexpected fragment spread")) + } + Selection::InlineFragment(inline) => inline + .with_updated_selection_set( + inline.selection_set.clone().normalize_defer(normalizer)?, + ) + .normalize_defer(normalizer) + .map(|inline| Self::InlineFragment(Arc::new(inline))), + } + } +} + +impl SelectionSet { + /// Create a new selection set without @defer directive applications. + fn without_defer( + &self, + filter: DeferFilter<'_>, + named_fragments: &NamedFragments, + ) -> Result { + let mut without_defer = + SelectionSet::empty(self.schema.clone(), self.type_position.clone()); + for selection in self.selections.values() { + without_defer + .add_local_selection(&selection.without_defer(filter, named_fragments)?)?; + } + Ok(without_defer) + } + + fn has_defer(&self) -> bool { + self.selections.values().any(|s| s.has_defer()) + } + + fn normalize_defer(self, normalizer: &mut DeferNormalizer) -> Result { + let Self { + schema, + type_position, + selections, + } = self; + Arc::unwrap_or_clone(selections) + .into_iter() + .map(|(_, sel)| sel.normalize_defer(normalizer)) + .try_collect() + .map(|selections| Self { + schema, + type_position, + selections: Arc::new(selections), + }) + } +} + +impl Operation { + fn has_defer(&self) -> bool { + self.selection_set.has_defer() + || self + .named_fragments + .fragments + .values() + .any(|f| f.has_defer()) + } + + /// Create a new operation without @defer directive applications. + pub(crate) fn without_defer(mut self) -> Result { + if self.has_defer() { + let named_fragments = self.named_fragments.without_defer(DeferFilter::All)?; + self.selection_set = self + .selection_set + .without_defer(DeferFilter::All, &named_fragments)?; + self.named_fragments = named_fragments; + } + debug_assert!(!self.has_defer()); + Ok(self) + } + + /// Create a new operation without specific @defer(label:) directive applications. + pub(crate) fn reduce_defer( + mut self, + labels: &IndexSet, + ) -> Result { + if self.has_defer() { + let named_fragments = self + .named_fragments + .without_defer(DeferFilter::Labels(labels))?; + self.selection_set = self + .selection_set + .without_defer(DeferFilter::Labels(labels), &named_fragments)?; + self.named_fragments = named_fragments; + } + Ok(self) + } + + /// Returns this operation but modified to "normalize" all the @defer applications. + /// + /// "Normalized" in this context means that all the `@defer` application in the resulting + /// operation will: + /// - have a (unique) label. Which implies that this method generates a label for any `@defer` + /// not having a label. + /// - have a non-trivial `if` condition, if any. By non-trivial, we mean that the condition + /// will be a variable and not an hard-coded `true` or `false`. To do this, this method will + /// remove the condition of any `@defer` that has `if: true`, and will completely remove any + /// `@defer` application that has `if: false`. + /// + /// Defer normalization does not support named fragment definitions, so it must only be called + /// if the operation had its fragments expanded. In effect, it means that this method may + /// modify the operation in a way that prevents fragments from being reused in + /// `.reuse_fragments()`. + pub(crate) fn with_normalized_defer(mut self) -> Result { + if self.has_defer() { + let mut normalizer = DeferNormalizer::new(&self.selection_set)?; + self.selection_set = self.selection_set.normalize_defer(&mut normalizer)?; + Ok(NormalizedDefer { + operation: self, + has_defers: true, + assigned_defer_labels: normalizer.assigned_labels, + defer_conditions: normalizer.conditions, + }) + } else { + Ok(NormalizedDefer { + operation: self, + has_defers: false, + assigned_defer_labels: IndexSet::default(), + defer_conditions: IndexMap::default(), + }) + } + } +} + // Collect fragment usages from operation types. impl Selection { diff --git a/apollo-federation/src/operation/tests/defer.rs b/apollo-federation/src/operation/tests/defer.rs new file mode 100644 index 0000000000..8c37ea164d --- /dev/null +++ b/apollo-federation/src/operation/tests/defer.rs @@ -0,0 +1,194 @@ +use super::parse_operation; +use super::parse_schema; + +const DEFAULT_SCHEMA: &str = r#" +type A { + one: Int + two: Int + three: Int + b: B +} + +type B { + one: Boolean + two: Boolean + three: Boolean + a: A +} + +union AorB = A | B + +type Query { + a: A + b: B + either: AorB +} + +directive @defer(if: Boolean! = true, label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT +"#; + +#[test] +fn without_defer_simple() { + let schema = parse_schema(DEFAULT_SCHEMA); + + let operation = parse_operation( + &schema, + r#" + { + ... @defer { a { one } } + b { + ... @defer { two } + } + } + "#, + ); + + let without_defer = operation.without_defer().unwrap(); + + insta::assert_snapshot!(without_defer, @r#" + { + ... { + a { + one + } + } + b { + ... { + two + } + } + } + "#); +} + +#[test] +fn without_defer_named_fragment() { + let schema = parse_schema(DEFAULT_SCHEMA); + + let operation = parse_operation( + &schema, + r#" + { + b { ...frag @defer } + either { ...frag } + } + fragment frag on B { + two + } + "#, + ); + + let without_defer = operation.without_defer().unwrap(); + + insta::assert_snapshot!(without_defer, @r#" + fragment frag on B { + two + } + + { + b { + ...frag + } + either { + ...frag + } + } + "#); +} + +#[test] +fn without_defer_merges_fragment() { + let schema = parse_schema(DEFAULT_SCHEMA); + + let operation = parse_operation( + &schema, + r#" + { + a { one } + either { + ... on B { + one + } + ... on B @defer { + two + } + } + } + "#, + ); + + let without_defer = operation.without_defer().unwrap(); + + insta::assert_snapshot!(without_defer, @r#" + { + a { + one + } + either { + ... on B { + one + two + } + } + } + "#); +} + +#[test] +fn without_defer_fragment_references() { + let schema = parse_schema(DEFAULT_SCHEMA); + + let operation = parse_operation( + &schema, + r#" + fragment a on A { + ... @defer { ...b } + } + fragment b on A { + one + b { + ...c @defer + } + } + fragment c on B { + two + } + fragment entry on Query { + a { ...a } + } + + { ...entry } + "#, + ); + + let without_defer = operation.without_defer().unwrap(); + + insta::assert_snapshot!(without_defer, @r###" + fragment c on B { + two + } + + fragment b on A { + one + b { + ...c + } + } + + fragment a on A { + ... { + ...b + } + } + + fragment entry on Query { + a { + ...a + } + } + + { + ...entry + } + "###); +} diff --git a/apollo-federation/src/operation/tests/mod.rs b/apollo-federation/src/operation/tests/mod.rs index 89c9817587..b5e4d8e591 100644 --- a/apollo-federation/src/operation/tests/mod.rs +++ b/apollo-federation/src/operation/tests/mod.rs @@ -17,6 +17,8 @@ use crate::schema::position::ObjectTypeDefinitionPosition; use crate::schema::ValidFederationSchema; use crate::subgraph::Subgraph; +mod defer; + pub(super) fn parse_schema_and_operation( schema_and_operation: &str, ) -> (ValidFederationSchema, ExecutableDocument) { diff --git a/apollo-federation/src/query_graph/graph_path.rs b/apollo-federation/src/query_graph/graph_path.rs index 7a3eb2b31f..af24a60d75 100644 --- a/apollo-federation/src/query_graph/graph_path.rs +++ b/apollo-federation/src/query_graph/graph_path.rs @@ -48,7 +48,6 @@ use crate::query_graph::QueryGraphEdgeTransition; use crate::query_graph::QueryGraphNodeType; use crate::query_plan::query_planner::EnabledOverrideConditions; use crate::query_plan::FetchDataPathElement; -use crate::query_plan::QueryPathElement; use crate::query_plan::QueryPlanCost; use crate::schema::position::AbstractTypeDefinitionPosition; use crate::schema::position::CompositeTypeDefinitionPosition; @@ -426,12 +425,12 @@ impl OpPathElement { /// ignored). pub(crate) fn without_defer(&self) -> Option { match self { - Self::Field(_) => Some(self.clone()), // unchanged + Self::Field(_) => Some(self.clone()), Self::InlineFragment(inline_fragment) => { - // TODO(@goto-bus-stop): is this not exactly the wrong way around? let updated_directives: DirectiveList = inline_fragment .directives - .get_all("defer") + .iter() + .filter(|directive| directive.name != "defer") .cloned() .collect(); if inline_fragment.type_condition_position.is_none() @@ -3784,25 +3783,6 @@ impl OpPath { } } -impl TryFrom<&'_ OpPath> for Vec { - type Error = FederationError; - - fn try_from(value: &'_ OpPath) -> Result { - value - .0 - .iter() - .map(|path_element| { - Ok(match path_element.as_ref() { - OpPathElement::Field(field) => QueryPathElement::Field(field.try_into()?), - OpPathElement::InlineFragment(inline) => { - QueryPathElement::InlineFragment(inline.try_into()?) - } - }) - }) - .collect() - } -} - pub(crate) fn concat_paths_in_parents( first: &Option>, second: &Option>, diff --git a/apollo-federation/src/query_plan/display.rs b/apollo-federation/src/query_plan/display.rs index a00efef669..b6416590e1 100644 --- a/apollo-federation/src/query_plan/display.rs +++ b/apollo-federation/src/query_plan/display.rs @@ -169,9 +169,9 @@ impl ConditionNode { state.indent()?; if_clause.write_indented(state)?; state.dedent()?; - state.write("},")?; + state.write("}")?; - state.write("Else {")?; + state.write(" Else {")?; state.indent()?; else_clause.write_indented(state)?; state.dedent()?; @@ -215,7 +215,7 @@ impl DeferNode { primary.write_indented(state)?; if !deferred.is_empty() { - state.write(", [")?; + state.write(" [")?; write_indented_lines(state, deferred, |state, deferred| { deferred.write_indented(state) })?; @@ -235,9 +235,12 @@ impl PrimaryDeferBlock { } = self; state.write("Primary {")?; if sub_selection.is_some() || node.is_some() { - state.indent()?; - if let Some(sub_selection) = sub_selection { + // Manually indent and write the newline + // to prevent a duplicate indent from `.new_line()` and `.initial_indent_level()`. + state.indent_no_new_line(); + state.write("\n")?; + state.write( sub_selection .serialize() @@ -247,7 +250,11 @@ impl PrimaryDeferBlock { state.write(":")?; state.new_line()?; } + } else { + // Indent to match the Some() case + state.indent()?; } + if let Some(node) = node { node.write_indented(state)?; } @@ -267,6 +274,7 @@ impl DeferredDeferBlock { sub_selection, node, } = self; + state.write("Deferred(depends: [")?; if let Some((DeferredDependency { id }, rest)) = depends.split_first() { state.write(id)?; @@ -285,16 +293,19 @@ impl DeferredDeferBlock { } state.write("\"")?; if let Some(label) = label { - state.write(", label: \"")?; - state.write(label)?; - state.write("\"")?; + state.write_fmt(format_args!(r#", label: "{label}""#))?; } state.write(") {")?; + if sub_selection.is_some() || node.is_some() { state.indent()?; if let Some(sub_selection) = sub_selection { write_selections(state, &sub_selection.selections)?; + state.write(":")?; + } + if sub_selection.is_some() && node.is_some() { + state.new_line()?; } if let Some(node) = node { node.write_indented(state)?; @@ -302,6 +313,7 @@ impl DeferredDeferBlock { state.dedent()?; } + state.write("},") } } diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index e5aeba3b69..ae26e4f1f3 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -9,6 +9,7 @@ use apollo_compiler::ast::Argument; use apollo_compiler::ast::Directive; use apollo_compiler::ast::OperationType; use apollo_compiler::ast::Type; +use apollo_compiler::collections::HashMap; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; use apollo_compiler::executable; @@ -27,6 +28,7 @@ use petgraph::visit::IntoNodeReferences; use serde::Serialize; use super::query_planner::SubgraphOperationCompression; +use crate::display_helpers::DisplayOption; use crate::error::FederationError; use crate::error::SingleFederationError; use crate::link::graphql_definition::DeferDirectiveArguments; @@ -82,7 +84,65 @@ use crate::utils::logging::snapshot; type DeferRef = String; /// Map of defer labels to nodes of the fetch dependency graph. -type DeferredNodes = multimap::MultiMap>; +/// +/// Like a multimap with a Set instead of a Vec for value storage. +#[derive(Debug, Clone, Default)] +struct DeferredNodes { + inner: HashMap>>, +} +impl DeferredNodes { + fn new() -> Self { + Self::default() + } + + fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + fn insert(&mut self, defer_ref: DeferRef, node: NodeIndex) { + self.inner.entry(defer_ref).or_default().insert(node); + } + + fn get_all<'map>(&'map self, defer_ref: &DeferRef) -> Option<&'map IndexSet>> { + self.inner.get(defer_ref) + } + + fn iter(&self) -> impl Iterator)> { + self.inner + .iter() + .flat_map(|(defer_ref, nodes)| std::iter::repeat(defer_ref).zip(nodes.iter().copied())) + } + + /// Consume the map and yield each element. This is provided as a standalone method and not an + /// `IntoIterator` implementation because it's hard to type :) + fn into_iter(self) -> impl Iterator)> { + self.inner.into_iter().flat_map(|(defer_ref, nodes)| { + // Cloning the key is a bit wasteful, but keys are typically very small, + // and this map is also very small. + std::iter::repeat_with(move || defer_ref.clone()).zip(nodes) + }) + } +} +impl Extend<(DeferRef, NodeIndex)> for DeferredNodes { + fn extend)>>(&mut self, iter: T) { + for (defer_ref, node) in iter.into_iter() { + self.insert(defer_ref, node); + } + } +} +impl FromIterator<(DeferRef, NodeIndex)> for DeferredNodes { + fn from_iter)>>(iter: T) -> Self { + let mut nodes = Self::new(); + nodes.extend(iter); + nodes + } +} +impl FromIterator<(DeferRef, IndexSet>)> for DeferredNodes { + fn from_iter>)>>(iter: T) -> Self { + let inner = iter.into_iter().collect(); + Self { inner } + } +} /// Represents a subgraph fetch of a query plan. // PORT_NOTE: The JS codebase called this `FetchGroup`, but this naming didn't make it apparent that @@ -131,14 +191,14 @@ pub(crate) struct FetchDependencyGraphNode { /// Safely generate IDs for fetch dependency nodes without mutable access. #[derive(Debug)] -struct FetchIdGenerator { +pub(crate) struct FetchIdGenerator { next: AtomicU64, } impl FetchIdGenerator { /// Create an ID generator, starting at the given value. - pub fn new(start_at: u64) -> Self { + pub(crate) fn new() -> Self { Self { - next: AtomicU64::new(start_at), + next: AtomicU64::new(0), } } @@ -148,14 +208,6 @@ impl FetchIdGenerator { } } -impl Clone for FetchIdGenerator { - fn clone(&self) -> Self { - Self { - next: AtomicU64::new(self.next.load(std::sync::atomic::Ordering::Relaxed)), - } - } -} - #[derive(Debug, Clone, Serialize)] pub(crate) struct FetchSelectionSet { /// The selection set to be fetched from the subgraph. @@ -220,11 +272,9 @@ pub(crate) struct FetchDependencyGraph { // serialized output will be needed. #[serde(skip)] pub(crate) defer_tracking: DeferTracking, - /// The initial fetch ID generation (used when handling `@defer`). - starting_id_generation: u64, /// The current fetch ID generation (used when handling `@defer`). #[serde(skip)] - fetch_id_generation: FetchIdGenerator, + pub(crate) fetch_id_generation: Arc, /// Whether this fetch dependency graph has undergone a transitive reduction. is_reduced: bool, /// Whether this fetch dependency graph has undergone optimization (e.g. transitive reduction, @@ -647,7 +697,7 @@ impl FetchDependencyGraph { supergraph_schema: ValidFederationSchema, federated_query_graph: Arc, root_type_for_defer: Option, - starting_id_generation: u64, + fetch_id_generation: Arc, ) -> Self { Self { defer_tracking: DeferTracking::empty(&supergraph_schema, root_type_for_defer), @@ -655,8 +705,7 @@ impl FetchDependencyGraph { federated_query_graph, graph: Default::default(), root_nodes_by_subgraph: Default::default(), - starting_id_generation, - fetch_id_generation: FetchIdGenerator::new(starting_id_generation), + fetch_id_generation, is_reduced: false, is_optimized: false, } @@ -1691,11 +1740,12 @@ impl FetchDependencyGraph { if node.defer_ref == child.defer_ref { children.push(child_index); } else { - let parent_defer_ref = node.defer_ref.as_ref().unwrap(); let Some(child_defer_ref) = &child.defer_ref else { - panic!("{} has defer_ref `{parent_defer_ref}`, so its child {} cannot have a top-level defer_ref.", - node.display(node_index), - child.display(child_index), + panic!( + "{} has defer_ref `{}`, so its child {} cannot have a top-level defer_ref.", + node.display(node_index), + DisplayOption(node.defer_ref.as_ref()), + child.display(child_index), ); }; @@ -1814,7 +1864,7 @@ impl FetchDependencyGraph { let (main, deferred_nodes, state_after_node) = self.process_node(processor, *node_index, handled_conditions.clone())?; processed_nodes.push(main); - all_deferred_nodes.extend(deferred_nodes); + all_deferred_nodes.extend(deferred_nodes.into_iter()); new_state = new_state.merge_with(state_after_node); } @@ -1859,7 +1909,7 @@ impl FetchDependencyGraph { process_in_parallel = true; main_sequence.push(processed); state = new_state; - all_deferred_nodes.extend(deferred_nodes); + all_deferred_nodes.extend(deferred_nodes.into_iter()); } Ok((main_sequence, all_deferred_nodes, state)) @@ -1897,7 +1947,7 @@ impl FetchDependencyGraph { .join(", "), ); let mut all_deferred_nodes = other_defer_nodes.cloned().unwrap_or_default(); - all_deferred_nodes.extend(deferred_nodes); + all_deferred_nodes.extend(deferred_nodes.into_iter()); // We're going to handle all `@defer`s at our "current" level (eg. at the top level, that's all the non-nested @defer), // and the "starting" node for those defers, if any, are in `all_deferred_nodes`. However, `all_deferred_nodes` @@ -1913,14 +1963,9 @@ impl FetchDependencyGraph { .map(|info| info.label.clone()) .collect::>(); let unhandled_defer_nodes = all_deferred_nodes - .keys() - .filter(|label| !handled_defers_in_current.contains(*label)) - .map(|label| { - ( - label.clone(), - all_deferred_nodes.get_vec(label).cloned().unwrap(), - ) - }) + .iter() + .filter(|(label, _index)| !handled_defers_in_current.contains(*label)) + .map(|(label, index)| (label.clone(), index)) .collect::(); let unhandled_defer_node = if unhandled_defer_nodes.is_empty() { None @@ -1938,9 +1983,10 @@ impl FetchDependencyGraph { let defers_in_current = defers_in_current.into_iter().cloned().collect::>(); for defer in defers_in_current { let nodes = all_deferred_nodes - .get_vec(&defer.label) - .cloned() - .unwrap_or_default(); + .get_all(&defer.label) + .map_or_else(Default::default, |indices| { + indices.iter().copied().collect() + }); let (main_sequence_of_defer, deferred_of_defer) = self.process_root_nodes( processor, nodes, @@ -3209,7 +3255,8 @@ impl DeferTracking { .label .as_ref() .expect("All @defer should have been labeled at this point"); - let _deferred_block = self.deferred.entry(label.clone()).or_insert_with(|| { + + self.deferred.entry(label.clone()).or_insert_with(|| { DeferredInfo::empty( primary_selection.schema.clone(), label.clone(), @@ -3444,7 +3491,7 @@ fn compute_nodes_for_key_resolution<'a>( conditions, stack_item.node_id, stack_item.node_path.clone(), - stack_item.defer_context.clone(), + stack_item.defer_context.for_conditions(), &Default::default(), )?; created_nodes.extend(conditions_nodes.iter().copied()); @@ -3675,12 +3722,12 @@ fn compute_nodes_for_root_type_resolution<'a>( }) } -#[cfg_attr(feature = "snapshot_tracing", tracing::instrument(skip_all, level = "trace", fields(label = operation.to_string())))] +#[cfg_attr(feature = "snapshot_tracing", tracing::instrument(skip_all, level = "trace", fields(label = operation_element.to_string())))] fn compute_nodes_for_op_path_element<'a>( dependency_graph: &mut FetchDependencyGraph, stack_item: &ComputeNodesStackItem<'a>, child: &'a Arc>>, - operation: &OpPathElement, + operation_element: &OpPathElement, created_nodes: &mut IndexSet, ) -> Result, FederationError> { let Some(edge_id) = child.edge else { @@ -3691,7 +3738,7 @@ fn compute_nodes_for_op_path_element<'a>( // to one for the defer in question. let (updated_operation, updated_defer_context) = extract_defer_from_operation( dependency_graph, - operation, + operation_element, &stack_item.defer_context, &stack_item.node_path, )?; @@ -3718,19 +3765,19 @@ fn compute_nodes_for_op_path_element<'a>( let dest = stack_item.tree.graph.node_weight(dest_id)?; if source.source != dest.source { return Err(FederationError::internal(format!( - "Collecting edge {edge_id:?} for {operation:?} \ - should not change the underlying subgraph" + "Collecting edge {edge_id:?} for {operation_element:?} \ + should not change the underlying subgraph" ))); } // We have a operation element, field or inline fragment. // We first check if it's been "tagged" to remember that __typename must be queried. // See the comment on the `optimize_sibling_typenames()` method to see why this exists. - if let Some(sibling_typename) = operation.sibling_typename() { + if let Some(sibling_typename) = operation_element.sibling_typename() { // We need to add the query __typename for the current type in the current node. let typename_field = Arc::new(OpPathElement::Field(Field::new_introspection_typename( - operation.schema(), - &operation.parent_type_position(), + operation_element.schema(), + &operation_element.parent_type_position(), sibling_typename.alias().cloned(), ))); let typename_path = stack_item @@ -3755,12 +3802,12 @@ fn compute_nodes_for_op_path_element<'a>( } let Ok((Some(updated_operation), updated_defer_context)) = extract_defer_from_operation( dependency_graph, - operation, + operation_element, &stack_item.defer_context, &stack_item.node_path, ) else { return Err(FederationError::internal(format!( - "Extracting @defer from {operation:?} should not have resulted in no operation" + "Extracting @defer from {operation_element:?} should not have resulted in no operation" ))); }; let mut updated = ComputeNodesStackItem { @@ -3993,15 +4040,15 @@ fn compute_input_rewrites_on_key_fetch( /// - The updated operation can be `None`, if operation is no longer necessary. fn extract_defer_from_operation( dependency_graph: &mut FetchDependencyGraph, - operation: &OpPathElement, + operation_element: &OpPathElement, defer_context: &DeferContext, node_path: &FetchDependencyGraphNodePath, ) -> Result<(Option, DeferContext), FederationError> { - let defer_args = operation.defer_directive_args(); + let defer_args = operation_element.defer_directive_args(); let Some(defer_args) = defer_args else { let updated_path_to_defer_parent = defer_context .path_to_defer_parent - .with_pushed(operation.clone().into()); + .with_pushed(operation_element.clone().into()); let updated_context = DeferContext { path_to_defer_parent: updated_path_to_defer_parent.into(), // Following fields are identical to those of `defer_context`. @@ -4009,16 +4056,16 @@ fn extract_defer_from_operation( active_defer_ref: defer_context.active_defer_ref.clone(), is_part_of_query: defer_context.is_part_of_query, }; - return Ok((Some(operation.clone()), updated_context)); + return Ok((Some(operation_element.clone()), updated_context)); }; - let updated_defer_ref = defer_args.label.as_ref().ok_or_else(|| - // PORT_NOTE: The original TypeScript code has an assertion here. - FederationError::internal( - "All defers should have a label at this point", - ))?; - let updated_operation = operation.without_defer(); - let updated_path_to_defer_parent = match updated_operation { + // PORT_NOTE: The original TypeScript code has an assertion here. + let updated_defer_ref = defer_args + .label + .as_ref() + .ok_or_else(|| FederationError::internal("All defers should have a label at this point"))?; + let updated_operation_element = operation_element.without_defer(); + let updated_path_to_defer_parent = match updated_operation_element { None => Default::default(), // empty OpPath Some(ref updated_operation) => OpPath(vec![Arc::new(updated_operation.clone())]), }; @@ -4027,7 +4074,7 @@ fn extract_defer_from_operation( defer_context, &defer_args, node_path.clone(), - operation.parent_type_position(), + operation_element.parent_type_position(), )?; let updated_context = DeferContext { @@ -4037,7 +4084,7 @@ fn extract_defer_from_operation( active_defer_ref: defer_context.active_defer_ref.clone(), is_part_of_query: defer_context.is_part_of_query, }; - Ok((updated_operation, updated_context)) + Ok((updated_operation_element, updated_context)) } fn handle_requires( @@ -4112,7 +4159,7 @@ fn handle_requires( requires_conditions, new_node_id, fetch_node_path.clone(), - defer_context_for_conditions(defer_context), + defer_context.for_conditions(), &OpGraphPathContext::default(), )?; if newly_created_node_ids.is_empty() { @@ -4344,7 +4391,7 @@ fn handle_requires( requires_conditions, fetch_node_id, fetch_node_path.clone(), - defer_context_for_conditions(defer_context), + defer_context.for_conditions(), &OpGraphPathContext::default(), )?; // If we didn't create any node, that means the whole condition was fetched from the current node @@ -4413,11 +4460,14 @@ fn handle_requires( } } -fn defer_context_for_conditions(base_context: &DeferContext) -> DeferContext { - let mut context = base_context.clone(); - context.is_part_of_query = false; - context.current_defer_ref = base_context.active_defer_ref.clone(); - context +impl DeferContext { + /// Create a sub-context for use in resolving conditions inside an @defer block. + fn for_conditions(&self) -> Self { + let mut context = self.clone(); + context.is_part_of_query = false; + context.current_defer_ref = self.active_defer_ref.clone(); + context + } } fn inputs_for_require( diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph_processor.rs b/apollo-federation/src/query_plan/fetch_dependency_graph_processor.rs index ab126dbcc6..37982b3ee2 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph_processor.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph_processor.rs @@ -6,9 +6,11 @@ use apollo_compiler::Name; use apollo_compiler::Node; use super::query_planner::SubgraphOperationCompression; +use super::QueryPathElement; use crate::error::FederationError; use crate::operation::DirectiveList; use crate::operation::SelectionSet; +use crate::query_graph::graph_path::OpPathElement; use crate::query_graph::QueryGraph; use crate::query_plan::conditions::Conditions; use crate::query_plan::fetch_dependency_graph::DeferredInfo; @@ -338,6 +340,32 @@ impl FetchDependencyGraphProcessor, DeferredDeferBlock> defer_info: &DeferredInfo, node: Option, ) -> Result { + /// Produce a query path with only the relevant elements: fields and type conditions. + fn op_path_to_query_path( + path: &[Arc], + ) -> Result, FederationError> { + path.iter() + .map( + |element| -> Result, FederationError> { + match &**element { + OpPathElement::Field(field) => { + Ok(Some(QueryPathElement::Field(field.try_into()?))) + } + OpPathElement::InlineFragment(inline) => { + match &inline.type_condition_position { + Some(_) => Ok(Some(QueryPathElement::InlineFragment( + inline.try_into()?, + ))), + None => Ok(None), + } + } + } + }, + ) + .filter_map(|result| result.transpose()) + .collect::, _>>() + } + Ok(DeferredDeferBlock { depends: defer_info .dependencies @@ -354,7 +382,7 @@ impl FetchDependencyGraphProcessor, DeferredDeferBlock> } else { Some(defer_info.label.clone()) }, - query_path: defer_info.path.full_path.as_ref().try_into()?, + query_path: op_path_to_query_path(&defer_info.path.full_path)?, // Note that if the deferred block has nested @defer, // then the `value` is going to be a `DeferNode` // and we'll use it's own `subselection`, so we don't need it here. diff --git a/apollo-federation/src/query_plan/query_planner.rs b/apollo-federation/src/query_plan/query_planner.rs index a64f79364c..6d75453422 100644 --- a/apollo-federation/src/query_plan/query_planner.rs +++ b/apollo-federation/src/query_plan/query_planner.rs @@ -12,11 +12,14 @@ use apollo_compiler::Name; use itertools::Itertools; use serde::Serialize; +use super::fetch_dependency_graph::FetchIdGenerator; +use super::ConditionNode; use crate::error::FederationError; use crate::error::SingleFederationError; use crate::link::federation_spec_definition::FederationSpecDefinition; use crate::operation::normalize_operation; use crate::operation::NamedFragments; +use crate::operation::NormalizedDefer; use crate::operation::Operation; use crate::operation::SelectionSet; use crate::query_graph::build_federated_query_graph; @@ -407,37 +410,29 @@ impl QueryPlanner { &self.interface_types_with_interface_objects, )?; - let (normalized_operation, assigned_defer_labels, defer_conditions, has_defers) = ( - normalized_operation.without_defer(), - None, - None::>>, - false, - ); - /* TODO(TylerBloom): After defer is impl-ed and after the private preview, the call - * above needs to be replaced with this if-else expression. - if self.config.incremental_delivery.enable_defer { - let NormalizedDefer { - operation, - assigned_defer_labels, - defer_conditions, - has_defers, - } = normalized_operation.with_normalized_defer(); - if has_defers && is_subscription { - return Err(SingleFederationError::DeferredSubscriptionUnsupported.into()); - } - ( - operation, - Some(assigned_defer_labels), - Some(defer_conditions), - has_defers, - ) - } else { - // If defer is not enabled, we remove all @defer from the query. This feels cleaner do this once here than - // having to guard all the code dealing with defer later, and is probably less error prone too (less likely - // to end up passing through a @defer to a subgraph by mistake). - (normalized_operation.without_defer(), None, None, false) - }; - */ + let (normalized_operation, assigned_defer_labels, defer_conditions, has_defers) = + if self.config.incremental_delivery.enable_defer { + let NormalizedDefer { + operation, + assigned_defer_labels, + defer_conditions, + has_defers, + } = normalized_operation.with_normalized_defer()?; + if has_defers && is_subscription { + return Err(SingleFederationError::DeferredSubscriptionUnsupported.into()); + } + ( + operation, + Some(assigned_defer_labels), + Some(defer_conditions), + has_defers, + ) + } else { + // If defer is not enabled, we remove all @defer from the query. This feels cleaner do this once here than + // having to guard all the code dealing with defer later, and is probably less error prone too (less likely + // to end up passing through a @defer to a subgraph by mistake). + (normalized_operation.without_defer()?, None, None, false) + }; if normalized_operation.selection_set.is_empty() { return Ok(QueryPlan::default()); @@ -503,11 +498,16 @@ impl QueryPlanner { override_conditions: EnabledOverrideConditions(HashSet::from_iter( options.override_conditions, )), + fetch_id_generator: Arc::new(FetchIdGenerator::new()), }; let root_node = match defer_conditions { Some(defer_conditions) if !defer_conditions.is_empty() => { - compute_plan_for_defer_conditionals(&mut parameters, defer_conditions)? + compute_plan_for_defer_conditionals( + &mut parameters, + &mut processor, + defer_conditions, + )? } _ => compute_plan_internal(&mut parameters, &mut processor, has_defers)?, }; @@ -621,7 +621,6 @@ fn compute_root_serial_dependency_graph( // We have to serially compute a plan for each top-level selection. let mut split_roots = operation.selection_set.clone().split_top_level_fields(); let mut digest = Vec::new(); - let mut starting_fetch_id = 0; let selection_set = split_roots .next() .ok_or_else(|| FederationError::internal("Empty top level fields"))?; @@ -653,7 +652,7 @@ fn compute_root_serial_dependency_graph( supergraph_schema.clone(), federated_query_graph.clone(), root_type.clone(), - starting_fetch_id, + fetch_dependency_graph.fetch_id_generation.clone(), ); compute_root_fetch_groups( operation.root_kind, @@ -666,7 +665,6 @@ fn compute_root_serial_dependency_graph( // the current ID that is inside the fetch dep graph's ID generator, or to use the // starting ID. Because this method ensure uniqueness between IDs, this approach was // taken; however, it could be the case that this causes unforseen issues. - starting_fetch_id = fetch_dependency_graph.next_fetch_id(); digest.push(std::mem::replace( &mut fetch_dependency_graph, new_dep_graph, @@ -835,16 +833,45 @@ fn compute_plan_internal( } } -// TODO: FED-95 fn compute_plan_for_defer_conditionals( - _parameters: &mut QueryPlanningParameters, - _defer_conditions: IndexMap>, + parameters: &mut QueryPlanningParameters, + processor: &mut FetchDependencyGraphToQueryPlanProcessor, + defer_conditions: IndexMap>, ) -> Result, FederationError> { - Err(SingleFederationError::UnsupportedFeature { - message: String::from("@defer is currently not supported"), - kind: crate::error::UnsupportedFeatureKind::Defer, + generate_condition_nodes( + parameters.operation.clone(), + defer_conditions.iter(), + &mut |op| { + parameters.operation = op; + compute_plan_internal(parameters, processor, true) + }, + ) +} + +fn generate_condition_nodes<'a>( + op: Arc, + mut conditions: impl Clone + Iterator)>, + on_final_operation: &mut impl FnMut(Arc) -> Result, FederationError>, +) -> Result, FederationError> { + match conditions.next() { + None => on_final_operation(op), + Some((cond, labels)) => { + let else_op = Arc::unwrap_or_clone(op.clone()).reduce_defer(labels)?; + let if_op = op; + let node = ConditionNode { + condition_variable: cond.clone(), + if_clause: generate_condition_nodes(if_op, conditions.clone(), on_final_operation)? + .map(Box::new), + else_clause: generate_condition_nodes( + Arc::new(else_op), + conditions.clone(), + on_final_operation, + )? + .map(Box::new), + }; + Ok(Some(PlanNode::Condition(Box::new(node)))) + } } - .into()) } /// Tracks fragments from the original operation, along with versions rebased on other subgraphs. diff --git a/apollo-federation/src/query_plan/query_planning_traversal.rs b/apollo-federation/src/query_plan/query_planning_traversal.rs index 6142cd908f..0fba2e801a 100644 --- a/apollo-federation/src/query_plan/query_planning_traversal.rs +++ b/apollo-federation/src/query_plan/query_planning_traversal.rs @@ -6,6 +6,7 @@ use petgraph::graph::NodeIndex; use serde::Serialize; use tracing::trace; +use super::fetch_dependency_graph::FetchIdGenerator; use crate::error::FederationError; use crate::operation::Operation; use crate::operation::Selection; @@ -60,6 +61,7 @@ pub(crate) struct QueryPlanningParameters<'a> { pub(crate) federated_query_graph: Arc, /// The operation to be query planned. pub(crate) operation: Arc, + pub(crate) fetch_id_generator: Arc, /// The query graph node at which query planning begins. pub(crate) head: NodeIndex, /// Whether the head must be a root node for query planning. @@ -84,8 +86,9 @@ pub(crate) struct QueryPlanningTraversal<'a, 'b> { /// True if query planner `@defer` support is enabled and the operation contains some `@defer` /// application. has_defers: bool, - /// The initial fetch ID generation (used when handling `@defer`). - starting_id_generation: u64, + /// A handle to the sole generator of fetch IDs. While planning an operation, only one of + /// generator can be used. + id_generator: Arc, /// A processor for converting fetch dependency graphs to cost. cost_processor: FetchDependencyGraphToCostProcessor, /// True if this query planning is at top-level (note that query planning can recursively start @@ -146,7 +149,7 @@ impl BestQueryPlanInfo { parameters.supergraph_schema.clone(), parameters.federated_query_graph.clone(), None, - 0, + parameters.fetch_id_generator.clone(), ), path_tree: OpPathTree::new(parameters.federated_query_graph.clone(), parameters.head) .into(), @@ -174,8 +177,8 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { Self::new_inner( parameters, selection_set, - 0, has_defers, + parameters.fetch_id_generator.clone(), root_kind, cost_processor, Default::default(), @@ -193,8 +196,8 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { fn new_inner( parameters: &'a QueryPlanningParameters, selection_set: SelectionSet, - starting_id_generation: u64, has_defers: bool, + id_generator: Arc, root_kind: SchemaRootDefinitionKind, cost_processor: FetchDependencyGraphToCostProcessor, initial_context: OpGraphPathContext, @@ -233,7 +236,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { parameters, root_kind, has_defers, - starting_id_generation, + id_generator, cost_processor, is_top_level, open_branches: Default::default(), @@ -913,7 +916,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { self.parameters.supergraph_schema.clone(), self.parameters.federated_query_graph.clone(), root_type, - self.starting_id_generation, + self.id_generator.clone(), ) } @@ -1016,12 +1019,13 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { config: self.parameters.config.clone(), statistics: self.parameters.statistics, override_conditions: self.parameters.override_conditions.clone(), + fetch_id_generator: self.parameters.fetch_id_generator.clone(), }; let best_plan_opt = QueryPlanningTraversal::new_inner( ¶meters, edge_conditions.clone(), - self.starting_id_generation, self.has_defers, + self.id_generator.clone(), self.root_kind, self.cost_processor, context.clone(), diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests.rs b/apollo-federation/tests/query_plan/build_query_plan_tests.rs index cf6b3009e7..1f9fd8587c 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests.rs @@ -32,6 +32,7 @@ fn some_name() { */ mod debug_max_evaluated_plans_configuration; +mod defer; mod fetch_operation_names; mod field_merging_with_skip_and_include; mod fragment_autogeneration; diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/defer.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/defer.rs new file mode 100644 index 0000000000..2dd9f28cdd --- /dev/null +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/defer.rs @@ -0,0 +1,3826 @@ +use apollo_federation::query_plan::query_planner::QueryPlannerConfig; + +fn config_with_defer() -> QueryPlannerConfig { + let mut config = QueryPlannerConfig::default(); + config.incremental_delivery.enable_defer = true; + config +} + +#[test] +fn defer_test_handles_simple_defer_without_defer_enabled() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v1: Int + v2: Int + } + "#, + ); + // without defer-support enabled + assert_plan!(planner, + r#" + { + t { + v1 + ... @defer { + v2 + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + v2 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn defer_test_normalizes_if_false() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v1: Int + v2: Int + } + "#, + ); + assert_plan!(planner, + r#" + { + t { + v1 + ... @defer(if: false) { + v2 + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + v2 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn defer_test_normalizes_if_true() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v1: Int + v2: Int + } + "#, + ); + assert_plan!(planner, + r#" + { + t { + v1 + ... @defer(if: true) { + v2 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v1 + } + }: + Sequence { + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + } + } + }, + }, + }, + }, [ + Deferred(depends: [0], path: "t") { + { + v2 + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_handles_simple_defer_with_defer_enabled() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v1: Int + v2: Int + } + "#, + ); + assert_plan!(planner, + r#" + { + t { + v1 + ... @defer { + v2 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v1 + } + }: + Sequence { + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + } + } + }, + }, + }, + }, [ + Deferred(depends: [0], path: "t") { + { + v2 + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_non_router_based_defer_case_one() { + // @defer on value type + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v: V + } + + type V { + a: Int + b: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + v { + a + ... @defer { + b + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v { + a + } + } + }: + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v { + a + b + } + } + } + }, + }, + }, + }, [ + Deferred(depends: [], path: "t/v") { + { + b + }: + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_non_router_based_defer_case_two() { + // @defer on entity but with no @key + // While the @defer in the operation is on an entity, the @key in the first subgraph + // is explicitely marked as non-resovable, so we cannot use it to actually defer the + // fetch to `v1`. Note that example still compose because, defer excluded, `v1` can + // still be fetched for all queries (which is only `t` here). + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id", resolvable: false) { + id: ID! + v1: String + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v2: String + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + ... @defer { + v1 + } + v2 + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v2 + } + }: + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + v1 + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + }, + }, [ + Deferred(depends: [], path: "t") { + { + v1 + }: + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_non_router_based_defer_case_three() { + // @defer on value type but with entity afterwards + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + } + + type U @key(fields: "id") { + id: ID! + x: Int + } + "#, + + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v: V + } + + type V { + a: Int + u: U + } + + type U @key(fields: "id") { + id: ID! + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + v { + a + ... @defer { + u { + x + } + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v { + a + } + } + }: + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2", id: 0) { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v { + a + u { + __typename + id + } + } + } + } + }, + }, + }, + }, [ + Deferred(depends: [0], path: "t/v") { + { + u { + x + } + }: + Flatten(path: "t.v.u") { + Fetch(service: "Subgraph1") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + x + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_resuming_in_the_same_subgraph() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + v0: String + v1: String + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + v0 + ... @defer { + v1 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v0 + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + v0 + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + v1 + }: + Flatten(path: "t") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_multiple_fields_in_different_subgraphs() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + v0: String + v1: String + } + "#, + + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v2: String + } + "#, + Subgraph3: r#" + type T @key(fields: "id") { + id: ID! + v3: String + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + v0 + ... @defer { + v1 + v2 + v3 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v0 + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + v0 + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + v1 + v2 + v3 + }: + Parallel { + Flatten(path: "t") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph3") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v3 + } + } + }, + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_multiple_non_nested_defer_plus_label_handling() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + v0: String + v1: String + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v2: String + v3: U + } + + type U @key(fields: "id") { + id: ID! + } + "#, + Subgraph3: r#" + type U @key(fields: "id") { + id: ID! + x: Int + y: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + v0 + ... @defer(label: "defer_v1") { + v1 + } + ... @defer { + v2 + } + v3 { + x + ... @defer(label: "defer_in_v3") { + y + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v0 + v3 { + x + } + } + }: + Sequence { + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + id + v0 + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2", id: 1) { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v3 { + __typename + id + } + } + } + }, + }, + Flatten(path: "t.v3") { + Fetch(service: "Subgraph3") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + x + } + } + }, + }, + }, + }, [ + Deferred(depends: [0], path: "t") { + { + v2 + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + }, + Deferred(depends: [0], path: "t", label: "defer_v1") { + { + v1 + }: + Flatten(path: "t") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + } + } + }, + }, + }, + Deferred(depends: [1], path: "t/v3", label: "defer_in_v3") { + { + y + }: + Flatten(path: "t.v3") { + Fetch(service: "Subgraph3") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + y + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_nested_defer_on_entities() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + me: User + } + + type User @key(fields: "id") { + id: ID! + name: String + } + "#, + Subgraph2: r#" + type User @key(fields: "id") { + id: ID! + messages: [Message] + } + + type Message @key(fields: "id") { + id: ID! + body: String + author: User + } + "#, + ); + + assert_plan!(planner, + r#" + { + me { + name + ... on User @defer { + messages { + body + author { + name + ... @defer { + messages { + body + } + } + } + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + me { + name + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + me { + __typename + name + id + } + } + }, + }, [ + Deferred(depends: [0], path: "me") { + Defer { + Primary { + { + ... on User { + messages { + body + author { + name + } + } + } + }: + Sequence { + Flatten(path: "me") { + Fetch(service: "Subgraph2", id: 1) { + { + ... on User { + __typename + id + } + } => + { + ... on User { + messages { + body + author { + __typename + id + } + } + } + } + }, + }, + Flatten(path: "me.messages.@.author") { + Fetch(service: "Subgraph1") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + name + } + } + }, + }, + }, + }, [ + Deferred(depends: [1], path: "me/messages/author") { + { + messages { + body + } + }: + Flatten(path: "me.messages.@.author") { + Fetch(service: "Subgraph2") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + messages { + body + } + } + } + }, + }, + }, + ] + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_on_value_types() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + me: User + } + + type User @key(fields: "id") { + id: ID! + name: String + } + "#, + Subgraph2: r#" + type User @key(fields: "id") { + id: ID! + messages: [Message] + } + + type Message { + id: ID! + body: MessageBody + } + + type MessageBody { + paragraphs: [String] + lines: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + me { + ... @defer { + messages { + ... @defer { + body { + lines + } + } + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + Fetch(service: "Subgraph1", id: 0) { + { + me { + __typename + id + } + } + }, + }, [ + Deferred(depends: [0], path: "me") { + Defer { + Primary { + Flatten(path: "me") { + Fetch(service: "Subgraph2") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + messages { + body { + lines + } + } + } + } + }, + }, + }, [ + Deferred(depends: [], path: "me/messages") { + { + body { + lines + } + }: + }, + ] + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_direct_nesting_on_entity() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + me: User + } + + type User @key(fields: "id") { + id: ID! + name: String + } + "#, + Subgraph2: r#" + type User @key(fields: "id") { + id: ID! + age: Int + address: String + } + "#, + ); + + assert_plan!(planner, + r#" + { + me { + name + ... @defer { + age + ... @defer { + address + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + me { + name + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + me { + __typename + name + id + } + } + }, + }, [ + Deferred(depends: [0], path: "me") { + Defer { + Primary { + { + age + }: + Flatten(path: "me") { + Fetch(service: "Subgraph2") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + age + } + } + }, + }, + }, [ + Deferred(depends: [0], path: "me") { + { + address + }: + Flatten(path: "me") { + Fetch(service: "Subgraph2") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + address + } + } + }, + }, + }, + ] + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_direct_nesting_on_value_type() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + me: User + } + + type User { + id: ID! + name: String + age: Int + address: String + } + "#, + ); + + assert_plan!(planner, + r#" + { + me { + name + ... @defer { + age + ... @defer { + address + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + me { + name + } + }: + Fetch(service: "Subgraph1") { + { + me { + name + age + address + } + } + }, + }, [ + Deferred(depends: [], path: "me") { + Defer { + Primary { + { + age + } + }, [ + Deferred(depends: [], path: "me") { + { + address + }: + }, + ] + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_on_enity_but_with_unuseful_key() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T { + id: ID! @shareable + a: Int + b: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + ... @defer { + a + ... @defer { + b + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + Fetch(service: "Subgraph1") { + { + t { + a + b + } + } + }, + }, [ + Deferred(depends: [], path: "t") { + Defer { + Primary { + { + a + } + }, [ + Deferred(depends: [], path: "t") { + { + b + }: + }, + ] + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_on_mutation_in_same_subgraph() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type Mutation { + update1: T + update2: T + } + + type T @key(fields: "id") { + id: ID! + v0: String + v1: String + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v2: String + } + "#, + ); + + // What matters here is that the updates (that go to different fields) are correctly done in sequence, + // and that defers have proper dependency set. + assert_plan!(planner, + r#" + mutation mut { + update1 { + v0 + ... @defer { + v1 + } + } + update2 { + v1 + ... @defer { + v0 + v2 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + update1 { + v0 + } + update2 { + v1 + } + }: + Fetch(service: "Subgraph1", id: 2) { + { + update1 { + __typename + v0 + id + } + update2 { + __typename + v1 + id + } + } + }, + }, [ + Deferred(depends: [2], path: "update1") { + { + v1 + }: + Flatten(path: "update1") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + } + } + }, + }, + }, + Deferred(depends: [2], path: "update2") { + { + v0 + v2 + }: + Parallel { + Flatten(path: "update2") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v0 + } + } + }, + }, + Flatten(path: "update2") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_on_mutation_on_different_subgraphs() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type Mutation { + update1: T + } + + type T @key(fields: "id") { + id: ID! + v0: String + v1: String + } + "#, + Subgraph2: r#" + type Mutation { + update2: T + } + + type T @key(fields: "id") { + id: ID! + v2: String + } + "#, + ); + + // What matters here is that the updates (that go to different fields) are correctly done in sequence, + // and that defers have proper dependency set. + assert_plan!(planner, + r#" + mutation mut { + update1 { + v0 + ... @defer { + v1 + } + } + update2 { + v1 + ... @defer { + v0 + v2 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + update1 { + v0 + } + update2 { + v1 + } + }: + Sequence { + Fetch(service: "Subgraph1", id: 0) { + { + update1 { + __typename + v0 + id + } + } + }, + Fetch(service: "Subgraph2", id: 1) { + { + update2 { + __typename + id + } + } + }, + Flatten(path: "update2") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + } + } + }, + }, + }, + }, [ + Deferred(depends: [0], path: "update1") { + { + v1 + }: + Flatten(path: "update1") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + } + } + }, + }, + }, + Deferred(depends: [1], path: "update2") { + { + v0 + v2 + }: + Parallel { + Flatten(path: "update2") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v0 + } + } + }, + }, + Flatten(path: "update2") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_on_multi_dependency_deferred_section() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id0") { + id0: ID! + v1: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id0") @key(fields: "id1") { + id0: ID! + id1: ID! + v2: Int + } + "#, + Subgraph3: r#" + type T @key(fields: "id0") @key(fields: "id2") { + id0: ID! + id2: ID! + v3: Int + } + "#, + Subgraph4: r#" + type T @key(fields: "id1 id2") { + id1: ID! + id2: ID! + v4: Int + } + "#, + ); + + assert_plan!(&planner, + r#" + { + t { + v1 + v2 + v3 + ... @defer { + v4 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v1 + v2 + v3 + } + }: + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id0 + v1 + } + } + }, + Parallel { + Flatten(path: "t") { + Fetch(service: "Subgraph2", id: 0) { + { + ... on T { + __typename + id0 + } + } => + { + ... on T { + v2 + id1 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph3", id: 1) { + { + ... on T { + __typename + id0 + } + } => + { + ... on T { + v3 + id2 + } + } + }, + }, + }, + }, + }, [ + Deferred(depends: [0, 1], path: "t") { + { + v4 + }: + Flatten(path: "t") { + Fetch(service: "Subgraph4") { + { + ... on T { + __typename + id1 + id2 + } + } => + { + ... on T { + v4 + } + } + }, + }, + }, + ] + }, + } + "### + ); + + // TODO: the following plan is admittedly not as effecient as it could be, as the 2 queries to + // subgraph 2 and 3 are done in the "primary" section, but all they do is handle transitive + // key dependencies for the deferred block, so it would make more sense to defer those fetches + // as well. It is however tricky to both improve this here _and_ maintain the plan generate + // just above (which is admittedly optimial). More precisely, what the code currently does is + // that when it gets to a defer, then it defers the fetch that gets the deferred fields (the + // fetch to subgraph 4 here), but it puts the "condition" resolution for the key of that fetch + // in the non-deferred section. Here, resolving that fetch conditions is what creates the + // dependency on the the fetches to subgraph 2 and 3, and so those get non-deferred. + // Now, it would be reasonably simple to say that when we resolve the "conditions" for a deferred + // fetch, then the first "hop" is non-deferred, but any following ones do get deferred, which + // would move the 2 fetches to subgraph 2 and 3 in the deferred section. The problem is that doing + // that wholesale means that in the previous example above, we'd keep the 2 non-deferred fetches + // to subgraph 2 and 3 for v2 and v3, but we would then have new deferred fetches to those + // subgraphs in the deferred section to now get the key id1 and id2, and that is in turn arguably + // non-optimal. So ideally, the code would be able to distinguish between those 2 cases and + // do the most optimal thing in each cases, but it's not that simple to do with the current + // code. + // Taking a step back, this "inefficiency" only exists where there is a @key "chain", and while + // such chains have their uses, they are likely pretty rare in the first place. And as the + // generated plan is not _that_ bad either, optimizing this feels fairly low priority and + // we leave it for "later". + assert_plan!(planner, + r#" + { + t { + v1 + ... @defer { + v4 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v1 + } + }: + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + v1 + id0 + } + } + }, + Parallel { + Flatten(path: "t") { + Fetch(service: "Subgraph2", id: 0) { + { + ... on T { + __typename + id0 + } + } => + { + ... on T { + id1 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph3", id: 1) { + { + ... on T { + __typename + id0 + } + } => + { + ... on T { + id2 + } + } + }, + }, + }, + }, + }, [ + Deferred(depends: [0, 1], path: "t") { + { + v4 + }: + Flatten(path: "t") { + Fetch(service: "Subgraph4") { + { + ... on T { + __typename + id1 + id2 + } + } => + { + ... on T { + v4 + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_requirements_of_deferred_fields_are_deferred() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + v1: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v2: Int @requires(fields: "v3") + v3: Int @external + } + "#, + Subgraph3: r#" + type T @key(fields: "id") { + id: ID! + v3: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + v1 + ... @defer { + v2 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v1 + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + v1 + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + v2 + }: + Sequence { + Flatten(path: "t") { + Fetch(service: "Subgraph3") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v3 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + v3 + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_provides_are_ignored_for_deferred_fields() { + // NOTE: this test tests the currently implemented behaviour, which ignore @provides when it + // concerns a deferred field. However, this is the behaviour implemented at the moment more + // because it is the simplest option and it's not illogical, but it is not the only possibly + // valid option. In particular, one could make the case that if a subgraph has a `@provides`, + // then this probably means that the subgraph can provide the field "cheaply" (why have + // a `@provides` otherwise?), and so that ignoring the @defer (instead of ignoring the @provides) + // is preferable. We can change to this behaviour later if we decide that it is preferable since + // the responses sent to the end-user would be the same regardless. + + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T @provides(fields: "v2") + } + + type T @key(fields: "id") { + id: ID! + v1: Int + v2: Int @external + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v2: Int @shareable + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + v1 + ... @defer { + v2 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v1 + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + v1 + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + v2 + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_on_query_root_type() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + op1: Int + op2: A + } + + type A { + x: Int + y: Int + next: Query + } + "#, + Subgraph2: r#" + type Query { + op3: Int + op4: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + op2 { + x + y + next { + op3 + ... @defer { + op1 + op4 + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + op2 { + x + y + next { + op3 + } + } + }: + Sequence { + Fetch(service: "Subgraph1", id: 0) { + { + op2 { + x + y + next { + __typename + } + } + } + }, + Flatten(path: "op2.next") { + Fetch(service: "Subgraph2") { + { + ... on Query { + op3 + } + } + }, + }, + }, + }, [ + Deferred(depends: [0], path: "op2/next") { + { + op1 + op4 + }: + Parallel { + Flatten(path: "op2.next") { + Fetch(service: "Subgraph1") { + { + ... on Query { + op1 + } + } + }, + }, + Flatten(path: "op2.next") { + Fetch(service: "Subgraph2") { + { + ... on Query { + op4 + } + } + }, + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_on_everything_queried() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + x: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + y: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + ... @defer { + t { + x + y + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary {}, [ + Deferred(depends: [], path: "") { + { + t { + x + y + } + }: + Sequence { + Flatten(path: "") { + Fetch(service: "Subgraph1") { + { + ... on Query { + t { + __typename + id + x + } + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_everything_within_entity() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + x: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + y: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + ... @defer { + x + y + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + x + y + }: + Parallel { + Flatten(path: "t") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + x + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_with_conditions_and_labels() { + let planner = planner!(config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + x: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + y: Int + } + "#, + ); + + // without explicit label + assert_plan!(&planner, + r#" + query($cond: Boolean) { + t { + x + ... @defer(if: $cond) { + y + } + } + } + "#, + @r###" + QueryPlan { + Condition(if: $cond) { + Then { + Defer { + Primary { + { + t { + x + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + x + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + y + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + ] + }, + } Else { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + x + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + }, + }, + } + "### + ); + // with explicit label + assert_plan!(planner, + r#" + query($cond: Boolean) { + t { + x + ... @defer(label: "testLabel" if: $cond) { + y + } + } + } + "#, + @r###" + QueryPlan { + Condition(if: $cond) { + Then { + Defer { + Primary { + { + t { + x + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + x + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t", label: "testLabel") { + { + y + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + ] + }, + } Else { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + x + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + }, + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_with_condition_on_single_subgraph() { + // This test mostly serves to illustrate why we handle @defer conditions with `ConditionNode` instead of + // just generating only the plan with the @defer and ignoring the `DeferNode` at execution: this is + // because doing can result in sub-par execution for the case where the @defer is disabled (unless of + // course the execution "merges" fetch groups, but it's not trivial to do so). + + let planner = planner!(config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + x: Int + y: Int + } + "#, + ); + assert_plan!(planner, + r#" + query ($cond: Boolean) { + t { + x + ... @defer(if: $cond) { + y + } + } + } + "#, + @r###" + QueryPlan { + Condition(if: $cond) { + Then { + Defer { + Primary { + { + t { + x + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + x + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + y + }: + Flatten(path: "t") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + ] + }, + } Else { + Fetch(service: "Subgraph1") { + { + t { + x + y + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_with_mutliple_conditions_and_labels() { + let planner = planner!(config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + x: Int + u: U + } + + type U @key(fields: "id") { + id: ID! + a: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + y: Int + } + "#, + Subgraph3: r#" + type U @key(fields: "id") { + id: ID! + b: Int + } + "#, + ); + assert_plan!(planner, + r#" + query ($cond1: Boolean, $cond2: Boolean) { + t { + x + ... @defer(if: $cond1, label: "foo") { + y + } + ... @defer(if: $cond2, label: "bar") { + u { + a + ... @defer(if: $cond1) { + b + } + } + } + } + } + "#, + @r###" + QueryPlan { + Condition(if: $cond1) { + Then { + Condition(if: $cond2) { + Then { + Defer { + Primary { + { + t { + x + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + x + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t", label: "bar") { + Defer { + Primary { + { + u { + a + } + }: + Flatten(path: "t") { + Fetch(service: "Subgraph1", id: 1) { + { + ... on T { + __typename + id + } + } => + { + ... on T { + u { + __typename + a + id + } + } + } + }, + }, + }, [ + Deferred(depends: [1], path: "t/u") { + { + b + }: + Flatten(path: "t.u") { + Fetch(service: "Subgraph3") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + b + } + } + }, + }, + }, + ] + }, + }, + Deferred(depends: [0], path: "t", label: "foo") { + { + y + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + ] + }, + } Else { + Defer { + Primary { + { + t { + x + u { + a + } + } + }: + Fetch(service: "Subgraph1", id: 2) { + { + t { + __typename + x + id + u { + __typename + a + id + } + } + } + }, + }, [ + Deferred(depends: [2], path: "t", label: "foo") { + { + y + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + Deferred(depends: [2], path: "t/u") { + { + b + }: + Flatten(path: "t.u") { + Fetch(service: "Subgraph3") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + b + } + } + }, + }, + }, + ] + }, + }, + }, + } Else { + Condition(if: $cond2) { + Then { + Defer { + Primary { + { + t { + x + y + } + }: + Sequence { + Fetch(service: "Subgraph1", id: 3) { + { + t { + __typename + id + x + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + }, [ + Deferred(depends: [3], path: "t", label: "bar") { + { + u { + a + b + } + }: + Sequence { + Flatten(path: "t") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + u { + __typename + id + a + } + } + } + }, + }, + Flatten(path: "t.u") { + Fetch(service: "Subgraph3") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + b + } + } + }, + }, + }, + }, + ] + }, + } Else { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + x + u { + __typename + id + a + } + } + } + }, + Parallel { + Flatten(path: "t.u") { + Fetch(service: "Subgraph3") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + b + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + }, + }, + }, + }, + }, + } + "### + ); +} + +#[test] +fn defer_test_interface_has_different_definitions_between_subgraphs() { + // This test exists to ensure an early bug is fixed: that bug was in the code building + // the `subselection` of `DeferNode` in the plan, and was such that those subselections + // were created with links to subgraph types instead the supergraph ones. As a result, + // we were sometimes trying to add a field (`b` in the example here) to version of a + // type that didn't had that field (the definition of `I` in Subgraph1 here), hence + // running into an assertion error. + + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + i: I + } + + interface I { + a: Int + c: Int + } + + type T implements I @key(fields: "id") { + id: ID! + a: Int + c: Int + } + "#, + Subgraph2: r#" + interface I { + b: Int + } + + type T implements I @key(fields: "id") { + id: ID! + a: Int @external + b: Int @requires(fields: "a") + } + "#, + ); + + assert_plan!(planner, + r#" + query Dimensions { + i { + a + b + ... @defer { + c + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + i { + a + ... on T { + b + } + } + }: + Sequence { + Fetch(service: "Subgraph1") { + { + i { + __typename + a + ... on T { + __typename + id + a + } + c + } + } + }, + Flatten(path: "i") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + a + } + } => + { + ... on T { + b + } + } + }, + }, + }, + }, [ + Deferred(depends: [], path: "i") { + { + c + }: + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_named_fragments_simple() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + x: Int + y: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + ...TestFragment @defer + } + } + + fragment TestFragment on T { + x + y + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + ... on T { + x + y + } + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + x + y + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_fragments_expand_into_same_field_regardless_of_defer() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + x: Int + y: Int + z: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + ...Fragment1 + ...Fragment2 @defer + } + } + + fragment Fragment1 on T { + x + y + } + + fragment Fragment2 on T { + y + z + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + x + y + } + }: + Sequence { + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + x + y + } + } + }, + }, + }, + }, [ + Deferred(depends: [0], path: "t") { + { + ... on T { + y + z + } + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + z + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_can_request_typename_in_fragment() { + // NOTE: There is nothing super special about __typename in theory, but because it's a field + // that is always available in all subghraph (for a type the subgraph has), it tends to create + // multiple options for the query planner, and so excercises some code-paths that triggered an + // early bug in the handling of `@defer` + // (https://github.com/apollographql/federation/issues/2128). + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + x: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + y: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + ...OnT @defer + x + } + } + + fragment OnT on T { + y + __typename + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + x + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + id + x + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + ... on T { + __typename + y + } + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + __typename + y + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_do_not_merge_query_branches_with_defer() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + a: Int + b: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + c: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + a + ... @defer { + b + } + ... @defer { + c + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + a + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + a + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + c + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + c + } + } + }, + }, + }, + Deferred(depends: [0], path: "t") { + { + b + }: + Flatten(path: "t") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + b + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_only_the_key_of_an_entity() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + v0: String + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + v0 + ... @defer { + id + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v0 + } + }: + Fetch(service: "Subgraph1") { + { + t { + v0 + id + } + } + }, + }, [ + Deferred(depends: [], path: "t") { + { + id + }: + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_the_path_in_defer_includes_traversed_fragments() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + i: I + } + + interface I { + x: Int + } + + type A implements I { + x: Int + t: T + } + + type T @key(fields: "id") { + id: ID! + v1: String + v2: String + } + "#, + ); + + assert_plan!(planner, + r#" + { + i { + ... on A { + t { + v1 + ... @defer { + v2 + } + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + i { + ... on A { + t { + v1 + } + } + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + i { + __typename + ... on A { + t { + __typename + v1 + id + } + } + } + } + }, + }, [ + Deferred(depends: [0], path: "i/... on A/t") { + { + v2 + }: + Flatten(path: "i.t") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + }, + ] + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/subscriptions.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/subscriptions.rs index 67c3367c31..b2bd3ed6fb 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/subscriptions.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/subscriptions.rs @@ -1,3 +1,5 @@ +use apollo_compiler::name; +use apollo_compiler::ExecutableDocument; use apollo_federation::query_plan::query_planner::QueryPlanIncrementalDeliveryConfig; use apollo_federation::query_plan::query_planner::QueryPlannerConfig; @@ -135,19 +137,13 @@ fn basic_subscription_with_single_subgraph() { ); } -// TODO(@TylerBloom): Currently, all defer directives are stripped out, so this does not panic -// quite as expected. Instead, it panics because the snapshots doesn't match. Once this behavior is -// changed, this should panic with an error along the lines of "@defer can't be used with -// subscriptions". #[test] -#[should_panic(expected = "snapshot assertion")] -// TODO: Subscription handling fn trying_to_use_defer_with_a_subcription_results_in_an_error() { let config = QueryPlannerConfig { incremental_delivery: QueryPlanIncrementalDeliveryConfig { enable_defer: true }, ..Default::default() }; - let planner = planner!( + let (api_schema, planner) = planner!( config = config, SubgraphA: r#" type Query { @@ -173,8 +169,9 @@ fn trying_to_use_defer_with_a_subcription_results_in_an_error() { address: String! } "#); - assert_plan!( - &planner, + + let document = ExecutableDocument::parse_and_validate( + api_schema.schema(), r#" subscription MySubscription { onNewUser { @@ -186,23 +183,11 @@ fn trying_to_use_defer_with_a_subcription_results_in_an_error() { } } "#, - // This is just a placeholder. We expect the planner to return an Err, which is then - // unwrapped. - @r###" - QueryPlan { - Subscription { - Primary: { - Fetch(service: "subgraphA") { - { - onNewUser { - id - name - } - } - } - }, - } - }, - "### - ); + "trying_to_use_defer_with_a_subcription_results_in_an_error.graphql", + ) + .unwrap(); + + planner + .build_query_plan(&document, Some(name!(MySubscription)), Default::default()) + .expect_err("should return an error"); } diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_can_request_typename_in_fragment.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_can_request_typename_in_fragment.graphql new file mode 100644 index 0000000000..20cd0d93cd --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_can_request_typename_in_fragment.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: e2543fc649c80a566b573ebfad36fc0f7458a3a4 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + x: Int @join__field(graph: SUBGRAPH1) + y: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_everything_within_entity.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_everything_within_entity.graphql new file mode 100644 index 0000000000..3fb06aff10 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_everything_within_entity.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: 428d657ed6389527be73c6ad949cd2fc4da01b20 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + x: Int @join__field(graph: SUBGRAPH1) + y: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_multiple_fields_in_different_subgraphs.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_multiple_fields_in_different_subgraphs.graphql new file mode 100644 index 0000000000..a03862f610 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_multiple_fields_in_different_subgraphs.graphql @@ -0,0 +1,67 @@ +# Composed from subgraphs with hash: 6de814e8aeb455e136ac8627284d67ef4806de57 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") + SUBGRAPH3 @join__graph(name: "Subgraph3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) + @join__type(graph: SUBGRAPH3) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") + @join__type(graph: SUBGRAPH3, key: "id") +{ + id: ID! + v0: String @join__field(graph: SUBGRAPH1) + v1: String @join__field(graph: SUBGRAPH1) + v2: String @join__field(graph: SUBGRAPH2) + v3: String @join__field(graph: SUBGRAPH3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_enity_but_with_unuseful_key.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_enity_but_with_unuseful_key.graphql new file mode 100644 index 0000000000..3467b225d6 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_enity_but_with_unuseful_key.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: e9d8806fbdfd92a23a7dd2af1e343ff3b6de4a7c +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + a: Int @join__field(graph: SUBGRAPH1) + b: Int @join__field(graph: SUBGRAPH1) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_everything_queried.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_everything_queried.graphql new file mode 100644 index 0000000000..3fb06aff10 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_everything_queried.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: 428d657ed6389527be73c6ad949cd2fc4da01b20 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + x: Int @join__field(graph: SUBGRAPH1) + y: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_multi_dependency_deferred_section.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_multi_dependency_deferred_section.graphql new file mode 100644 index 0000000000..987e5b30e3 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_multi_dependency_deferred_section.graphql @@ -0,0 +1,74 @@ +# Composed from subgraphs with hash: cb3bc5e47277ef2c4a364fa62067cfc2426974ec +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") + SUBGRAPH3 @join__graph(name: "Subgraph3", url: "none") + SUBGRAPH4 @join__graph(name: "Subgraph4", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) + @join__type(graph: SUBGRAPH3) + @join__type(graph: SUBGRAPH4) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id0") + @join__type(graph: SUBGRAPH2, key: "id0") + @join__type(graph: SUBGRAPH2, key: "id1") + @join__type(graph: SUBGRAPH3, key: "id0") + @join__type(graph: SUBGRAPH3, key: "id2") + @join__type(graph: SUBGRAPH4, key: "id1 id2") +{ + id0: ID! @join__field(graph: SUBGRAPH1) @join__field(graph: SUBGRAPH2) @join__field(graph: SUBGRAPH3) + v1: Int @join__field(graph: SUBGRAPH1) + id1: ID! @join__field(graph: SUBGRAPH2) @join__field(graph: SUBGRAPH4) + v2: Int @join__field(graph: SUBGRAPH2) + id2: ID! @join__field(graph: SUBGRAPH3) @join__field(graph: SUBGRAPH4) + v3: Int @join__field(graph: SUBGRAPH3) + v4: Int @join__field(graph: SUBGRAPH4) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_in_same_subgraph.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_in_same_subgraph.graphql new file mode 100644 index 0000000000..e2f0483965 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_in_same_subgraph.graphql @@ -0,0 +1,71 @@ +# Composed from subgraphs with hash: e33e3ea89ff4340ccd53321764ac7f1a2684eb3f +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query + mutation: Mutation +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Mutation + @join__type(graph: SUBGRAPH1) +{ + update1: T + update2: T +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v0: String @join__field(graph: SUBGRAPH1) + v1: String @join__field(graph: SUBGRAPH1) + v2: String @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_on_different_subgraphs.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_on_different_subgraphs.graphql new file mode 100644 index 0000000000..b2e03a0693 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_on_different_subgraphs.graphql @@ -0,0 +1,72 @@ +# Composed from subgraphs with hash: 557c634741b7e9f2f4288edb43724e47f1983d19 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query + mutation: Mutation +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Mutation + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + update1: T @join__field(graph: SUBGRAPH1) + update2: T @join__field(graph: SUBGRAPH2) +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v0: String @join__field(graph: SUBGRAPH1) + v1: String @join__field(graph: SUBGRAPH1) + v2: String @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_query_root_type.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_query_root_type.graphql new file mode 100644 index 0000000000..74e78631de --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_query_root_type.graphql @@ -0,0 +1,64 @@ +# Composed from subgraphs with hash: 0a1dc62b0c2282030c10f0e0f777f635d6abd3ba +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type A + @join__type(graph: SUBGRAPH1) +{ + x: Int + y: Int + next: Query +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + op1: Int @join__field(graph: SUBGRAPH1) + op2: A @join__field(graph: SUBGRAPH1) + op3: Int @join__field(graph: SUBGRAPH2) + op4: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_value_types.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_value_types.graphql new file mode 100644 index 0000000000..6167ec55b4 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_value_types.graphql @@ -0,0 +1,76 @@ +# Composed from subgraphs with hash: 01170823ab6c07812976d0983a101d49a319af5c +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Message + @join__type(graph: SUBGRAPH2) +{ + id: ID! + body: MessageBody +} + +type MessageBody + @join__type(graph: SUBGRAPH2) +{ + paragraphs: [String] + lines: Int +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + me: User @join__field(graph: SUBGRAPH1) +} + +type User + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + name: String @join__field(graph: SUBGRAPH1) + messages: [Message] @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_only_the_key_of_an_entity.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_only_the_key_of_an_entity.graphql new file mode 100644 index 0000000000..f41a6d9198 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_only_the_key_of_an_entity.graphql @@ -0,0 +1,58 @@ +# Composed from subgraphs with hash: 176bb27a622184464209652e70141da92aeef370 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) +{ + t: T +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + v0: String +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_resuming_in_the_same_subgraph.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_resuming_in_the_same_subgraph.graphql new file mode 100644 index 0000000000..33e9d9c2d9 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_resuming_in_the_same_subgraph.graphql @@ -0,0 +1,59 @@ +# Composed from subgraphs with hash: 0e99f2e41acb0f707744e78845a55271589146e6 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) +{ + t: T +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + v0: String + v1: String +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_condition_on_single_subgraph.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_condition_on_single_subgraph.graphql new file mode 100644 index 0000000000..b185de90ba --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_condition_on_single_subgraph.graphql @@ -0,0 +1,59 @@ +# Composed from subgraphs with hash: 59e305d633b6e337422f6431c32cc58defa07302 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) +{ + t: T +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + x: Int + y: Int +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_conditions_and_labels.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_conditions_and_labels.graphql new file mode 100644 index 0000000000..20cd0d93cd --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_conditions_and_labels.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: e2543fc649c80a566b573ebfad36fc0f7458a3a4 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + x: Int @join__field(graph: SUBGRAPH1) + y: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_mutliple_conditions_and_labels.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_mutliple_conditions_and_labels.graphql new file mode 100644 index 0000000000..1b7e7bdb70 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_mutliple_conditions_and_labels.graphql @@ -0,0 +1,74 @@ +# Composed from subgraphs with hash: 2135cbed03439b8658c0113e4663849c991e244b +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") + SUBGRAPH3 @join__graph(name: "Subgraph3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) + @join__type(graph: SUBGRAPH3) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + x: Int @join__field(graph: SUBGRAPH1) + u: U @join__field(graph: SUBGRAPH1) + y: Int @join__field(graph: SUBGRAPH2) +} + +type U + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH3, key: "id") +{ + id: ID! + a: Int @join__field(graph: SUBGRAPH1) + b: Int @join__field(graph: SUBGRAPH3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_entity.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_entity.graphql new file mode 100644 index 0000000000..004ed2cf9c --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_entity.graphql @@ -0,0 +1,63 @@ +# Composed from subgraphs with hash: 355f15f0f2699fd21731b0403286c513357860d3 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + me: User @join__field(graph: SUBGRAPH1) +} + +type User + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + name: String @join__field(graph: SUBGRAPH1) + age: Int @join__field(graph: SUBGRAPH2) + address: String @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_value_type.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_value_type.graphql new file mode 100644 index 0000000000..992f860b3a --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_value_type.graphql @@ -0,0 +1,60 @@ +# Composed from subgraphs with hash: f507628640adf8891453e78dbd4280a132706b11 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) +{ + me: User +} + +type User + @join__type(graph: SUBGRAPH1) +{ + id: ID! + name: String + age: Int + address: String +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_do_not_merge_query_branches_with_defer.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_do_not_merge_query_branches_with_defer.graphql new file mode 100644 index 0000000000..6beeae8fd0 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_do_not_merge_query_branches_with_defer.graphql @@ -0,0 +1,63 @@ +# Composed from subgraphs with hash: 6af2331e8c3844fbee6c3888b80b44f51cd5bd3d +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + a: Int @join__field(graph: SUBGRAPH1) + b: Int @join__field(graph: SUBGRAPH1) + c: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_fragments_expand_into_same_field_regardless_of_defer.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_fragments_expand_into_same_field_regardless_of_defer.graphql new file mode 100644 index 0000000000..c952a4d998 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_fragments_expand_into_same_field_regardless_of_defer.graphql @@ -0,0 +1,63 @@ +# Composed from subgraphs with hash: 5382aae137e16d1dfb9955e2a5118b49c75320ea +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + x: Int @join__field(graph: SUBGRAPH2) + y: Int @join__field(graph: SUBGRAPH2) + z: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_with_defer_enabled.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_with_defer_enabled.graphql new file mode 100644 index 0000000000..385d1b4566 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_with_defer_enabled.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: f7c59d88291d4be94f4c484dc849118a08361e69 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v1: Int @join__field(graph: SUBGRAPH2) + v2: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_without_defer_enabled.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_without_defer_enabled.graphql new file mode 100644 index 0000000000..385d1b4566 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_without_defer_enabled.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: f7c59d88291d4be94f4c484dc849118a08361e69 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v1: Int @join__field(graph: SUBGRAPH2) + v2: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_interface_has_different_definitions_between_subgraphs.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_interface_has_different_definitions_between_subgraphs.graphql new file mode 100644 index 0000000000..3fc0c2ca0c --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_interface_has_different_definitions_between_subgraphs.graphql @@ -0,0 +1,74 @@ +# Composed from subgraphs with hash: d280d3aae78ad7080c8df8b5a8982d70a4000a78 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +interface I + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + a: Int @join__field(graph: SUBGRAPH1) + c: Int @join__field(graph: SUBGRAPH1) + b: Int @join__field(graph: SUBGRAPH2) +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + i: I @join__field(graph: SUBGRAPH1) +} + +type T implements I + @join__implements(graph: SUBGRAPH1, interface: "I") + @join__implements(graph: SUBGRAPH2, interface: "I") + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + a: Int @join__field(graph: SUBGRAPH1) @join__field(graph: SUBGRAPH2, external: true) + c: Int @join__field(graph: SUBGRAPH1) + b: Int @join__field(graph: SUBGRAPH2, requires: "a") +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_multiple_non_nested_defer_plus_label_handling.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_multiple_non_nested_defer_plus_label_handling.graphql new file mode 100644 index 0000000000..e35ec30241 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_multiple_non_nested_defer_plus_label_handling.graphql @@ -0,0 +1,75 @@ +# Composed from subgraphs with hash: 09643fc5e0d3abcab8a0f25c0c1e4e16da4169a4 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") + SUBGRAPH3 @join__graph(name: "Subgraph3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) + @join__type(graph: SUBGRAPH3) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v0: String @join__field(graph: SUBGRAPH1) + v1: String @join__field(graph: SUBGRAPH1) + v2: String @join__field(graph: SUBGRAPH2) + v3: U @join__field(graph: SUBGRAPH2) +} + +type U + @join__type(graph: SUBGRAPH2, key: "id") + @join__type(graph: SUBGRAPH3, key: "id") +{ + id: ID! + x: Int @join__field(graph: SUBGRAPH3) + y: Int @join__field(graph: SUBGRAPH3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_named_fragments_simple.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_named_fragments_simple.graphql new file mode 100644 index 0000000000..e8552cbd81 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_named_fragments_simple.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: 395eb0d8e844d7a54e82eec772f007fafcc37a8e +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + x: Int @join__field(graph: SUBGRAPH2) + y: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_nested_defer_on_entities.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_nested_defer_on_entities.graphql new file mode 100644 index 0000000000..10c0f0e1cd --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_nested_defer_on_entities.graphql @@ -0,0 +1,70 @@ +# Composed from subgraphs with hash: 1ddb9374f772bf962933f20e08543315e8df2b01 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Message + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + body: String + author: User +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + me: User @join__field(graph: SUBGRAPH1) +} + +type User + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + name: String @join__field(graph: SUBGRAPH1) + messages: [Message] @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_one.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_one.graphql new file mode 100644 index 0000000000..6f00c992a6 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_one.graphql @@ -0,0 +1,68 @@ +# Composed from subgraphs with hash: 3bee990c996344aa1eb3a7211e3f497fcbe5e1a6 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v: V @join__field(graph: SUBGRAPH2) +} + +type V + @join__type(graph: SUBGRAPH2) +{ + a: Int + b: Int +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_three.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_three.graphql new file mode 100644 index 0000000000..b5cdcdd682 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_three.graphql @@ -0,0 +1,76 @@ +# Composed from subgraphs with hash: c6019a1bd80338506615bb1b60a44fc384586a47 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v: V @join__field(graph: SUBGRAPH2) +} + +type U + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + x: Int @join__field(graph: SUBGRAPH1) +} + +type V + @join__type(graph: SUBGRAPH2) +{ + a: Int + u: U +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_two.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_two.graphql new file mode 100644 index 0000000000..a132722729 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_two.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: fc9fa92848d4ab0e200dcfd09762db2e4281efae +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id", resolvable: false) + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v1: String @join__field(graph: SUBGRAPH1) + v2: String @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_false.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_false.graphql new file mode 100644 index 0000000000..385d1b4566 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_false.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: f7c59d88291d4be94f4c484dc849118a08361e69 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v1: Int @join__field(graph: SUBGRAPH2) + v2: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_true.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_true.graphql new file mode 100644 index 0000000000..385d1b4566 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_true.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: f7c59d88291d4be94f4c484dc849118a08361e69 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v1: Int @join__field(graph: SUBGRAPH2) + v2: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_provides_are_ignored_for_deferred_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_provides_are_ignored_for_deferred_fields.graphql new file mode 100644 index 0000000000..a18f8f7682 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_provides_are_ignored_for_deferred_fields.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: 39be4a27050623ad7a03d8ae9fed684d1e0f3088 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1, provides: "v2") +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v1: Int @join__field(graph: SUBGRAPH1) + v2: Int @join__field(graph: SUBGRAPH1, external: true) @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_requirements_of_deferred_fields_are_deferred.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_requirements_of_deferred_fields_are_deferred.graphql new file mode 100644 index 0000000000..0e2903ca92 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_requirements_of_deferred_fields_are_deferred.graphql @@ -0,0 +1,66 @@ +# Composed from subgraphs with hash: b3f138a89fd35476e9e36002ae4c8c1ab51c4530 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") + SUBGRAPH3 @join__graph(name: "Subgraph3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) + @join__type(graph: SUBGRAPH3) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") + @join__type(graph: SUBGRAPH3, key: "id") +{ + id: ID! + v1: Int @join__field(graph: SUBGRAPH1) + v2: Int @join__field(graph: SUBGRAPH2, requires: "v3") + v3: Int @join__field(graph: SUBGRAPH2, external: true) @join__field(graph: SUBGRAPH3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_the_path_in_defer_includes_traversed_fragments.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_the_path_in_defer_includes_traversed_fragments.graphql new file mode 100644 index 0000000000..82c2cfae78 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_the_path_in_defer_includes_traversed_fragments.graphql @@ -0,0 +1,73 @@ +# Composed from subgraphs with hash: eea1ddd3f3e944aaeb5f39f8f9018fd32bcdaaf6 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type A implements I + @join__implements(graph: SUBGRAPH1, interface: "I") + @join__type(graph: SUBGRAPH1) +{ + x: Int + t: T +} + +interface I + @join__type(graph: SUBGRAPH1) +{ + x: Int +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) +{ + i: I +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + v1: String + v2: String +} From 80dbdc4c993515e75873bef3c14a625f5985a6f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e?= Date: Wed, 25 Sep 2024 12:04:48 +0100 Subject: [PATCH 10/23] fix(deps): remove unused dependency "directories" (#6055) --- Cargo.lock | 10 ---------- apollo-router/Cargo.toml | 1 - 2 files changed, 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d72deae33f..9bf29683e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -268,7 +268,6 @@ dependencies = [ "derive_more", "dhat", "diff", - "directories", "displaydoc", "ecdsa", "flate2", @@ -2303,15 +2302,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "directories" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" -dependencies = [ - "dirs-sys", -] - [[package]] name = "dirs" version = "5.0.1" diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 4675c9a5d7..5d7c36d89f 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -101,7 +101,6 @@ derive_more = { version = "0.99.17", default-features = false, features = [ ] } dhat = { version = "0.3.3", optional = true } diff = "0.1.13" -directories = "5.0.1" displaydoc = "0.2" flate2 = "1.0.30" fred = { version = "7.1.2", features = ["enable-rustls"] } From 53b6a64732ce580be09c6ddeee0c0752b9cadd8d Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Wed, 25 Sep 2024 14:25:49 -0700 Subject: [PATCH 11/23] fix(federation): Unnecessary fragment removal should occur before rebasing (#6060) This PR fixes a bug where unnecessary fragment removal was occurring after rebasing instead of before it (like in JS QP), resulting in cases where the field's parent type wouldn't match the selection set it was being added to. --- apollo-federation/src/operation/mod.rs | 52 +++++++++++++++----------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/apollo-federation/src/operation/mod.rs b/apollo-federation/src/operation/mod.rs index bfab878313..029294e9ac 100644 --- a/apollo-federation/src/operation/mod.rs +++ b/apollo-federation/src/operation/mod.rs @@ -2596,18 +2596,29 @@ impl SelectionSet { self.add_local_selection(&selection)? } } else { + let sub_selection_type_pos = element.sub_selection_type_position()?.ok_or_else(|| { + FederationError::internal("unexpected: Element has a selection set with non-composite base type") + })?; let selection_set = selection_set .map(|selection_set| { - selection_set.rebase_on( - &element.sub_selection_type_position()?.ok_or_else(|| { - FederationError::internal("unexpected: Element has a selection set with non-composite base type") - })?, - &NamedFragments::default(), + let selections = selection_set.without_unnecessary_fragments( + &sub_selection_type_pos, &self.schema, - ) + ); + let mut selection_set = SelectionSet::empty( + self.schema.clone(), + sub_selection_type_pos.clone(), + ); + for selection in selections.iter() { + selection_set.add_local_selection(&selection.rebase_on( + &sub_selection_type_pos, + &NamedFragments::default(), + &self.schema, + )?)?; + } + Ok::<_, FederationError>(selection_set) }) - .transpose()? - .map(|selection_set| selection_set.without_unnecessary_fragments()); + .transpose()?; let selection = Selection::from_element(element, selection_set)?; self.add_local_selection(&selection)? } @@ -2812,12 +2823,15 @@ impl SelectionSet { /// JS PORT NOTE: In Rust implementation we are doing the selection set updates in-place whereas /// JS code was pooling the updates and only apply those when building the final selection set. /// See `makeSelectionSet` method for details. - fn without_unnecessary_fragments(&self) -> SelectionSet { - let parent_type = &self.type_position; - let mut final_selections = SelectionMap::new(); + fn without_unnecessary_fragments( + &self, + parent_type: &CompositeTypeDefinitionPosition, + schema: &ValidFederationSchema, + ) -> Vec { + let mut final_selections = vec![]; fn process_selection_set( selection_set: &SelectionSet, - final_selections: &mut SelectionMap, + final_selections: &mut Vec, parent_type: &CompositeTypeDefinitionPosition, schema: &ValidFederationSchema, ) { @@ -2832,22 +2846,18 @@ impl SelectionSet { schema, ); } else { - final_selections.insert(selection.clone()); + final_selections.push(selection.clone()); } } _ => { - final_selections.insert(selection.clone()); + final_selections.push(selection.clone()); } } } } - process_selection_set(self, &mut final_selections, parent_type, &self.schema); + process_selection_set(self, &mut final_selections, parent_type, schema); - SelectionSet { - schema: self.schema.clone(), - type_position: parent_type.clone(), - selections: Arc::new(final_selections), - } + final_selections } pub(crate) fn iter(&self) -> impl Iterator { @@ -3355,7 +3365,7 @@ impl InlineFragmentSelection { let Some(type_condition) = &self.inline_fragment.type_condition_position else { return true; }; - type_condition == parent + type_condition.type_name() == parent.type_name() || schema .schema() .is_subtype(type_condition.type_name(), parent.type_name()) From 1b11e27b875e77493e2551ad2e503571a7ad5b51 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Thu, 26 Sep 2024 11:43:18 +0200 Subject: [PATCH 12/23] Ensure that we actually check the presence of the cache key for the updated config (#6030) --- apollo-router/tests/integration/redis.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index cbd1f5dc0a..03f0006e4f 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -1064,6 +1064,7 @@ async fn test_redis_query_plan_config_update(updated_config: &str, new_cache_key // If the tests above are failing, this is the key that needs to be changed first. let starting_key = "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:a52c81e3e2e47c8363fbcd2653e196431c15716acc51fce4f58d9368ac4c2d8d"; + assert_ne!(starting_key, new_cache_key, "starting_key (cache key for the initial config) and new_cache_key (cache key with the updated config) should not be equal. This either means that the cache key is not being generated correctly, or that the test is not actually checking the updated key."); router.execute_default_query().await; router.assert_redis_cache_contains(starting_key, None).await; From 35d83a44ae713d584b41e7ffa22069ff0dbe8a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e?= Date: Thu, 26 Sep 2024 10:55:51 +0100 Subject: [PATCH 13/23] telemetry: add metrics for Rust vs. Deno configuration values (#6056) --- .../config_renee_router_768_mode_metrics.md | 9 +++ apollo-router/src/configuration/metrics.rs | 69 +++++++++++++++++++ ...rics__test__experimental_mode_metrics.snap | 16 +++++ ...cs__test__experimental_mode_metrics_2.snap | 16 +++++ ...cs__test__experimental_mode_metrics_3.snap | 16 +++++ 5 files changed, 126 insertions(+) create mode 100644 .changesets/config_renee_router_768_mode_metrics.md create mode 100644 apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics.snap create mode 100644 apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_2.snap create mode 100644 apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_3.snap diff --git a/.changesets/config_renee_router_768_mode_metrics.md b/.changesets/config_renee_router_768_mode_metrics.md new file mode 100644 index 0000000000..840f45ab75 --- /dev/null +++ b/.changesets/config_renee_router_768_mode_metrics.md @@ -0,0 +1,9 @@ +### Add metrics for Rust vs. Deno configuration values ([PR #6056](https://github.com/apollographql/router/pull/6056)) + +We are working on migrating the implementation of several JavaScript components in the router to native Rust versions. + +To track this work, the router now reports the values of the following configuration options to Apollo: +- `apollo.router.config.experimental_query_planner_mode` +- `apollo.router.config.experimental_introspection_mode` + +By [@goto-bus-stop](https://github.com/goto-bus-stop) in https://github.com/apollographql/router/pull/6056 \ No newline at end of file diff --git a/apollo-router/src/configuration/metrics.rs b/apollo-router/src/configuration/metrics.rs index 8e87fa74d0..b8bd132912 100644 --- a/apollo-router/src/configuration/metrics.rs +++ b/apollo-router/src/configuration/metrics.rs @@ -48,6 +48,7 @@ impl Metrics { data.populate_license_instrument(license_state); data.populate_user_plugins_instrument(configuration); data.populate_query_planner_experimental_parallelism(configuration); + data.populate_deno_or_rust_mode_instruments(configuration); data.into() } } @@ -532,6 +533,36 @@ impl InstrumentData { ); } } + + /// Populate metrics on the rollout of experimental Rust replacements of JavaScript code. + pub(crate) fn populate_deno_or_rust_mode_instruments(&mut self, configuration: &Configuration) { + let experimental_query_planner_mode = match configuration.experimental_query_planner_mode { + super::QueryPlannerMode::Legacy => "legacy", + super::QueryPlannerMode::Both => "both", + super::QueryPlannerMode::BothBestEffort => "both_best_effort", + super::QueryPlannerMode::New => "new", + }; + let experimental_introspection_mode = match configuration.experimental_introspection_mode { + super::IntrospectionMode::Legacy => "legacy", + super::IntrospectionMode::Both => "both", + super::IntrospectionMode::New => "new", + }; + + self.data.insert( + "apollo.router.config.experimental_query_planner_mode".to_string(), + ( + 1, + HashMap::from_iter([("mode".to_string(), experimental_query_planner_mode.into())]), + ), + ); + self.data.insert( + "apollo.router.config.experimental_introspection_mode".to_string(), + ( + 1, + HashMap::from_iter([("mode".to_string(), experimental_introspection_mode.into())]), + ), + ); + } } impl From for Metrics { @@ -564,6 +595,8 @@ mod test { use crate::configuration::metrics::InstrumentData; use crate::configuration::metrics::Metrics; + use crate::configuration::IntrospectionMode; + use crate::configuration::QueryPlannerMode; use crate::uplink::license_enforcement::LicenseState; use crate::Configuration; @@ -638,4 +671,40 @@ mod test { let _metrics: Metrics = data.into(); assert_non_zero_metrics_snapshot!(); } + + #[test] + fn test_experimental_mode_metrics() { + let mut data = InstrumentData::default(); + data.populate_deno_or_rust_mode_instruments(&Configuration { + experimental_introspection_mode: IntrospectionMode::Legacy, + experimental_query_planner_mode: QueryPlannerMode::Both, + ..Default::default() + }); + let _metrics: Metrics = data.into(); + assert_non_zero_metrics_snapshot!(); + } + + #[test] + fn test_experimental_mode_metrics_2() { + let mut data = InstrumentData::default(); + // Default query planner value should still be reported + data.populate_deno_or_rust_mode_instruments(&Configuration { + experimental_introspection_mode: IntrospectionMode::New, + ..Default::default() + }); + let _metrics: Metrics = data.into(); + assert_non_zero_metrics_snapshot!(); + } + + #[test] + fn test_experimental_mode_metrics_3() { + let mut data = InstrumentData::default(); + data.populate_deno_or_rust_mode_instruments(&Configuration { + experimental_introspection_mode: IntrospectionMode::New, + experimental_query_planner_mode: QueryPlannerMode::New, + ..Default::default() + }); + let _metrics: Metrics = data.into(); + assert_non_zero_metrics_snapshot!(); + } } diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics.snap new file mode 100644 index 0000000000..3bd3c58289 --- /dev/null +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/configuration/metrics.rs +expression: "&metrics.non_zero()" +--- +- name: apollo.router.config.experimental_introspection_mode + data: + datapoints: + - value: 1 + attributes: + mode: legacy +- name: apollo.router.config.experimental_query_planner_mode + data: + datapoints: + - value: 1 + attributes: + mode: both diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_2.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_2.snap new file mode 100644 index 0000000000..660542eeba --- /dev/null +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_2.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/configuration/metrics.rs +expression: "&metrics.non_zero()" +--- +- name: apollo.router.config.experimental_introspection_mode + data: + datapoints: + - value: 1 + attributes: + mode: new +- name: apollo.router.config.experimental_query_planner_mode + data: + datapoints: + - value: 1 + attributes: + mode: both_best_effort diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_3.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_3.snap new file mode 100644 index 0000000000..ba8bdf43af --- /dev/null +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_3.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/configuration/metrics.rs +expression: "&metrics.non_zero()" +--- +- name: apollo.router.config.experimental_introspection_mode + data: + datapoints: + - value: 1 + attributes: + mode: new +- name: apollo.router.config.experimental_query_planner_mode + data: + datapoints: + - value: 1 + attributes: + mode: new From abe40b6efbb6daa3f40c93c629f3ffbcd702a36e Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Thu, 26 Sep 2024 19:21:32 -0700 Subject: [PATCH 14/23] fix(federation): Check `merge_at`/`inputs` against parent instead of grand parent during grand child merge check (#6068) This PR fixes a bug where in `handle_requires()`, when checking whether a grand child can be merged into its grand parent, we mistakenly compare its `merge_at`/`inputs` against the grand parent instead of the parent (like in JS QP). This can result in cases where a grand child is mistakenly hoisted (or not hoisted) onto its grand parent. --- .../src/query_plan/fetch_dependency_graph.rs | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index ae26e4f1f3..e3ab57f441 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -2094,29 +2094,34 @@ impl FetchDependencyGraph { fn can_merge_grand_child_in( &self, node_id: NodeIndex, - grand_child_id: NodeIndex, + child_id: NodeIndex, + maybe_grand_child_id: NodeIndex, ) -> Result { - let grand_child_parent_relations: Vec = - self.parents_relations_of(grand_child_id).collect(); - if grand_child_parent_relations.len() != 1 { + let Some(grand_child_parent_relation) = + iter_into_single_item(self.parents_relations_of(maybe_grand_child_id)) + else { return Ok(false); - } + }; + let Some(grand_child_parent_parent_relation) = + self.parent_relation(grand_child_parent_relation.parent_node_id, node_id) + else { + return Ok(false); + }; let node = self.node_weight(node_id)?; - let grand_child = self.node_weight(grand_child_id)?; - let grand_child_parent_parent_relation = - self.parent_relation(grand_child_parent_relations[0].parent_node_id, node_id); + let child = self.node_weight(child_id)?; + let grand_child = self.node_weight(maybe_grand_child_id)?; - let (Some(node_inputs), Some(grand_child_inputs)) = (&node.inputs, &grand_child.inputs) + let (Some(child_inputs), Some(grand_child_inputs)) = (&child.inputs, &grand_child.inputs) else { return Ok(false); }; // we compare the subgraph names last because on average it improves performance - Ok(grand_child_parent_relations[0].path_in_parent.is_some() - && grand_child_parent_parent_relation.is_some_and(|r| r.path_in_parent.is_some()) - && node.merge_at == grand_child.merge_at - && node_inputs.contains(grand_child_inputs) + Ok(grand_child_parent_relation.path_in_parent.is_some() + && grand_child_parent_parent_relation.path_in_parent.is_some() + && child.merge_at == grand_child.merge_at + && child_inputs.contains(grand_child_inputs) && node.defer_ref == grand_child.defer_ref && node.subgraph_name == grand_child.subgraph_name) } @@ -4278,9 +4283,11 @@ fn handle_requires( // can merge the node). if parent.path_in_parent.is_some() { for created_node_id in newly_created_node_ids { - if dependency_graph - .can_merge_grand_child_in(parent.parent_node_id, created_node_id)? - { + if dependency_graph.can_merge_grand_child_in( + parent.parent_node_id, + fetch_node_id, + created_node_id, + )? { dependency_graph .merge_grand_child_in(parent.parent_node_id, created_node_id)?; } else { From 07c17829db77a990dc364ddf010a54c1ff35a354 Mon Sep 17 00:00:00 2001 From: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> Date: Fri, 27 Sep 2024 04:16:12 -0500 Subject: [PATCH 15/23] chore: update router-bridge@0.6.3+v2.9.2 (#6069) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Iryna Shestak Co-authored-by: RenΓ©e --- .changesets/chore_update_router_bridge_292.md | 5 +++++ Cargo.lock | 4 ++-- apollo-router/Cargo.toml | 2 +- apollo-router/tests/integration/redis.rs | 14 +++++++------- fuzz/Cargo.toml | 2 +- 5 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 .changesets/chore_update_router_bridge_292.md diff --git a/.changesets/chore_update_router_bridge_292.md b/.changesets/chore_update_router_bridge_292.md new file mode 100644 index 0000000000..ba482bd648 --- /dev/null +++ b/.changesets/chore_update_router_bridge_292.md @@ -0,0 +1,5 @@ +### Update to Federation v2.9.2 ([PR #6069](https://github.com/apollographql/router/pull/6069)) + +This release updates to Federation v2.9.2, with a small fix to the internal `__typename` optimization and a fix to prevent argument name collisions in the `@context`/`@fromContext` directives. + +By [@dariuszkuc](https://github.com/dariuszkuc) in https://github.com/apollographql/router/pull/6069 diff --git a/Cargo.lock b/Cargo.lock index 9bf29683e0..e0ae6cb85f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5599,9 +5599,9 @@ dependencies = [ [[package]] name = "router-bridge" -version = "0.6.2+v2.9.1" +version = "0.6.3+v2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82c217157e756750386a5371da31590c89c315d953ae6d299d73949138e332f" +checksum = "2f183e217b4010e7d37d581b7919ca5e0136a46b6d6b1ff297c52e702bce1089" dependencies = [ "anyhow", "async-channel 1.9.0", diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 5d7c36d89f..14bb6bbf19 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -196,7 +196,7 @@ regex = "1.10.5" reqwest.workspace = true # note: this dependency should _always_ be pinned, prefix the version with an `=` -router-bridge = "=0.6.2+v2.9.1" +router-bridge = "=0.6.3+v2.9.2" rust-embed = { version = "8.4.0", features = ["include-exclude"] } rustls = "0.21.12" diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index 03f0006e4f..bb8bb5c38e 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -48,7 +48,7 @@ use crate::integration::IntegrationTest; async fn query_planner_cache() -> Result<(), BoxError> { // If this test fails and the cache key format changed you'll need to update the key here. // Look at the top of the file for instructions on getting the new cache key. - let known_cache_key = "plan:0:v2.9.1:70f115ebba5991355c17f4f56ba25bb093c519c4db49a30f3b10de279a4e3fa4:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:4f9f0183101b2f249a364b98adadfda6e5e2001d1f2465c988428cf1ac0b545f"; + let known_cache_key = "plan:0:v2.9.2:70f115ebba5991355c17f4f56ba25bb093c519c4db49a30f3b10de279a4e3fa4:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:4f9f0183101b2f249a364b98adadfda6e5e2001d1f2465c988428cf1ac0b545f"; let config = RedisConfig::from_url("redis://127.0.0.1:6379").unwrap(); let client = RedisClient::new(config, None, None, None); @@ -944,7 +944,7 @@ async fn connection_failure_blocks_startup() { async fn query_planner_redis_update_query_fragments() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_query_fragments.router.yaml"), - "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:78f3ccab3def369f4b809a0f8c8f6e90545eb08cd1efeb188ffc663b902c1f2d", + "plan:0:v2.9.2:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:78f3ccab3def369f4b809a0f8c8f6e90545eb08cd1efeb188ffc663b902c1f2d", ) .await; } @@ -974,7 +974,7 @@ async fn query_planner_redis_update_introspection() { // test just passes locally. test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_introspection.router.yaml"), - "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:99a70d6c967eea3bc68721e1094f586f5ae53c7e12f83a650abd5758c372d048", + "plan:0:v2.9.2:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:99a70d6c967eea3bc68721e1094f586f5ae53c7e12f83a650abd5758c372d048", ) .await; } @@ -994,7 +994,7 @@ async fn query_planner_redis_update_defer() { // test just passes locally. test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_defer.router.yaml"), - "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:d6a3d7807bb94cfb26be4daeb35e974680b53755658fafd4c921c70cec1b7c39", + "plan:0:v2.9.2:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:d6a3d7807bb94cfb26be4daeb35e974680b53755658fafd4c921c70cec1b7c39", ) .await; } @@ -1016,7 +1016,7 @@ async fn query_planner_redis_update_type_conditional_fetching() { include_str!( "fixtures/query_planner_redis_config_update_type_conditional_fetching.router.yaml" ), - "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:8991411cc7b66d9f62ab1e661f2ce9ccaf53b0d203a275e43ced9a8b6bba02dd", + "plan:0:v2.9.2:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:8991411cc7b66d9f62ab1e661f2ce9ccaf53b0d203a275e43ced9a8b6bba02dd", ) .await; } @@ -1038,7 +1038,7 @@ async fn query_planner_redis_update_reuse_query_fragments() { include_str!( "fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml" ), - "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:c05e89caeb8efc4e8233e8648099b33414716fe901e714416fd0f65a67867f07", + "plan:0:v2.9.2:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:c05e89caeb8efc4e8233e8648099b33414716fe901e714416fd0f65a67867f07", ) .await; } @@ -1063,7 +1063,7 @@ async fn test_redis_query_plan_config_update(updated_config: &str, new_cache_key router.clear_redis_cache().await; // If the tests above are failing, this is the key that needs to be changed first. - let starting_key = "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:a52c81e3e2e47c8363fbcd2653e196431c15716acc51fce4f58d9368ac4c2d8d"; + let starting_key = "plan:0:v2.9.2:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:a52c81e3e2e47c8363fbcd2653e196431c15716acc51fce4f58d9368ac4c2d8d"; assert_ne!(starting_key, new_cache_key, "starting_key (cache key for the initial config) and new_cache_key (cache key with the updated config) should not be equal. This either means that the cache key is not being generated correctly, or that the test is not actually checking the updated key."); router.execute_default_query().await; diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 38603f704b..797f895ed3 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -20,7 +20,7 @@ reqwest = { workspace = true, features = ["json", "blocking"] } serde_json.workspace = true tokio.workspace = true # note: this dependency should _always_ be pinned, prefix the version with an `=` -router-bridge = "=0.6.2+v2.9.1" +router-bridge = "=0.6.3+v2.9.2" [dev-dependencies] anyhow = "1" From 03a743e03391ca823e760ce9c87c41d562533fca Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 27 Sep 2024 12:03:27 +0200 Subject: [PATCH 16/23] refactor integration tests (#6015) --- apollo-router/tests/common.rs | 5 + apollo-router/tests/samples_tests.rs | 156 +++++++++++++-------------- 2 files changed, 83 insertions(+), 78 deletions(-) diff --git a/apollo-router/tests/common.rs b/apollo-router/tests/common.rs index 664f8970a6..826a377e04 100644 --- a/apollo-router/tests/common.rs +++ b/apollo-router/tests/common.rs @@ -488,6 +488,11 @@ impl IntegrationTest { .expect("must be able to write config"); } + #[allow(dead_code)] + pub fn update_subgraph_overrides(&mut self, overrides: HashMap) { + self._subgraph_overrides = overrides; + } + #[allow(dead_code)] pub async fn update_schema(&self, supergraph_path: &PathBuf) { fs::copy(supergraph_path, &self.test_schema_location).expect("could not write schema"); diff --git a/apollo-router/tests/samples_tests.rs b/apollo-router/tests/samples_tests.rs index e3fd0d5264..22e3b31e18 100644 --- a/apollo-router/tests/samples_tests.rs +++ b/apollo-router/tests/samples_tests.rs @@ -161,7 +161,13 @@ impl TestExecution { Action::ReloadSchema { schema_path } => { self.reload_schema(schema_path, path, out).await } - Action::ReloadSubgraphs { subgraphs } => self.reload_subgraphs(subgraphs, out).await, + Action::ReloadSubgraphs { + subgraphs, + update_url_overrides, + } => { + self.reload_subgraphs(subgraphs, *update_url_overrides, out) + .await + } Action::Request { request, query_path, @@ -193,50 +199,11 @@ impl TestExecution { path: &Path, out: &mut String, ) -> Result<(), Failed> { - let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap(); - let address = listener.local_addr().unwrap(); - let url = format!("http://{address}/"); - - let subgraphs_server = wiremock::MockServer::builder() - .listener(listener) - .start() - .await; - - writeln!(out, "subgraphs listening on {url}").unwrap(); - - let mut subgraph_overrides = HashMap::new(); - - for (name, subgraph) in subgraphs { - for SubgraphRequestMock { request, response } in &subgraph.requests { - let mut builder = Mock::given(body_partial_json(&request.body)); - - if let Some(s) = request.method.as_deref() { - builder = builder.and(method(s)); - } - - if let Some(s) = request.path.as_deref() { - builder = builder.and(wiremock::matchers::path(s)); - } - - for (header_name, header_value) in &request.headers { - builder = builder.and(header(header_name.as_str(), header_value.as_str())); - } - - let mut res = ResponseTemplate::new(response.status.unwrap_or(200)); - for (header_name, header_value) in &response.headers { - res = res.append_header(header_name.as_str(), header_value.as_str()); - } - builder - .respond_with(res.set_body_json(&response.body)) - .mount(&subgraphs_server) - .await; - } + self.subgraphs = subgraphs.clone(); + let (mut subgraphs_server, url) = self.start_subgraphs(out).await; - // Add a default override for products, if not specified - subgraph_overrides - .entry(name.to_string()) - .or_insert(url.clone()); - } + let subgraph_overrides = self.load_subgraph_mocks(&mut subgraphs_server, &url).await; + writeln!(out, "got subgraph mocks: {subgraph_overrides:?}").unwrap(); let config = open_file(&path.join(configuration_path), out)?; let schema_path = path.join(schema_path); @@ -253,7 +220,6 @@ impl TestExecution { self.router = Some(router); self.subgraphs_server = Some(subgraphs_server); - self.subgraphs = subgraphs.clone(); self.configuration_path = Some(configuration_path.to_string()); Ok(()) @@ -265,6 +231,21 @@ impl TestExecution { path: &Path, out: &mut String, ) -> Result<(), Failed> { + let mut subgraphs_server = match self.subgraphs_server.take() { + Some(subgraphs_server) => subgraphs_server, + None => self.start_subgraphs(out).await.0, + }; + subgraphs_server.reset().await; + + let subgraph_url = Self::subgraph_url(&subgraphs_server); + let subgraph_overrides = self + .load_subgraph_mocks(&mut subgraphs_server, &subgraph_url) + .await; + + let config = open_file(&path.join(configuration_path), out)?; + self.configuration_path = Some(configuration_path.to_string()); + self.subgraphs_server = Some(subgraphs_server); + let router = match self.router.as_mut() { None => { writeln!( @@ -277,6 +258,14 @@ impl TestExecution { Some(router) => router, }; + router.update_subgraph_overrides(subgraph_overrides); + router.update_config(&config).await; + router.assert_reloaded().await; + + Ok(()) + } + + async fn start_subgraphs(&mut self, out: &mut String) -> (MockServer, String) { let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap(); let address = listener.local_addr().unwrap(); let url = format!("http://{address}/"); @@ -288,6 +277,18 @@ impl TestExecution { writeln!(out, "subgraphs listening on {url}").unwrap(); + (subgraphs_server, url) + } + + fn subgraph_url(server: &MockServer) -> String { + format!("http://{}/", server.address()) + } + + async fn load_subgraph_mocks( + &mut self, + subgraphs_server: &mut MockServer, + url: &str, + ) -> HashMap { let mut subgraph_overrides = HashMap::new(); for (name, subgraph) in &self.subgraphs { @@ -312,61 +313,57 @@ impl TestExecution { } builder .respond_with(res.set_body_json(&response.body)) - .mount(&subgraphs_server) + .mount(subgraphs_server) .await; } // Add a default override for products, if not specified subgraph_overrides .entry(name.to_string()) - .or_insert(url.clone()); + .or_insert(url.to_owned()); } - let config = open_file(&path.join(configuration_path), out)?; - self.configuration_path = Some(configuration_path.to_string()); - self.subgraphs_server = Some(subgraphs_server); - - router.update_config(&config).await; - router.assert_reloaded().await; - - Ok(()) + subgraph_overrides } async fn reload_subgraphs( &mut self, subgraphs: &HashMap, + update_url_overrides: bool, out: &mut String, ) -> Result<(), Failed> { writeln!(out, "reloading subgraphs with: {subgraphs:?}").unwrap(); - let subgraphs_server = self.subgraphs_server.as_mut().unwrap(); + let mut subgraphs_server = match self.subgraphs_server.take() { + Some(subgraphs_server) => subgraphs_server, + None => self.start_subgraphs(out).await.0, + }; subgraphs_server.reset().await; - for subgraph in subgraphs.values() { - for SubgraphRequestMock { request, response } in &subgraph.requests { - let mut builder = Mock::given(body_partial_json(&request.body)); - - if let Some(s) = request.method.as_deref() { - builder = builder.and(method(s)); - } - - if let Some(s) = request.path.as_deref() { - builder = builder.and(wiremock::matchers::path(s)); - } + self.subgraphs = subgraphs.clone(); - for (header_name, header_value) in &request.headers { - builder = builder.and(header(header_name.as_str(), header_value.as_str())); - } + let subgraph_url = Self::subgraph_url(&subgraphs_server); + let subgraph_overrides = self + .load_subgraph_mocks(&mut subgraphs_server, &subgraph_url) + .await; + self.subgraphs_server = Some(subgraphs_server); - let mut res = ResponseTemplate::new(response.status.unwrap_or(200)); - for (header_name, header_value) in &response.headers { - res = res.append_header(header_name.as_str(), header_value.as_str()); - } - builder - .respond_with(res.set_body_json(&response.body)) - .mount(subgraphs_server) - .await; + let router = match self.router.as_mut() { + None => { + writeln!( + out, + "cannot reload subgraph overrides: router was not started" + ) + .unwrap(); + return Err(out.into()); } + Some(router) => router, + }; + + if update_url_overrides { + router.update_subgraph_overrides(subgraph_overrides); + router.touch_config().await; + router.assert_reloaded().await; } Ok(()) @@ -576,6 +573,9 @@ enum Action { }, ReloadSubgraphs { subgraphs: HashMap, + // set to true if subgraph URL overrides should be updated (ex: a new subgraph is added) + #[serde(default)] + update_url_overrides: bool, }, Request { request: Value, From 7ae8b4cc06ca45456395b80eeb6429b7ff79defb Mon Sep 17 00:00:00 2001 From: Coenen Benjamin Date: Fri, 27 Sep 2024 13:49:15 +0200 Subject: [PATCH 17/23] fix(telemetry): display custom event attributes properly on subscription events (#6033) Signed-off-by: Benjamin <5719034+bnjjj@users.noreply.github.com> Co-authored-by: Jesse Rosenberger Co-authored-by: Iryna Shestak --- .../fix_bnjjj_fix_telemetry_on_event.md | 23 +++++++++++++++++++ .changesets/fix_latte_lens_tent_blaze.md | 5 ++++ .../plugins/telemetry/config_new/events.rs | 6 ++++- .../plugins/telemetry/dynamic_attribute.rs | 4 +++- .../src/plugins/telemetry/tracing/jaeger.rs | 3 ++- .../telemetry/fixtures/json.router.yaml | 6 +++++ .../fixtures/json.sampler_off.router.yaml | 6 +++++ .../telemetry/fixtures/text.router.yaml | 6 +++++ .../tests/integration/telemetry/logging.rs | 12 ++++++++++ 9 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 .changesets/fix_bnjjj_fix_telemetry_on_event.md create mode 100644 .changesets/fix_latte_lens_tent_blaze.md diff --git a/.changesets/fix_bnjjj_fix_telemetry_on_event.md b/.changesets/fix_bnjjj_fix_telemetry_on_event.md new file mode 100644 index 0000000000..5ad8607118 --- /dev/null +++ b/.changesets/fix_bnjjj_fix_telemetry_on_event.md @@ -0,0 +1,23 @@ +### Display custom event attributes properly on subscription events ([PR #6033](https://github.com/apollographql/router/pull/6033)) + +Custom event attributes set using selectors at the supergraph level is now displayed properly. Example of configuration: + +```yaml title=router.yaml +telemetry: + instrumentation: + events: + supergraph: + supergraph.event: + message: supergraph event + on: event_response # on every supergraph event (like subscription event for example) + level: info + attributes: + test: + static: foo + response.data: + response_data: $ # Display all the response data payload + response.errors: + response_errors: $ # Display all the response errors payload +``` + +By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/6033 \ No newline at end of file diff --git a/.changesets/fix_latte_lens_tent_blaze.md b/.changesets/fix_latte_lens_tent_blaze.md new file mode 100644 index 0000000000..c8a401efa7 --- /dev/null +++ b/.changesets/fix_latte_lens_tent_blaze.md @@ -0,0 +1,5 @@ +### Internal `apollo_private.*` attributes are not sent to the Jaeger collector ([PR #6033](https://github.com/apollographql/router/pull/6033)) + +When using Jaeger collector to send traces you will no longer receive span attributes with the `apollo_private.` prefix which is a reserved internal keyword not intended to be externalized. + +By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/6033 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/events.rs b/apollo-router/src/plugins/telemetry/config_new/events.rs index e3bbd668f1..a58264bfb7 100644 --- a/apollo-router/src/plugins/telemetry/config_new/events.rs +++ b/apollo-router/src/plugins/telemetry/config_new/events.rs @@ -9,6 +9,7 @@ use parking_lot::Mutex; use schemars::JsonSchema; use serde::Deserialize; use tower::BoxError; +use tracing::info_span; use tracing::Span; use super::instruments::Instrumented; @@ -738,7 +739,10 @@ where let mut new_attributes = selectors.on_response_event(response, ctx); attributes.append(&mut new_attributes); } - + // Stub span to make sure the custom attributes are saved in current span extensions + // It won't be extracted or sampled at all + let span = info_span!("supergraph_event_send_event"); + let _entered = span.enter(); inner.send_event(attributes); } diff --git a/apollo-router/src/plugins/telemetry/dynamic_attribute.rs b/apollo-router/src/plugins/telemetry/dynamic_attribute.rs index 0b4af0964d..d9cde3ca21 100644 --- a/apollo-router/src/plugins/telemetry/dynamic_attribute.rs +++ b/apollo-router/src/plugins/telemetry/dynamic_attribute.rs @@ -240,7 +240,9 @@ impl EventDynAttribute for ::tracing::Span { self.with_subscriber(move |(id, dispatch)| { if let Some(reg) = dispatch.downcast_ref::() { match reg.span(id) { - None => eprintln!("no spanref, this is a bug"), + None => { + eprintln!("no spanref, this is a bug"); + } Some(s) => { if s.is_sampled() { let mut extensions = s.extensions_mut(); diff --git a/apollo-router/src/plugins/telemetry/tracing/jaeger.rs b/apollo-router/src/plugins/telemetry/tracing/jaeger.rs index 13f61da5c4..f50aebefc2 100644 --- a/apollo-router/src/plugins/telemetry/tracing/jaeger.rs +++ b/apollo-router/src/plugins/telemetry/tracing/jaeger.rs @@ -139,7 +139,8 @@ impl TracingConfigurator for Config { Ok(builder.with_span_processor( BatchSpanProcessor::builder(exporter, runtime::Tokio) .with_batch_config(batch_processor.clone().into()) - .build(), + .build() + .filtered(), )) } _ => Ok(builder), diff --git a/apollo-router/tests/integration/telemetry/fixtures/json.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/json.router.yaml index 2e8b047638..fa8fba775e 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/json.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/json.router.yaml @@ -68,6 +68,12 @@ telemetry: eq: - "log" - response_header: "x-log-request" + my.response_event.on_event: + message: "my response event message" + level: warn + on: event_response + attributes: + on_supergraph_response_event: on_supergraph_event subgraph: # Standard events request: info diff --git a/apollo-router/tests/integration/telemetry/fixtures/json.sampler_off.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/json.sampler_off.router.yaml index 1bbd2ac994..3190c14d34 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/json.sampler_off.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/json.sampler_off.router.yaml @@ -51,6 +51,12 @@ telemetry: eq: - "log" - request_header: "x-log-request" + my.response_event.on_event: + message: "my response event message" + level: warn + on: event_response + attributes: + on_supergraph_response_event: on_supergraph_event my.request.event: message: "my event message" level: info diff --git a/apollo-router/tests/integration/telemetry/fixtures/text.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/text.router.yaml index 66c7508443..ef009e55bd 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/text.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/text.router.yaml @@ -52,6 +52,12 @@ telemetry: eq: - "log" - request_header: "x-log-request" + my.response_event.on_event: + message: "my response event message" + level: warn + on: event_response + attributes: + on_supergraph_response_event: on_supergraph_event my.request.event: message: "my event message" level: info diff --git a/apollo-router/tests/integration/telemetry/logging.rs b/apollo-router/tests/integration/telemetry/logging.rs index a50f0fa20e..64b43fe032 100644 --- a/apollo-router/tests/integration/telemetry/logging.rs +++ b/apollo-router/tests/integration/telemetry/logging.rs @@ -30,6 +30,10 @@ async fn test_json() -> Result<(), BoxError> { router.execute_query(&query).await; router.assert_log_contains(r#""static_one":"test""#).await; router.execute_query(&query).await; + router + .assert_log_contains(r#""on_supergraph_response_event":"on_supergraph_event""#) + .await; + router.execute_query(&query).await; router.assert_log_contains(r#""response_status":200"#).await; router.graceful_shutdown().await; @@ -160,6 +164,10 @@ async fn test_json_sampler_off() -> Result<(), BoxError> { router.execute_query(&query).await; router.assert_log_contains(r#""static_one":"test""#).await; router.execute_query(&query).await; + router + .assert_log_contains(r#""on_supergraph_response_event":"on_supergraph_event""#) + .await; + router.execute_query(&query).await; router.assert_log_contains(r#""response_status":200"#).await; router.graceful_shutdown().await; @@ -188,6 +196,10 @@ async fn test_text() -> Result<(), BoxError> { router.assert_log_contains("trace_id").await; router.execute_query(&query).await; router.assert_log_contains("span_id").await; + router + .assert_log_contains(r#"on_supergraph_response_event=on_supergraph_event"#) + .await; + router.execute_query(&query).await; router.execute_query(&query).await; router.assert_log_contains("response_status=200").await; router.graceful_shutdown().await; From 7a5ab18faba2bf99a94949efda3adeab30e61130 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 27 Sep 2024 15:01:26 +0300 Subject: [PATCH 18/23] prep release: v1.56.0-rc.0 --- Cargo.lock | 8 ++++---- apollo-federation/Cargo.toml | 2 +- apollo-router-benchmarks/Cargo.toml | 2 +- apollo-router-scaffold/Cargo.toml | 2 +- apollo-router-scaffold/templates/base/Cargo.template.toml | 2 +- .../templates/base/xtask/Cargo.template.toml | 2 +- apollo-router/Cargo.toml | 4 ++-- dockerfiles/tracing/docker-compose.datadog.yml | 2 +- dockerfiles/tracing/docker-compose.jaeger.yml | 2 +- dockerfiles/tracing/docker-compose.zipkin.yml | 2 +- helm/chart/router/Chart.yaml | 4 ++-- helm/chart/router/README.md | 6 +++--- licenses.html | 3 +-- scripts/install.sh | 2 +- 14 files changed, 21 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e0ae6cb85f..6ebad5b9f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,7 +178,7 @@ dependencies = [ [[package]] name = "apollo-federation" -version = "1.55.0" +version = "1.56.0-rc.0" dependencies = [ "apollo-compiler", "derive_more", @@ -229,7 +229,7 @@ dependencies = [ [[package]] name = "apollo-router" -version = "1.55.0" +version = "1.56.0-rc.0" dependencies = [ "access-json", "ahash", @@ -399,7 +399,7 @@ dependencies = [ [[package]] name = "apollo-router-benchmarks" -version = "1.55.0" +version = "1.56.0-rc.0" dependencies = [ "apollo-parser", "apollo-router", @@ -415,7 +415,7 @@ dependencies = [ [[package]] name = "apollo-router-scaffold" -version = "1.55.0" +version = "1.56.0-rc.0" dependencies = [ "anyhow", "cargo-scaffold", diff --git a/apollo-federation/Cargo.toml b/apollo-federation/Cargo.toml index 5de40a5380..34095017a6 100644 --- a/apollo-federation/Cargo.toml +++ b/apollo-federation/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-federation" -version = "1.55.0" +version = "1.56.0-rc.0" authors = ["The Apollo GraphQL Contributors"] edition = "2021" description = "Apollo Federation" diff --git a/apollo-router-benchmarks/Cargo.toml b/apollo-router-benchmarks/Cargo.toml index 38759d2feb..f0100d5cc0 100644 --- a/apollo-router-benchmarks/Cargo.toml +++ b/apollo-router-benchmarks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-benchmarks" -version = "1.55.0" +version = "1.56.0-rc.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/Cargo.toml b/apollo-router-scaffold/Cargo.toml index 4801a6c616..80d9ac6cf2 100644 --- a/apollo-router-scaffold/Cargo.toml +++ b/apollo-router-scaffold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-scaffold" -version = "1.55.0" +version = "1.56.0-rc.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/templates/base/Cargo.template.toml b/apollo-router-scaffold/templates/base/Cargo.template.toml index a1c3d2a42d..7051dcfa2b 100644 --- a/apollo-router-scaffold/templates/base/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/Cargo.template.toml @@ -22,7 +22,7 @@ apollo-router = { path ="{{integration_test}}apollo-router" } apollo-router = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} # Note if you update these dependencies then also update xtask/Cargo.toml -apollo-router = "1.55.0" +apollo-router = "1.56.0-rc.0" {{/if}} {{/if}} async-trait = "0.1.52" diff --git a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml index c566a3a0ca..66bdda3358 100644 --- a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml @@ -13,7 +13,7 @@ apollo-router-scaffold = { path ="{{integration_test}}apollo-router-scaffold" } {{#if branch}} apollo-router-scaffold = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} -apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.55.0" } +apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.56.0-rc.0" } {{/if}} {{/if}} anyhow = "1.0.58" diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 14bb6bbf19..bbe7a8d85e 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router" -version = "1.55.0" +version = "1.56.0-rc.0" authors = ["Apollo Graph, Inc. "] repository = "https://github.com/apollographql/router/" documentation = "https://docs.rs/apollo-router" @@ -68,7 +68,7 @@ askama = "0.12.1" access-json = "0.1.0" anyhow = "1.0.86" apollo-compiler.workspace = true -apollo-federation = { path = "../apollo-federation", version = "=1.55.0" } +apollo-federation = { path = "../apollo-federation", version = "=1.56.0-rc.0" } arc-swap = "1.6.0" async-channel = "1.9.0" async-compression = { version = "0.4.6", features = [ diff --git a/dockerfiles/tracing/docker-compose.datadog.yml b/dockerfiles/tracing/docker-compose.datadog.yml index 3b58c1d702..1b92f0a758 100644 --- a/dockerfiles/tracing/docker-compose.datadog.yml +++ b/dockerfiles/tracing/docker-compose.datadog.yml @@ -3,7 +3,7 @@ services: apollo-router: container_name: apollo-router - image: ghcr.io/apollographql/router:v1.55.0 + image: ghcr.io/apollographql/router:v1.56.0-rc.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/datadog.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.jaeger.yml b/dockerfiles/tracing/docker-compose.jaeger.yml index fcb75930d2..7a98df892d 100644 --- a/dockerfiles/tracing/docker-compose.jaeger.yml +++ b/dockerfiles/tracing/docker-compose.jaeger.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router #build: ./router - image: ghcr.io/apollographql/router:v1.55.0 + image: ghcr.io/apollographql/router:v1.56.0-rc.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/jaeger.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.zipkin.yml b/dockerfiles/tracing/docker-compose.zipkin.yml index 0cb933a7ac..312787b9eb 100644 --- a/dockerfiles/tracing/docker-compose.zipkin.yml +++ b/dockerfiles/tracing/docker-compose.zipkin.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router build: ./router - image: ghcr.io/apollographql/router:v1.55.0 + image: ghcr.io/apollographql/router:v1.56.0-rc.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/zipkin.router.yaml:/etc/config/configuration.yaml diff --git a/helm/chart/router/Chart.yaml b/helm/chart/router/Chart.yaml index 7a1c6d615a..b7cfba00c1 100644 --- a/helm/chart/router/Chart.yaml +++ b/helm/chart/router/Chart.yaml @@ -20,10 +20,10 @@ type: application # so it matches the shape of our release process and release automation. # By proxy of that decision, this version uses SemVer 2.0.0, though the prefix # of "v" is not included. -version: 1.55.0 +version: 1.56.0-rc.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v1.55.0" +appVersion: "v1.56.0-rc.0" diff --git a/helm/chart/router/README.md b/helm/chart/router/README.md index fb09765b10..5621d63d49 100644 --- a/helm/chart/router/README.md +++ b/helm/chart/router/README.md @@ -2,7 +2,7 @@ [router](https://github.com/apollographql/router) Rust Graph Routing runtime for Apollo Federation -![Version: 1.55.0](https://img.shields.io/badge/Version-1.55.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.55.0](https://img.shields.io/badge/AppVersion-v1.55.0-informational?style=flat-square) +![Version: 1.56.0-rc.0](https://img.shields.io/badge/Version-1.56.0--rc.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.56.0-rc.0](https://img.shields.io/badge/AppVersion-v1.56.0--rc.0-informational?style=flat-square) ## Prerequisites @@ -11,7 +11,7 @@ ## Get Repo Info ```console -helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0 +helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.56.0-rc.0 ``` ## Install Chart @@ -19,7 +19,7 @@ helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0 **Important:** only helm3 is supported ```console -helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0 --values my-values.yaml +helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.56.0-rc.0 --values my-values.yaml ``` _See [configuration](#configuration) below._ diff --git a/licenses.html b/licenses.html index e415d536c4..764c755e37 100644 --- a/licenses.html +++ b/licenses.html @@ -44,7 +44,7 @@

Third Party Licenses

Overview of licenses:

    -
  • Apache License 2.0 (448)
  • +
  • Apache License 2.0 (447)
  • MIT License (155)
  • BSD 3-Clause "New" or "Revised" License (11)
  • ISC License (8)
  • @@ -5057,7 +5057,6 @@

    Used by:

    Apache License 2.0

    Used by:

    diff --git a/scripts/install.sh b/scripts/install.sh index f03b383d8d..92757f9108 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -11,7 +11,7 @@ BINARY_DOWNLOAD_PREFIX="https://github.com/apollographql/router/releases/downloa # Router version defined in apollo-router's Cargo.toml # Note: Change this line manually during the release steps. -PACKAGE_VERSION="v1.55.0" +PACKAGE_VERSION="v1.56.0-rc.0" download_binary() { downloader --check From b46050272b6ff9f246d3ff25a44bc42594fd4308 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 27 Sep 2024 15:04:18 +0300 Subject: [PATCH 19/23] Changelog and docs prep to save time later --- .changesets/chore_update_router_bridge.md | 8 -------- .changesets/chore_update_router_bridge_292.md | 3 +++ docs/source/federation-version-support.mdx | 10 +++++++++- 3 files changed, 12 insertions(+), 9 deletions(-) delete mode 100644 .changesets/chore_update_router_bridge.md diff --git a/.changesets/chore_update_router_bridge.md b/.changesets/chore_update_router_bridge.md deleted file mode 100644 index bc23827874..0000000000 --- a/.changesets/chore_update_router_bridge.md +++ /dev/null @@ -1,8 +0,0 @@ -> [!IMPORTANT] -> If you have enabled [Distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), this release changes the hashing algorithm used for the cache keys. On account of this, you should anticipate additional cache regeneration cost when updating between these versions while the new hashing algorithm comes into service. - -### Update to Federation v2.9.1 ([PR #6029](https://github.com/apollographql/router/pull/6029)) - -This release updates to Federation v2.9.1, which fixes edge cases in subgraph extraction logic when using spec renaming or spec URLs (e.g., `specs.apollo.dev`) that could impact the planner's ability to plan a query. - -By [@lrlna](https://github.com/lrlna) in https://github.com/apollographql/router/pull/6027 diff --git a/.changesets/chore_update_router_bridge_292.md b/.changesets/chore_update_router_bridge_292.md index ba482bd648..f31efb3bd5 100644 --- a/.changesets/chore_update_router_bridge_292.md +++ b/.changesets/chore_update_router_bridge_292.md @@ -1,3 +1,6 @@ +> [!IMPORTANT] +> If you have enabled [Distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), this release changes the hashing algorithm used for the cache keys. On account of this, you should anticipate additional cache regeneration cost when updating between these versions while the new hashing algorithm comes into service. + ### Update to Federation v2.9.2 ([PR #6069](https://github.com/apollographql/router/pull/6069)) This release updates to Federation v2.9.2, with a small fix to the internal `__typename` optimization and a fix to prevent argument name collisions in the `@context`/`@fromContext` directives. diff --git a/docs/source/federation-version-support.mdx b/docs/source/federation-version-support.mdx index 80ff912745..defc58f1c4 100644 --- a/docs/source/federation-version-support.mdx +++ b/docs/source/federation-version-support.mdx @@ -37,7 +37,15 @@ The table below shows which version of federation each router release is compile - v1.55.0 and later (see latest releases) + v1.56.0 and later (see latest releases) + + + 2.9.2 + + + + + v1.55.0 2.9.1 From f1c88672ca585486284f68829c12947add3e8d29 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 1 Oct 2024 16:44:23 +0300 Subject: [PATCH 20/23] Apply suggestions from code review Co-authored-by: Edward Huang --- .changesets/config_renee_router_768_mode_metrics.md | 3 +-- .changesets/feat_feat_key_from_file.md | 8 ++++---- .changesets/fix_bnjjj_fix_telemetry_on_event.md | 6 ++++-- .changesets/fix_latte_lens_tent_blaze.md | 4 ++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.changesets/config_renee_router_768_mode_metrics.md b/.changesets/config_renee_router_768_mode_metrics.md index 840f45ab75..9a11c7e618 100644 --- a/.changesets/config_renee_router_768_mode_metrics.md +++ b/.changesets/config_renee_router_768_mode_metrics.md @@ -1,8 +1,7 @@ ### Add metrics for Rust vs. Deno configuration values ([PR #6056](https://github.com/apollographql/router/pull/6056)) -We are working on migrating the implementation of several JavaScript components in the router to native Rust versions. +To help track the migration from JavaScript (Deno) to native Rust implementations, the router now reports the values of the following configuration options to Apollo: -To track this work, the router now reports the values of the following configuration options to Apollo: - `apollo.router.config.experimental_query_planner_mode` - `apollo.router.config.experimental_introspection_mode` diff --git a/.changesets/feat_feat_key_from_file.md b/.changesets/feat_feat_key_from_file.md index afa3090192..b4307e207c 100644 --- a/.changesets/feat_feat_key_from_file.md +++ b/.changesets/feat_feat_key_from_file.md @@ -1,9 +1,9 @@ -### feat: allow users to load apollo key from file ([PR #5917](https://github.com/apollographql/router/pull/5917)) +### Support loading Apollo key from file ([PR #5917](https://github.com/apollographql/router/pull/5917)) -Users sometimes would rather not pass sensitive keys to the router through environment variables out of an abundance of caution. To help address this, you can now pass an argument `--apollo-key-path` or env var `APOLLO_KEY_PATH`, that takes a file location as an argument which is read and then used as the Apollo key for use with Uplink and usage reporting. +You can now specific the location to a file containing the Apollo key that's used by Apollo Uplink and usage reporting. The router now supports both the `--apollo-key-path` CLI argument and the `APOLLO_KEY_PATH` environment variable for passing the file containing your Apollo key. -This addresses a portion of #3264, specifically the APOLLO_KEY. +Previously, the router supported only the `APOLLO_KEY` environment variable to provide the key. The new CLI argument and environment variable help users who prefer not to pass sensitive keys through environment variables. -Note: This feature is not available on Windows. +Note: This feature is unavailable for Windows. By [@lleadbet](https://github.com/lleadbet) in https://github.com/apollographql/router/pull/5917 diff --git a/.changesets/fix_bnjjj_fix_telemetry_on_event.md b/.changesets/fix_bnjjj_fix_telemetry_on_event.md index 5ad8607118..92d83288d8 100644 --- a/.changesets/fix_bnjjj_fix_telemetry_on_event.md +++ b/.changesets/fix_bnjjj_fix_telemetry_on_event.md @@ -1,6 +1,8 @@ -### Display custom event attributes properly on subscription events ([PR #6033](https://github.com/apollographql/router/pull/6033)) +### Fix displaying custom event attributes on subscription events ([PR #6033](https://github.com/apollographql/router/pull/6033)) -Custom event attributes set using selectors at the supergraph level is now displayed properly. Example of configuration: +The router now properly displays custom event attributes that are set with selectors at the supergraph level. + +An example configuration: ```yaml title=router.yaml telemetry: diff --git a/.changesets/fix_latte_lens_tent_blaze.md b/.changesets/fix_latte_lens_tent_blaze.md index c8a401efa7..ca139696e8 100644 --- a/.changesets/fix_latte_lens_tent_blaze.md +++ b/.changesets/fix_latte_lens_tent_blaze.md @@ -1,5 +1,5 @@ -### Internal `apollo_private.*` attributes are not sent to the Jaeger collector ([PR #6033](https://github.com/apollographql/router/pull/6033)) +### Prevent sending internal `apollo_private.*` attributes to Jaeger collector ([PR #6033](https://github.com/apollographql/router/pull/6033)) -When using Jaeger collector to send traces you will no longer receive span attributes with the `apollo_private.` prefix which is a reserved internal keyword not intended to be externalized. +When using the router's Jaeger collector to send traces, you will no longer receive span attributes with the `apollo_private.` prefix. Those attributes were incorrectly sent, as that prefix is reserved for internal attributes. By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/6033 \ No newline at end of file From 99e472de8ff6fed81486d492336b60b1e9c3d817 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 1 Oct 2024 17:44:57 +0300 Subject: [PATCH 21/23] prep release: v1.56.0 --- .changesets/chore_update_router_bridge_292.md | 8 --- .../config_renee_router_768_mode_metrics.md | 8 --- .changesets/feat_feat_key_from_file.md | 9 --- .../fix_bnjjj_fix_telemetry_on_event.md | 25 ------- .changesets/fix_latte_lens_tent_blaze.md | 5 -- CHANGELOG.md | 70 +++++++++++++++++++ Cargo.lock | 8 +-- apollo-federation/Cargo.toml | 2 +- apollo-router-benchmarks/Cargo.toml | 2 +- apollo-router-scaffold/Cargo.toml | 2 +- .../templates/base/Cargo.template.toml | 2 +- .../templates/base/xtask/Cargo.template.toml | 2 +- apollo-router/Cargo.toml | 4 +- .../tracing/docker-compose.datadog.yml | 2 +- dockerfiles/tracing/docker-compose.jaeger.yml | 2 +- dockerfiles/tracing/docker-compose.zipkin.yml | 2 +- helm/chart/router/Chart.yaml | 4 +- helm/chart/router/README.md | 6 +- scripts/install.sh | 2 +- 19 files changed, 90 insertions(+), 75 deletions(-) delete mode 100644 .changesets/chore_update_router_bridge_292.md delete mode 100644 .changesets/config_renee_router_768_mode_metrics.md delete mode 100644 .changesets/feat_feat_key_from_file.md delete mode 100644 .changesets/fix_bnjjj_fix_telemetry_on_event.md delete mode 100644 .changesets/fix_latte_lens_tent_blaze.md diff --git a/.changesets/chore_update_router_bridge_292.md b/.changesets/chore_update_router_bridge_292.md deleted file mode 100644 index f31efb3bd5..0000000000 --- a/.changesets/chore_update_router_bridge_292.md +++ /dev/null @@ -1,8 +0,0 @@ -> [!IMPORTANT] -> If you have enabled [Distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), this release changes the hashing algorithm used for the cache keys. On account of this, you should anticipate additional cache regeneration cost when updating between these versions while the new hashing algorithm comes into service. - -### Update to Federation v2.9.2 ([PR #6069](https://github.com/apollographql/router/pull/6069)) - -This release updates to Federation v2.9.2, with a small fix to the internal `__typename` optimization and a fix to prevent argument name collisions in the `@context`/`@fromContext` directives. - -By [@dariuszkuc](https://github.com/dariuszkuc) in https://github.com/apollographql/router/pull/6069 diff --git a/.changesets/config_renee_router_768_mode_metrics.md b/.changesets/config_renee_router_768_mode_metrics.md deleted file mode 100644 index 9a11c7e618..0000000000 --- a/.changesets/config_renee_router_768_mode_metrics.md +++ /dev/null @@ -1,8 +0,0 @@ -### Add metrics for Rust vs. Deno configuration values ([PR #6056](https://github.com/apollographql/router/pull/6056)) - -To help track the migration from JavaScript (Deno) to native Rust implementations, the router now reports the values of the following configuration options to Apollo: - -- `apollo.router.config.experimental_query_planner_mode` -- `apollo.router.config.experimental_introspection_mode` - -By [@goto-bus-stop](https://github.com/goto-bus-stop) in https://github.com/apollographql/router/pull/6056 \ No newline at end of file diff --git a/.changesets/feat_feat_key_from_file.md b/.changesets/feat_feat_key_from_file.md deleted file mode 100644 index b4307e207c..0000000000 --- a/.changesets/feat_feat_key_from_file.md +++ /dev/null @@ -1,9 +0,0 @@ -### Support loading Apollo key from file ([PR #5917](https://github.com/apollographql/router/pull/5917)) - -You can now specific the location to a file containing the Apollo key that's used by Apollo Uplink and usage reporting. The router now supports both the `--apollo-key-path` CLI argument and the `APOLLO_KEY_PATH` environment variable for passing the file containing your Apollo key. - -Previously, the router supported only the `APOLLO_KEY` environment variable to provide the key. The new CLI argument and environment variable help users who prefer not to pass sensitive keys through environment variables. - -Note: This feature is unavailable for Windows. - -By [@lleadbet](https://github.com/lleadbet) in https://github.com/apollographql/router/pull/5917 diff --git a/.changesets/fix_bnjjj_fix_telemetry_on_event.md b/.changesets/fix_bnjjj_fix_telemetry_on_event.md deleted file mode 100644 index 92d83288d8..0000000000 --- a/.changesets/fix_bnjjj_fix_telemetry_on_event.md +++ /dev/null @@ -1,25 +0,0 @@ -### Fix displaying custom event attributes on subscription events ([PR #6033](https://github.com/apollographql/router/pull/6033)) - -The router now properly displays custom event attributes that are set with selectors at the supergraph level. - -An example configuration: - -```yaml title=router.yaml -telemetry: - instrumentation: - events: - supergraph: - supergraph.event: - message: supergraph event - on: event_response # on every supergraph event (like subscription event for example) - level: info - attributes: - test: - static: foo - response.data: - response_data: $ # Display all the response data payload - response.errors: - response_errors: $ # Display all the response errors payload -``` - -By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/6033 \ No newline at end of file diff --git a/.changesets/fix_latte_lens_tent_blaze.md b/.changesets/fix_latte_lens_tent_blaze.md deleted file mode 100644 index ca139696e8..0000000000 --- a/.changesets/fix_latte_lens_tent_blaze.md +++ /dev/null @@ -1,5 +0,0 @@ -### Prevent sending internal `apollo_private.*` attributes to Jaeger collector ([PR #6033](https://github.com/apollographql/router/pull/6033)) - -When using the router's Jaeger collector to send traces, you will no longer receive span attributes with the `apollo_private.` prefix. Those attributes were incorrectly sent, as that prefix is reserved for internal attributes. - -By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/6033 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 419cee0eb9..4afaf10640 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,76 @@ All notable changes to Router will be documented in this file. This project adheres to [Semantic Versioning v2.0.0](https://semver.org/spec/v2.0.0.html). +# [1.56.0] - 2024-10-01 + +> [!IMPORTANT] +> If you have enabled [Distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), this release changes the hashing algorithm used for the cache keys. On account of this, you should anticipate additional cache regeneration cost when updating between these versions while the new hashing algorithm comes into service. + +## πŸš€ Features + +### Support loading Apollo key from file ([PR #5917](https://github.com/apollographql/router/pull/5917)) + +You can now specific the location to a file containing the Apollo key that's used by Apollo Uplink and usage reporting. The router now supports both the `--apollo-key-path` CLI argument and the `APOLLO_KEY_PATH` environment variable for passing the file containing your Apollo key. + +Previously, the router supported only the `APOLLO_KEY` environment variable to provide the key. The new CLI argument and environment variable help users who prefer not to pass sensitive keys through environment variables. + +Note: This feature is unavailable for Windows. + +By [@lleadbet](https://github.com/lleadbet) in https://github.com/apollographql/router/pull/5917 + +## πŸ› Fixes + +### Prevent sending internal `apollo_private.*` attributes to Jaeger collector ([PR #6033](https://github.com/apollographql/router/pull/6033)) + +When using the router's Jaeger collector to send traces, you will no longer receive span attributes with the `apollo_private.` prefix. Those attributes were incorrectly sent, as that prefix is reserved for internal attributes. + +By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/6033 + +### Fix displaying custom event attributes on subscription events ([PR #6033](https://github.com/apollographql/router/pull/6033)) + +The router now properly displays custom event attributes that are set with selectors at the supergraph level. + +An example configuration: + +```yaml title=router.yaml +telemetry: + instrumentation: + events: + supergraph: + supergraph.event: + message: supergraph event + on: event_response # on every supergraph event (like subscription event for example) + level: info + attributes: + test: + static: foo + response.data: + response_data: $ # Display all the response data payload + response.errors: + response_errors: $ # Display all the response errors payload +``` + +By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/6033 + +### Update to Federation v2.9.2 ([PR #6069](https://github.com/apollographql/router/pull/6069)) + +This release updates to Federation v2.9.2, with a small fix to the internal `__typename` optimization and a fix to prevent argument name collisions in the `@context`/`@fromContext` directives. + +By [@dariuszkuc](https://github.com/dariuszkuc) in https://github.com/apollographql/router/pull/6069 + +## πŸ“ƒ Configuration + +### Add metrics for Rust vs. Deno configuration values ([PR #6056](https://github.com/apollographql/router/pull/6056)) + +To help track the migration from JavaScript (Deno) to native Rust implementations, the router now reports the values of the following configuration options to Apollo: + +- `apollo.router.config.experimental_query_planner_mode` +- `apollo.router.config.experimental_introspection_mode` + +By [@goto-bus-stop](https://github.com/goto-bus-stop) in https://github.com/apollographql/router/pull/6056 + + + # [1.55.0] - 2024-09-24 > [!IMPORTANT] diff --git a/Cargo.lock b/Cargo.lock index 6ebad5b9f0..8c5d20f688 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,7 +178,7 @@ dependencies = [ [[package]] name = "apollo-federation" -version = "1.56.0-rc.0" +version = "1.56.0" dependencies = [ "apollo-compiler", "derive_more", @@ -229,7 +229,7 @@ dependencies = [ [[package]] name = "apollo-router" -version = "1.56.0-rc.0" +version = "1.56.0" dependencies = [ "access-json", "ahash", @@ -399,7 +399,7 @@ dependencies = [ [[package]] name = "apollo-router-benchmarks" -version = "1.56.0-rc.0" +version = "1.56.0" dependencies = [ "apollo-parser", "apollo-router", @@ -415,7 +415,7 @@ dependencies = [ [[package]] name = "apollo-router-scaffold" -version = "1.56.0-rc.0" +version = "1.56.0" dependencies = [ "anyhow", "cargo-scaffold", diff --git a/apollo-federation/Cargo.toml b/apollo-federation/Cargo.toml index 34095017a6..448a7398cd 100644 --- a/apollo-federation/Cargo.toml +++ b/apollo-federation/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-federation" -version = "1.56.0-rc.0" +version = "1.56.0" authors = ["The Apollo GraphQL Contributors"] edition = "2021" description = "Apollo Federation" diff --git a/apollo-router-benchmarks/Cargo.toml b/apollo-router-benchmarks/Cargo.toml index f0100d5cc0..65114fead0 100644 --- a/apollo-router-benchmarks/Cargo.toml +++ b/apollo-router-benchmarks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-benchmarks" -version = "1.56.0-rc.0" +version = "1.56.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/Cargo.toml b/apollo-router-scaffold/Cargo.toml index 80d9ac6cf2..1f0b865959 100644 --- a/apollo-router-scaffold/Cargo.toml +++ b/apollo-router-scaffold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-scaffold" -version = "1.56.0-rc.0" +version = "1.56.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/templates/base/Cargo.template.toml b/apollo-router-scaffold/templates/base/Cargo.template.toml index 7051dcfa2b..8dd36bf095 100644 --- a/apollo-router-scaffold/templates/base/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/Cargo.template.toml @@ -22,7 +22,7 @@ apollo-router = { path ="{{integration_test}}apollo-router" } apollo-router = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} # Note if you update these dependencies then also update xtask/Cargo.toml -apollo-router = "1.56.0-rc.0" +apollo-router = "1.56.0" {{/if}} {{/if}} async-trait = "0.1.52" diff --git a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml index 66bdda3358..b6d72bde79 100644 --- a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml @@ -13,7 +13,7 @@ apollo-router-scaffold = { path ="{{integration_test}}apollo-router-scaffold" } {{#if branch}} apollo-router-scaffold = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} -apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.56.0-rc.0" } +apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.56.0" } {{/if}} {{/if}} anyhow = "1.0.58" diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index bbe7a8d85e..fc48d928eb 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router" -version = "1.56.0-rc.0" +version = "1.56.0" authors = ["Apollo Graph, Inc. "] repository = "https://github.com/apollographql/router/" documentation = "https://docs.rs/apollo-router" @@ -68,7 +68,7 @@ askama = "0.12.1" access-json = "0.1.0" anyhow = "1.0.86" apollo-compiler.workspace = true -apollo-federation = { path = "../apollo-federation", version = "=1.56.0-rc.0" } +apollo-federation = { path = "../apollo-federation", version = "=1.56.0" } arc-swap = "1.6.0" async-channel = "1.9.0" async-compression = { version = "0.4.6", features = [ diff --git a/dockerfiles/tracing/docker-compose.datadog.yml b/dockerfiles/tracing/docker-compose.datadog.yml index 1b92f0a758..4b3558d572 100644 --- a/dockerfiles/tracing/docker-compose.datadog.yml +++ b/dockerfiles/tracing/docker-compose.datadog.yml @@ -3,7 +3,7 @@ services: apollo-router: container_name: apollo-router - image: ghcr.io/apollographql/router:v1.56.0-rc.0 + image: ghcr.io/apollographql/router:v1.56.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/datadog.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.jaeger.yml b/dockerfiles/tracing/docker-compose.jaeger.yml index 7a98df892d..ca7b4ab265 100644 --- a/dockerfiles/tracing/docker-compose.jaeger.yml +++ b/dockerfiles/tracing/docker-compose.jaeger.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router #build: ./router - image: ghcr.io/apollographql/router:v1.56.0-rc.0 + image: ghcr.io/apollographql/router:v1.56.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/jaeger.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.zipkin.yml b/dockerfiles/tracing/docker-compose.zipkin.yml index 312787b9eb..564933ab0c 100644 --- a/dockerfiles/tracing/docker-compose.zipkin.yml +++ b/dockerfiles/tracing/docker-compose.zipkin.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router build: ./router - image: ghcr.io/apollographql/router:v1.56.0-rc.0 + image: ghcr.io/apollographql/router:v1.56.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/zipkin.router.yaml:/etc/config/configuration.yaml diff --git a/helm/chart/router/Chart.yaml b/helm/chart/router/Chart.yaml index b7cfba00c1..9cbe1d5e12 100644 --- a/helm/chart/router/Chart.yaml +++ b/helm/chart/router/Chart.yaml @@ -20,10 +20,10 @@ type: application # so it matches the shape of our release process and release automation. # By proxy of that decision, this version uses SemVer 2.0.0, though the prefix # of "v" is not included. -version: 1.56.0-rc.0 +version: 1.56.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v1.56.0-rc.0" +appVersion: "v1.56.0" diff --git a/helm/chart/router/README.md b/helm/chart/router/README.md index 5621d63d49..181bb9602c 100644 --- a/helm/chart/router/README.md +++ b/helm/chart/router/README.md @@ -2,7 +2,7 @@ [router](https://github.com/apollographql/router) Rust Graph Routing runtime for Apollo Federation -![Version: 1.56.0-rc.0](https://img.shields.io/badge/Version-1.56.0--rc.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.56.0-rc.0](https://img.shields.io/badge/AppVersion-v1.56.0--rc.0-informational?style=flat-square) +![Version: 1.56.0](https://img.shields.io/badge/Version-1.56.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.56.0](https://img.shields.io/badge/AppVersion-v1.56.0-informational?style=flat-square) ## Prerequisites @@ -11,7 +11,7 @@ ## Get Repo Info ```console -helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.56.0-rc.0 +helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.56.0 ``` ## Install Chart @@ -19,7 +19,7 @@ helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.56.0-rc.0 **Important:** only helm3 is supported ```console -helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.56.0-rc.0 --values my-values.yaml +helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.56.0 --values my-values.yaml ``` _See [configuration](#configuration) below._ diff --git a/scripts/install.sh b/scripts/install.sh index 92757f9108..1346bb9b5e 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -11,7 +11,7 @@ BINARY_DOWNLOAD_PREFIX="https://github.com/apollographql/router/releases/downloa # Router version defined in apollo-router's Cargo.toml # Note: Change this line manually during the release steps. -PACKAGE_VERSION="v1.56.0-rc.0" +PACKAGE_VERSION="v1.56.0" download_binary() { downloader --check From e0849ab65bdbf7f9e5a1da183f206b9861044a0f Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 1 Oct 2024 19:00:33 +0300 Subject: [PATCH 22/23] Changelog additions for query planner preview --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4afaf10640..9944c873fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,14 @@ This project adheres to [Semantic Versioning v2.0.0](https://semver.org/spec/v2. ## πŸš€ Features +## Native query planner is now in public preview + +The native query planner is now in public preview. You can configure the `experimental_query_planner_mode` option in the router configuration YAML to change the mode of the native query planner. The following modes are available: + +- `new`: Enable _only_ the new Rust-native query planner in the hot-path of query execution. +- `legacy`: Enable _only_ the legacy JavaScript query planner in the hot-path of query execution. +- `both_best_effort`: Enables _both_ the new and legacy query planners. They are configured in a comparison-based mode of operation with the legacy planner in the hot-path and the and the new planner in the cold-path. Comparisions are made between the two plans on a sampled basis and metrics are available to analyze the differences in aggregate. + ### Support loading Apollo key from file ([PR #5917](https://github.com/apollographql/router/pull/5917)) You can now specific the location to a file containing the Apollo key that's used by Apollo Uplink and usage reporting. The router now supports both the `--apollo-key-path` CLI argument and the `APOLLO_KEY_PATH` environment variable for passing the file containing your Apollo key. @@ -21,6 +29,7 @@ Note: This feature is unavailable for Windows. By [@lleadbet](https://github.com/lleadbet) in https://github.com/apollographql/router/pull/5917 + ## πŸ› Fixes ### Prevent sending internal `apollo_private.*` attributes to Jaeger collector ([PR #6033](https://github.com/apollographql/router/pull/6033)) From ce58a2527471ebaf324e756bc859dc4e219e2a0f Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 1 Oct 2024 19:36:08 +0300 Subject: [PATCH 23/23] Update CHANGELOG.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: RenΓ©e --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9944c873fe..24f87558fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ The native query planner is now in public preview. You can configure the `experi - `new`: Enable _only_ the new Rust-native query planner in the hot-path of query execution. - `legacy`: Enable _only_ the legacy JavaScript query planner in the hot-path of query execution. -- `both_best_effort`: Enables _both_ the new and legacy query planners. They are configured in a comparison-based mode of operation with the legacy planner in the hot-path and the and the new planner in the cold-path. Comparisions are made between the two plans on a sampled basis and metrics are available to analyze the differences in aggregate. +- `both_best_effort`: Enables _both_ the new and legacy query planners. They are configured in a comparison-based mode of operation with the legacy planner in the hot-path and the and the new planner in the cold-path. Comparisons are made between the two plans on a sampled basis and metrics are available to analyze the differences in aggregate. ### Support loading Apollo key from file ([PR #5917](https://github.com/apollographql/router/pull/5917))