From 5a0826e6f22568bf61b99447501e2711bbf36edc Mon Sep 17 00:00:00 2001 From: Chris Lenfest Date: Thu, 23 May 2024 04:42:23 -0500 Subject: [PATCH] Add ability for router to deal with query plans with contextual rewrites (#5097) Two main things that we're doing in this PR. 1. We've added a variable to FetchNode called `context_rewrites`. This is a vector of DataRewrite::KeyRenamer that are specifically taking data from their path (which will be relative and can traverse up the data path) and writes the data into an argument that is passed to the selection set. 2. There are two cases. In the most straightforward, the data that is passed to the selection set is the same for every entity. This case is pretty easy and doesn't require any special handling. In the second case, the value of the variable may be different per entity. If that is true, we need to use aliasing and duplication in our query in order to send it to subgraphs. Once https://github.com/graphql/composite-schemas-spec/issues/25 is decided and has subgraph support, this query cloning will be able to go away. Co-authored-by: o0Ignition0o Co-authored-by: Gary Pennington --- .changesets/feat_clenfest_set_context.md | 7 + .config/nextest.toml | 6 +- Cargo.lock | 54 +- .../src/link/join_spec_definition.rs | 4 + apollo-federation/src/query_plan/display.rs | 1 + .../src/query_plan/fetch_dependency_graph.rs | 1 + apollo-federation/src/query_plan/mod.rs | 3 + .../src/query_plan/query_planner.rs | 1 + .../query_plan/build_query_plan_tests.rs | 29 +- .../build_query_plan_tests/provides.rs | 171 +++---- apollo-router/Cargo.toml | 4 +- ...dden_field_yields_expected_query_plan.snap | 1 + ...dden_field_yields_expected_query_plan.snap | 2 + ...y_plan__tests__it_expose_query_plan-2.snap | 4 + ...ery_plan__tests__it_expose_query_plan.snap | 4 + apollo-router/src/query_planner/convert.rs | 3 + apollo-router/src/query_planner/execution.rs | 7 + apollo-router/src/query_planner/fetch.rs | 73 ++- apollo-router/src/query_planner/mod.rs | 1 + apollo-router/src/query_planner/rewrites.rs | 2 +- ...ridge_query_planner__tests__plan_root.snap | 1 + ..._planner__tests__query_plan_from_json.snap | 5 + .../src/query_planner/subgraph_context.rs | 460 ++++++++++++++++++ .../src/query_planner/subscription.rs | 1 + apollo-router/src/query_planner/tests.rs | 11 + .../src/services/execution/service.rs | 3 + .../src/uplink/license_enforcement.rs | 42 ++ ...icense_enforcement__test__set_context.snap | 12 + .../src/uplink/testdata/set_context.graphql | 165 +++++++ .../tests/fixtures/set_context/one.json | 414 ++++++++++++++++ .../fixtures/set_context/supergraph.graphql | 165 +++++++ .../tests/fixtures/set_context/two.json | 43 ++ apollo-router/tests/integration/redis.rs | 14 +- ...ts__integration__redis__query_planner.snap | 1 + apollo-router/tests/set_context.rs | 325 +++++++++++++ .../snapshots/set_context__set_context.snap | 101 ++++ ...__set_context_dependent_fetch_failure.snap | 98 ++++ .../set_context__set_context_list.snap | 108 ++++ ...et_context__set_context_list_of_lists.snap | 113 +++++ ...set_context__set_context_no_typenames.snap | 99 ++++ ...et_context__set_context_type_mismatch.snap | 99 ++++ .../set_context__set_context_union.snap | 157 ++++++ ...__set_context_unrelated_fetch_failure.snap | 170 +++++++ .../set_context__set_context_with_null.snap | 99 ++++ ..._conditions__type_conditions_disabled.snap | 2 + ...e_conditions__type_conditions_enabled.snap | 3 + ...ions_enabled_generate_query_fragments.snap | 3 + ..._type_conditions_enabled_list_of_list.snap | 3 + ...nditions_enabled_list_of_list_of_list.snap | 3 + ...s_enabled_shouldnt_make_article_fetch.snap | 3 + fuzz/Cargo.toml | 2 +- 51 files changed, 2953 insertions(+), 150 deletions(-) create mode 100644 .changesets/feat_clenfest_set_context.md create mode 100644 apollo-router/src/query_planner/subgraph_context.rs create mode 100644 apollo-router/src/uplink/snapshots/apollo_router__uplink__license_enforcement__test__set_context.snap create mode 100644 apollo-router/src/uplink/testdata/set_context.graphql create mode 100644 apollo-router/tests/fixtures/set_context/one.json create mode 100644 apollo-router/tests/fixtures/set_context/supergraph.graphql create mode 100644 apollo-router/tests/fixtures/set_context/two.json create mode 100644 apollo-router/tests/set_context.rs create mode 100644 apollo-router/tests/snapshots/set_context__set_context.snap create mode 100644 apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure.snap create mode 100644 apollo-router/tests/snapshots/set_context__set_context_list.snap create mode 100644 apollo-router/tests/snapshots/set_context__set_context_list_of_lists.snap create mode 100644 apollo-router/tests/snapshots/set_context__set_context_no_typenames.snap create mode 100644 apollo-router/tests/snapshots/set_context__set_context_type_mismatch.snap create mode 100644 apollo-router/tests/snapshots/set_context__set_context_union.snap create mode 100644 apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure.snap create mode 100644 apollo-router/tests/snapshots/set_context__set_context_with_null.snap diff --git a/.changesets/feat_clenfest_set_context.md b/.changesets/feat_clenfest_set_context.md new file mode 100644 index 0000000000..025348f883 --- /dev/null +++ b/.changesets/feat_clenfest_set_context.md @@ -0,0 +1,7 @@ +### Add ability for router to deal with query plans with contextual rewrites ([PR #5097](https://github.com/apollographql/router/pull/5097)) + +Adds the ability for the router to execute query plans with context rewrites on them. These are generated by the @fromContext directive and are used to map a Value in the collected data JSON onto a variable which will in turn be used as an argument to a field resolver. + +⚠️ This ships with a new version of federation, which means distributed caches will be repopulated. + +By [@clenfest](https://github.com/clenfest) in https://github.com/apollographql/router/pull/5097 \ No newline at end of file diff --git a/.config/nextest.toml b/.config/nextest.toml index f081bbc39a..df8bab66e6 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -19,6 +19,6 @@ path = "junit.xml" # Integration tests require more than one thread. The default setting of 1 will cause too many integration tests to run # at the same time and causes tests to fail where timing is involved. # This filter applies only to to the integration tests in the apollo-router package. -[[profile.default.overrides]] -filter = 'package(apollo-router) & kind(test)' -threads-required = 2 +[[profile.ci.overrides]] +filter = 'test(/^apollo-router::/)' +threads-required = 4 diff --git a/Cargo.lock b/Cargo.lock index c7e0d1f43d..4b039c23aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1374,9 +1374,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "bytes-utils" @@ -1940,9 +1940,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.0.0" +version = "4.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f711ade317dd348950a9910f81c5947e3d8907ebd2b83f76203ff1807e6a2bc2" +checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" dependencies = [ "cfg-if", "cpufeatures", @@ -2419,7 +2419,7 @@ dependencies = [ "digest 0.10.7", "elliptic-curve 0.13.8", "rfc6979 0.4.0", - "signature 2.0.0", + "signature 2.2.0", "spki 0.7.2", ] @@ -2659,9 +2659,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.1.20" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" @@ -3954,9 +3954,9 @@ dependencies = [ [[package]] name = "libz-ng-sys" -version = "1.1.12" +version = "1.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dd9f43e75536a46ee0f92b758f6b63846e594e86638c61a9251338a65baea63" +checksum = "c6409efc61b12687963e602df8ecf70e8ddacf95bc6576bcf16e3ac6328083c5" dependencies = [ "cmake", "libc", @@ -4150,9 +4150,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] @@ -5195,7 +5195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit 0.19.14", + "toml_edit 0.19.15", ] [[package]] @@ -5776,9 +5776,9 @@ dependencies = [ [[package]] name = "router-bridge" -version = "0.5.21+v2.7.5" +version = "0.5.24+v2.8.0-alpha.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2142445fe3fe2aae7a3c3c5083d1211a448a0dabb489a14dd90d427cf6c0b13" +checksum = "4a4b92a40b68c797d2d624716d5671e80de578f00c5012af37f19c74b175566b" dependencies = [ "anyhow", "async-channel 1.9.0", @@ -6149,9 +6149,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" dependencies = [ "serde_derive", ] @@ -6167,9 +6167,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" dependencies = [ "proc-macro2 1.0.76", "quote 1.0.35", @@ -6201,9 +6201,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "indexmap 2.2.3", "itoa", @@ -6385,9 +6385,9 @@ dependencies = [ [[package]] name = "signature" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", "rand_core 0.6.4", @@ -7070,9 +7070,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.14" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.2.3", "toml_datetime", @@ -7609,9 +7609,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-id" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d70b6494226b36008c8366c288d77190b3fad2eb4c10533139c1c1f461127f1a" +checksum = "b1b6def86329695390197b82c1e244a54a131ceb66c996f2088a3876e2ae083f" [[package]] name = "unicode-ident" @@ -8205,7 +8205,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" dependencies = [ - "curve25519-dalek 4.0.0", + "curve25519-dalek 4.1.2", "rand_core 0.6.4", "serde", "zeroize", diff --git a/apollo-federation/src/link/join_spec_definition.rs b/apollo-federation/src/link/join_spec_definition.rs index c2b011e244..bf8734a4e6 100644 --- a/apollo-federation/src/link/join_spec_definition.rs +++ b/apollo-federation/src/link/join_spec_definition.rs @@ -336,6 +336,10 @@ lazy_static! { Version { major: 0, minor: 3 }, Some(Version { major: 2, minor: 0 }), )); + definitions.add(JoinSpecDefinition::new( + Version { major: 0, minor: 5 }, + Some(Version { major: 2, minor: 8 }), + )); definitions }; diff --git a/apollo-federation/src/query_plan/display.rs b/apollo-federation/src/query_plan/display.rs index 8c91f3eac1..37042d08d7 100644 --- a/apollo-federation/src/query_plan/display.rs +++ b/apollo-federation/src/query_plan/display.rs @@ -83,6 +83,7 @@ impl FetchNode { operation_kind: _, input_rewrites: _, output_rewrites: _, + context_rewrites: _, } = self; state.write(format_args!("Fetch(service: {subgraph_name:?}"))?; if let Some(id) = id { diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index c23054a4e8..271cf95dc0 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -1320,6 +1320,7 @@ impl FetchDependencyGraphNode { operation_kind: self.root_kind.into(), input_rewrites: self.input_rewrites.clone(), output_rewrites, + context_rewrites: Default::default(), })); Ok(Some(if let Some(path) = self.merge_at.clone() { diff --git a/apollo-federation/src/query_plan/mod.rs b/apollo-federation/src/query_plan/mod.rs index 14659b5935..ce19b6cb7f 100644 --- a/apollo-federation/src/query_plan/mod.rs +++ b/apollo-federation/src/query_plan/mod.rs @@ -86,6 +86,9 @@ pub struct FetchNode { /// Similar to `input_rewrites`, but for optional "rewrites" to apply to the data that is /// received from a fetch (and before it is applied to the current in-memory results). pub output_rewrites: Vec>, + /// Similar to the other kinds of rewrites. This is a mechanism to convert a contextual path into + /// an argument to a resolver + pub context_rewrites: Vec>, } #[derive(Debug, Clone)] diff --git a/apollo-federation/src/query_plan/query_planner.rs b/apollo-federation/src/query_plan/query_planner.rs index 8697dd118c..ae5085c9ab 100644 --- a/apollo-federation/src/query_plan/query_planner.rs +++ b/apollo-federation/src/query_plan/query_planner.rs @@ -349,6 +349,7 @@ impl QueryPlanner { requires: Default::default(), input_rewrites: Default::default(), output_rewrites: Default::default(), + context_rewrites: Default::default(), }; return Ok(QueryPlan::new(node, statistics)); 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 4c8bf1f13b..0e56f58f87 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests.rs @@ -153,8 +153,6 @@ fn pick_keys_that_minimize_fetches() { /// (more precisely, this force the query planner to _consider_ type explosion; the generated /// query plan still ends up not type-exploding in practice since as it's not necessary). #[test] -#[should_panic(expected = "snapshot assertion")] -// TODO: investigate this failure fn field_covariance_and_type_explosion() { let planner = planner!( Subgraph1: r#" @@ -195,23 +193,22 @@ fn field_covariance_and_type_explosion() { } "#, @r###" - QueryPlan { - Fetch(service: "Subgraph1") { - { - dummy { + QueryPlan { + Fetch(service: "Subgraph1") { + { + dummy { + field { + __typename + ... on Object { + field { __typename - field { - __typename - ... on Object { - field { - __typename - } - } - } } } - }, + } } - "### + } + }, + } + "### ); } diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/provides.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/provides.rs index 23ba713f10..79cc960d0e 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/provides.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/provides.rs @@ -302,49 +302,58 @@ fn it_works_on_unions() { // This is our sanity check: we first query _without_ the provides // to make sure we _do_ need to go the the second subgraph. @r###" - QueryPlan { - Sequence { - Fetch(service: "Subgraph1") { + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + noProvides { + ... on T1 { + __typename + id + } + ... on T2 { + __typename + id + a + } + } + } + }, + Parallel { + Flatten(path: "noProvides") { + Fetch(service: "Subgraph2") { { - noProvides { + ... on T2 { __typename - ... on T1 { - __typename - id - } - ... on T2 { - __typename - id - a - } + id + } + } => + { + ... on T2 { + b } } }, - Flatten(path: "noProvides") { - Fetch(service: "Subgraph2") { - { - ... on T1 { - __typename - id - } - ... on T2 { - __typename - id - } - } => - { - ... on T1 { - a - } - ... on T2 { - b - } + }, + Flatten(path: "noProvides") { + Fetch(service: "Subgraph2") { + { + ... on T1 { + __typename + id } - }, + } => + { + ... on T1 { + a + } + } }, }, - } - "### + }, + }, + } + "### ); // Ensuring that querying only `a` can be done with subgraph1 only when provided. @@ -363,22 +372,21 @@ fn it_works_on_unions() { } "#, @r###" - QueryPlan { - Fetch(service: "Subgraph1") { - { - withProvidesForT1 { - __typename - ... on T1 { - a - } - ... on T2 { - a - } - } + QueryPlan { + Fetch(service: "Subgraph1") { + { + withProvidesForT1 { + ... on T1 { + a } - }, + ... on T2 { + a + } + } } - "### + }, + } + "### ); // But ensure that querying `b` still goes to subgraph2 if only a is provided. @@ -398,41 +406,40 @@ fn it_works_on_unions() { } "#, @r###" - QueryPlan { - Sequence { - Fetch(service: "Subgraph1") { - { - withProvidesForT1 { - __typename - ... on T1 { - a - } - ... on T2 { - __typename - id - a - } - } + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + withProvidesForT1 { + ... on T1 { + a } - }, - Flatten(path: "withProvidesForT1") { - Fetch(service: "Subgraph2") { - { - ... on T2 { - __typename - id - } - } => - { - ... on T2 { - b - } - } - }, - }, + ... on T2 { + __typename + id + a + } + } + } + }, + Flatten(path: "withProvidesForT1") { + Fetch(service: "Subgraph2") { + { + ... on T2 { + __typename + id + } + } => + { + ... on T2 { + b + } + } }, - } - "### + }, + }, + } + "### ); // Lastly, if both are provided, ensures we only hit subgraph1. diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 02c72d6e4e..943057738a 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -82,7 +82,7 @@ axum = { version = "0.6.20", features = ["headers", "json", "original-uri"] } base64 = "0.21.7" bloomfilter = "1.0.13" buildstructor = "0.5.4" -bytes = "1.5.0" +bytes = "1.6.0" clap = { version = "4.5.1", default-features = false, features = [ "env", "derive", @@ -188,7 +188,7 @@ regex = "1.10.3" reqwest.workspace = true # note: this dependency should _always_ be pinned, prefix the version with an `=` -router-bridge = "=0.5.21+v2.7.5" +router-bridge = "=0.5.24+v2.8.0-alpha.1" rust-embed = "8.2.0" rustls = "0.21.11" diff --git a/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__non_overridden_field_yields_expected_query_plan.snap b/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__non_overridden_field_yields_expected_query_plan.snap index d680c0b53f..cb657dcdce 100644 --- a/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__non_overridden_field_yields_expected_query_plan.snap +++ b/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__non_overridden_field_yields_expected_query_plan.snap @@ -18,6 +18,7 @@ expression: query_plan "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "12dda6193654ae4fe6e38bc09d4f81cc73d0c9e098692096f72d2158eef4776f", "authorization": { "is_authenticated": false, diff --git a/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__overridden_field_yields_expected_query_plan.snap b/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__overridden_field_yields_expected_query_plan.snap index 612b147fc2..d18a3e2b11 100644 --- a/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__overridden_field_yields_expected_query_plan.snap +++ b/apollo-router/src/plugins/progressive_override/snapshots/apollo_router__plugins__progressive_override__tests__overridden_field_yields_expected_query_plan.snap @@ -23,6 +23,7 @@ expression: query_plan "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "00ad582ea45fc1bce436b36b21512f3d2c47b74fdbdc61e4b349289722c9ecf2", "authorization": { "is_authenticated": false, @@ -61,6 +62,7 @@ expression: query_plan "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "a8ebdc2151a2e5207882e43c6906c0c64167fd9a8e0c7c4becc47736a5105096", "authorization": { "is_authenticated": false, diff --git a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap index b32a905c0e..0d6ab611f6 100644 --- a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap +++ b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap @@ -68,6 +68,7 @@ expression: "serde_json::to_value(response).unwrap()" "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "7245d488e97c3b2ac9f5fa4dd4660940b94ad81af070013305b2c0f76337b2f9", "authorization": { "is_authenticated": false, @@ -107,6 +108,7 @@ expression: "serde_json::to_value(response).unwrap()" "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "6e0b4156706ea0cf924500cfdc99dd44b9f0ed07e2d3f888d4aff156e6a33238", "authorization": { "is_authenticated": false, @@ -153,6 +155,7 @@ expression: "serde_json::to_value(response).unwrap()" "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "ff649f3d70241d5a8cd5f5d03ff4c41ecff72b0e4129a480207b05ac92318042", "authorization": { "is_authenticated": false, @@ -196,6 +199,7 @@ expression: "serde_json::to_value(response).unwrap()" "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "bf9f3beda78a7a565e47c862157bad4ec871d724d752218da1168455dddca074", "authorization": { "is_authenticated": false, diff --git a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap index b32a905c0e..0d6ab611f6 100644 --- a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap +++ b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap @@ -68,6 +68,7 @@ expression: "serde_json::to_value(response).unwrap()" "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "7245d488e97c3b2ac9f5fa4dd4660940b94ad81af070013305b2c0f76337b2f9", "authorization": { "is_authenticated": false, @@ -107,6 +108,7 @@ expression: "serde_json::to_value(response).unwrap()" "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "6e0b4156706ea0cf924500cfdc99dd44b9f0ed07e2d3f888d4aff156e6a33238", "authorization": { "is_authenticated": false, @@ -153,6 +155,7 @@ expression: "serde_json::to_value(response).unwrap()" "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "ff649f3d70241d5a8cd5f5d03ff4c41ecff72b0e4129a480207b05ac92318042", "authorization": { "is_authenticated": false, @@ -196,6 +199,7 @@ expression: "serde_json::to_value(response).unwrap()" "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "bf9f3beda78a7a565e47c862157bad4ec871d724d752218da1168455dddca074", "authorization": { "is_authenticated": false, diff --git a/apollo-router/src/query_planner/convert.rs b/apollo-router/src/query_planner/convert.rs index 2f24e35b9d..e75a2474b9 100644 --- a/apollo-router/src/query_planner/convert.rs +++ b/apollo-router/src/query_planner/convert.rs @@ -74,6 +74,7 @@ impl From<&'_ Box> for plan::PlanNode { operation_kind, input_rewrites, output_rewrites, + context_rewrites, } = &**value; Self::Fetch(super::fetch::FetchNode { service_name: subgraph_name.clone(), @@ -86,6 +87,7 @@ impl From<&'_ Box> for plan::PlanNode { id: id.map(|id| id.to_string().into()), input_rewrites: option_vec(input_rewrites), output_rewrites: option_vec(output_rewrites), + context_rewrites: option_vec(context_rewrites), schema_aware_hash: Default::default(), authorization: Default::default(), }) @@ -153,6 +155,7 @@ impl From<&'_ next::FetchNode> for subscription::SubscriptionNode { operation_kind, input_rewrites, output_rewrites, + context_rewrites: _, } = value; Self { service_name: subgraph_name.clone(), diff --git a/apollo-router/src/query_planner/execution.rs b/apollo-router/src/query_planner/execution.rs index 801069afd1..dc1e27123e 100644 --- a/apollo-router/src/query_planner/execution.rs +++ b/apollo-router/src/query_planner/execution.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::sync::Arc; +use apollo_compiler::validation::Valid; use apollo_compiler::NodeStr; use futures::future::join_all; use futures::prelude::*; @@ -50,6 +51,7 @@ impl QueryPlan { service_factory: &'a Arc, supergraph_request: &'a Arc>, schema: &'a Arc, + subgraph_schemas: &'a Arc>>>, sender: mpsc::Sender, subscription_handle: Option, subscription_config: &'a Option, @@ -73,6 +75,7 @@ impl QueryPlan { root_node: &self.root, subscription_handle: &subscription_handle, subscription_config, + subgraph_schemas, }, &root, &initial_value.unwrap_or_default(), @@ -100,6 +103,7 @@ pub(crate) struct ExecutionParameters<'a> { pub(crate) context: &'a Context, pub(crate) service_factory: &'a Arc, pub(crate) schema: &'a Arc, + pub(crate) subgraph_schemas: &'a Arc>>>, pub(crate) supergraph_request: &'a Arc>, pub(crate) deferred_fetches: &'a HashMap)>>, pub(crate) query: &'a Arc, @@ -293,6 +297,7 @@ impl PlanNode { root_node: parameters.root_node, subscription_handle: parameters.subscription_handle, subscription_config: parameters.subscription_config, + subgraph_schemas: parameters.subgraph_schemas, }, current_dir, &value, @@ -435,6 +440,7 @@ impl DeferredNode { let label = self.label.as_ref().map(|l| l.to_string()); let tx = sender; let sc = parameters.schema.clone(); + let subgraph_schemas = parameters.subgraph_schemas.clone(); let orig = parameters.supergraph_request.clone(); let sf = parameters.service_factory.clone(); let root_node = parameters.root_node.clone(); @@ -481,6 +487,7 @@ impl DeferredNode { root_node: &root_node, subscription_handle: &subscription_handle, subscription_config: &subscription_config, + subgraph_schemas: &subgraph_schemas, }, &Path::default(), &value, diff --git a/apollo-router/src/query_planner/fetch.rs b/apollo-router/src/query_planner/fetch.rs index f76a6a2d2d..a20ad3ffda 100644 --- a/apollo-router/src/query_planner/fetch.rs +++ b/apollo-router/src/query_planner/fetch.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::fmt::Display; use std::sync::Arc; +use apollo_compiler::ast; use apollo_compiler::validation::Valid; use apollo_compiler::ExecutableDocument; use apollo_compiler::NodeStr; @@ -17,6 +18,9 @@ use super::execution::ExecutionParameters; use super::rewrites; use super::selection::execute_selection_set; use super::selection::Selection; +use super::subgraph_context::build_operation_with_aliasing; +use super::subgraph_context::ContextualArguments; +use super::subgraph_context::SubgraphContext; use crate::error::Error; use crate::error::FetchError; use crate::error::ValidationErrors; @@ -70,22 +74,22 @@ impl OperationKind { } } -impl From for apollo_compiler::ast::OperationType { +impl From for ast::OperationType { fn from(value: OperationKind) -> Self { match value { - OperationKind::Query => apollo_compiler::ast::OperationType::Query, - OperationKind::Mutation => apollo_compiler::ast::OperationType::Mutation, - OperationKind::Subscription => apollo_compiler::ast::OperationType::Subscription, + OperationKind::Query => ast::OperationType::Query, + OperationKind::Mutation => ast::OperationType::Mutation, + OperationKind::Subscription => ast::OperationType::Subscription, } } } -impl From for OperationKind { - fn from(value: apollo_compiler::ast::OperationType) -> Self { +impl From for OperationKind { + fn from(value: ast::OperationType) -> Self { match value { - apollo_compiler::ast::OperationType::Query => OperationKind::Query, - apollo_compiler::ast::OperationType::Mutation => OperationKind::Mutation, - apollo_compiler::ast::OperationType::Subscription => OperationKind::Subscription, + ast::OperationType::Query => OperationKind::Query, + ast::OperationType::Mutation => OperationKind::Mutation, + ast::OperationType::Subscription => OperationKind::Subscription, } } } @@ -125,6 +129,9 @@ pub(crate) struct FetchNode { // Optionally describes a number of "rewrites" to apply to the data that received from a fetch (and before it is applied to the current in-memory results). pub(crate) output_rewrites: Option>, + // Optionally describes a number of "rewrites" to apply to the data that has already been received further up the tree + pub(crate) context_rewrites: Option>, + // hash for the query and relevant parts of the schema. if two different schemas provide the exact same types, fields and directives // affecting the query, then they will have the same hash #[serde(default)] @@ -243,6 +250,7 @@ impl Display for QueryHash { pub(crate) struct Variables { pub(crate) variables: Object, pub(crate) inverted_paths: Vec>, + pub(crate) contextual_arguments: Option, } impl Variables { @@ -256,8 +264,10 @@ impl Variables { request: &Arc>, schema: &Schema, input_rewrites: &Option>, + context_rewrites: &Option>, ) -> Option { let body = request.body(); + let mut subgraph_context = SubgraphContext::new(data, schema, context_rewrites); if !requires.is_empty() { let mut variables = Object::with_capacity(1 + variable_usages.len()); @@ -269,8 +279,12 @@ impl Variables { let mut inverted_paths: Vec> = Vec::new(); let mut values: IndexSet = IndexSet::new(); - data.select_values_and_paths(schema, current_dir, |path, value| { + // first get contextual values that are required + if let Some(context) = subgraph_context.as_mut() { + context.execute_on_path(path); + } + let mut value = execute_selection_set(value, requires, schema, None); if value.as_object().map(|o| !o.is_empty()).unwrap_or(false) { rewrites::apply_rewrites(schema, &mut value, input_rewrites); @@ -292,11 +306,16 @@ impl Variables { } let representations = Value::Array(Vec::from_iter(values)); + let contextual_arguments = match subgraph_context.as_mut() { + Some(context) => context.add_variables_and_get_args(&mut variables), + None => None, + }; variables.insert("representations", representations); Some(Variables { variables, inverted_paths, + contextual_arguments, }) } else { // with nested operations (Query or Mutation has an operation returning a Query or Mutation), @@ -323,6 +342,7 @@ impl Variables { }) .collect::(), inverted_paths: Vec::new(), + contextual_arguments: None, }) } } @@ -355,6 +375,7 @@ impl FetchNode { let Variables { variables, inverted_paths: paths, + contextual_arguments, } = match Variables::new( &self.requires, &self.variable_usages, @@ -364,6 +385,7 @@ impl FetchNode { parameters.supergraph_request, parameters.schema, &self.input_rewrites, + &self.context_rewrites, ) { Some(variables) => variables, None => { @@ -371,6 +393,35 @@ impl FetchNode { } }; + let alias_query_string; // this exists outside the if block to allow the as_str() to be longer lived + let aliased_operation = if let Some(ctx_arg) = contextual_arguments { + if let Some(subgraph_schema) = + parameters.subgraph_schemas.get(&service_name.to_string()) + { + match build_operation_with_aliasing(operation, &ctx_arg, subgraph_schema) { + Ok(op) => { + alias_query_string = op.serialize().no_indent().to_string(); + alias_query_string.as_str() + } + Err(errors) => { + tracing::debug!( + "couldn't generate a valid executable document? {:?}", + errors + ); + operation.as_serialized() + } + } + } else { + tracing::debug!( + "couldn't find a subgraph schema for service {:?}", + &service_name + ); + operation.as_serialized() + } + } else { + operation.as_serialized() + }; + let mut subgraph_request = SubgraphRequest::builder() .supergraph_request(parameters.supergraph_request.clone()) .subgraph_request( @@ -389,7 +440,7 @@ impl FetchNode { ) .body( Request::builder() - .query(operation.as_serialized()) + .query(aliased_operation) .and_operation_name(operation_name.as_ref().map(|n| n.to_string())) .variables(variables.clone()) .build(), diff --git a/apollo-router/src/query_planner/mod.rs b/apollo-router/src/query_planner/mod.rs index a11a5cb072..58285e3638 100644 --- a/apollo-router/src/query_planner/mod.rs +++ b/apollo-router/src/query_planner/mod.rs @@ -20,6 +20,7 @@ mod labeler; mod plan; pub(crate) mod rewrites; mod selection; +mod subgraph_context; pub(crate) mod subscription; pub(crate) const FETCH_SPAN_NAME: &str = "fetch"; diff --git a/apollo-router/src/query_planner/rewrites.rs b/apollo-router/src/query_planner/rewrites.rs index 94453630df..f3b10a7fa4 100644 --- a/apollo-router/src/query_planner/rewrites.rs +++ b/apollo-router/src/query_planner/rewrites.rs @@ -47,7 +47,7 @@ pub(crate) struct DataKeyRenamer { } impl DataRewrite { - fn maybe_apply(&self, schema: &Schema, data: &mut Value) { + pub(crate) fn maybe_apply(&self, schema: &Schema, data: &mut Value) { match self { DataRewrite::ValueSetter(setter) => { // The `path` of rewrites can only be either `Key` or `Fragment`, and so far diff --git a/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__bridge_query_planner__tests__plan_root.snap b/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__bridge_query_planner__tests__plan_root.snap index 974ccc03c5..d49c351866 100644 --- a/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__bridge_query_planner__tests__plan_root.snap +++ b/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__bridge_query_planner__tests__plan_root.snap @@ -13,6 +13,7 @@ Fetch( id: None, input_rewrites: None, output_rewrites: None, + context_rewrites: None, schema_aware_hash: QueryHash( "a4ab3ffe0fd7863aea8cd1e85d019d2c64ec0351d62f9759bed3c9dc707ea315", ), diff --git a/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__tests__query_plan_from_json.snap b/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__tests__query_plan_from_json.snap index ef8a64f2a6..c18018d7a2 100644 --- a/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__tests__query_plan_from_json.snap +++ b/apollo-router/src/query_planner/snapshots/apollo_router__query_planner__tests__query_plan_from_json.snap @@ -17,6 +17,7 @@ Sequence { id: None, input_rewrites: None, output_rewrites: None, + context_rewrites: None, schema_aware_hash: QueryHash( "", ), @@ -81,6 +82,7 @@ Sequence { id: None, input_rewrites: None, output_rewrites: None, + context_rewrites: None, schema_aware_hash: QueryHash( "", ), @@ -155,6 +157,7 @@ Sequence { id: None, input_rewrites: None, output_rewrites: None, + context_rewrites: None, schema_aware_hash: QueryHash( "", ), @@ -216,6 +219,7 @@ Sequence { id: None, input_rewrites: None, output_rewrites: None, + context_rewrites: None, schema_aware_hash: QueryHash( "", ), @@ -287,6 +291,7 @@ Sequence { id: None, input_rewrites: None, output_rewrites: None, + context_rewrites: None, schema_aware_hash: QueryHash( "", ), diff --git a/apollo-router/src/query_planner/subgraph_context.rs b/apollo-router/src/query_planner/subgraph_context.rs new file mode 100644 index 0000000000..6f1e03c4ad --- /dev/null +++ b/apollo-router/src/query_planner/subgraph_context.rs @@ -0,0 +1,460 @@ +use std::collections::HashMap; +use std::collections::HashSet; + +use apollo_compiler::ast; +use apollo_compiler::ast::Name; +use apollo_compiler::ast::VariableDefinition; +use apollo_compiler::executable; +use apollo_compiler::executable::Operation; +use apollo_compiler::executable::Selection; +use apollo_compiler::executable::SelectionSet; +use apollo_compiler::validation::Valid; +use apollo_compiler::validation::WithErrors; +use apollo_compiler::ExecutableDocument; +use apollo_compiler::Node; +use serde_json_bytes::ByteString; +use serde_json_bytes::Map; + +use super::fetch::SubgraphOperation; +use super::rewrites::DataKeyRenamer; +use super::rewrites::DataRewrite; +use crate::json_ext::Path; +use crate::json_ext::PathElement; +use crate::json_ext::Value; +use crate::json_ext::ValueExt; +use crate::spec::Schema; + +#[derive(Debug)] +pub(crate) struct ContextualArguments { + pub(crate) arguments: HashSet, // a set of all argument names that will be passed to the subgraph. This is the unmodified name from the query plan + pub(crate) count: usize, // the number of different sets of arguments that exist. This will either be 1 or the number of entities +} + +pub(crate) struct SubgraphContext<'a> { + pub(crate) data: &'a Value, + pub(crate) schema: &'a Schema, + pub(crate) context_rewrites: &'a Vec, + pub(crate) named_args: Vec>, +} + +// context_path is a non-standard relative path which may navigate up the tree +// from the current position. This is indicated with a ".." PathElement::Key +// note that the return value is an absolute path that may be used anywhere +fn merge_context_path( + current_dir: &Path, + context_path: &Path, +) -> Result { + let mut i = 0; + let mut j = current_dir.len(); + // iterate over the context_path(i), every time we encounter a '..', we want + // to go up one level in the current_dir(j) + while i < context_path.len() { + match &context_path.0.get(i) { + Some(PathElement::Key(e, _)) => { + let mut found = false; + if e == ".." { + while !found { + if j == 0 { + return Err(ContextBatchingError::InvalidRelativePath); + } + j -= 1; + + if let Some(PathElement::Key(_, _)) = current_dir.0.get(j) { + found = true; + } + } + i += 1; + } else { + break; + } + } + _ => break, + } + } + + let mut return_path: Vec = current_dir.iter().take(j).cloned().collect(); + + context_path.iter().skip(i).for_each(|e| { + return_path.push(e.clone()); + }); + Ok(Path(return_path.into_iter().collect())) +} + +impl<'a> SubgraphContext<'a> { + pub(crate) fn new( + data: &'a Value, + schema: &'a Schema, + context_rewrites: &'a Option>, + ) -> Option> { + if let Some(rewrites) = context_rewrites { + if !rewrites.is_empty() { + return Some(SubgraphContext { + data, + schema, + context_rewrites: rewrites, + named_args: Vec::new(), + }); + } + } + None + } + + // For each of the rewrites, start collecting data for the data at path. + // Once we find a Value for a given variable, skip additional rewrites that + // reference the same variable + pub(crate) fn execute_on_path(&mut self, path: &Path) { + let mut found_rewrites: HashSet = HashSet::new(); + let hash_map: HashMap = self + .context_rewrites + .iter() + .filter_map(|rewrite| { + match rewrite { + DataRewrite::KeyRenamer(item) => { + if !found_rewrites.contains(item.rename_key_to.as_str()) { + let wrapped_data_path = merge_context_path(path, &item.path); + if let Ok(data_path) = wrapped_data_path { + let val = self.data.get_path(self.schema, &data_path); + + if let Ok(v) = val { + // add to found + found_rewrites.insert(item.rename_key_to.clone().to_string()); + // TODO: not great + let mut new_value = v.clone(); + if let Some(values) = new_value.as_array_mut() { + for v in values { + let data_rewrite = DataRewrite::KeyRenamer({ + DataKeyRenamer { + path: data_path.clone(), + rename_key_to: item.rename_key_to.clone(), + } + }); + data_rewrite.maybe_apply(self.schema, v); + } + } else { + let data_rewrite = DataRewrite::KeyRenamer({ + DataKeyRenamer { + path: data_path.clone(), + rename_key_to: item.rename_key_to.clone(), + } + }); + data_rewrite.maybe_apply(self.schema, &mut new_value); + } + return Some((item.rename_key_to.to_string(), new_value)); + } + } + } + None + } + DataRewrite::ValueSetter(_) => None, + } + }) + .collect(); + self.named_args.push(hash_map); + } + + // Once all a value has been extracted for every variable, go ahead and add all + // variables to the variables map. Additionally, return a ContextualArguments structure if + // values of variables are entity dependent + pub(crate) fn add_variables_and_get_args( + &self, + variables: &mut Map, + ) -> Option { + let (extended_vars, contextual_args) = if let Some(first_map) = self.named_args.first() { + if self.named_args.iter().all(|map| map == first_map) { + ( + first_map + .iter() + .map(|(k, v)| (k.as_str().into(), v.clone())) + .collect(), + None, + ) + } else { + let mut hash_map: HashMap = HashMap::new(); + let arg_names: HashSet<_> = first_map.keys().cloned().collect(); + for (index, item) in self.named_args.iter().enumerate() { + // append _ to each of the arguments and push all the values into hash_map + hash_map.extend(item.iter().map(|(k, v)| { + let mut new_named_param = k.clone(); + new_named_param.push_str(&format!("_{}", index)); + (new_named_param, v.clone()) + })); + } + ( + hash_map, + Some(ContextualArguments { + arguments: arg_names, + count: self.named_args.len(), + }), + ) + } + } else { + (HashMap::new(), None) + }; + + variables.extend( + extended_vars + .iter() + .map(|(key, value)| (key.as_str().into(), value.clone())), + ); + + contextual_args + } +} + +// Take the existing subgraph operation and rewrite it to use aliasing. This will occur in the case +// where we are collecting entites and different entities may have different variables passed to the resolver. +pub(crate) fn build_operation_with_aliasing( + subgraph_operation: &SubgraphOperation, + contextual_arguments: &ContextualArguments, + subgraph_schema: &Valid, +) -> Result, ContextBatchingError> { + let ContextualArguments { arguments, count } = contextual_arguments; + let parsed_document = subgraph_operation.as_parsed(subgraph_schema); + + let mut ed = ExecutableDocument::new(); + + // for every operation in the document, go ahead and transform even though it's likely that only one exists + if let Ok(document) = parsed_document { + if let Some(anonymous_op) = &document.anonymous_operation { + let mut cloned = anonymous_op.clone(); + transform_operation(&mut cloned, arguments, count)?; + ed.insert_operation(cloned); + } + + for (_, op) in &document.named_operations { + let mut cloned = op.clone(); + transform_operation(&mut cloned, arguments, count)?; + ed.insert_operation(cloned); + } + + return ed + .validate(subgraph_schema) + .map_err(ContextBatchingError::InvalidDocumentGenerated); + } + Err(ContextBatchingError::NoSelectionSet) +} + +fn transform_operation( + operation: &mut Node, + arguments: &HashSet, + count: &usize, +) -> Result<(), ContextBatchingError> { + let mut selections: Vec = vec![]; + let mut new_variables: Vec> = vec![]; + operation.variables.iter().for_each(|v| { + if arguments.contains(v.name.as_str()) { + for i in 0..*count { + new_variables.push(Node::new(VariableDefinition { + name: Name::new_unchecked(format!("{}_{}", v.name.as_str(), i).into()), + ty: v.ty.clone(), + default_value: v.default_value.clone(), + directives: v.directives.clone(), + })); + } + } else { + new_variables.push(v.clone()); + } + }); + + // there should only be one selection that is a field selection that we're going to rename, but let's count to be sure + // and error if that's not the case + // also it's possible that there could be an inline fragment, so if that's the case, just add those to the new selections once + let mut field_selection: Option> = None; + for selection in &operation.selection_set.selections { + match selection { + Selection::Field(f) => { + if field_selection.is_some() { + // if we get here, there is more than one field selection, which should not be the case + // at the top level of a _entities selection set + return Err(ContextBatchingError::UnexpectedSelection); + } + field_selection = Some(f.clone()); + } + _ => { + // again, if we get here, something is wrong. _entities selection sets should have just one field selection + return Err(ContextBatchingError::UnexpectedSelection); + } + } + } + + let field_selection = field_selection.ok_or(ContextBatchingError::UnexpectedSelection)?; + + for i in 0..*count { + // If we are aliasing, we know that there is only one selection in the top level SelectionSet + // it is a field selection for _entities, so it's ok to reach in and give it an alias + let mut cloned = field_selection.clone(); + let cfs = cloned.make_mut(); + cfs.alias = Some(Name::new_unchecked(format!("_{}", i).into())); + + transform_field_arguments(&mut cfs.arguments, arguments, i); + transform_selection_set(&mut cfs.selection_set, arguments, i); + selections.push(Selection::Field(cloned)); + } + let operation = operation.make_mut(); + operation.variables = new_variables; + operation.selection_set = SelectionSet { + ty: operation.selection_set.ty.clone(), + selections, + }; + Ok(()) +} + +// This function will take the selection set (which has been cloned from the original) +// and transform it so that all contextual variables in the selection set will be appended with a _ +// to match the index in the alias that it is +fn transform_selection_set( + selection_set: &mut SelectionSet, + arguments: &HashSet, + index: usize, +) { + selection_set + .selections + .iter_mut() + .for_each(|selection| match selection { + executable::Selection::Field(node) => { + let node = node.make_mut(); + transform_field_arguments(&mut node.arguments, arguments, index); + transform_selection_set(&mut node.selection_set, arguments, index); + } + executable::Selection::InlineFragment(node) => { + let node = node.make_mut(); + transform_selection_set(&mut node.selection_set, arguments, index); + } + _ => (), + }); +} + +// transforms the variable name on the field argment +fn transform_field_arguments( + arguments_in_selection: &mut [Node], + arguments: &HashSet, + index: usize, +) { + arguments_in_selection.iter_mut().for_each(|arg| { + let arg = arg.make_mut(); + if let Some(v) = arg.value.as_variable() { + if arguments.contains(v.as_str()) { + arg.value = Node::new(ast::Value::Variable(Name::new_unchecked( + format!("{}_{}", v.as_str(), index).into(), + ))); + } + } + }); +} + +#[derive(Debug)] +pub(crate) enum ContextBatchingError { + NoSelectionSet, + InvalidDocumentGenerated(WithErrors), + InvalidRelativePath, + UnexpectedSelection, +} + +#[cfg(test)] +mod subgraph_context_unit_tests { + use super::*; + + #[test] + fn test_merge_context_path() { + let current_dir: Path = serde_json::from_str(r#"["t","u"]"#).unwrap(); + let relative_path: Path = serde_json::from_str(r#"["..","... on T","prop"]"#).unwrap(); + let expected = r#"["t","... on T","prop"]"#; + + let result = merge_context_path(¤t_dir, &relative_path).unwrap(); + assert_eq!(expected, serde_json::to_string(&result).unwrap(),); + } + + #[test] + fn test_merge_context_path_invalid() { + let current_dir: Path = serde_json::from_str(r#"["t","u"]"#).unwrap(); + let relative_path: Path = + serde_json::from_str(r#"["..","..","..","... on T","prop"]"#).unwrap(); + + let result = merge_context_path(¤t_dir, &relative_path); + match result { + Ok(_) => panic!("Expected an error, but got Ok"), + Err(e) => match e { + ContextBatchingError::InvalidRelativePath => (), + _ => panic!("Expected InvalidRelativePath, but got a different error"), + }, + } + } + + #[test] + fn test_transform_selection_set() { + let type_name = executable::Name::new("Hello").unwrap(); + let field_name = executable::Name::new("f").unwrap(); + let field_definition = ast::FieldDefinition { + description: None, + name: field_name.clone(), + arguments: vec![Node::new(ast::InputValueDefinition { + description: None, + name: executable::Name::new("param").unwrap(), + ty: Node::new(ast::Type::Named( + executable::Name::new("ParamType").unwrap(), + )), + default_value: None, + directives: ast::DirectiveList(vec![]), + })], + ty: ast::Type::Named(executable::Name::new("FieldType").unwrap()), + directives: ast::DirectiveList(vec![]), + }; + let mut selection_set = SelectionSet::new(type_name); + let field = executable::Field::new( + executable::Name::new("f").unwrap(), + Node::new(field_definition), + ) + .with_argument( + executable::Name::new("param").unwrap(), + Node::new(ast::Value::Variable( + executable::Name::new("variable").unwrap(), + )), + ); + + selection_set.push(Selection::Field(Node::new(field))); + + // before modifications + assert_eq!( + "{ f(param: $variable) }", + selection_set.serialize().no_indent().to_string() + ); + + let mut hash_set = HashSet::new(); + + // create a hash set that will miss completely. transform has no effect + hash_set.insert("one".to_string()); + hash_set.insert("two".to_string()); + hash_set.insert("param".to_string()); + let mut clone = selection_set.clone(); + transform_selection_set(&mut clone, &hash_set, 7); + assert_eq!( + "{ f(param: $variable) }", + clone.serialize().no_indent().to_string() + ); + + // add variable that will hit and cause a rewrite + hash_set.insert("variable".to_string()); + let mut clone = selection_set.clone(); + transform_selection_set(&mut clone, &hash_set, 7); + assert_eq!( + "{ f(param: $variable_7) }", + clone.serialize().no_indent().to_string() + ); + + // add_alias = true will add a "_3:" alias + let clone = selection_set.clone(); + let mut operation = Node::new(executable::Operation { + operation_type: executable::OperationType::Query, + name: None, + variables: vec![], + directives: ast::DirectiveList(vec![]), + selection_set: clone, + }); + let count = 3; + transform_operation(&mut operation, &hash_set, &count).unwrap(); + assert_eq!( + "{ _0: f(param: $variable_0) _1: f(param: $variable_1) _2: f(param: $variable_2) }", + operation.serialize().no_indent().to_string() + ); + } +} diff --git a/apollo-router/src/query_planner/subscription.rs b/apollo-router/src/query_planner/subscription.rs index caf90730e6..6a327fdc60 100644 --- a/apollo-router/src/query_planner/subscription.rs +++ b/apollo-router/src/query_planner/subscription.rs @@ -208,6 +208,7 @@ impl SubscriptionNode { parameters.supergraph_request, parameters.schema, &self.input_rewrites, + &None, ) { Some(variables) => variables, None => { diff --git a/apollo-router/src/query_planner/tests.rs b/apollo-router/src/query_planner/tests.rs index 3431fda2b5..15f2d3b421 100644 --- a/apollo-router/src/query_planner/tests.rs +++ b/apollo-router/src/query_planner/tests.rs @@ -116,6 +116,7 @@ async fn mock_subgraph_service_withf_panics_should_be_reported_as_service_closed &sf, &Default::default(), &Arc::new(Schema::parse_test(test_schema!(), &Default::default()).unwrap()), + &Default::default(), sender, None, &None, @@ -177,6 +178,7 @@ async fn fetch_includes_operation_name() { &sf, &Default::default(), &Arc::new(Schema::parse_test(test_schema!(), &Default::default()).unwrap()), + &Default::default(), sender, None, &None, @@ -235,6 +237,7 @@ async fn fetch_makes_post_requests() { &sf, &Default::default(), &Arc::new(Schema::parse_test(test_schema!(), &Default::default()).unwrap()), + &Default::default(), sender, None, &None, @@ -266,6 +269,7 @@ async fn defer() { id: Some("fetch1".into()), input_rewrites: None, output_rewrites: None, + context_rewrites: None, schema_aware_hash: Default::default(), authorization: Default::default(), }))), @@ -311,6 +315,7 @@ async fn defer() { id: Some("fetch2".into()), input_rewrites: None, output_rewrites: None, + context_rewrites: None, schema_aware_hash: Default::default(), authorization: Default::default(), })), @@ -385,6 +390,7 @@ async fn defer() { &sf, &Default::default(), &schema, + &Default::default(), sender, None, &None, @@ -493,6 +499,7 @@ async fn defer_if_condition() { .unwrap(), ), &schema, + &Default::default(), sender, None, &None, @@ -515,6 +522,7 @@ async fn defer_if_condition() { &service_factory, &Default::default(), &schema, + &Default::default(), default_sender, None, &None, @@ -546,6 +554,7 @@ async fn defer_if_condition() { .unwrap(), ), &schema, + &Default::default(), sender, None, &None, @@ -667,6 +676,7 @@ async fn dependent_mutations() { &sf, &Default::default(), &Arc::new(Schema::parse_test(schema, &Default::default()).unwrap()), + &Default::default(), sender, None, &None, @@ -1799,6 +1809,7 @@ fn broken_plan_does_not_panic() { id: Some("fetch1".into()), input_rewrites: None, output_rewrites: None, + context_rewrites: None, schema_aware_hash: Default::default(), authorization: Default::default(), }), diff --git a/apollo-router/src/services/execution/service.rs b/apollo-router/src/services/execution/service.rs index ddd9f96ca6..fbde3de97a 100644 --- a/apollo-router/src/services/execution/service.rs +++ b/apollo-router/src/services/execution/service.rs @@ -57,6 +57,7 @@ use crate::spec::Schema; #[derive(Clone)] pub(crate) struct ExecutionService { pub(crate) schema: Arc, + pub(crate) subgraph_schemas: Arc>>>, pub(crate) subgraph_service_factory: Arc, /// Subscription config if enabled subscription_config: Option, @@ -148,6 +149,7 @@ impl ExecutionService { &self.subgraph_service_factory, &Arc::new(req.supergraph_request), &self.schema, + &self.subgraph_schemas, sender, subscription_handle.clone(), &self.subscription_config, @@ -618,6 +620,7 @@ impl ServiceFactory for ExecutionServiceFactory { schema: self.schema.clone(), subgraph_service_factory: self.subgraph_service_factory.clone(), subscription_config: subscription_plugin_conf, + subgraph_schemas: self.subgraph_schemas.clone(), } .boxed(), |acc, (_, e)| e.execution_service(acc), diff --git a/apollo-router/src/uplink/license_enforcement.rs b/apollo-router/src/uplink/license_enforcement.rs index 50b6c410c6..ecc671cfad 100644 --- a/apollo-router/src/uplink/license_enforcement.rs +++ b/apollo-router/src/uplink/license_enforcement.rs @@ -408,6 +408,19 @@ impl LicenseEnforcementReport { }], }, }, + SchemaRestriction::Spec { + name: "context".to_string(), + spec_url: "https://specs.apollo.dev/context".to_string(), + version_req: semver::VersionReq { + comparators: vec![semver::Comparator { + op: semver::Op::Exact, + major: 0, + minor: 1.into(), + patch: 0.into(), + pre: semver::Prerelease::EMPTY, + }], + }, + }, SchemaRestriction::Spec { name: "requiresScopes".to_string(), spec_url: "https://specs.apollo.dev/requiresScopes".to_string(), @@ -436,6 +449,21 @@ impl LicenseEnforcementReport { }, explanation: "The `overrideLabel` argument on the join spec's @field directive is restricted to Enterprise users. This argument exists in your supergraph as a result of using the `@override` directive with the `label` argument in one or more of your subgraphs.".to_string() }, + SchemaRestriction::DirectiveArgument { + name: "field".to_string(), + argument: "contextArguments".to_string(), + spec_url: "https://specs.apollo.dev/join".to_string(), + version_req: semver::VersionReq { + comparators: vec![semver::Comparator { + op: semver::Op::GreaterEq, + major: 0, + minor: 5.into(), + patch: 0.into(), + pre: semver::Prerelease::EMPTY, + }], + }, + explanation: "The `contextArguments` argument on the join spec's @field directive is restricted to Enterprise users. This argument exists in your supergraph as a result of using the `@fromContext` directive in one or more of your subgraphs.".to_string() + }, ] } } @@ -785,6 +813,20 @@ mod test { assert_snapshot!(report.to_string()); } + #[test] + fn set_context() { + let report = check( + include_str!("testdata/oss.router.yaml"), + include_str!("testdata/set_context.graphql"), + ); + + assert!( + !report.restricted_schema_in_use.is_empty(), + "should have found restricted features" + ); + assert_snapshot!(report.to_string()); + } + #[test] fn progressive_override_with_renamed_join_spec() { let report = check( diff --git a/apollo-router/src/uplink/snapshots/apollo_router__uplink__license_enforcement__test__set_context.snap b/apollo-router/src/uplink/snapshots/apollo_router__uplink__license_enforcement__test__set_context.snap new file mode 100644 index 0000000000..79ea2bbce0 --- /dev/null +++ b/apollo-router/src/uplink/snapshots/apollo_router__uplink__license_enforcement__test__set_context.snap @@ -0,0 +1,12 @@ +--- +source: apollo-router/src/uplink/license_enforcement.rs +expression: report.to_string() +--- +Schema features: +* @context + https://specs.apollo.dev/context/v0.1 + +* @join__field.contextArguments + https://specs.apollo.dev/join/v0.5 + +The `contextArguments` argument on the join spec's @field directive is restricted to Enterprise users. This argument exists in your supergraph as a result of using the `@fromContext` directive in one or more of your subgraphs. diff --git a/apollo-router/src/uplink/testdata/set_context.graphql b/apollo-router/src/uplink/testdata/set_context.graphql new file mode 100644 index 0000000000..16b9ba0019 --- /dev/null +++ b/apollo-router/src/uplink/testdata/set_context.graphql @@ -0,0 +1,165 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link(url: "https://specs.apollo.dev/context/v0.1", for: SECURITY) { + query: Query +} + +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + +directive @context__fromContext(field: String) on ARGUMENT_DEFINITION + +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 + contextArguments: [join__ContextArgument!] +) 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 context__context + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "https://Subgraph1") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "https://Subgraph2") +} + +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) + tList: [T]! @join__field(graph: SUBGRAPH1) + a: Int! @join__field(graph: SUBGRAPH2) + k: K! @join__field(graph: SUBGRAPH1) +} + +union K + @join__type(graph: SUBGRAPH1) + @join__unionMember(graph: SUBGRAPH1, member: "A") + @join__unionMember(graph: SUBGRAPH1, member: "B") + @context(name: "Subgraph1__context2") = + A + | B + +type A @join__type(graph: SUBGRAPH1, key: "id") { + id: ID! + v: V! + prop: String! +} + +type B @join__type(graph: SUBGRAPH1, key: "id") { + id: ID! + v: V! + prop: String! +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @context(name: "Subgraph1__context") { + id: ID! + u: U! + uList: [U]! + prop: String! +} + +type U + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") { + id: ID! + b: String! @join__field(graph: SUBGRAPH2) + field: Int! + @join__field( + graph: SUBGRAPH1 + contextArguments: [ + { + context: "Subgraph1__context" + name: "a" + type: "String" + selection: "{ prop }" + } + ] + ) +} + +type V + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") { + id: ID! + b: String! @join__field(graph: SUBGRAPH2) + field: Int! + @join__field( + graph: SUBGRAPH1 + contextArguments: [ + { + context: "Subgraph1__context2" + name: "a" + type: "String" + selection: "... on A { prop } ... on B { prop }" + } + ] + ) +} diff --git a/apollo-router/tests/fixtures/set_context/one.json b/apollo-router/tests/fixtures/set_context/one.json new file mode 100644 index 0000000000..e552a0f47b --- /dev/null +++ b/apollo-router/tests/fixtures/set_context/one.json @@ -0,0 +1,414 @@ +{ + "mocks": [ + { + "request": { + "query": "query Query__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationName": "Query__Subgraph1__0" + }, + "response": { + "data": { + "t": { + "__typename": "T", + "prop": "prop value", + "id": "1", + "u": { + "__typename": "U", + "id": "1" + } + } + } + } + }, + { + "request": { + "query": "query Query__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationName": "Query__Subgraph1__0" + }, + "response": { + "data": { + "t": { + "__typename": "T", + "prop": "prop value", + "id": "1", + "u": { + "__typename": "U", + "id": "1" + } + } + } + } + }, + { + "request": { + "query": "query Query__Subgraph1__0{t{__typename prop id uList{__typename id}}}", + "operationName": "Query__Subgraph1__0" + }, + "response": { + "data": { + "t": { + "__typename": "T", + "prop": "prop value", + "id": "1", + "uList": [ + { + "__typename": "U", + "id": "1" + }, + { + "__typename": "U", + "id": "2" + }, + { + "__typename": "U", + "id": "3" + } + ] + } + } + } + }, + { + "request": { + "query": "query QueryLL__Subgraph1__0{tList{__typename prop id uList{__typename id}}}", + "operationName": "QueryLL__Subgraph1__0" + }, + "response": { + "data": { + "tList": [ + { + "__typename": "T", + "prop": "prop value 1", + "id": "1", + "uList": [ + { + "__typename": "U", + "id": "3" + } + ] + }, + { + "__typename": "T", + "prop": "prop value 2", + "id": "2", + "uList": [ + { + "__typename": "U", + "id": "4" + } + ] + } + ] + } + } + }, + { + "request": { + "query": "query QueryUnion__Subgraph1__0{k{__typename ...on A{__typename prop v{__typename id}}...on B{__typename prop v{__typename id}}}}", + "operationName": "QueryUnion__Subgraph1__0" + }, + "response": { + "data": { + "k": { + "__typename": "A", + "prop": "prop value 3", + "id": 1, + "v": { + "__typename": "V", + "id": "2" + } + } + } + } + }, + { + "request": { + "query": "query Query__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationName": "Query__Subgraph1__1", + "variables": { + "contextualArgument_1_0": "prop value", + "representations": [{ "__typename": "U", "id": "1" }] + } + }, + "response": { + "data": { + "_entities": [ + { + "id": "1", + "field": 1234 + } + ] + } + } + }, + { + "request": { + "query": "query Query__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationName": "Query__Subgraph1__1", + "variables": { + "contextualArgument_1_0": "prop value", + "representations": [{ "__typename": "U", "id": "1" }] + } + }, + "response": { + "data": { + "_entities": [ + { + "__typename": "U", + "id": "1", + "field": 1234 + } + ] + } + } + }, + { + "request": { + "query": "query Query__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationName": "Query__Subgraph1__1", + "variables": { + "contextualArgument_1_0": "prop value", + "representations": [ + { "__typename": "U", "id": "1" }, + { "__typename": "U", "id": "2" }, + { "__typename": "U", "id": "3" } + ] + } + }, + "response": { + "data": { + "_entities": [ + { + "id": "1", + "field": 1234 + }, + { + "id": "2", + "field": 2345 + }, + { + "id": "3", + "field": 3456 + } + ] + } + } + }, + { + "request": { + "query": "query QueryLL__Subgraph1__1($representations: [_Any!]!, $contextualArgument_1_0_0: String, $contextualArgument_1_0_1: String) { _0: _entities(representations: $representations) { ... on U { field(a: $contextualArgument_1_0_0) } } _1: _entities(representations: $representations) { ... on U { field(a: $contextualArgument_1_0_1) } } }", + "operationName": "QueryLL__Subgraph1__1", + "variables": { + "contextualArgument_1_0_0": "prop value 1", + "contextualArgument_1_0_1": "prop value 2", + "representations": [ + { "__typename": "U", "id": "3" }, + { "__typename": "U", "id": "4" } + ] + } + }, + "response": { + "data": { + "_entities": [ + { + "id": "3", + "field": 3456 + }, + { + "id": "4", + "field": 4567 + } + ] + } + } + }, + { + "request": { + "query": "query QueryLL__Subgraph1__1($representations: [_Any!]!, $contextualArgument_1_0_0: String, $contextualArgument_1_0_1: String) { _0: _entities(representations: $representations) { ... on U { field(a: $contextualArgument_1_0_0) } } _1: _entities(representations: $representations) { ... on U { field(a: $contextualArgument_1_0_1) } } }", + "operationName": "QueryLL__Subgraph1__1", + "variables": { + "contextualArgument_1_0_1": "prop value 2", + "contextualArgument_1_0_0": "prop value 1", + "representations": [ + { "__typename": "U", "id": "3" }, + { "__typename": "U", "id": "4" } + ] + } + }, + "response": { + "data": { + "_entities": [ + { + "id": "3", + "field": 3456 + }, + { + "id": "4", + "field": 4567 + } + ] + } + } + }, + { + "request": { + "query": "query QueryUnion__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_1:String){_entities(representations:$representations){...on V{field(a:$contextualArgument_1_1)}}}", + "operationName": "QueryUnion__Subgraph1__1", + "variables": { + "contextualArgument_1_1": "prop value 3", + "representations": [{ "__typename": "V", "id": "2" }] + } + }, + "response": { + "data": { + "_entities": [ + { + "id": "3", + "field": 3456 + } + ] + } + } + }, + { + "request": { + "query": "query Query_Null_Param__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationName": "Query_Null_Param__Subgraph1__0" + }, + "response": { + "data": { + "t": { + "__typename": "T", + "prop": null, + "id": "1", + "u": { + "__typename": "U", + "id": "1" + } + } + } + } + }, + { + "request": { + "query": "query Query_Null_Param__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationName": "Query_Null_Param__Subgraph1__1", + "variables": { + "contextualArgument_1_0": null, + "representations": [{ "__typename": "U", "id": "1" }] + } + }, + "response": { + "data": { + "_entities": [ + { + "id": "1", + "field": 1234 + } + ] + } + } + }, + { + "request": { + "query": "query Query_type_mismatch__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationName": "Query_type_mismatch__Subgraph1__0" + }, + "response": { + "data": { + "t": { + "__typename": "T", + "prop": 7, + "id": "1", + "u": { + "__typename": "U", + "id": "1" + } + } + } + } + }, + { + "request": { + "query": "query Query_type_mismatch__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationName": "Query_type_mismatch__Subgraph1__1", + "variables": { + "contextualArgument_1_0": 7, + "representations": [{ "__typename": "U", "id": "1" }] + } + }, + "response": { + "data": { + "_entities": [ + { + "id": "1", + "field": 1234 + } + ] + } + } + }, + { + "request": { + "query": "query Query_fetch_failure__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationName": "Query_fetch_failure__Subgraph1__0" + }, + "response": { + "data": { + "t": { + "__typename": "T", + "prop": "prop value", + "id": "1", + "u": { + "__typename": "U", + "id": "1" + } + } + } + } + }, + { + "request": { + "query": "query Query_fetch_failure__Subgraph1__2($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationName": "Query_fetch_failure__Subgraph1__2", + "variables": { + "contextualArgument_1_0": "prop value", + "representations": [{ "__typename": "U", "id": "1" }] + } + }, + "response": { + "data": { + "t": { + "__typename": "T", + "prop": "prop value", + "id": "1", + "u": { + "__typename": "U", + "id": "1" + } + } + } + } + }, + { + "request": { + "query": "query Query_fetch_dependent_failure__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationName": "Query_fetch_dependent_failure__Subgraph1__0" + }, + "response": { + "response": { + "data": null, + "errors": [{ + "message": "Some error", + "locations": [ + { + "line": 3, + "column": 5 + } + ], + "path": ["t", "u"] + }] + } + } + } + ] +} diff --git a/apollo-router/tests/fixtures/set_context/supergraph.graphql b/apollo-router/tests/fixtures/set_context/supergraph.graphql new file mode 100644 index 0000000000..16b9ba0019 --- /dev/null +++ b/apollo-router/tests/fixtures/set_context/supergraph.graphql @@ -0,0 +1,165 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link(url: "https://specs.apollo.dev/context/v0.1", for: SECURITY) { + query: Query +} + +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + +directive @context__fromContext(field: String) on ARGUMENT_DEFINITION + +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 + contextArguments: [join__ContextArgument!] +) 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 context__context + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "https://Subgraph1") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "https://Subgraph2") +} + +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) + tList: [T]! @join__field(graph: SUBGRAPH1) + a: Int! @join__field(graph: SUBGRAPH2) + k: K! @join__field(graph: SUBGRAPH1) +} + +union K + @join__type(graph: SUBGRAPH1) + @join__unionMember(graph: SUBGRAPH1, member: "A") + @join__unionMember(graph: SUBGRAPH1, member: "B") + @context(name: "Subgraph1__context2") = + A + | B + +type A @join__type(graph: SUBGRAPH1, key: "id") { + id: ID! + v: V! + prop: String! +} + +type B @join__type(graph: SUBGRAPH1, key: "id") { + id: ID! + v: V! + prop: String! +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @context(name: "Subgraph1__context") { + id: ID! + u: U! + uList: [U]! + prop: String! +} + +type U + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") { + id: ID! + b: String! @join__field(graph: SUBGRAPH2) + field: Int! + @join__field( + graph: SUBGRAPH1 + contextArguments: [ + { + context: "Subgraph1__context" + name: "a" + type: "String" + selection: "{ prop }" + } + ] + ) +} + +type V + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") { + id: ID! + b: String! @join__field(graph: SUBGRAPH2) + field: Int! + @join__field( + graph: SUBGRAPH1 + contextArguments: [ + { + context: "Subgraph1__context2" + name: "a" + type: "String" + selection: "... on A { prop } ... on B { prop }" + } + ] + ) +} diff --git a/apollo-router/tests/fixtures/set_context/two.json b/apollo-router/tests/fixtures/set_context/two.json new file mode 100644 index 0000000000..6ab18d052b --- /dev/null +++ b/apollo-router/tests/fixtures/set_context/two.json @@ -0,0 +1,43 @@ +{ + "mocks": [ + { + "request": { + "query": "query Query__two__2($representations:[_Any!]!){_entities(representations:$representations){...on U{k}}}", + "operationName": "Query__two__2", + "variables": { "representations": [{ "__typename": "U", "id": "1" }] } + }, + "response": { + "data": { + "_entities": [ + { + "k": "k value" + } + ] + } + } + }, + { + "request": { + "query": "query Query_fetch_failure__Subgraph2__1($representations:[_Any!]!){_entities(representations:$representations){...on U{b}}}", + "operationName": "Query_fetch_failure__Subgraph2__1", + "variables": { + "representations": [{ "__typename": "U", "id": "1" }] + } + }, + "response": { + "data": null, + "errors": [{ + "message": "Some error", + "locations": [ + { + "line": 3, + "column": 5 + } + ], + "path": ["t", "u"] + } + ] + } + } + ] +} diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index 4b3aa46d78..29d932c911 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -28,7 +28,7 @@ async fn query_planner() -> Result<(), BoxError> { // 2. run `docker compose up -d` and connect to the redis container by running `docker-compose exec redis /bin/bash`. // 3. Run the `redis-cli` command from the shell and start the redis `monitor` command. // 4. Run this test and yank the updated cache key from the redis logs. - let known_cache_key = "plan:0:v2.7.5:16385ebef77959fcdc520ad507eb1f7f7df28f1d54a0569e3adabcb4cd00d7ce:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:9c26cb1f820a78848ba3d5d3295c16aa971368c5295422fd33cc19d4a6006a9c"; + let known_cache_key = "plan:0:v2.8.0-alpha.1:16385ebef77959fcdc520ad507eb1f7f7df28f1d54a0569e3adabcb4cd00d7ce:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:9c26cb1f820a78848ba3d5d3295c16aa971368c5295422fd33cc19d4a6006a9c"; let config = RedisConfig::from_url("redis://127.0.0.1:6379").unwrap(); let client = RedisClient::new(config, None, None, None); @@ -902,7 +902,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.7.5:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:ae8b525534cb7446a34715fc80edd41d4d29aa65c5f39f9237d4ed8459e3fe82", + "plan:0:v2.8.0-alpha.1:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:ae8b525534cb7446a34715fc80edd41d4d29aa65c5f39f9237d4ed8459e3fe82", ) .await; } @@ -921,7 +921,7 @@ async fn query_planner_redis_update_planner_mode() { async fn query_planner_redis_update_introspection() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_introspection.router.yaml"), - "plan:0:v2.7.5:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:1910d63916aae7a1066cb8c7d622fc3a8e363ed1b6ac8e214deed4046abae85c", + "plan:0:v2.8.0-alpha.1:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:1910d63916aae7a1066cb8c7d622fc3a8e363ed1b6ac8e214deed4046abae85c", ) .await; } @@ -930,7 +930,7 @@ async fn query_planner_redis_update_introspection() { async fn query_planner_redis_update_defer() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_defer.router.yaml"), - "plan:0:v2.7.5:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:8a17c5b196af5e3a18d24596424e9849d198f456dd48297b852a5f2ca847169b", + "plan:0:v2.8.0-alpha.1:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:8a17c5b196af5e3a18d24596424e9849d198f456dd48297b852a5f2ca847169b", ) .await; } @@ -941,7 +941,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.7.5:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:275f78612ed3d45cdf6bf328ef83e368b5a44393bd8c944d4a7d694aed61f017", + "plan:0:v2.8.0-alpha.1:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:275f78612ed3d45cdf6bf328ef83e368b5a44393bd8c944d4a7d694aed61f017", ) .await; } @@ -952,7 +952,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.7.5:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:15fbb62c94e8da6ea78f28a6eb86a615dcaf27ff6fd0748fac4eb614b0b17662", + "plan:0:v2.8.0-alpha.1:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:15fbb62c94e8da6ea78f28a6eb86a615dcaf27ff6fd0748fac4eb614b0b17662", ) .await; } @@ -972,7 +972,7 @@ async fn test_redis_query_plan_config_update(updated_config: &str, new_cache_key router.assert_started().await; router.clear_redis_cache().await; - let starting_key = "plan:0:v2.7.5:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:1910d63916aae7a1066cb8c7d622fc3a8e363ed1b6ac8e214deed4046abae85c"; + let starting_key = "plan:0:v2.8.0-alpha.1:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:1910d63916aae7a1066cb8c7d622fc3a8e363ed1b6ac8e214deed4046abae85c"; router.execute_default_query().await; router.assert_redis_cache_contains(starting_key, None).await; router.update_config(updated_config).await; diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__query_planner.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__query_planner.snap index 4714fe2240..f90305be82 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__query_planner.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__query_planner.snap @@ -12,6 +12,7 @@ expression: query_plan "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "121b9859eba2d8fa6dde0a54b6e3781274cf69f7ffb0af912e92c01c6bfff6ca", "authorization": { "is_authenticated": false, diff --git a/apollo-router/tests/set_context.rs b/apollo-router/tests/set_context.rs new file mode 100644 index 0000000000..bc5f44d2da --- /dev/null +++ b/apollo-router/tests/set_context.rs @@ -0,0 +1,325 @@ +//! +//! Please ensure that any tests added to this file use the tokio multi-threaded test executor. +//! + +use apollo_router::graphql::Request; +use apollo_router::graphql::Response; +use apollo_router::plugin::test::MockSubgraph; +use apollo_router::services::supergraph; +use apollo_router::MockedSubgraphs; +use apollo_router::TestHarness; +use serde::Deserialize; +use serde_json::json; +use tower::ServiceExt; + +#[derive(Deserialize)] +struct SubgraphMock { + mocks: Vec, +} + +#[derive(Deserialize)] +struct RequestAndResponse { + request: Request, + response: Response, +} + +macro_rules! snap +{ + ($result:ident) => { + insta::with_settings!({sort_maps => true}, { + insta::assert_json_snapshot!($result); + }); + } +} + +async fn run_single_request(query: &str, mocks: &[(&'static str, &'static str)]) -> Response { + let harness = setup_from_mocks( + json! {{ + "experimental_type_conditioned_fetching": true, + // will make debugging easier + "plugins": { + "experimental.expose_query_plan": true + }, + "include_subgraph_errors": { + "all": true + } + }}, + mocks, + ); + let supergraph_service = harness.build_supergraph().await.unwrap(); + let request = supergraph::Request::fake_builder() + .query(query.to_string()) + .header("Apollo-Expose-Query-Plan", "true") + .variables(Default::default()) + .build() + .expect("expecting valid request"); + + supergraph_service + .oneshot(request) + .await + .unwrap() + .next_response() + .await + .unwrap() +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context() { + static QUERY: &str = r#" + query Query { + t { + __typename + id + u { + __typename + field + } + } + }"#; + + let response = run_single_request( + QUERY, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_no_typenames() { + static QUERY_NO_TYPENAMES: &str = r#" + query Query { + t { + id + u { + field + } + } + }"#; + + let response = run_single_request( + QUERY_NO_TYPENAMES, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_list() { + static QUERY_WITH_LIST: &str = r#" + query Query { + t { + id + uList { + field + } + } + }"#; + + let response = run_single_request( + QUERY_WITH_LIST, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_list_of_lists() { + static QUERY_WITH_LIST_OF_LISTS: &str = r#" + query QueryLL { + tList { + id + uList { + field + } + } + }"#; + + let response = run_single_request( + QUERY_WITH_LIST_OF_LISTS, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_union() { + static QUERY_WITH_UNION: &str = r#" + query QueryUnion { + k { + ... on A { + v { + field + } + } + ... on B { + v { + field + } + } + } + }"#; + + let response = run_single_request( + QUERY_WITH_UNION, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_with_null() { + static QUERY: &str = r#" + query Query_Null_Param { + t { + id + u { + field + } + } + }"#; + + let response = run_single_request( + QUERY, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + insta::assert_json_snapshot!(response); +} + +// this test returns the contextual value with a different than expected type +// this currently works, but perhaps should do type valdiation in the future to reject +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_type_mismatch() { + static QUERY: &str = r#" + query Query_type_mismatch { + t { + id + u { + field + } + } + }"#; + + let response = run_single_request( + QUERY, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +// fetch from unrelated (to context) subgraph fails +// validates that the error propagation is correct +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_unrelated_fetch_failure() { + static QUERY: &str = r#" + query Query_fetch_failure { + t { + id + u { + field + b + } + } + }"#; + + let response = run_single_request( + QUERY, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +// subgraph fetch fails where context depends on results of fetch. +// validates that no fetch will get called that passes context +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_dependent_fetch_failure() { + static QUERY: &str = r#" + query Query_fetch_dependent_failure { + t { + id + u { + field + } + } + }"#; + + let response = run_single_request( + QUERY, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +fn setup_from_mocks( + configuration: serde_json::Value, + mocks: &[(&'static str, &'static str)], +) -> TestHarness<'static> { + let mut mocked_subgraphs = MockedSubgraphs::default(); + + for (name, m) in mocks { + let subgraph_mock: SubgraphMock = serde_json::from_str(m).unwrap(); + + let mut builder = MockSubgraph::builder(); + + for mock in subgraph_mock.mocks { + builder = builder.with_json( + serde_json::to_value(mock.request).unwrap(), + serde_json::to_value(mock.response).unwrap(), + ); + } + + mocked_subgraphs.insert(name, builder.build()); + } + + let schema = include_str!("fixtures/set_context/supergraph.graphql"); + TestHarness::builder() + .try_log_level("info") + .configuration_json(configuration) + .unwrap() + .schema(schema) + .extra_plugin(mocked_subgraphs) +} diff --git a/apollo-router/tests/snapshots/set_context__set_context.snap b/apollo-router/tests/snapshots/set_context__set_context.snap new file mode 100644 index 0000000000..2e11680753 --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context.snap @@ -0,0 +1,101 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "t": { + "__typename": "T", + "id": "1", + "u": { + "__typename": "U", + "field": 1234 + } + } + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationKind": "query", + "operationName": "Query__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "d7cb2d1809789d49360ca0a60570555f83855f00547675f366915c9d9d90fef9", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationKind": "query", + "operationName": "Query__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "66b954f39aead8436321c671eb71e56ce15bbe0c7b82f06b2f8f70473ce1cb6e", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "", + "t", + "u" + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n u {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \".t.u\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure.snap b/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure.snap new file mode 100644 index 0000000000..703d8f9c59 --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure.snap @@ -0,0 +1,98 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": null, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_fetch_dependent_failure__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationKind": "query", + "operationName": "Query_fetch_dependent_failure__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "595c36c322602fefc4658fc0070973b51800c2d2debafae5571a7c9811d80745", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_fetch_dependent_failure__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationKind": "query", + "operationName": "Query_fetch_dependent_failure__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "37bef7ad43bb477cdec4dfc02446bd2e11a6919dc14ab90e266af85fefde4abd", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "", + "t", + "u" + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n u {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \".t.u\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + }, + "valueCompletion": [ + { + "message": "Cannot return null for non-nullable field Query.t", + "path": [] + } + ] + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_list.snap b/apollo-router/tests/snapshots/set_context__set_context_list.snap new file mode 100644 index 0000000000..095326167e --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_list.snap @@ -0,0 +1,108 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "t": { + "id": "1", + "uList": [ + { + "field": 1234 + }, + { + "field": 2345 + }, + { + "field": 3456 + } + ] + } + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query__Subgraph1__0{t{__typename prop id uList{__typename id}}}", + "operationKind": "query", + "operationName": "Query__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "4f746b9319e3ca4f234269464b6815eb97782f2ffe36774b998e7fb78f30abef", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationKind": "query", + "operationName": "Query__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "66b954f39aead8436321c671eb71e56ce15bbe0c7b82f06b2f8f70473ce1cb6e", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "", + "t", + "uList", + "@" + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n uList {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \".t.uList.@\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_list_of_lists.snap b/apollo-router/tests/snapshots/set_context__set_context_list_of_lists.snap new file mode 100644 index 0000000000..e7fbee2a8b --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_list_of_lists.snap @@ -0,0 +1,113 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "tList": [ + { + "id": "1", + "uList": [ + { + "field": 3456 + } + ] + }, + { + "id": "2", + "uList": [ + { + "field": 4567 + } + ] + } + ] + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query QueryLL__Subgraph1__0{tList{__typename prop id uList{__typename id}}}", + "operationKind": "query", + "operationName": "QueryLL__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "babf88ea82c1330e535966572a55b03a2934097cd1cf905303b86ae7c197ccaf", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query QueryLL__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationKind": "query", + "operationName": "QueryLL__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "a9b24549250c12e38c398c32e9218134fab000be3b934ebc6bb38ea096343646", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "", + "tList", + "@", + "uList", + "@" + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n tList {\n __typename\n prop\n id\n uList {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \".tList.@.uList.@\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_no_typenames.snap b/apollo-router/tests/snapshots/set_context__set_context_no_typenames.snap new file mode 100644 index 0000000000..8eaa5b0202 --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_no_typenames.snap @@ -0,0 +1,99 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "t": { + "id": "1", + "u": { + "field": 1234 + } + } + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationKind": "query", + "operationName": "Query__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "d7cb2d1809789d49360ca0a60570555f83855f00547675f366915c9d9d90fef9", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationKind": "query", + "operationName": "Query__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "66b954f39aead8436321c671eb71e56ce15bbe0c7b82f06b2f8f70473ce1cb6e", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "", + "t", + "u" + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n u {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \".t.u\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_type_mismatch.snap b/apollo-router/tests/snapshots/set_context__set_context_type_mismatch.snap new file mode 100644 index 0000000000..1df052723e --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_type_mismatch.snap @@ -0,0 +1,99 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "t": { + "id": "1", + "u": { + "field": 1234 + } + } + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_type_mismatch__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationKind": "query", + "operationName": "Query_type_mismatch__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "7eae890e61f5ae512e112f5260abe0de3504041c92dbcc7aae0891c9bdf2222b", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_type_mismatch__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationKind": "query", + "operationName": "Query_type_mismatch__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "d8ea99348ab32931371c85c09565cfb728d2e48cf017201cd79cb9ef860eb9c2", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "", + "t", + "u" + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n u {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \".t.u\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_union.snap b/apollo-router/tests/snapshots/set_context__set_context_union.snap new file mode 100644 index 0000000000..e382988a8b --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_union.snap @@ -0,0 +1,157 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "k": { + "v": { + "field": 3456 + } + } + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query QueryUnion__Subgraph1__0{k{__typename ...on A{__typename prop v{__typename id}}...on B{__typename prop v{__typename id}}}}", + "operationKind": "query", + "operationName": "QueryUnion__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "b9124cd1daa6e8347175ffe2108670a31c73cbc983e7812ee39f415235541005", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on A", + "prop" + ], + "renameKeyTo": "contextualArgument_1_1" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query QueryUnion__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_1:String){_entities(representations:$representations){...on V{field(a:$contextualArgument_1_1)}}}", + "operationKind": "query", + "operationName": "QueryUnion__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "V" + } + ], + "schemaAwareHash": "c50ca82d402a330c1b35a6d76332094c40b00d6dec6f6b2a9b0a32ced68f4e95", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_1" + ] + }, + "path": [ + "", + "k|[A]", + "v" + ] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on B", + "prop" + ], + "renameKeyTo": "contextualArgument_1_1" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query QueryUnion__Subgraph1__2($representations:[_Any!]!$contextualArgument_1_1:String){_entities(representations:$representations){...on V{field(a:$contextualArgument_1_1)}}}", + "operationKind": "query", + "operationName": "QueryUnion__Subgraph1__2", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "V" + } + ], + "schemaAwareHash": "ec99886497fee9b4f13565e19cadb13ae85c83de93acb53f298944b7a29e630e", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_1" + ] + }, + "path": [ + "", + "k|[B]", + "v" + ] + } + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n k {\n __typename\n ... on A {\n __typename\n prop\n v {\n __typename\n id\n }\n }\n ... on B {\n __typename\n prop\n v {\n __typename\n id\n }\n }\n }\n }\n },\n Parallel {\n Flatten(path: \".k|[A].v\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on V {\n __typename\n id\n }\n } =>\n {\n ... on V {\n field(a: $contextualArgument_1_1)\n }\n }\n },\n },\n Flatten(path: \".k|[B].v\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on V {\n __typename\n id\n }\n } =>\n {\n ... on V {\n field(a: $contextualArgument_1_1)\n }\n }\n },\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure.snap b/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure.snap new file mode 100644 index 0000000000..605fd4570a --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure.snap @@ -0,0 +1,170 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": null, + "errors": [ + { + "message": "Some error", + "path": [ + "t", + "u" + ] + } + ], + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_fetch_failure__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationKind": "query", + "operationName": "Query_fetch_failure__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "1813ba1c272be0201096b4c4c963a07638e4f4b4ac1b97e0d90d634f2fcbac11", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_fetch_failure__Subgraph2__1($representations:[_Any!]!){_entities(representations:$representations){...on U{b}}}", + "operationKind": "query", + "operationName": "Query_fetch_failure__Subgraph2__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "1fdff97ad7facf07690c3e75e3dc7f1b11ff509268ef999250912a728e7a94c9", + "serviceName": "Subgraph2", + "variableUsages": [] + }, + "path": [ + "", + "t", + "u" + ] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_fetch_failure__Subgraph1__2($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationKind": "query", + "operationName": "Query_fetch_failure__Subgraph1__2", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "c9c571eac5df81ff34e5e228934d029ed322640c97ab6ad061cbee3cd81040dc", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "", + "t", + "u" + ] + } + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n u {\n __typename\n id\n }\n }\n }\n },\n Parallel {\n Flatten(path: \".t.u\") {\n Fetch(service: \"Subgraph2\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n b\n }\n }\n },\n },\n Flatten(path: \".t.u\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n },\n}" + }, + "valueCompletion": [ + { + "message": "Cannot return null for non-nullable field U.field", + "path": [ + "t", + "u" + ] + }, + { + "message": "Cannot return null for non-nullable field T.u", + "path": [ + "t", + "u" + ] + }, + { + "message": "Cannot return null for non-nullable field T!.t", + "path": [ + "t" + ] + } + ] + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_with_null.snap b/apollo-router/tests/snapshots/set_context__set_context_with_null.snap new file mode 100644 index 0000000000..1e361f0a83 --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_with_null.snap @@ -0,0 +1,99 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "t": { + "id": "1", + "u": { + "field": 1234 + } + } + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "Subgraph1", + "variableUsages": [], + "operation": "query Query_Null_Param__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationName": "Query_Null_Param__Subgraph1__0", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "19bd66a3ecc2d9495dffce2279774de3275cb027254289bb61b0c1937a7738b4", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + }, + { + "kind": "Flatten", + "path": [ + "", + "t", + "u" + ], + "node": { + "kind": "Fetch", + "serviceName": "Subgraph1", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "U", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "contextualArgument_1_0" + ], + "operation": "query Query_Null_Param__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationName": "Query_Null_Param__Subgraph1__1", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "schemaAwareHash": "010ba25ca76f881bd9f0d5e338f9c07829d4d00e183828b6577d593aea0cf21e", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n u {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \".t.u\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_disabled.snap b/apollo-router/tests/snapshots/type_conditions__type_conditions_disabled.snap index e1b3c3bba7..84b137aa01 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_disabled.snap +++ b/apollo-router/tests/snapshots/type_conditions__type_conditions_disabled.snap @@ -78,6 +78,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "0144f144d271437ed45f9d20706be86ffbf1e124d77c7add3db17d4a1498ce97", "authorization": { "is_authenticated": false, @@ -135,6 +136,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "23759b36e5149924c757a8b9586adec2c0f6be04ecdf2c3c3ea277446daa690b", "authorization": { "is_authenticated": false, diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled.snap b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled.snap index 49e1fef4bc..e41aeefee5 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled.snap +++ b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled.snap @@ -78,6 +78,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "0144f144d271437ed45f9d20706be86ffbf1e124d77c7add3db17d4a1498ce97", "authorization": { "is_authenticated": false, @@ -139,6 +140,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "23759b36e5149924c757a8b9586adec2c0f6be04ecdf2c3c3ea277446daa690b", "authorization": { "is_authenticated": false, @@ -198,6 +200,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "8ee58ad8b4823bcbda9126d2565e1cb04bf91ff250b1098476a1d7614a870121", "authorization": { "is_authenticated": false, diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_generate_query_fragments.snap b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_generate_query_fragments.snap index b04c2208ec..d92517b39d 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_generate_query_fragments.snap +++ b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_generate_query_fragments.snap @@ -78,6 +78,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "844dc4e409cdca1334abe37c347bd4e330123078dd7e65bda8dbb57ea5bdf59c", "authorization": { "is_authenticated": false, @@ -139,6 +140,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "ad82ce0af279c6a012d6b349ff823ba1467902223312aed1cdfc494ec3100b3e", "authorization": { "is_authenticated": false, @@ -198,6 +200,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "7c267302cf4a44a4463820237830155ab50be32c8860371d8a5c8ca905476360", "authorization": { "is_authenticated": false, diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list.snap b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list.snap index 45697537d8..acffc62599 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list.snap +++ b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list.snap @@ -140,6 +140,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "1343b4972ec8be54afe990c69711ce790992a814f9654e34e2ee2b25e4097e45", "authorization": { "is_authenticated": false, @@ -202,6 +203,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "23759b36e5149924c757a8b9586adec2c0f6be04ecdf2c3c3ea277446daa690b", "authorization": { "is_authenticated": false, @@ -262,6 +264,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "8ee58ad8b4823bcbda9126d2565e1cb04bf91ff250b1098476a1d7614a870121", "authorization": { "is_authenticated": false, diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list_of_list.snap b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list_of_list.snap index 528a658fe6..2b8feaafc3 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list_of_list.snap +++ b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list_of_list.snap @@ -144,6 +144,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "3698f4e74ead34f43a949e1e8459850337a1a07245f8ed627b9203904b4cfff4", "authorization": { "is_authenticated": false, @@ -207,6 +208,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "23759b36e5149924c757a8b9586adec2c0f6be04ecdf2c3c3ea277446daa690b", "authorization": { "is_authenticated": false, @@ -268,6 +270,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "8ee58ad8b4823bcbda9126d2565e1cb04bf91ff250b1098476a1d7614a870121", "authorization": { "is_authenticated": false, diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_shouldnt_make_article_fetch.snap b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_shouldnt_make_article_fetch.snap index 38ef6bc51a..5020d447b4 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_shouldnt_make_article_fetch.snap +++ b/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_shouldnt_make_article_fetch.snap @@ -53,6 +53,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "0144f144d271437ed45f9d20706be86ffbf1e124d77c7add3db17d4a1498ce97", "authorization": { "is_authenticated": false, @@ -114,6 +115,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "23759b36e5149924c757a8b9586adec2c0f6be04ecdf2c3c3ea277446daa690b", "authorization": { "is_authenticated": false, @@ -173,6 +175,7 @@ expression: response "id": null, "inputRewrites": null, "outputRewrites": null, + "contextRewrites": null, "schemaAwareHash": "8ee58ad8b4823bcbda9126d2565e1cb04bf91ff250b1098476a1d7614a870121", "authorization": { "is_authenticated": false, diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 8151da867a..da27ae250f 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.5.21+v2.7.5" +router-bridge = "=0.5.24+v2.8.0-alpha.1" [dev-dependencies] anyhow = "1"